feat(web,desktop): an inbox item points at exact lines of the agent's repository, read at the commit it named
Stacks on #2218 (blocks and a response). Sibling of the image-blocks PR: whichever merges second rebases, bumps INBOX_WIRE_VERSION again, and re-records goldens.
Summary
A coding agent can now point the developer at exact lines of its repository in an inbox item. The new code block is drawn on the card the way the Code tab draws a card, and opens the side panel's File tab at those lines.
- Wire (
@sidecar/hosted/inbox-wire): a newinboxBlockSchemamember{ type: "code", repo, path, startLine, endLine, sha }.repoisowner/name,pathis bounded like a code ref, the lines are counted from 1, andshais a whole 40-character lowercase commit.INBOX_BOUNDS.MAX_CODE_LINESis 60 andINBOX_WIRE_VERSIONis now 3. An older desktop reads the block as an unknown type and draws "Open this in a newer Luke…", as #2218 set up. The item stores no file contents, only the reference. - Validation on post (
coder-host/inbox.ts):postRefusalnow checks blocks before the response.- A code block must name the agent's own repository, compared case-insensitively against the agent row's
repository. Otherwise:CODE_REPOSITORY. - Its lines must run forward and span at most 60. Otherwise:
CODE_LINES. - A sha that isn't 40 lowercase hex fails the schema with
UNREADABLE_POST, which now names the code block and the sha's shape.
- A code block must name the agent's own repository, compared case-insensitively against the agent row's
- Reading the lines: the existing File tab path, extended with a pinned commit (agreed with the orchestrator).
planFileRequestSchemagains an optionalref(commitShaSchema: exactly 40 lowercase hex, so no branch name or path text reaches GitHub's?ref=). The chain is:act(PLANNING_FILE)→ mainhost-operator.ts(which forwarded fields one by one and now carriesref) → hostcompose-planning→plan-client(planFilePathaddsref=) →GET /api/plans/{id}/file?ref=→hosted/plan-file.ts.- The service reads at the ref with the same plan-ownership check, the same
repositoryReadTokenreach of the plan's own repository, and the sameshow_coderule. The answer'sbranch(and a missing file's) names the commit. - A
refthat is malformed or given twice is refusedinvalid-requestbefore GitHub is asked. - Nothing is stored.
- Desktop card (
inbox-board.tsx):- The card reuses the Code tab's own pieces. The card header is now an exported
CodeCardHeaderincode-pane.tsx(path, range, copy, open), and the lines come fromCodeLines/CodeLineRowswith the.code-cardstyles and the host's colouring. - It reads the block's lines plus 3 on each side at the commit, with the block's own lines lit.
- A refusal uses the File tab's words and its
.file-tab-noticestyle. GitHub being unavailable offers Retry. A file that ends inside the lines says so rather than drawing part of the range. A missing file at a commit says "This file is not at commit abc1234 on GitHub, or the commit was never pushed." - Lines in a repository the plan no longer reads (the plan's repository changed) are not read and open nothing.
- Path and Open file open the side panel's File tab at those lines at that commit.
OpenFilegainscommit, and such a tab is drawn on the open plan though Luke never showed the file.FileViewreads, copies, and links GitHub at the commit, and keys the tab by commit so lines of two commits are never joined. - The block is
ph-no-capture, like the Code tab.
- The card reuses the Code tab's own pieces. The card header is now an exported
- Transcript row for
post_to_inboxlists a code block aspath:start-end at abc1234.
What agents are told (changed)
post_to_inboxdescription now has: "…and code blocks pointing at the exact lines a decision is about: repo is your repository as owner/name, path is from its root, startLine to endLine spans at most 60 lines, and sha is the full 40-character commit you read them at, one already on GitHub; they see those lines as GitHub holds them at that commit."- Instructions, "The developer's inbox": the blocks clause now reads "(short Markdown, a link to the page a task is done on, and the exact lines of your repository a decision is about, at a commit already on GitHub)".
- Refusal words:
UNREADABLE_POSTnow names the code block and the sha's shape.CODE_REPOSITORYandCODE_LINESare new.
JSON Schema goldens (re-recorded with LUKE_UPDATE_FIXTURES=1)
inbox-wire-inboxPostInputSchema(in every coding agent's prompt cache) gains the one code member:typeenumcode,repomaxLength 140,path1–1024,startLine/endLineinteger ≥ 1,sha40–40, all required,additionalProperties: false. That is +42 lines pretty-printed, with no descriptions; the words stay in the tool description.inbox-wire-inboxBlockSchema,inboxBlocksSchema,inboxReadBlockSchema,inboxItemSchema,inboxAnswerSchema,inboxAnsweredSchema,coding-agent-view-codingAgentInboxAnswerViewSchema, and…AnsweredViewSchemagain the same member where they hold blocks.plan-wire-planFileRequestSchemagains optionalref(string, 40–40).- New:
plan-wire-codeLineSchemaandplan-wire-commitShaSchema, now exported fromplan-wirefor the code block, so the golden suite records them. - Note that the wire's emitter writes no
pattern, so the model sees the sha's length. The lowercase-hex rule is enforced on parse and stated in the tool description.
PRIVACY.md (changed: a product decision)
- "A plan's inbox": an item's context may include "references to lines of the agent's repository (a path, the lines, and the commit it read them at, never the lines themselves)".
- "A whole file opened from a snippet": an inbox reference is read the same way, at the named commit, both on the card and in the File tab it opens. Nothing of the file is stored.
Bundle budget (renderer.js re-recorded)
apps/desktop/bundle-budget.json's renderer/renderer.js baseline moves from 733,811 to 771,155 gzipped bytes; the other bundles are untouched. The merge queue refused this PR on the ceiling: 771,155 against 770,501.
main(50d559af) alone measured 770,140, so it sat 361 bytes under the ceiling after about 36 KB of growth since #2080.- This PR adds about 1 KB (1,015 gzipped bytes, 3.4 KB minified), all its own code: the code card, its read model, and the File tab's commit handling.
- No library was adopted.
The orchestrator approved the re-record. There is precedent: the original inbox PR in this rollout, and #2080 and #1962 moved it for features. The image-blocks PR (#2238) drops its own re-record if this baseline covers it.
Test fixed in #2218's suite
tests/inbox-migration.test.ts asserted that migrated rows carry the current INBOX_WIRE_VERSION. Migration 0088 stamps them with a literal 2, the shape it converts to, so the bump to 3 broke that assertion. The test now names BLOCKS_VERSION = 2, which was always what it meant.
Evidence
- Platform-independent checks:
./scripts/check.shgreen on this head. - macOS Electron verification (
./scripts/verify.sh): not run. This is a cloud Linux workspace with no Mac, and CI cannot verify the UI: a pull request builds nothing for the Mac.
Visual evidence
CI could not verify this UI. These are throwaway renders made in the workspace, not verify.sh fixtures:
- The page renders the real
InboxBoardwith the real renderer stylesheet (styles/index.cssthrough the Tailwind plugin), and lines coloured by the host's ownhighlightFile. - They were captured with headless Chrome on Linux, so the fonts are not SF.
- The first card's code block is read at a commit, with lines 11–14 lit and three around them. The second is at a commit GitHub lacks.


- Fixture screenshots inspected: the two renders above.
Physical-device evidence
- Screenshot or screen recording:
not attached - Device/display configuration:
not recorded
Invariants
-
./scripts/check.shgreen../scripts/verify.shnot run (no Mac; see above). - JSON Schema goldens:
LUKE_UPDATE_FIXTURES=1run; the diff is explained above. - Every behavior change has, in this PR, the test that fails without it; no change-detector test added.
- Web: post refusals, and a code block posted and read back (
coder-inbox.test.ts); a pinned read asking GitHub for that sha and naming it, and bad or repeated refs refused before GitHub (plans-app.test.ts). - Client:
refin the path, and a branch name refused (plan-client.test.ts). - Main:
refforwarded to the host (host-operator.test.ts). - Desktop:
- the card's window, lit lines, header, Open, and the refusal, retry, elsewhere, and unpushed cases (
inbox-board.test.ts); - window clamping, a file ending early, and a backwards range (
inbox-model.test.ts); - a commit-opened tab drawn on the plan and dropped back to head by a snippet (
use-side-panel.test.ts); - reads and GitHub link at the commit, a fresh read at another commit with no lines kept, and the missing-at-commit words (
file-view.test.ts); - the transcript row (
tool-call-model.test.ts).
- the card's window, lit lines, header, Open, and the refusal, retry, elsewhere, and unpushed cases (
- Web: post refusals, and a code block posted and read back (
- Gateway envelope goldens unchanged.
- No new
astype assertion outside the allowlisted files. - No
Effect.runPromise/runSync/runForkoutside the runtime edges and recorded shims. - No new raw async primitives outside the recorded files.
- No Effect, Stream, or Layer written as a statement and dropped.
- Test count delta: +9 (7 new tests, plus 2 new goldens:
codeLineSchema,commitShaSchema); 3 existing tests extended and the migration test fixed as described above. - No hand-rolled file replaced.
- Bundle budget:
renderer.jsbaseline re-recorded (LUKE_UPDATE_BUNDLE_BUDGET=1, that bundle only), as explained above. - No
CLAUDE.md/AGENTS.mdsentence names a changed part; root pair untouched.apps/web/server/README.md(file route) anddocs/PLANNING.md(File tab) now mention the commit read. -
PRIVACY.mdchanged, as called out above. - Package deps match imports;
effectviacatalog:. - Barrel/door rule kept: the renderer reaches
@sidecar/hosted/inbox-wireandplan-wire(Schema vocabulary) as it already did.
Notes
These are deliberately left out:
- No check at post time that the sha exists on GitHub. That would cost a GitHub call per post. The description tells the agent to use a pushed commit, and the card says plainly when it isn't one.
- The File tab's tree and breadcrumbs still list the default branch's head, so browsing from a commit-pinned tab opens the next file at head. A File tab pinned to the sidebar comes back at head after a relaunch, since pins store the path alone.
- A card reads its lines each time it mounts, for example on switching back to the Inbox tab. The lines are fixed at a sha, so a small cache could follow if the reads matter.
🤖 Generated with Claude Code