Repository navigation
Plannotator Inbox step 2: attachments, versioned, annotated in the Inbox - #1759
Merged
Merged
Conversation
backnotprop
force-pushed
the
feat/inbox-attachments
branch
from
October 7, 2026 15:30
9431c1b to
7615abf
Compare
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
force-pushed
the
feat/inbox-attachments
branch
from
October 7, 2026 15:51
7615abf to
1d535b0
Compare
1 task
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_replyandread_threadreturn 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_messagetakesattachments: [path]: absolute, or relative toproject_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.envis 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 answers409 attachment_changed_type(or404 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):attachmentsfield on messages;annotationininbox/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);feedbackandannotation_ids: the text goes after the answers, and the annotations are marked sent (a crash before the reply line puts them back);deleteThreadanddeleteProject: 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;Routes (
packages/server/inbox-attachments.ts, new). One block at the end of the route table inpackages/server/inbox.ts:GET /api/inbox/threads/:id/attachments;GET /api/inbox/attachments/:id(raw bytes, alwaystext/plainwith CSPsandbox;?version=sentfor the blob);GET /api/inbox/attachments/:id/view;/api/html-assets/<token>/route, throughcreateHtmlAssetRegistryandresolveHtmlAssetRoute;POST /api/inbox/annotationsandPOST /api/inbox/annotations/:id/remove;POST /api/inbox/threads/:id/deleteandPOST /api/inbox/projects/:id/delete.Every writing route runs the same-origin guard and the
serverSessionguard. The reply route forwardsfeedbackandannotation_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 usesViewer,AnnotationToolstripand the selection toolbar withCommentPopover. Diagrams useVieweroverdiagramDocumentBlocks(DiagramViewer, lazy). HTML usesHtmlViewerwith pinpoint and theHtmlSurfaceControlspen.AnnotationPanelsits 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 withexportLinkedDocAnnotations.App.tsx: the route carriesfile,vandat.Docs. The
AGENTS.mdInbox 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: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 withbun run test:e2e:inbox. Playwright Chromium drives the compiled binary under a tempPLANNOTATOR_DATA_DIRand HOME, and agents attach real files throughplannotator inbox mcpwith the SDK stdio client:fetch('/api/inbox/threads')is refused, and a marker is pinned on "Retry all (40)" (htmlAnchoris a button);diagramAnchornode with its label;?version=sentequals the original bytes;## docs/retry-plan.mdand## proto/admin-view.html;wait_for_replyandread_threadreturn it;../),.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);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:tests/e2e/inbox.spec.tsnow expects Delete project to be enabled.Gates run
bun run typecheck: green.bun test packages apps/hook tests scriptsunder a temp data dir and HOME: 5784 pass, 1458 skip, 1 fail. The failure isapps/hook/server/annotate-cli.test.ts"annotate CLI on an occupied fixed port". It fails the same way on a cleanorigin/maincheckout 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.bun run test:e2e:inbox(both specs): 20 passed (10 window, 10 attachments).HANDOFF.mdentry. No new DOM-gated test files.Deviations from the record, with reasons
base-urichanges 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.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 ownfetch('./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.HtmlSurfaceControls) in the pane header for Annotate/Interact.exportLinkedDocAnnotations, notexportAnnotations. 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".What it leaves out
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.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.