feat(inbox): close an Inbox item without answering it
Merged
DA
6d ago+14995+14995+01234567890123456789012345678901234567890123456789-30-30-01234567890123456789
Summary
The request was to add a way for the developer to archive or close an Inbox item without having to reply to it.
- Close in the item's pane. An open item now shows Close in its pane's toolbar, with the tooltip "Close without answering". Only the agent can reopen a closed item, so Close asks first, following the panel's Confirms rule: a
ConfirmDialogwith Cancel focused and Close in red. The question is keyed to the item. Confirming closes the item at the revision the developer was looking at, the same way an answer is pinned to a revision. If the agent changed the item since, the close is not taken and the card says so. The window marks the item closed straight away (inbox-settling.ts), the same way it shows an answer before the service confirms it. - Who closed it. A closed item now records the member who closed it in
inbox_item.closed_by(a new column; its migration is idempotent). The thread shows a comment from that member reading "Closed without answering", followed by either "Sent to " or "Saved for the agent to read". The list preview reads "You: Closed without answering". - What the agent hears.
- If the item asked to be heard back (
needsReply), the agent gets a message whose first line startsInbox closed on "<title>" (key …): Closed without an answer., so it isn't left waiting. - If the item didn't ask to be heard back, the agent sees the close the next time it calls
read_inbox, which now names the member inclosedBy. Theread_inboxtool description explains both cases. - A close resent under the same key is accepted only while the item is still that close at that revision. Taking a member's close back never reopens a close the agent made afterwards.
- If that message can't be delivered, the item opens again, so it never shows as closed while the agent was never told. This works the same way as a failed answer hand-off.
- When the agent posts under the item's key again, the item reopens, as it already does after the agent's own close.
- If the item asked to be heard back (
- Wire path.
POST /api/inbox/{id}/close(new route file, rewrite, and the regeneratedapi-rewrites.jsonandvercel.json) →codingAgents.inboxCloseact and gateway method →closeInboxon the client. The request body is{ revision, clientKey }. The reply has the same{ item, taken }shape as an answer's, and a retry sent with the same key is accepted once. - Older desktops. A member's close uses the existing
closedstatus, with the new optionalclosedBybeside it. An older desktop ignores the unknown key and still reads the inbox, but it will show such an item as "Withdrawn by the agent". The agent's own close always gives a reason, so a closed item with no reason is read as a member's close, by both the agent and the window, even after that member's account is deleted.
Evidence
pnpm check:affected: knip found an unused export, which is fixed. Typecheck was killed for lack of memory (exit 137) and one features schema test timed out under the sandbox's load. I then ran each step on its own:pnpm knip,pnpm lint(biome and oxlint) andpnpm discarded-effectpass.typecheckpasses for features, service, host, panel, gateway, database, web and desktop.pnpm --filter @luke/database check:migrationspasses.- Vitest passes on
packages/panel/src/desktop,packages/panel/src/tasks,packages/host/src,packages/service/src/agents/coding-agents-app.test.ts,packages/service/src/agents/coder-host,packages/service/src/inbox,packages/features/src/hosted-wire-schemas.test.tsandapps/web/tests/vercel-{services,functions}.test.ts.
- New tests:
- Store: a member's close is taken only at the revision they saw; a closed item takes no answer; the close can be withdrawn only under its own key; a re-post reopens the item.
- Route: an agent waiting on the item is told, once per key; an item that asks nothing back sends no message; a close at a stale revision is not taken; a failed hand-off reopens the item.
- Agent:
read_inboxnames who closed an item. - Wire: how the close message reads back to the developer.
- Host: the act router and the gateway pass the close through.
- Panel: the preview, and pressing Close in the pane.
- JSON Schema goldens (
LUKE_UPDATE_FIXTURES=1):- Two new:
inbox-wire-inboxCloseRequestSchemaandcoding-agent-view-codingAgentInboxCloseParamsSchema. - Every schema that embeds
inboxItemSchemagains the optionalclosedBymember ref, and nothing else.
- Two new:
- Not covered: I have not run this against a Preview on a Mac (
./scripts/run.sh --preview) yet.
Invariants
-
./scripts/check.shgreen. (CI) - JSON Schema goldens changed; the diff is explained above.
- Every behavior change has, in this PR, the test that fails without it; no change-detector test added.
- Gateway envelope goldens unchanged.
- No new
astype assertion outside the allowlisted files. - No
Effect.runPromise/runSync/runForkoutside the runtime edges. - No new
setTimeout/setInterval/new Promise/AbortController/fs.watchoutside the recorded files. - No Effect, Stream, or Layer written as a statement and dropped.
- Test count delta: +9 tests, plus review regressions folded into the store, route, UI and model tests (store 1, route 2, agent tool 1, wire 1, host 1, panel UI 1, panel model 1, crumbs test edited).
- No hand-rolled file replaced.
- No
CLAUDE.md/AGENTS.mdsentence names a changed part. -
PRIVACY.mdunchanged. - Package deps unchanged.
- Barrel/door rule unchanged.
Notes
- The label is Close rather than "Archive", because the item's status is
closedand in Luke "archive" already means putting a whole task or agent away. - Not in this PR: a Close in the row's right-click menu or a keyboard shortcut. The press lives in the open item's pane.
Made with Luke