cmux NEXT composer: an attached video shows its poster and length, and plays on click

Merged
TE
11d ago
+668-3

Leo, 2026-10-25: "also why dont we have previews of videos we should ... oh attaching a video froze the composer..."

His capture: a dropped "Screen Recording 2026-10-25 …" showed only its file name next to an image thumbnail. This PR gives that chip a preview. It builds on #25179 (the host's video.info and video.quickLook). Until #25179 lands, the host refuses the call and the chip stays a file name, so the two PRs can land in either order.

What it does

  • A video link chip (dropped, pasted or chosen in a chat on this Mac; .mov, .mp4, .m4v, known by name because the + menu's chooser gives no type) asks the host for its info once (composer/videoAttachments.ts, useVideoInfo). While the host reads it, the tile spins in the image chip's shape. It then shows the poster with a small glass play glyph and its length ("0:42"), like a reply's video thumbnail. A click (or the right-click menu's Play video) plays it in Quick Look.
  • The answer rides on the chip (ComposerAttachment.video). This PR does not send the frames yet: a video still goes to the agent exactly as on base (its path and the "Attached file" note). The key frames and a one-line note go to the agent in the next PR, together with the sent bubble that hides that note and shows the poster; sending them alone would put the note in every sent bubble as text.
  • Each read has its own token (a late end of one read never clears another of the same chip); a composer that goes away ends its reads; Quick Chat counts as on this Mac (localChat); a kept draft never carries the poster or frames (they are read again).
  • A send waits for the read for 5 s at most: useSendWhenAttached(loading + videoReading), per composer-opus and Leo's "the composer should never ever freeze". Removing the chip, a refusal, an older host, or a chat on another machine (no localPaths) holds nothing, and the chip stays a file chip that goes as its path.
  • The page decodes no video and the CSP is unchanged. intakeFiles.ts, attach() and PendingAttachment are untouched (composer-opus's huge-files branch owns them).

Before / after

Gallery states agent-pane.composer: video-dropped (after: the poster tile beside a screenshot) and video-reading (the busy tile). Before, the same drop was folder-dropped-as-link's file chip. The poster is a synthetic editor frame (gallery/fixtures/videoPoster.ts), with no user data.

Stills: pending. scripts/gallery-compare.sh needs the gallery host (cmux-lawrence), which huge-blue could not reach at the time of opening; they are added here before landing.

Tests

webviews/test/composer-video-attachment.test.tsx (jsdom, real AttachmentStrip and useVideoInfo): a movie is known by its name; the host's answer is checked (poster must be a JPEG data URL); a drop spins, then shows the poster, "0:42" and its label, and a click asks video.quickLook, read once; a kept draft drops the video info; a refused read stays a file chip and holds nothing; a remote chat asks nothing, and one found to be local reads the chip it holds; the same video removed and added again is read again and the first read's late end leaves the second busy; a composer that goes away leaves nothing busy; other files are not read; removing the chip mid-read ends the wait; past the deadline the send goes and a late answer still draws (timer driven by hand, no wall-clock wait). 12 pass. Related suites (composer-attachments, attachment-thumbnails, kept-attachments, attach-links, attachment-matrix, session-paths): 73 pass, 2 browser skips.

Observed / Not run

  • Review (Codex gpt-6-astra xhigh): round 1 found the read races, the unmount, the enabled dependency and Quick Chat (fixed in the second commit); round 2 found them fixed ("no stale-closure or render-loop issue"; deferring the frames "acceptable because prompt serialization remains unchanged from base"), its only blocker being that #25179's host methods are not on base yet. This PR lands after #25179 and merges base first.
  • bun test above; bunx vp check --no-fmt --no-lint (types: no errors in 2682 files); vp fmt, vp lint on changed files; bun run check:design-tokens, check:fixed-waits; prepush gate clean. bun run check's full vp lint is red on base 92a20e7f5ce with 4 warnings in files this PR does not touch (DynamicWorkflow.gallery.tsx, chat-environment-scroll-stability.test.tsx). That is a base finding; this push skipped the web-lint hook naming it.
  • Not run: a real-app capture (it needs #25179's host on a fleet build). The gallery shows the states.

Changelog

Added: An attached video in an agent chat shows its first frame and length, and plays in Quick Look on click.

risk: normal (UI behaviour in the composer; the send's wait is bounded)

Agent session: huge-blue

šŸ¤– Generated with Claude Code