Allow agent to control light/dark color theme, and display screenshots in the chat#3982
Allow agent to control light/dark color theme, and display screenshots in the chat#3982shaunandrews wants to merge 11 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds first-class screenshot artifacts to Studio’s agent conversations and expands screenshot/inspection tools to support explicit light/dark capture, improving theme-aware visual verification in the Agentic UI and CLI runtime.
Changes:
- Render
studio.chat_artifactmedia widgets (local screenshots) inline in the classic conversation transcript UI, with a loading/unavailable state. - Extend
take_screenshot,share_screenshot, andinspect_designtools withcolorSchemesupport (light/dark, plusallfortake_screenshot). - Suppress raw
mediaWidgetPayload*=marker lines from the conversation’s expanded tool-details UI while still emitting artifacts via chat events.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| apps/ui/src/ui-classic/components/session-view/conversation/style.module.css | Adds layout + skeleton/unavailable styles for inline media artifacts. |
| apps/ui/src/ui-classic/components/session-view/conversation/index.tsx | Renders chat artifact images inline; hides screenshot payload markers in tool output. |
| apps/ui/src/ui-classic/components/session-view/conversation/index.test.ts | Adds coverage for payload-line hiding and inline local screenshot rendering. |
| apps/cli/ai/tools/take-screenshot.ts | Adds colorScheme + multi-capture labeling and returns studioArtifacts for artifact emission. |
| apps/cli/ai/tools/share-screenshot.ts | Adds colorScheme option for prefers-color-scheme sharing captures. |
| apps/cli/ai/tools/screenshot-helpers.ts | Threads colorScheme into Playwright emulateMedia and temp filename suffixing. |
| apps/cli/ai/tools/inspect-design.ts | Adds colorScheme emulation for rendered DOM inspection. |
| apps/cli/ai/tools/index.ts | Exports withChatArtifactEmission for reuse and ensures tool execution can emit artifacts. |
| apps/cli/ai/tests/tools.test.ts | Adds/extends tests for colorScheme, multi-scheme capture, and artifact emission behavior. |
| apps/cli/ai/runtimes/pi/index.ts | Wraps remote-site screenshot tool to emit chat artifacts when enabled. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
📊 Performance Test ResultsComparing a6c3f8f vs trunk app-size
site-editor
site-startup
Results are median values from multiple test runs. Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff) |
| } ); | ||
| } | ||
|
|
||
| function stripScreenshotMediaPayloadLines( text: string ): string { |
There was a problem hiding this comment.
stripScreenshotMediaPayloadLines is duplicated in apps/studio/src/components/studio-code-session/conversation/index.tsx:68, and the mediaWidgetPayload=/mediaWidgetPayloads= prefixes are hardcoded here, there, and in the producer (apps/cli/ai/tools/take-screenshot.ts:146). If the CLI renames the prefix, both strippers silently break. Lift the prefixes + helper into tools/common? Non-blocking.
| return optionLabels.has( answer ) ? answer : undefined; | ||
| } | ||
|
|
||
| function isRecord( value: unknown ): value is Record< string, unknown > { |
There was a problem hiding this comment.
Dupes the unexported isRecord in tools/common/ai/chat-artifacts.ts:38 — export that one and import it instead?
Its taking the screenshot in light and dark mode, but since your site doesn't change anything, it looks weird. |
Hmmmm, maybe not. Looks like it might be showing them twice for some reason. Looking at a fix now. |
9757d73 to
f54ed57
Compare
…io tools Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
f54ed57 to
df716ff
Compare
…o/common Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…urally Removes the mediaWidgetPayload= lines from take_screenshot output and replaces the screenshot-local-media presentation rule, which told the model to re-present the auto-emitted screenshot via studio_present and produced duplicate images in the conversation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Screenshot artifacts persisted into session transcripts pointed at mkdtemp directories that macOS purges within days, leaving permanent 'Image unavailable' boxes. Captures now live in <session>.screenshots/ next to the session JSONL (with unique filenames) and are deleted with the session; standalone CLI/MCP runs still fall back to a temp directory. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Validates persisted chat-artifact entries before rendering, caches local screenshot reads through react-query and object URLs instead of base64 data URLs, hides local-only artifacts on connectors that cannot read local files, and moves legacy payload-line stripping into the memoized render-item pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion The legacy view hid the payload line but had no way to see the capture at all. Chat artifact entries now render as inline images (read over IPC with a per-path cache), using frame color tokens for light/dark support. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
blob: URLs are not allowed by the desktop renderer CSP (img-src permits data:), and revoke-on-cleanup broke the image when the artifact row remounted after a run finished. Data URLs need no lifecycle; conversion happens once inside the cache layer so files are still read over IPC only once per path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
👋 Update on the review feedback — a batch of fixes just landed on this branch (rebased onto trunk first). Here's the tour: The double screenshot @bcotrim spotted turned out not to be two captures at all — it was one capture presented twice. The tool now auto-emits its artifact, but the old Which also resolves the two duplication comments: with the producer no longer writing marker lines, the strippers only exist for old transcripts — there's now a single On Copilot's data-URL performance note: the conversion now happens once inside a cache layer — react-query in the classic UI, a per-path map in the legacy view — so each file crosses IPC and gets encoded a single time per app lifetime. (Fun detour: we tried A few more things surfaced while testing that seemed worth fixing in the same pass:
Typecheck is clean and the affected suites pass (~235 tests, including new coverage for the storage, delete sweep, and malformed-entry cases). Still worth a human eyeball on the inline images in light + dark mode. Written by Claude (working with @shaunandrews), who also did most of the implementation in this batch. |


Proposed Changes
mediaWidgetPayload=text lines are gone from tool output (a shared stripper keeps old transcripts clean), and thestudio_presentscreenshot rule was replaced so the agent no longer re-presents the same capture, which caused the duplicate image reported in review.<session>.screenshots/directory next to the session file instead of the OS temp dir, so inline images keep working in older transcripts; deleting a session removes its screenshots.take_screenshotwas wrapped, so e.g.site_createnever emitted its site-preview artifact).studio ui) can't read local files, so local-only screenshot artifacts are hidden there via a newreadLocalMediaconnector capability instead of rendering guaranteed "Image unavailable" boxes.Screenshots
Testing Instructions
npm test -- apps/cli/ai/tests packages/common/ai apps/ui/src/ui-classic/components/session-view/conversation apps/studio/src/components/studio-code-session/conversationnpm run typecheck~/Library/Application Support/Studio/sessions/<date>/<session>.screenshots/and still renders after restarting the app and reopening the session.colorScheme: "all"and confirm both artifacts are produced, labeled(desktop light)/(desktop dark)..screenshots/directory is removed.mediaWidgetPayload=lines and confirm they stay hidden in the expanded tool details.