feat(panel): inbox Markdown draws pictures, pairs, and lone-link cards
Step 1 of 7 of the inbox-threads rollout. The Markdown an Inbox item draws can now hold everything a message body needs, and the Inbox's Markdown is drawn through it. Only the panel and features change.
What changed
An inbox picture address (@luke/features/inbox-attachment-link). A picture in an item's Markdown is written . The address is the item's own Luke link with /files/<attachmentId> after it:
https://tryluke.dev/<ws>/plan/<task>/inbox/<item>/files/<attachment>- or
/agent/<agent>/inbox/<item>/files/<attachment>for an agent with no task
This follows how plan-attachment-link.ts places a plan's picture under its plan. inboxAttachmentAddress writes the address and stays exported for step 4, the store's rewrite of sandbox paths. inboxAttachmentOf reads one back, and inboxAttachmentIdsIn finds them in text. luke-link reads the address as the item it belongs to, so opening it lands on the item. I didn't use a holder-free /<ws>/inbox/<item>/… form because luke-link has no place for an item without its task or agent. The orchestrator agreed.
Pictures in the Inbox's Markdown (inbox-pictures.tsx). InboxPictures is a provider modelled on PlanPictures. PictureElement draws the item's own pictures through it, with their captions, using the Inbox's existing reads (images / onImage, the CODING_AGENTS_INBOX_ATTACHMENT act). Pressing a picture opens it in AttachmentViewer. A picture of another item stays a link and is never fetched. The image block now draws through the same InboxPicture, moved out of inbox-pane.tsx. use-task-inbox.ts now also counts the item's own Markdown pictures (blocks, messages, the answer's note) as named, so their bytes are kept once the pane's own read lands.
Opt-in document drawing (TranscriptWords / SentWords asDocument):
- A paragraph holding only pictures becomes a row, so a before and an after stand side by side.
- A link alone in a top-level paragraph becomes a one-line card (
lone-link-card.tsx; the parsing is@luke/features/lone-link):- a pull request: a one-row card like the one the agent's tab pins over its box. It shows the PR's mark, number and title, state, and +/− lines, using what
useLinkStandingalready reads. Until GitHub answers it shows the link's own words. - a Luke page: a row with the page's glyph and name. Hovering it shows the page's own card from
page-card.tsxwhere the window holds the page. - a
blob/<sha>/<path>#La-Lblink: the Code tab's card, throughInboxCodeCard. That isinbox-pane.tsx'sCodeBlock, moved intoinbox-code-card.tsxand shared rather than copied. - Any other lone link, and every link inside a sentence, stays its chip.
- a pull request: a one-row card like the one the agent's tab pins over its box. It shows the PR's mark, number and title, state, and +/− lines, using what
In the Inbox: Markdown blocks, thread messages (the agent's and members'), and the answer's note draw with asDocument, inside InboxPictures and LoneLinkCards. Everything sits inside the pane's existing ph-no-capture region. Blocks, responses, and the layout are otherwise unchanged.
A bug this work found
Streamdown keeps one processor per plugin list and keys it by the plugins' function names. Two unnamed () => mark… steps therefore share a processor. The transcript's step is now a named function, markLinksStep, beside markDocumentStep. Without that, words drawn as a document and as a transcript would draw each other's marks. The last test in transcript-document.test.ts catches it, because it runs after a document render.
Screenshot
The Inbox with a fixture item whose Markdown holds a before/after pair and a lone PR link. It was rendered from the panel's own DesktopInbox with the test fakes, and that harness is not committed.

Tests
features/inbox-attachment-link.test.ts: write, read back, open as the item, find in text.features/lone-link.test.ts: which links earn a card.panel/tasks/transcript-document.test.ts: cards vs chips, picture rows, asking for and opening pictures, another item's picture, and transcript mode unchanged.panel/desktop/desktop-inbox.test.ts: an item's Markdown pictures are read and kept once its own read lands, the pair stands in one row, and a lone PR is a card inside the masked region. This one fails without theuse-task-inbox.tschange.
No golden changed: the transcript and the plan draw exactly as before.
pnpm check:affected passed: lint, knip, discarded-effect, typecheck, and every affected test. The sandbox has 8 GB, so the parallel run ran out of memory; I ran its parts one at a time instead, @luke/web's typecheck with a larger heap. app-menu.test.ts needed the Electron binary fetched first.
Left for later steps
The wire (2), the tables (3), the store and its rewrite of sandbox paths (4), answering (5), the agent's tools (6), and the thread layout (7). Issue links stay chips here, as the brief scoped; the plan's issue card can come with step 7.
Made with Luke