Skip to content

Play a video or recording an agent writes inline in the chat - #18402

Merged
teamleaderleo merged 56 commits into
feat-cmux-nextfrom
feat-cmux-next-media-video
Oct 7, 2026
Merged

teamleaderleo merged 56 commits into
feat-cmux-nextfrom
feat-cmux-next-media-video

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

A video or audio file an agent writes or links now plays inline in the chat. Before this, it showed only as a path chip.

  • Host check: the page asks media.load {src} with the path from the reply or tool output. The host checks it like a reply image: inside the session's folders, not on the deny list, a regular file, a known type, at most 4 GB. Video types are mp4, m4v, mov and webm; audio types are mp3, m4a, aac, wav and flac.
  • Granted URL: a checked file gets an unguessable cmux-agent://pane/__media/<token>.<ext> URL. The page never names a file itself.
  • Serving: the pane's scheme handler serves a granted file in byte ranges, up to 8 MB per response. It refuses a file that was swapped for a link after the grant. The page CSP adds media-src 'self'; connect-src stays 'none'.
  • Player: it mounts only once it nears the viewport and asks for metadata only, so a long transcript loads no media it doesn't show. A video plays inline with controls, muted while the pointer rests on it. Expand shows it large over the pane. Nothing opens a browser.
  • Where: tool output paths and reply images (![](demo.mp4)) use it. If the host refuses a file, it shows as a name card.

Web media (CI artifact URLs, GitHub user-attachments) is refused for now; it follows in its own PR. With the agent.pageHost tunable on, the page runs under the page host's CSP, which has no media-src, so video there falls back to the name card.

