Agent pane: keep a sent prompt's images; bigger, clearer composer thumbnails

Merged
TE
22d ago
+705-74

Leo's dogfood bug (16): images attached to a prompt vanished once it was sent. acpmux's user_message carried only text, so the sent bubble had no images, and Edit or Retry after a reload sent the prompt without them.

Captures

Real cmux NEXT DEV builds on a fleet capture mini (base = feat-cmux-next 8d6b1526, head = 92aca21f), mock chat, three dark 1200×800 terminal screenshots pasted. Left is base, right is this PR. The machine name is masked.

composer thumbnails dark base vs head
Composer, dark: base crops each screenshot to a small square with a faint edge; head shows 80 px chips in the image's own shape with a visible 1 px edge.

composer thumbnails light base vs head
Composer, light: same comparison; the edge still separates the dark images from the light composer.

sent bubble dark base vs head
Sent prompt, dark: base drops the images once sent; head shows them as thumbnails above the bubble.

sent bubble light base vs head
Sent prompt, light: same.

image viewer from sent image head dark and light
Head only (dark, light): clicking a sent thumbnail opens the image viewer. Base has no sent thumbnails, so there is nothing to open there.

attach send open viewer dark head
Head, dark, sped up: paste three screenshots, type, send, then open the first sent image. Full quality:

What changes

acpmux

  • Each image in a prompt is kept once under <ACPMUX_HOME>/attachments/<sha256> (written aside and renamed). The prompt's user_message names its images: images: [{id, mimeType, name}]. Steered prompts do the same. The log never holds the base64.
  • New _acpmux/attachment {sessionId, id} returns {mimeType, data}, but only for an id that the session's own log names. An unknown id, or anything that isn't a 64-hex hash, such as a path, gets not-found.
  • Since the image refs live in the event log and the bytes live under the acpmux home, the images survive a restart and are carried into a fork (which copies the log).
  • To keep the god files from growing, _acpmux/events and _acpmux/attachment move to server/requests/events.rs. hub/turns.rs and server/requests.rs both end smaller than on base.

webviews

  • A sent prompt's bubble shows its images as thumbnails above the text, each read once by id and cached. Clicking one opens the image viewer. They use the same hairline and checker backing as the composer chips, so a dark screenshot still shows.
  • Edit, Retry and Retry with another model send the prompt's images again. They use the attachments this page sent, or read them back from acpmux after a reload or from another window.
  • The mock daemon keeps images and answers _acpmux/attachment, and its new chats now carry family as real acpmux summaries do. That lets Edit show in the dev page.

app relay (Swift)

  • AcpmuxPaneMethods allowlists _acpmux/attachment with params sessionId and id only. Without it the app's host transport refuses the read (default deny), so the app showed nothing.
    • It is a read like _acpmux/events, which is also not in sessionScoped, since a read-only view shows its bubbles too.
    • acpmux answers only for an id that the chat's own log names, and the id is a 64-hex content hash, never a path.
    • It grants nothing, so there is no gesture rule. @ad349 (protocol/origin lead): this changes the list you review.

Also: composer image thumbnails, bug (19) (folded in from #20433)

Leo's dogfood bug (19), a follow-up to (5) in #20378: dark screenshot thumbnails in the composer were still tiny and nearly invisible.

Cause: thumbnail() cropped every image to a centered 112 px square, drawn at 56 px. A wide screenshot became a sliver of its text. The edge was a 0.5 px hairline in the border color, which disappears on a dark composer, especially on Retina.

Fix:

  • Image chips are 80 px tall and keep the image's own shape. Aspect ratios between 0.7:1 and 2:1 show uncropped; anything wider or taller is center-cropped to those bounds, and the width is held between 56 and 160 px. The thumbnail is drawn at 2× (160 px tall).
  • The edge is a 1 px line in the theme's text color at 45%, drawn over the image. It stands out from both a dark image and the composer, in dark and light themes.

Test (red → green): CMUX_BROWSER_TESTS=1 bun test test/agent-pane-attach-typing.test.ts in Chromium. Three pasted 1200×800 near-black screenshots must each be:

  • ≥ 72 px tall;
  • wider than 1.3:1;
  • edged by pixels that differ by > 45 luminance from both the composer around the chip and the image's middle.

Results:

  • Red at the test commit: height 56, expected ≥ 72. The old edge measured 65 against a composer at 34, a margin of 31, below the new bar of 45.
  • Green after the fix, three runs, and also passing in local WebKit (run with the engine swapped). The img has an explicit 80 px height so WebKit sizes its width from the image shape, as the review suggested. bun run check, ui-rules, theme-colors and tooltip-rules pass.

Sent-bubble thumbnails use the same 1 px edge.

Known limits (follow-ups)

  • SSH-remote chats (RemoteAcpmuxWire) don't relay the method yet, so their bubbles show no images.
  • Kept image bytes are shared by hash and never deleted, including by _acpmux/kill {purge}.
  • A queued prompt shows its images only once its turn starts (that is when user_message is written).

Tests (red, then green)

  • Daemon: tests/prompt_images.rs (a_prompts_images_are_named_in_its_message_and_read_back_after_a_restart), hosted cmux-tui focused.

  • Web: test/agent-pane-sent-images.test.ts (Chromium). Paste an image, send, then check:

    • the bubble shows one thumbnail with a data:image/png src;
    • clicking it opens the viewer;
    • Edit → Send forks with the edited text and the image.

    Results, all with CMUX_BROWSER_TESTS=1 bun test test/agent-pane-sent-images.test.ts on the merged head:

    • Red with the web fix reversed: times out waiting for .cv-user__images img.
    • Green: 3 of 3 runs passed.
  • Relay (Swift): AgentPaneAttachmentRelayTests, fleet run-swift-testing-suites.sh. Fleet steps on aws-m4pro, Xcode 26.6:

    • Red at a1b0de2 (step d319a83b0aae): the transport refuses the read, and nothing reaches acpmux.
    • Green at 5ce1ba6 (step 3ed3dc3baad6): the test passes.
    • Both steps exit 1 on the same-tree guard (the head's cmux-tui tree isn't published yet). Their other suite failures differ between runs (NewTabStyleSwitchTests passed on red and failed on green) and none involve this change.
  • bun run check, crash ratchet, check-concurrency, check-system-blue and the Rust god-file check pass locally.

Before / after

Real cmux NEXT DEV builds on a capture mini (--showcase, in-page mock daemon), merge base a5341bc5d774 (before) against this branch at 92aca21fbbd0 (after). Two screenshots are pasted into a new chat, then sent. The host name in the location chip is masked.

Composer thumbnails. Top row is before, bottom row is after; light on the left, dark on the right.

composer thumbnails before after

The sent prompt, right after Send. Before (top), the images are gone; after (bottom), the bubble keeps them.

sent prompt images before after

Paste, type and send. Before on the left, after on the right.

attach send before after

Full quality:

Changelog

  • Fixed: Images sent with a prompt now show in its message, open in the image viewer, and are sent again by Edit and Retry.
  • Fixed: Image thumbnails in the agent composer are larger, keep the screenshot's shape, and have a visible edge on dark images.

Verification note: after the last base merge (0a4f5df), big-red was out of memory (earlyoom killing every large node process), so bun run check and the browser tests could not run locally on that merge. They passed on the pre-merge commits, the merge had no conflicts, and hosted react-apps-check covers the merged head.

🤖 Generated with Claude Code