feat(web,desktop): coding agents post what they need of the developer to the plan's Inbox
Summary
Coding agents often need a decision or a manual step from the developer, and today that ask gets lost in the transcript. This adds a plan Inbox: agents post items to it, the developer answers them in a tab of their own, and the answer goes back to the agent.
-
Items are rows of their own on the service (
inbox_item, migration0082), not transcript parts, so an agent can update or withdraw them. Each row is one item per agent and key. -
The coder gets three tools:
post_to_inboxcreates an item, or replaces the one under the same key and reopens it.close_inbox_itemtakes an item back, with a reason.read_inboxreads the agent's items.
Every call returns the agent's whole inbox, answers included, so the model keeps track of its items even after its context is compacted. Workers don't get these tools.
-
Four kinds of item:
- choice: options, one may be marked recommended, or the developer answers in their own words;
- approval: Approve or Decline;
- question: free text;
- task: something the developer does by hand, such as a GitHub setting, answered Done or Can't do this.
Each item also says whether the agent needs to hear back. A task that doesn't is answered without messaging the agent; the agent sees the answer the next time it reads its inbox.
-
Instructions: the agent is told to put what it needs from the developer in the inbox rather than a message, never to repost an item as a reminder, and never to restate open items in messages. The final-message instruction no longer asks it to list things the developer must decide.
-
Routes:
GET /api/plans/{id}/inboxis a long-poll held open while nothing changes, like the transcript read. It returns every open item, plus the latest settled ones up to a cap. Its cursor is a digest of where every item stands, taken in id order, so no change is missed and the cursor stays put while nothing changes. Each poll inside the hold reads only that status projection; full rows are read once something moves. A plan with no agents holds no read open.POST /api/inbox/{id}answers an item. The answer names the revision the developer saw, and is recorded first, only if that revision still stands, so an answer to an item the agent changed meanwhile never reaches the agent. Only then, if the agent asked to hear back, is it handed over as one of the developer's messages, through the same steer path the composer uses, under the answer's own key, so a retry is taken once. If the hand-off fails, is interrupted or dies, the recorded answer is undone, so an item never reads answered unless the agent was told. A retry under the same key hands it off again, and the steer's own key makes it reach the agent once. An item whose agent's session has ended for good keeps the answer instead of waiting forever.
-
Desktop:
- Two calls added through every layer (client, gateway, host, main process, act kinds).
- An Inbox tab after the plan's tab, shown once the plan has an agent and never closed. The count follows the name, dimmed like a model's effort ("Inbox 3").
- The tab is built from the transcript's own pieces:
- each waiting item is a card the shape of the transcript's (the plan card's border, ground and type);
- its answer box is the agent composer's, with its round Send;
- an approval's or task's two buttons are the confirm dialog's pair, in that box's footer;
- settled items fold under one line, as a finished turn's steps fold, each a closed plan card;
- the empty, loading and failed states are the transcript's;
- the tab is laid out at the agent tab's width by the same CSS rules;
- there's no heading, since the tab's own label names it and counts what waits.
- The only bespoke control is a choice's option row. It's styled after Settings' segmented buttons, because no existing pick-one control has a line of detail.
- In an agent's transcript, inbox calls show as rows such as "Posted to your inbox: ā¦".
-
Privacy:
PRIVACY.mdgains a paragraph on what the inbox stores and where an answer goes (a separate commit). This is a product decision to review.
Evidence
- Platform-independent checks:
./scripts/check.shpasses on the head commit: knip, lint, discarded-effect, typecheck, all four test shards, and the build, including the renderer bundle budget. The renderer is 742,781 gzipped bytes against main's re-recorded baseline of 733,811 plus 5%, so the baseline is unchanged. - macOS Electron verification (
./scripts/verify.sh): not run. This was built in a Linux cloud workspace. CI cannot verify this UI; a Mac break would be caught at the release rehearsal on a tag, not at review. ./scripts/run.sh --previewagainst this PR's Vercel Preview: not run (it needs a Mac).- Preview shape: passed against the Vercel preview of
3f03916f(the head before the rebase onto main). Every client path was answered by its handler, including/api/plans/{id}/inboxand/api/inbox/{id}. After the rebase, the migration had to be renumbered0082. The PR's preview database had already recorded it under the old number, so I reset that preview database (only that branch) and the redeploy built and migrated cleanly.
Visual evidence
These are not verify.sh captures. The real renderer components and stylesheet were drawn in headless Chrome over mock data. I also put a clickable demo of the plan column over a mock agent on Dean's Mac.
- Fixture screenshots inspected: below.
The plan column with the Inbox tab chosen (the panel at bottom right is the demo's mock agent, not product UI):

Settled items fold under one line, each a closed plan card:

Physical-device evidence
- Screenshot or screen recording:
not attached - Device/display configuration:
not recorded
Invariants
-
./scripts/check.shgreen../scripts/verify.shwas not run (no Mac; see above). - JSON Schema goldens: no existing golden changed. New goldens were recorded for
inbox-wire(11 schemas) and the four newcoding-agent-viewinbox schemas. - Every behavior change has, in this PR, a test that fails without it; no change-detector tests.
- Gateway envelope goldens unchanged.
- No new
asassertion outside the allowlisted files. The three tool files use the sameSAFETY-commentedas WireBoundaryInputasedit_plan.ts, and anti-slop passes. - No
Effect.runPromise/runSync/runForkoutside the runtime edges. The tools run throughcoder/host.ts's existingrunWebedge. - No new raw async primitives outside the recorded files. The long-polls are awaited loops, like
use-agent-transcript.ts. - No Effect, Stream, or Layer dropped as a statement.
- Test count delta, +46 tests, none removed:
coder-inbox7,coding-agents-app10,coder-agent2 (web);coding-agent-client4,service-paths1 (hosted);inbox-board8,inbox-model4,use-main-tabs1,desktop-plans1,tool-call-model1 (renderer);host-operator1,coding-agent-acts1 (main);- plus new compose cases and JSON Schema golden cases.
- No hand-rolled file replaced.
- No
CLAUDE.md/AGENTS.mdsentence names a changed part. -
PRIVACY.mdchanged: a product decision, called out above. - Package deps match imports.
@sidecar/hostedgains the./inbox-wiresubpath export. - Barrel/door rule: no Node-reaching module behind a barrel the renderer opens. The renderer imports
@sidecar/hosted/inbox-wire, which is Schema only.
Notes
- The migration runs on deploy (
pnpm db:migrate). It only adds a table. - Follow-ups:
- Nothing notifies the developer when an item lands while the app isn't showing that plan, beyond the tab's count. A notification from the main process, like the existing agent-finished one, is the natural next step.
- The inbox is per plan; a board across all plans would need a second read route.
- The offline coder eval doesn't cover the inbox tools yet.
š¤ Generated with Claude Code