aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers
diff options
context:
space:
mode:
Diffstat (limited to 'docs/superpowers')
-rw-r--r--docs/superpowers/specs/2026-10-16-file-ops-design.md86
1 files changed, 86 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-10-16-file-ops-design.md b/docs/superpowers/specs/2026-10-16-file-ops-design.md
new file mode 100644
index 0000000..9f0d7c0
--- /dev/null
+++ b/docs/superpowers/specs/2026-10-16-file-ops-design.md
@@ -0,0 +1,86 @@
+# File operations (the Do row): design
+
+Date: 2026-10-16. Status: approved in brainstorming, not implemented.
+
+## Goal
+
+Piece 3 of the file work (piece 1 `2026-10-16-file-drop-design.md`, piece 2
+`2026-10-16-ask-presets-design.md`). The file state gains a **Do** row of
+local operations on the staged files: convert, extract, merge, compress.
+They run without the model, except To .md, which asks it to rewrite a PDF
+as Markdown and writes the reply to a file (desktop-assistant,
+`docs/superpowers/specs/2026-10-16-write-design.md`).
+
+Results go to one folder, `$(xdg-user-dir DOCUMENTS)/Assistant`, created
+on first use. A taken name becomes `name (1).ext`, `name (2).ext`; nothing
+is overwritten.
+
+## Operations
+
+| kind | label | op | does |
+| --- | --- | --- | --- |
+| pdf | To .md | (daemon `write`) | the model rewrites it as Markdown into `name.md` |
+| pdf | To images | `pdf-images` | `pdftoppm -r 150 -png` into a folder `name (pages)` |
+| pdf, 2+ | Merge | `pdf-merge` | `qpdf --empty --pages ... -- merged.pdf`, in staging order |
+| video | Extract audio | `audio` | AAC copied into `.m4a` (`ffprobe` tells), anything else to `.mp3` at `-q:a 2` |
+| video | To MP4 | `mp4` | libx264 + AAC |
+| video | To WebM | `webm` | libvpx-vp9 + Opus |
+| video | To GIF | `gif` | 12 fps, 480 px wide, palettegen + paletteuse |
+| video | Trackcrop | `trackcrop` | `trackcrop in out` from PATH; it opens its own object picker window and the op waits for it |
+| image | To PNG, To JPEG, To WebP | `png`, `jpeg`, `webp` | `magick in out`; the image's own format is not offered |
+| image | Resize 50% | `half` | `magick in -resize 50% out` |
+| archive | Extract | `extract` | `bsdtar -xf` into a folder named after the archive |
+| any | Compress… | `zip`, `tgz`, `txz`, `tbz2` | one archive of everything staged; the button opens a row of the four formats |
+
+Matching follows the presets: an operation is offered when every staged
+file is of one of its kinds. Merge needs two or more files, all PDF.
+Compress fits any mix. Several staged files of one kind run the operation
+on each (four images, To PNG, four PNGs), except Merge and Compress, which
+make one result.
+
+## fileops
+
+`desktop/assistant/fileops`, stdlib Python on `/usr/bin/python3`:
+`fileops <op> <outdir> <file...>`, printing one JSON line,
+`{"ok": true, "out": [paths]}` or `{"ok": false, "error": "..."}`, exit
+status 0 or 1.
+
+- Each result is written to a temporary name in the output folder and
+ renamed into place on success, so a failure or a stop leaves nothing
+ half written.
+- Tools run in their own process group; SIGTERM (the canvas's Stop) kills
+ the group, then removes the temporary file, so no `ffmpeg` keeps running.
+- A missing tool is an error naming it, as is trackcrop not on PATH:
+ "trackcrop not found: link it into ~/bin".
+- A tool's failure reports its last stderr line.
+
+## Card
+
+- The Do row sits under the Ask row in the file state. To .md sends the
+ daemon's `write`; every other operation starts `fileops` with `Process`.
+- One operation at a time. While it runs, a status line says "Working on
+ To GIF…" (for To .md, "To .md… 3.4k characters", from the daemon's
+ `progress`), and a Stop button replaces the rows. Stop stops the
+ `fileops` process, or sends `stop` to the daemon.
+- The result line: "Saved report.md · Open · Folder" (`xdg-open` on the
+ result, or the output folder); several results say "Saved 4 files".
+ An error shows in red. The card stays in the file state with the chips,
+ so another operation can follow.
+- To .md while a chat reply streams stops that reply first: llama-server
+ has one slot.
+
+`desktop/assistant/Ops.js` holds the list (`{label, op, kinds}`), the
+matching, and the format row of Compress…; `test_ops.js` checks it.
+
+## Testing
+
+- `test_ops.js`: matching for one PDF, two PDFs (Merge), a PDF and an
+ image (Compress only), one PNG (no To PNG), a video, an archive.
+- `test_fileops.py`: builds a PDF, a PNG, a one-second ffmpeg `testsrc`
+ clip with a sine tone, and a tarball in a temp folder; runs every
+ operation except trackcrop into a temp output folder and checks each
+ result's mime type with `file`; checks a taken name gets ` (1)`; checks
+ a failing input leaves no file behind; checks SIGTERM during a slow
+ ffmpeg leaves neither the process nor a partial file.
+- By hand: each row on a real file, Stop on a long video conversion,
+ trackcrop's window opening, To .md on a scanned PDF.