Skip to content

Plannotator Inbox step 2: attachments, versioned, annotated in the Inbox - #1759

Merged
backnotprop merged 3 commits into
mainfrom
feat/inbox-attachments
Oct 7, 2026
Merged

backnotprop merged 3 commits into
mainfrom
feat/inbox-attachments

Conversation

@backnotprop

Copy link
Copy Markdown
Owner

The story

An agent can now attach files to an Inbox message by path. The Inbox records each file by its realpath, keeps the bytes it sent as a version, serves the file only by attachment id, and shows it as it is now on disk with "Changed since … Open the version it sent". The person annotates markdown, HTML and diagrams with Plannotator's own viewers inside the Inbox, and the annotations ride the next Send after the picks as Plannotator's feedback text, so wait_for_reply and read_thread return them.

Step 2 of research/plans/plannotator-inbox-2026-10-06/PLAN.md (Workspaces meta repo). It was branched from the window branch (PR 1755) and rebased onto main once 1755 merged, so it is based on main.

What changed

  • Agent side. send_message takes attachments: [path]: absolute, or relative to project_path (packages/server/inbox-mcp.ts). The tool description and the server instructions show a real call. The answer lists each attachment's id, name, kind and path.

  • Recording and reading files (packages/shared/inbox/attachments.ts, new). A send is refused when a file is outside the project, is .env (checked by the agent's name for it and by the target's name, so a symlink to .env is refused too), is not a regular file, or is outside annotate's annotatable set. There is no size bound. Every file is checked before anything is written. The bytes go to a content-addressed blob, inbox/blobs/<sha256>. To read the current file, the recorded realpath must still be that same regular file, with no path component turned into a symlink, and a symlink the agent named must still point at it. Otherwise the Inbox answers 409 attachment_changed_type (or 404 attachment_missing), and the sent version still opens.

  • Shapes (packages/core/inbox-attachments.ts, new). The attachment record, the annotation record and the name rules. They are re-exported through the already published @plannotator/core/inbox-types, so core's export map does not change (the ui package smoke passes against core 0.25.12).

  • Store (packages/shared/inbox/store.ts, schema.ts):

    • an additive attachments field on messages;
    • a new line kind annotation in inbox/projects/<key>/annotations.jsonl, in the seq log, keyed by the file's path and version (current, or the sent blob's sha256; the Annotate drafts follow the file's path (0.29 PR 2) #1710 lesson);
    • a Send's feedback and annotation_ids: the text goes after the answers, and the annotations are marked sent (a crash before the reply line puts them back);
    • deleteThread and deleteProject: the JSONL files are rewritten (temp file, then rename), the store is read again from disk, and every blob no remaining record uses is removed;
    • inbox/seq.json, so a deleted seq is never handed out again;
    • the store size counts the blobs.
  • Routes (packages/server/inbox-attachments.ts, new). One block at the end of the route table in packages/server/inbox.ts:

    • GET /api/inbox/threads/:id/attachments;
    • GET /api/inbox/attachments/:id (raw bytes, always text/plain with CSP sandbox; ?version=sent for the blob);
    • GET /api/inbox/attachments/:id/view;
    • annotate's /api/html-assets/<token>/ route, through createHtmlAssetRegistry and resolveHtmlAssetRoute;
    • POST /api/inbox/annotations and POST /api/inbox/annotations/:id/remove;
    • POST /api/inbox/threads/:id/delete and POST /api/inbox/projects/:id/delete.

    Every writing route runs the same-origin guard and the serverSession guard. The reply route forwards feedback and annotation_ids.

  • Window (packages/inbox):

    • AttachmentTiles.tsx: the tiles at the foot of a message.
    • AttachmentPane.tsx: the pane beside the thread, with Full screen and Exit full screen; HTML opens full screen by default. Markdown uses Viewer, AnnotationToolstrip and the selection toolbar with CommentPopover. Diagrams use Viewer over diagramDocumentBlocks (DiagramViewer, lazy). HTML uses HtmlViewer with pinpoint and the HtmlSurfaceControls pen. AnnotationPanel sits at the right, grouped by file when the thread's other files carry comments. The pane shows the changed line, and full screen shows the strip.
    • AnnotationsChip.tsx: "N annotations" in the reply box beside "N picks", opening the list. Each row can be edited, removed, or opened at its place in the file.
    • attachments.ts: the feedback text, built with exportLinkedDocAnnotations.
    • Settings: Delete thread and Delete project now work, and the store line names the attached files.
    • App.tsx: the route carries file, v and at.
  • Docs. The AGENTS.md Inbox section has a new "Attachments" paragraph, eight new rows in the Server API table, and the proofs.

