1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
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` | the track copied as it is when it is AAC (`.m4a`), MP3 or Opus (`ffprobe` tells), anything else encoded to `.mp3` at `-q:a 2` |
| video | To MP4 | `mp4` | H.264 on the GPU (`h264_vaapi`) + MP3: this ffmpeg has no libx264 and no AAC encoder |
| 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.
|