Targets feat-cmux-next (#18401 and #18357 landed as 7672e43). Part of bead cx-ncc.51.

Testing

  • Red commit ceefe3b, then the fix.
  • bun test src/agent-session/acpmux/chips/ReplyMedia.test.tsx (local): failed at the red commit because the module was missing; 3 pass after the fix.
  • bun test src/agent-session/acpmux src/gallery: 1197 pass. The 4 failures (composer pickers, chat menu, palette Continue in, automation chat menu) also fail on clean feat-cmux-next.
  • AgentPaneReplyMediaTests cover the grant (inside, outside, wrong type, web URL, same file same URL, unknown token), byte ranges (206 slice, open-ended, whole file, 416 past the end) and the params contract. Fleet Swift suite (step 18909ba90b44, run-swift-testing-suites.sh at c24deb9): all three pass. The later merges changed no Swift.
  • After the base merge (head 41ccb71, base 6314b98), local in webviews/: bun test src/agent-session/acpmux/chips src/agent-session/acpmux/conversation test/gallery-coverage.test.ts test/pane-english.test.ts test/ui-rules.test.ts 222 pass, 0 fail; the typecheck passes on all 1047 files. Fleet Swift suite on 41ccb71 (merged head): it compiles, and AgentPaneReplyImageTests and AgentPaneReplyMediaTests pass. A later base merge (9506c26, with ci: run webviews tests per file with a timeout, on the 8 vCPU runner #18423) brought only unrelated Swift (terminal sizing, the context-menu export fix); react-apps-check is green on 9506c26
  • vp check passes on the changed files. verify-local.py --only swift-syntax --swift-changed passes. The agent pane bundle is no longer committed (base dropped it); builds generate it.

Changelog

Added: A video or audio file an agent writes or links plays inline in the agent chat, with an Expand view

Proof

Before (feat-cmux-next): a video a tool writes shows nothing under the call, and a reply link to one is only a path chip. Base gallery, the same transcript without the video variant: /live/?entry=agent-pane.transcript#/agent-pane.transcript/tool-images

After: variant tool-video plays a real 7 s terminal recording of a test run inline, muted on hover, with controls and Expand. Preview: /wt/feat-cmux-next-media-video/?entry=agent-pane.transcript#/agent-pane.transcript/tool-video

🤖 Generated with Claude Code


Note

Medium Risk
New host-mediated file access and custom scheme byte-range serving touch WebKit security boundaries; path checks mirror images but large-file streaming and grant lifecycle add new attack surface to validate.

Overview
Adds inline video and audio playback for local files an agent links or tools write in the agent chat, instead of showing only path chips or treating .mp4 paths as images.

The native host exposes a passive media.load {src} bridge call (same path rules as reply images: in-session roots, allowed extensions, up to 4 GB). Approved files get opaque cmux-agent://pane/__media/<token>.<ext> URLs via AgentPaneMediaGrants; the scheme handler serves them with HTTP byte ranges (206/416), rejects symlinks swapped after grant, and adds link.media_refused. Agent pane CSP gains media-src 'self'; remote/https media is refused.

The acpmux UI adds ReplyMedia: lazy media.load near the viewport, inline <video>/<audio> with expand dialog for video, wired from ReplyImage, tool output strips (toolImages + ToolRow), plus styles, i18n, gallery tool-video, and Swift/TS tests.

Reviewed by Cursor Bugbot for commit 9506c26. Bugbot is set up for automated code reviews on this repo. Configure here.

teamleaderleo and others added 30 commits October 6, 2026 20:03
A reply's inline image opens a viewer over the pane: zoom about the
pointer (pinch, Command-scroll, double click, + - 0), pan a zoomed image,
arrows and the arrow keys through every image of the chat, copy as PNG,
Escape or a click beside the image to close. The mock light-theme chat
shows three screens to open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
agent-pane.image-viewer: a reply with three screenshots, and images in
two replies the viewer's arrows go through. The opened viewer needs a
click, so its states wait for play steps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Red: the viewer's image list keeps an image on a fence's later line,
loses images after a lone backtick, with parentheses in the target or
inside bold; Tab leaves the viewer; an image the list misses opens
without its name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The viewer's image list now comes from Markdown.tsx's own block parser
and inline pattern (inlineImages), so it holds exactly the images a
reply draws: none from code, and those after a lone backtick, with
parentheses in the target or inside bold. An image the list still
misses opens under its own name. WebKit's pinch (gesture events) zooms
about the pointer; the stage's padding is symmetric so zoom stays about
the pointer; Tab wraps inside the viewer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e data URLs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Notes draw under the reply by first reference, so the viewer lists their
images there too; a reply whose only image has an upper-case data URL
scheme is read, as Markdown.tsx draws it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The mock chat and the gallery entry show a chart, a gradient and a
diagram instead of drawn app screens: previews are never mockups of cmux.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The viewer is a ui/Dialog (Base UI): it traps focus, makes the pane
behind inert, closes on Escape and returns focus to the image, replacing
the hand-rolled role, tab index and Tab handling the UI rules forbid.
Only its own image keys (arrows, + - 0) stay a key handler, marked
ui-allow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ext-image-viewer

# Conflicts:
#	webviews/src/agent-session/acpmux/conversation/Markdown.tsx
…e reply budget

Red: since base moved data URLs over 4 KB behind short refs and capped a
reply at 8 MB, chatImages lists none of a reply's long images.

Focused: bun test src/agent-session/acpmux/conversation/imageViewer.test.tsx -t budget
(fail: 0 listed, 6 drawn)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he reply budget

inlineImages mirrors renderInline again: it takes the URLs back from
capDataUrls' short refs (passed down into bold, italic and link text),
and chatImages drops the reply's images past dataUrlsOverBudget, which
draw as their name.

Focused: bun test src/agent-session/acpmux/conversation/imageViewer.test.tsx -t budget
(pass)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Red since the move to ui/Dialog: a press inside the viewer focuses the
dialog popup, above the body that handled the image keys, and the
wheel and gesture listeners ran before the portal mounted the stage.

Focused: bun test src/agent-session/acpmux/conversation/imageViewer.test.tsx
(fail: keys stay at scale(1); Control-scroll not prevented)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ui/Dialog passes onKeyDown to its popup, which takes focus after a press
inside, so the viewer's arrows and + - 0 keys go there. The stage is a
callback ref, so the pinch and wheel listeners attach once the portal
mounts it.

Focused: bun test src/agent-session/acpmux/conversation/imageViewer.test.tsx
(11 pass)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ext-image-viewer

# Conflicts:
#	webviews/src/agent-session/acpmux/App.tsx
The opened viewer as play steps on the reply with three images, and
anchors that prove nothing under the viewer moves. Regenerated agent
pane bundle for the base merge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ext-image-viewer

# Conflicts:
#	webviews/src/agent-session/acpmux/Localizable.xcstrings
#	webviews/src/agent-session/acpmux/conversation/Markdown.tsx
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ext-image-viewer

# Conflicts:
#	webviews/src/agent-session/acpmux/conversation/Markdown.tsx
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Red: direct.ts drops ACP image content blocks, and a tool that writes or
prints an image path (a screenshot, a rendered chart) shows only text.

Red (local): bun test src/agent-session/acpmux/conversation/toolImages.test.tsx
fails: ../toolImages does not exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An agent's image outputs now show inline under the tool row, without
opening it:
- images a call returns (ACP image content blocks), kept as data URLs
  up to 8 MB each;
- image files it wrote or printed (a saved screenshot, a rendered
  chart): its edited file, its locations and absolute image paths in
  its output, loaded through the host like a reply's local image.

At most four per call, capped at 360x220 and lazily decoded. A click
opens the image viewer, as does a local or web image in a reply now
(ReplyImage's shown image is OpenableImage). Gallery: transcript
variant tool-images with a returned screenshot and a saved chart.

Focused (local): bun test src/agent-session/acpmux/conversation/toolImages.test.tsx
4 pass (red at 5c3dbb8).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ext-image-viewer

# Conflicts:
#	webviews/src/agent-session/acpmux/App.tsx
…r branch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Red: the host's image.load refuses a PDF (not a raster or SVG), and a
tool's PDF output path shows no preview.

Red (local): bun test src/agent-session/acpmux/conversation/toolImages.test.tsx
fails on the report.pdf case; AgentPaneReplyImageTests
.aPDFShowsItsFirstPageAsAPNG names thumbnailSide, which does not exist
yet (runs in hosted cmux-next swift test).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The host draws page 1 of a local PDF (CoreGraphics, on white, longest side
1024) and hands the pane a PNG data URL, so the chip and the tool image strip
show it like any image and a click opens the in-pane viewer. Only page 1 is
decoded, so PDFs up to 100 MB qualify; a file that is not a PDF is refused.
Tool output and edited paths ending in .pdf now count as image outputs.

Gallery: the transcript tool-images variant adds a build-report PDF thumbnail.

Focused: bun test toolImages.test.tsx (red 3/1 at 788b662, green 4/0);
AgentPaneReplyImageTests.aPDFShowsItsFirstPageAsAPNG on the fleet suite runner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
teamleaderleo and others added 5 commits October 7, 2026 08:33
Base no longer commits the web bundles: the agent-pane bundle files are
dropped; only GENERATED.md stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drops the committed agent-pane bundle files, as base did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drops the committed agent-pane bundle files, as base did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drops the committed agent-pane bundle files, as base did; the page CSP change
stays in scripts/cmux-next/build-agent-pane-web.sh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@blacksmith-sh

blacksmith-sh Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Blacksmith runners detected OOM events on the following jobs:

Job Details
web / react-apps-check View Job

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread webviews/src/agent-session/acpmux/chips/ReplyMedia.tsx
Comment thread webviews/src/agent-session/acpmux/toolImages.ts
teamleaderleo and others added 14 commits October 7, 2026 11:24
Picks up the bundle-removal CI fixes. The string catalog keeps base's text
and adds only this branch's keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Carries feat-cmux-next 754e544 (#18409) down the stack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Carries feat-cmux-next 754e544 (#18409) down the stack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Carries feat-cmux-next 754e544 (#18409) down the stack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CGPDFPageGetDrawingTransform only scales a page down, so a page smaller
than the thumbnail drew at its natural size in white margins. Build the
transform by hand and clip to the crop box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The merge kept this branch's generated strings and dropped base's
composer.connectSSH and composer.connectCloud.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	webviews/src/agent-session/acpmux/conversation/Markdown.tsx
Base automatically changed from feat-cmux-next-media-thumbnails to feat-cmux-next October 7, 2026 21:25
teamleaderleo and others added 3 commits October 7, 2026 14:26
# Conflicts:
#	webviews/src/agent-session/acpmux/conversation/ToolRow.tsx
#	webviews/src/agent-session/acpmux/conversation/conversation.css
#	webviews/src/agent-session/acpmux/conversation/transcript.gallery.ts
#	webviews/src/agent-session/acpmux/toolImages.ts
#	webviews/src/gallery/fixtures/toolImages.ts
…after the base merge

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 41ccb71. Configure here.

return () => {
live = false;
};
}, [near, loadable, src, failed, path]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Player state ignores path changes

Low Severity

src, failed, and expanded are initialized from the first path and never reset when that prop changes. The load effect also returns early while src or failed is still set, so a reused ReplyMedia keeps playing or hiding the previous file after markdown or a tool row updates the path.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 41ccb71. Configure here.

@teamleaderleo
teamleaderleo merged commit 3f199b5 into feat-cmux-next Oct 7, 2026
66 of 70 checks passed
@teamleaderleo
teamleaderleo deleted the feat-cmux-next-media-video branch October 7, 2026 21:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant