feat(web,desktop): an inbox item points at exact lines of the agent's repository, read at the commit it named

Merged
DA
7d ago
+1442-168

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 new inboxBlockSchema member { type: "code", repo, path, startLine, endLine, sha }. repo is owner/name, path is bounded like a code ref, the lines are counted from 1, and sha is a whole 40-character lowercase commit. INBOX_BOUNDS.MAX_CODE_LINES is 60 and INBOX_WIRE_VERSION is 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): postRefusal now 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.
  • Reading the lines: the existing File tab path, extended with a pinned commit (agreed with the orchestrator). planFileRequestSchema gains an optional ref (commitShaSchema: exactly 40 lowercase hex, so no branch name or path text reaches GitHub's ?ref=). The chain is:
    • act(PLANNING_FILE) → main host-operator.ts (which forwarded fields one by one and now carries ref) → host compose-planning → plan-client (planFilePath adds ref=) → GET /api/plans/{id}/file?ref= → hosted/plan-file.ts.
    • The service reads at the ref with the same plan-ownership check, the same repositoryReadToken reach of the plan's own repository, and the same show_code rule. The answer's branch (and a missing file's) names the commit.
    • A ref that is malformed or given twice is refused invalid-request before 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 CodeCardHeader in code-pane.tsx (path, range, copy, open), and the lines come from CodeLines/CodeLineRows with the .code-card styles 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-notice style. 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. OpenFile gains commit, and such a tab is drawn on the open plan though Luke never showed the file. FileView reads, 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.
  • Transcript row for post_to_inbox lists a code block as path:start-end at abc1234.

What agents are told (changed)

  • post_to_inbox description 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_POST now names the code block and the sha's shape. CODE_REPOSITORY and CODE_LINES are 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: type enum code, repo maxLength 140, path 1–1024, startLine/endLine integer ≥ 1, sha 40–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 …AnsweredViewSchema gain the same member where they hold blocks.
  • plan-wire-planFileRequestSchema gains optional ref (string, 40–40).
  • New: plan-wire-codeLineSchema and plan-wire-commitShaSchema, now exported from plan-wire for 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.sh green 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 InboxBoard with the real renderer stylesheet (styles/index.css through the Tailwind plugin), and lines coloured by the host's own highlightFile.
  • 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.

Inbox code block, light

Inbox code block, dark

  • Fixture screenshots inspected: the two renders above.

Physical-device evidence

  • Screenshot or screen recording: not attached
  • Device/display configuration: not recorded

Invariants

  • ./scripts/check.sh green. ./scripts/verify.sh not run (no Mac; see above).
  • JSON Schema goldens: LUKE_UPDATE_FIXTURES=1 run; 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: ref in the path, and a branch name refused (plan-client.test.ts).
    • Main: ref forwarded 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).
  • Gateway envelope goldens unchanged.
  • No new as type assertion outside the allowlisted files.
  • No Effect.runPromise/runSync/runFork outside 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.js baseline re-recorded (LUKE_UPDATE_BUNDLE_BUDGET=1, that bundle only), as explained above.
  • No CLAUDE.md/AGENTS.md sentence names a changed part; root pair untouched. apps/web/server/README.md (file route) and docs/PLANNING.md (File tab) now mention the commit read.
  • PRIVACY.md changed, as called out above.
  • Package deps match imports; effect via catalog:.
  • Barrel/door rule kept: the renderer reaches @sidecar/hosted/inbox-wire and plan-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