feat(desktop,host,features): an Inbox thread draws its messages, and every item keeps its box
Merged
DA
11d ago+1157+1157+0123456789012345678901234567890123456789-126-126-012345678901234567890123456789
Summary
This is the desktop half of "a message on an Inbox thread at any point". It stacks on #3469 (the thread's wire and service).
- The thread draws its messages. Messages show as
Comments, mixed in by time with the question (atpostedAt) and the answer or withdrawal (atsettledAt, falling back toupdatedAton an older service). A message written while the item was open therefore shows before the answer that came after it.- A member's message shows their face and name (
personActor), and "You" for your own. A member whose account is gone shows as someone unnamed. - The agent's message shows its tile and name.
- Words go through
SentWords, so mentions show as chips. Files show the way an answer's do. A message with files and no words shows the files alone. - When the thread holds
MAX_MESSAGES_READ(50) messages, a line says earlier messages are in the agent's tab. Pressing the agent's name opens that tab. - The answer's and withdrawal's times now read
settledAt, so a later message no longer moves them.
- A member's message shows their face and name (
- The box is on every item.
- While the item is open, nothing changes: presses, choice, form, and the quiet line work as before, and a free note is still the answer.
- Once the item is settled, the same composer posts a message: placeholder "Message …", Send or Enter, files by picker, paste, or drop, up to 8,000 characters.
InboxNextUpnow sits above the box.
- Posting. This adds a new
codingAgents.inboxMessageact: anACT_KINDentry with its payload schema (codingAgentInboxMessageParamsSchema) and result guard, a router row, a gateway method, aHostOperatormethod, and a composer row that callsHostedCodingAgentClient.messageInbox.- The returned item replaces the one held, and the item keeps its status.
- Your own new comment scrolls into view inside the board's own scroller, not with
scrollIntoView. - The draft is kept per item. A settled item now keeps its draft until the item changes revision.
- A refusal shows under the card.
- The send key is reused only for a retry of the same words and is dropped once the message lands, because #3469 now refuses a reused key.
- The agents are read again afterwards, since a message may wake a stopped agent.
reply_inboxrow. The agent'sreply_inboxcall (#3482) gets its own collapsed row,CALL_KIND.INBOX_REPLY("Replied in your inbox:"). The row names the item's key and lists the reply's words and image paths. The tool name is matched as a string, so this PR doesn't depend on #3482.
Screenshots


Both were rendered from the jsdom fixtures' DOM with the built styles.css and captured with Playwright.
Evidence
- Platform-independent checks:
- The affected vitest runs pass:
desktop-inbox,composer-drafts,tool-call-model,inbox-model, all ofhost,features, andgateway. tscpasses for desktop, host, and service. Biome and oxlint are clean.- A whole
check:affectedrun had its@luke/service#typecheckOOM-killed (exit 137) in the sandbox. Run alone, that typecheck passes.
- The affected vitest runs pass:
- New tests in
desktop-inbox.test.ts:- Member and agent messages drawn in order with their faces, including one written while the item was open, which draws before the answer.
- A message on a settled item: the act's payload, no answer sent, the item replaced, the status still "Answered", the box emptied, and the board scrolled.
- A refusal.
- Files alone, both sent and drawn.
- An open item still answers and sends no message.
- The 50-message cut line, which opens the agent's tab.
- Other new tests:
- A router row for the act.
- The composer relaying the message.
- The
reply_inboxrow.
- One existing assertion changed: "a settled item takes no answer" now checks that the box asks to message the agent.
Invariants
-
./scripts/check.shgreen. (CI) - JSON Schema goldens unchanged (
LUKE_UPDATE_FIXTURESnot run, or diff explained line by line). (CI) One golden was added:coding-agent-view-codingAgentInboxMessageParamsSchema.json, for the new act's params, which areitemIdplusinboxMessageRequestSchema's fields. - Every behavior change has, in this PR, the test that fails without it; no change-detector test added. (manual; root
AGENTS.md"Testing") - Gateway envelope goldens unchanged. (CI)
- No new
astype assertion outside the allowlisted files. (CI via anti-slop) - No
Effect.runPromise/runSync/runForkoutside the runtime edges and the shimstools/anti-slop/effect-edges.jsonrecords. (CI viaanti-slop/no-run-promise-outside-edges) - No new
setTimeout/setInterval/new Promise/AbortController/fs.watchoutside the filestools/anti-slop/effect-edges.jsonrecords. (CI viaanti-slop/no-raw-async-primitives) - No Effect, Stream, or Layer written as a statement and dropped; a description a caller walks past runs nothing. (CI via
pnpm discarded-effect) - Test count equals the pre-PR count or the delta is listed. (+6 in
desktop-inbox.test.ts, +1 incoding-agent-acts.test.ts, +1 intool-call-model.test.ts) - Each hand-rolled file the PR replaces is deleted, or the part that remains is a permanent adaptor named in root
AGENTS.md's "The permanent adaptors" with the reason it stays. - Every
CLAUDE.md/AGENTS.mdsentence naming a changed part is edited; root pair stays byte-identical. (CI for pair existence; manual for wording) -
PRIVACY.mdunchanged, or the change is a product decision called out in the PR body. - Package
package.jsondeps match what the sources import by bare specifier;effectviacatalog:. (CI) - Barrel/door rule: no Node-reaching Effect module (
@effect/platform-node,effect/sql) behind a barrel the renderer or a web function opens. (CI: existing renderernode:grep plus a new grep for those specifiers in renderer bundles)
Notes
- Blockers or follow-up verification: when #3482 lands,
REPLY_INBOX_TOOLintool-call-model.tscan readINBOX_TOOL.REPLYinstead.
Made with Luke
Summary by cubic
Lets an Inbox thread draw its messages and keeps the message box docked on every item, so a settled item can still be discussed without changing its status.
New Features
- The thread now interleaves member and agent messages with the question (at
postedAt) and the answer or withdrawal (atsettledAt, falling back toupdatedAton older services), so a message written while the item was open shows before the answer that followed it; the answer's and withdrawal's times no longer move when a later message lands. - When a thread read holds 50 messages, a line says the earlier ones are in the agent's tab, and pressing the agent's name opens that tab.
- Once an item is settled, the same composer writes a message instead of an answer: "Message
<agent>…", Send or Enter, files by picker, paste, or drop, up to 8,000 characters, while the next item that needs you stays offered above the box. - Adds a
codingAgents.inboxMessageact and matching gateway method and host operator; a refusal is shown under the card, and the send key is reused only for a retry of the same words, because the service now refuses a reused key. - An agent's
reply_inboxcall draws as its own collapsed row naming the item's key and listing the reply's words and image paths. - Drafts now know whether they are an answer or a thread's message: an answer left unsent on an item a teammate settled in the meanwhile is let go rather than sent as a message, and a message being written on a settled item outlasts the reads after it.