OpenGenie
Repository on GitHub

009 · Files and screenshots

On this page

A specification, an implementation plan, and the implementation: give the agent the bytes the user is already holding — by paste, drop, or picker — without ever breaking the fence, the limits, or the transcript's honesty.

Status: specification complete; implementation in progress. 9 November 2026. Scope: the composer's attach surface (web/src/components/Composer.tsx and its neighbourhood), a new blob stage in the API (api/domain/blob.py, api/services/blobs.py, api/adapters/blob/), three routes, one context branch, one MCP tool, and one event type. It changes no existing attachment behaviour: the five reference kinds work exactly as they do today. Relation to earlier specs: extends the context model of 006 · On par with Genie Code and the shell of 007 · The shell, the keys, and the scroll. It reuses 007's audits (a11y, overflow, CLS) as its definition of "not clunky". It does not touch 008 · the public site. Audience: product, full-stack, database, UX/UI, front-end, growth.


The one sentence

A user presses ⌘V with a screenshot on the clipboard, a chip appears in the same frame, and by the time they have typed their question the agent can see what they are looking at — and if it cannot, it says so in one sentence that names the limit.

Everything in this folder is an argument about that sentence.


Read in this order

#DocumentAnswers
01Five whys and first principlesWhy this is not a small feature, and the eight principles it must satisfy
02Grounded assessmentWhat is actually in the code, traced end to end, with the commands that count it
03RequirementsAFC-*, BLB-*, CMP-*, CAP-*, SEC-* — each with its test
04Wireframes (ASCII)Every screen and state, drawn to the column
05Diagrams (Mermaid)Sequences, the blob's state machine, the ER model, the ports
06Six lensesSix owners, and the one thing each vetoes
07Architecture and decisions[D1..D12], where each rule lives, SOLID and DRY point by point
08Implementation planTen phases, the files, the tests, the order
09Edge casesThirty cases, their copy, their tests, two accepted limitations
10E2E planTen scenes, the inspection loop, full traceability
11OutcomeWhat shipped, what the screenshots changed, what is still not true

Cross-reference convention: a requirement is cited as [AFC-3], a decision [D5], an edge case [E-11], a scene [S2], a first principle [P1], a veto [V4]. Every id is defined exactly once — in 03, 07, 09, 01, or 06 — and linked from everywhere else.


The problem

The composer can attach five kinds of thing, and all five are references to workspace objects:

KINDS = ("notebook", "file", "folder", "table", "cell")

That is a good design with one consequence: anything that is not already in the workspace cannot be attached at all. A screenshot on the clipboard, a CSV from a colleague's message, a diagram exported from a design tool — none of these are workspace paths, so there is no chip, no picker, and no wire field for them.

The security rule that produced this is worth keeping:

"Attachments are references (kind + path); the server re-reads the object with the user's own token. The browser never supplies content, so it cannot attach something it cannot read."

The feature is therefore not "add an upload button". It is: let the browser supply content through the same fence, limits, validation, and audit trail that every other write uses — which is P1.


The shape of the answer

  clipboard / drag / picker
        │
        ▼  bytes (validated twice: client courtesy, server rule)
  ┌──────────────┐
  │  BlobStore   │  durable, addressable, swept, never in the event log
  └──────┬───────┘
         │  {kind:"blob", ref:"blb_7f3a"}
         ▼
  turn.attachments ──▶ context.render() ──▶ the model
         │                                    │
         │                                    └── dbx_read_blob for the rest
         ▼
  the transcript (chips) + the evidence (what was read)

Four properties fall out of that shape, and each is a test:

PropertyTest
The composer never waits for the networkS2 — chip in ≤ 1 frame
The model never receives base64test_binding.py — no /9j/ in the prompt
The fence still decides writesS6
A missing attachment is never silentS9c

The honest limitation, up front

The ACP wire this app speaks carries {type: "text"} and nothing else (api/adapters/acp/runtime.py:197 and :218), and none of the 27 tools reads a byte range from a binary file. So "attach an image to a vision model" cannot mean "send the pixels".

It means: the server describes the image and exposes its bytes to a tool, and the manifest says exactly what the model was given. On a model with no vision the image is still accepted — the chip says "not visible to this model" and the manifest tells the model to ask rather than guess — because a refusal would leave the user no way to hand over a screenshot at all.

Sending a real image content block is D7 option B — named, not done, and repeated in 11 so nobody discovers it later and calls it a bug.


What must be true when this is done

The short form; the long form is 03.

  1. ⌘V with an image attaches it, and the chip exists before the upload starts.
  2. Every refusal is one sentence naming the limit and the value.
  3. An image on a model that cannot see is refused before the upload, naming the model.
  4. Writing a blob into the workspace goes through the fence, with the same approval as Save.
  5. The transcript reproduces on reload, from the event log, with the same chips.
  6. The model receives a fenced description, never base64, and can page the rest through a tool.
  7. The composer's resting height does not change by a pixel.
  8. a11y 0, overflow 0, CLS ≤ baseline — the 007 bar, unchanged.

What this changes in code, at a glance

LayerNewChanged
domainapi/domain/blob.pycontext.py (+1 kind)
portsBlobStore, BlobReader—
adaptersblob/memory.py, blob/files.py—
servicesblobs.pycontext.py (+1 branch), orchestrator.py (tool count)
routesPOST /api/blobs, GET /api/blobs/{id}, POST /api/blobs/{id}/materialise—
MCPdbx_read_blobtool count 19 → 20
web domainblob.tsevents.ts (+1 kind, +1 event), api/client.ts (+3)
web componentsAttachMenu.tsx, Chip.tsx, useBlobUpload.tsComposer.tsx, styles.css

≈ 1,830 lines of implementation, ~124 unit tests, 10 e2e scenes. The estimate and its risk per phase are in 08 §effort.


Status of the documents

DocState
01–10complete, reviewed against the code
11written after the implementation, from the screenshots
e2e/screenshots/filled by the loop in 10