feat(inbox): close an Inbox item without answering it

Merged
DA
6d ago
+14995-30

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 ConfirmDialog with 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 starts Inbox 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 in closedBy. The read_inbox tool 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.
  • Wire path. POST /api/inbox/{id}/close (new route file, rewrite, and the regenerated api-rewrites.json and vercel.json) → codingAgents.inboxClose act and gateway method → closeInbox on 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 closed status, with the new optional closedBy beside 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) and pnpm discarded-effect pass.
    • typecheck passes for features, service, host, panel, gateway, database, web and desktop.
    • pnpm --filter @luke/database check:migrations passes.
    • 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.ts and apps/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_inbox names 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-inboxCloseRequestSchema and coding-agent-view-codingAgentInboxCloseParamsSchema.
    • Every schema that embeds inboxItemSchema gains the optional closedBy member ref, and nothing else.
  • Not covered: I have not run this against a Preview on a Mac (./scripts/run.sh --preview) yet.

Invariants

  • ./scripts/check.sh green. (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 as type assertion outside the allowlisted files.
  • No Effect.runPromise/runSync/runFork outside the runtime edges.
  • No new setTimeout/setInterval/new Promise/AbortController/fs.watch outside 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.md sentence names a changed part.
  • PRIVACY.md unchanged.
  • Package deps unchanged.
  • Barrel/door rule unchanged.

Notes

  • The label is Close rather than "Archive", because the item's status is closed and 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


Devin Review