Record sections this makes real

The approved window record, .product/approved/plannotator-inbox-window-2026-10-07/approval.md, section 2.x, renders 2.1 to 2.8:

  • 2.1: the tiles at the foot, including the changed tile.
  • 2.2: the pane beside the thread, a selection with the comment popover, the changed line.
  • 2.3: Full screen with the strip.
  • 2.4: HTML full screen, a marker pinned on the button, the panel grouped by file.
  • 2.5: a diagram node comment.
  • 2.6, 2.7, 2.8: the chips in the reply box and the popover list.

Also the owner's decision Q2 ("versioned, let the user know it changed") and section 7.x (the store size, Delete thread, Delete project).

Proofs (each runs in CI under the existing path-gated inbox-e2e.yml)

  • tests/e2e/inbox-attachments.spec.ts, 10 tests, run with bun run test:e2e:inbox. Playwright Chromium drives the compiled binary under a temp PLANNOTATOR_DATA_DIR and HOME, and agents attach real files through plannotator inbox mcp with the SDK stdio client:

    • the tiles show;
    • the plan opens beside the thread with Full screen; two selection comments are saved per attachment and version, then listed and edited in the chip's popover;
    • the HTML prototype opens full screen: its relative image resolves (naturalWidth 88), its relative iframe shows the sibling page from its folder and not the Inbox (feat(review): add support for patch/diff files #1554), its script's fetch('/api/inbox/threads') is refused, and a marker is pinned on "Retry all (40)" (htmlAnchor is a button);
    • a Mermaid node comment carries a diagramAnchor node with its label;
    • the plan is edited on disk: the changed line appears, both comments stay anchored, "Open the version it sent" shows the original, and the raw ?version=sent equals the original bytes;
    • Send carries picks, words, then "Feedback on the attached files" with ## docs/retry-plan.md and ## proto/admin-view.html; wait_for_reply and read_thread return it;
    • refused: a path outside the project (absolute and ../), .env, a symlink to .env, a symlink retargeted to a directory (409), a target turned into a directory (409), and requests by query path, by encoded path and by traversal (404);
    • Delete thread removes the diagram's blob and the store size falls; Delete project removes the rest;
    • no page errors.

    PNGs, light and dark, are in .local/proof/attachments/ with a contact sheet.

  • packages/server/inbox-attachments.test.ts, 3 tests, through real fetch and the SDK client:

    • annotations ride a Send without picks, and once sent they are closed to edits;
    • cross-site and stale-tab requests to the new writing routes are refused;
    • a deleted thread's seq is never reused after a restart.
  • tests/e2e/inbox.spec.ts now expects Delete project to be enabled.

Gates run

  • bun run typecheck: green.
  • bun test packages apps/hook tests scripts under a temp data dir and HOME: 5784 pass, 1458 skip, 1 fail. The failure is apps/hook/server/annotate-cli.test.ts "annotate CLI on an occupied fixed port". It fails the same way on a clean origin/main checkout on this machine (stderr carries a second line), so it predates this PR and has nothing to do with the Inbox.
  • bun run --cwd packages/ui smoke:package: green, because core's export map is unchanged.
  • bun run check:release-version: green.
  • The build plus the compile with the release flags, then bun run test:e2e:inbox (both specs): 20 passed (10 window, 10 attachments).
  • No ui change, so no HANDOFF.md entry. No new DOM-gated test files.

Deviations from the record, with reasons

  1. The window CSP's base-uri changes from 'none' to 'self'. The srcdoc frame of an HTML attachment inherits the window's policy. 'none' blocked annotate's <base href>, so the page's frames resolved onto the Inbox: the first proof run caught the iframe at /detail.html. 'self' admits only this origin, so no other site's base is allowed.
  2. The asset route keeps annotate's Access-Control-Allow-Origin: *. The step 1 docs said "No CORS headers". The asset route is annotate's own path, reused as is: the page's own fetch('./data.json') comes from an opaque origin and needs that header. Its reach is the folder of an attached HTML file, behind an unguessable per-folder token and the Host allowlist. The Inbox's own routes still carry no CORS headers.
  3. The HTML pane has no Select/Pinpoint/Markup/Comment toolstrip (renders 2.4 and 2.5 draw one, on HTML and on the diagram). Plannotator's HTML and diagram surfaces are pinpoint-only and click-to-comment, and have no input-method strip (AGENTS.md "Annotation System"). The Inbox uses those components as they are. HTML gets Plannotator's own pen (HtmlSurfaceControls) in the pane header for Annotate/Interact.
  4. The markdown and diagram card shows Viewer's own "Global comment" and "Copy file" actions (not drawn). They are part of Plannotator's Viewer and no prop hides them without a ui seam. A global comment exports with the rest of the feedback.
  5. On the sent version, the line reads "The version sent at 10:42 AM. Open the file as it is now" (not drawn). The record draws only the way to the sent version, and a way back is needed. When the current file is gone or changed type, the line gives the reason and the sent version opens.
  6. Delete thread and Delete project ask once more ("Delete? Click again" for 4 seconds) (the record draws a single red link). A delete cannot be undone, and one stray click must not remove a project.
  7. When a file is open beside the thread, the thread's foot drops "Not sent". In the 440 px column it wrapped to two lines; 2.2's foot does not draw it either.
  8. The feedback text uses exportLinkedDocAnnotations, not exportAnnotations. It is the multi-file form, with the same entry renderer (renderAnnotationEntries), giving one section per file and version under "Feedback on the attached files".
  9. Comments take no images and offer no skill references. The Inbox has no upload route and no skill catalog, so the upload transport says so and the skill catalog is empty. Not drawn either way.

What it leaves out

  • An attached HTML page's external https: scripts, styles and images are blocked by the window CSP the srcdoc inherits. This is a follow-up decision if the owner wants CDN-styled prototypes to render.
  • Relative links between attached HTML documents are not followed. Links to other origins open in a new tab.
  • File choices drawn as previews, and folder attachments (PLAN leaves them out).
  • No live refresh of the changed line while a file stays open: it updates on reopening, on window focus, and on every thread event.
  • A retried send_message (same idempotency key) with new attachments answers the first message. Its new blobs stay until the next delete removes unused blobs.

Dependency

Rebased onto main after the window (1755) merged. No other lane's branch is needed.

@backnotprop
backnotprop force-pushed the feat/inbox-attachments branch from 9431c1b to 7615abf Compare October 7, 2026 15:30
send_message takes attachments by path: recorded by realpath inside the
project, annotate's file rules without a size bound, the sent bytes in a
content-addressed blob, served only by attachment id. The window shows the
tiles, opens a file beside the thread (HTML full screen) in Plannotator's
viewers with the annotation panel, says when a file changed since it was
sent and opens the version it sent; the annotations chip rides the reply
and Send carries them after the picks as Plannotator's feedback text.
Settings' Delete thread and Delete project remove records and unused blobs.
…CORS header documented, html-assets in the e2e paths
@backnotprop
backnotprop force-pushed the feat/inbox-attachments branch from 7615abf to 1d535b0 Compare October 7, 2026 15:51
@backnotprop
backnotprop merged commit 6dff12a into main Oct 7, 2026
backnotprop added a commit that referenced this pull request Oct 20, 2026
…HTML page (#2019)

The Inbox's attached-page viewer passed maxAdditionalTargets={0}, so the
bridge was armed with a cap of 0 and every shift-click was dropped. It has
been that way since attachments shipped (#1759); not a regression from #2000.
Annotate never set the cap and works. The viewer's own cap (16) now applies.

Co-authored-by: Claude <noreply@anthropic.com>
utpalsinghdev pushed a commit to utpalsinghdev/plannotator that referenced this pull request Oct 31, 2026
A Markdown file with 3 or more headings opened in the Inbox now shows the
headings list plan review shows. The Inbox built its document view from
@plannotator/ui's leaf parts in backnotprop#1759 and the Contents list is wired only
inside plan review's App, so it was never mounted there.

- ui: tocHeadings() is the one place the list's headings are picked
  (buildTocHierarchy and plan review's hasTocEntries use it);
  TableOfContents takes an optional scrollViewport (default: the context,
  plan review unchanged); DocumentContents binds the list and
  useActiveSection to one scroll element.
- Inbox: beside the thread, the top of the comments column above
  Annotations (the page keeps its width); full screen, a 240 px rail on the
  left with the page and toolstrip moved right of it; narrow, phone and the
  phone surface, a Contents button and a sheet. Hide is remembered.
- Tests: documentContents.test.tsx (DOM), inbox-contents.spec.ts (e2e).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant