diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-16 14:56:57 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-16 14:56:57 +0200 |
| commit | a63bbf64fd5773b1c4497e803c1d4ba12d0eaad7 (patch) | |
| tree | fdccbeb677b8bf4e6bad5eabb3c9aa5b2a656b9b | |
| parent | 4a1f1166ac27b9016a4e7b4d1468c2410ac1213d (diff) | |
| download | desktop-assistant-a63bbf64fd5773b1c4497e803c1d4ba12d0eaad7.tar.gz desktop-assistant-a63bbf64fd5773b1c4497e803c1d4ba12d0eaad7.zip | |
Spec attachments
ask takes file paths; text and PDFs are extracted and inlined within half
the free context, images and scanned pages go as image parts to a model
that reports vision. History keeps extracted text inline and images as a
path and hash, re-read on reopen while unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| -rw-r--r-- | docs/superpowers/specs/2026-10-16-attachments-design.md | 99 |
1 files changed, 99 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-10-16-attachments-design.md b/docs/superpowers/specs/2026-10-16-attachments-design.md new file mode 100644 index 0000000..d8a8092 --- /dev/null +++ b/docs/superpowers/specs/2026-10-16-attachments-design.md @@ -0,0 +1,99 @@ +# Attachments: design + +Date: 2026-10-16. Status: approved in brainstorming, not implemented. + +## Goal + +Let a question carry files. The quickshell canvas stages files dropped on +the island and sends their paths with `ask`; the daemon reads them, gives +the model their content, and keeps enough in the history for a reopened +conversation to still talk about them. The canvas side is specified in the +quickshell repo, `docs/superpowers/specs/2026-10-16-file-drop-design.md`. + +This is piece 1 of three. Piece 2 is per-type model prompt presets +(summarize, describe, explain) and piece 3 local file operations (PDF to +markdown, merge, video to audio or GIF, trackcrop); each gets its own spec. + +## Verified before designing + +- Gemma4-12B on the router reports `input_modalities` of + `["text", "image", "audio"]` in `/v1/models`; Qwen3.8-9B reports + `["text"]`. The field is how vision is detected. +- `pdftotext` and `pdftoppm` (poppler 26.09.0) are installed. +- `tesseract` is installed with **no language data** (`/usr/share/tessdata` + lists 0 languages), so it cannot OCR. Scanned pages go to the model as + images instead, which Gemma reads. +- llamachat's attachment code (`llamachat/backend.py`) is the model: + `--- file: name ---` blocks for text, `image_url` data URLs for images, + truncation to a character budget. Read for ideas; this is a separate + implementation. + +## Protocol + +`ask` gains an optional `files`: a list of absolute paths, at most 8. + +```json +{"type": "ask", "text": "What changed?", "files": ["/home/you/report.pdf"]} +``` + +The daemon runs as the user and reads the paths itself, so nothing is +copied over the socket. `text` may be empty when files are given; the +model then gets the files alone. + +The `user` event and each shown turn gain `files`: +`[{name, kind, pages, truncated}]` (`pages` for a PDF, else absent), for +the canvas's chips. + +Errors answer with `error` (with `turn`, since they end the reply before it +starts) and nothing is sent to the model: + +- not an absolute path, missing, not a regular file, or unreadable; +- more than 8 files; +- an image over 20 MB; +- a kind chat does not take: "chat supports text, images and PDF"; +- an image, or a PDF needing page images, for a model whose + `input_modalities` lacks `image`: "<model> cannot see images". + +## Extraction + +| kind | detected by | the model gets | +| --- | --- | --- | +| text | a `text/*` mime type, or a code suffix (llamachat's list) | the file as UTF-8 (errors replaced) in a `--- file: name ---` block | +| image | png, jpeg, webp, gif | an `image_url` part, a base64 data URL | +| pdf | `.pdf` | `pdftotext -layout` per page in one block; a page with no text is rendered with `pdftoppm -r 150 -png` and sent as an image, at most 4 such pages | +| other | anything else | refused, see above | + +Text blocks share a budget: half the free context, +`(ctx_max - ctx_used) / 2` tokens at 3 characters per token. A file cut to +fit is marked `truncated` in its block header and its chip. With +`ctx_max` unknown (0), the budget is 24000 characters. + +## History + +`turns` gains a `files` column (JSON, the chip list), added with +`ALTER TABLE turns ADD COLUMN files TEXT` when `PRAGMA table_info` shows it +missing, so an existing `history.db` keeps working. + +In `turns.messages`, extracted text is stored inline: a reopened +conversation still has the content. Images are stored as a reference part +`{"type": "image_ref", "name", "path", "sha256"}`. When history is sent to +the model, each reference is resolved: if the path still holds a file with +that hash, it becomes the `image_url` again; otherwise the text +`[image <name> is no longer available]`. Rendered PDF pages are stored the +same way, with the PDF's path, its hash and the page number, and re-rendered +on resolve. + +## Testing + +`python3 assistant.py --test` gains: + +- classification of a text file, a code suffix, an image, a PDF and an + unknown file; +- a PDF built in the test with `magick` (one page of text, one blank), whose + extraction gives the text and one page image; +- truncation at a small budget, with the block marked; +- an image reference that resolves after a save and reload, and becomes the + unavailable text once the file changes; +- the vision error, against the fake server's `/models` reporting + `["text"]`; +- each refusal above. |
