Skip to content

feat(client): render viewed images in expanded tool entries - #7724

Open
SunkenInTime wants to merge 10 commits into
pingdotgg:mainfrom
SunkenInTime:t3code/render-viewed-image-tool-entries
Open

feat(client): render viewed images in expanded tool entries#7724
SunkenInTime wants to merge 10 commits into
pingdotgg:mainfrom
SunkenInTime:t3code/render-viewed-image-tool-entries

Conversation

@SunkenInTime

@SunkenInTime SunkenInTime commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Stacked on #6433 — only the commits after the parent PR are new; it builds on the signed workspace-file asset URLs introduced there.

Problem

Expanding a "Viewed (image)" / image-read tool call in the timeline only showed the file path as text — the image the agent looked at was never visible (raised by maria on Discord as a follow-up to #6433).

Fix

Read-classified tool entries (image_view, file-read, Cursor's "Read File") whose detail is a single workspace image path now render the image itself above the text detail when the row is expanded:

  • Web: a new ToolCallExpandedImage in MessagesTimeline loads the image through the signed workspace-file asset URL, only while the row is expanded (collapsed rows never fetch). Clicking opens the existing image lightbox. On load failure it renders nothing — the path stays visible in the text body.
  • Mobile: ThreadFeedActivity rows carry a viewedImagePath, and the expanded work-log detail reuses ThreadMarkdownImage (loading/unavailable states, fullscreen viewer on tap) via a render prop from ThreadFeed.

The image-path predicate reuses isWorkspaceImagePreviewPath from @t3tools/shared, so the client only attempts extensions the asset route will actually serve. Unit tests cover the web predicate; the mobile activity fixture gained the new field.

Web

Before

Before: expanding a viewed-image tool entry shows only the file path as text

After

After: the expanded entry renders the viewed image above the path

Mobile (iOS Simulator)

Before

Mobile before: expanding the viewed-image work-log row shows only the path

After

Mobile after: the expanded row renders the viewed image with the path beneath


Built with Claude Fable 5 on Claude Code.


Note

Medium Risk
Touches markdown image rendering and rehype-sanitize (adds file on src) plus signed workspace-file fetches. Remote images still load as normal <img>; only workspace paths go through the asset route.

Overview
Workspace-file images now show in chat markdown and in expanded read/view tool rows, loaded through signed asset URLs instead of raw paths.

Tool entries: workEntryViewedImagePath picks a single-line image path from read/view work (image_view, file-read, “Read File”). Web’s expanded timeline row renders ToolCallExpandedImage (lightbox on click; fetch only while expanded). Mobile work-log rows carry viewedImagePath and reuse ThreadMarkdownImage.

Markdown: Web ChatMarkdown resolves relative, absolute, and file:// srcs (including Windows drive and UNC) via ChatMarkdownWorkspaceImage, with loading/unavailable UI. Sanitizer now allows file on src and rewrites drive-absolute HTML src before sanitize. Mobile native markdown gains an optional renderImage context so the host can swap in signed-URL images; remote http(s) still uses the default Image.

useAssetUrlState on mobile exposes Loading/Failure/Success so those UIs can distinguish spinner vs unavailable.

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

Note

Render workspace images in expanded tool entries and markdown

  • Adds cross-platform support for rendering workspace-file images referenced by tool calls and markdown. Expanded work log and tool call rows now show an image preview loaded via a signed asset URL with loading and failure UI.
  • Introduces workEntryViewedImagePath in threadActivity.ts and MessagesTimeline.logic.ts to extract a workspace image path from read/view work log entries when the detail is a single-line image path.
  • On mobile, SelectableMarkdownText gains an optional renderImage prop propagated via MarkdownImageRendererContext; NativeMarkdownImage uses it to render custom nodes.
  • On web, ChatMarkdown.tsx replaces default <img> rendering to resolve workspace paths (relative, absolute, file://) through ChatMarkdownWorkspaceImage. Adds rehypeNormalizeWindowsImageSrc to rewrite drive-absolute src values to file:/// URLs before sanitization, and permits the file protocol in CHAT_MARKDOWN_SANITIZE_SCHEMA.
  • Introduces useAssetUrlState in assets.ts (mobile) returning a Loading | Failure | Success union; useAssetUrl is refactored to delegate to it.
  • Risk: markdownComponents img renderer in ChatMarkdown.tsx changes default image rendering for all markdown; non-workspace src values still render as normal <img>, but Windows drive-absolute paths are now rewritten to file:/// URLs, and file protocol is newly allowed by CHAT_MARKDOWN_SANITIZE_SCHEMA.

Macroscope summarized cb07d87.

@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7fe64fd1-a491-46ab-a8e2-4ba15bba5036

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 20, 2026
Comment thread apps/web/src/markdown-links.ts

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

UI consistency review found one keyboard-interaction regression and two smaller ownership/consistency issues in the changed web UI. Details inline.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/ChatMarkdown.tsx Outdated
Comment thread apps/web/src/components/ChatMarkdown.tsx
Comment thread apps/web/src/components/chat/MessagesTimeline.tsx
@macroscopeapp

macroscopeapp Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR introduces a new feature enabling workspace image rendering in markdown and expanded tool entries across mobile and web. The scope includes new components, path resolution logic, and loading states — warranting human review for this new capability.

You can add or adjust custom eligibility rules. Learn more.

SunkenInTime pushed a commit to SunkenInTime/t3code that referenced this pull request Aug 20, 2026
SunkenInTime pushed a commit to SunkenInTime/t3code that referenced this pull request Aug 20, 2026
Dara Adedeji and others added 10 commits August 20, 2026 17:03
Agents reference screenshots they saved into the workspace by path
(relative, absolute, or file://). Those bytes only exist on the
environment host, so both clients rendered a broken or empty box.

Web: a custom img renderer resolves workspace paths through the signed
workspace-file asset endpoint; remote http(s)/data images keep their
existing inline rendering.

iOS: the t3-markdown-text module accepts a renderImage hook (provided
via context) and ThreadFeed supplies a component that resolves paths
against the workspace root, loads via useAssetUrl, and supports
tap-to-expand into the existing full-screen viewer.

Android: the nitro-markdown fallback path registers an image renderer
backed by the same component.
- Surface asset-URL failures as 'Image unavailable' instead of an
  indefinite spinner (useAssetUrlState on mobile mirrors web's states).
- Normalize file:///C:/... drive URIs (drop the URL parser's leading
  slash) and map file://host/share URIs to UNC paths.
- Treat protocol-relative //cdn... image srcs as directly loadable on
  web and non-workspace on mobile.
…play

Review bot follow-ups: the mobile markdown-image parser rewrote the
historical file://localhost/... spelling into a UNC path, and the web
workspace image's 'block' utility lost to the unlayered
.chat-markdown img rule, so its display flipped when loading finished.
Also announce the loading placeholder to assistive tech via role=status.
Expanding a 'Viewed (image)' or image-read tool call previously showed
only the file path as text. Read-classified entries whose detail is a
single workspace image path now render the image itself above the text
detail, loaded through the same signed workspace-file asset URL the
chat markdown images use. Web rows open the existing lightbox on click;
mobile reuses ThreadMarkdownImage with its fullscreen viewer.
Review bot follow-ups: keydown on the preview button bubbled to the
row's role=button handler, whose preventDefault cancelled the button's
click activation — Enter/Space collapsed the row instead of opening the
lightbox. Stop key events at the expanded-body wrapper like pointer
events, give the button a focus-visible ring, and swap the hand-rolled
loading placeholder for the shared Skeleton with role=status.
@SunkenInTime
SunkenInTime force-pushed the t3code/render-viewed-image-tool-entries branch from 94d9de4 to cb07d87 Compare August 21, 2026 00:06

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One finding on the new expanded-row image preview: the blanket onKeyDown={stopRowToggle} also swallows window-level keyboard shortcuts while the preview button is focused. Details inline.

Posted via Macroscope — UI Consistency

Comment on lines +2759 to +2762
// Keys pressed on the expanded body (e.g. Enter on the image
// preview button) must not reach the row's toggle handler — its
// preventDefault would also cancel the button's click activation.
onKeyDown={stopRowToggle}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

stopRowToggle stops every keydown, not just the row's toggle keys. React dispatches from the root container and SyntheticEvent.stopPropagation() also calls nativeEvent.stopPropagation(), so while the new preview button has focus no keydown reaches the bubble-phase window listeners — e.g. the command-palette keybinding (CommandPalette.tsx), the thread-jump shortcuts (Sidebar.tsx), and the _chat.tsx window handler all stop working until focus leaves the row.

Suggest narrowing the stop to the keys the row actually toggles on (or, equivalently, having the row handler ignore events whose target is not currentTarget) so unrelated shortcuts keep bubbling:

Suggested change
// Keys pressed on the expanded body (e.g. Enter on the image
// preview button) must not reach the row's toggle handler — its
// preventDefault would also cancel the button's click activation.
onKeyDown={stopRowToggle}
// Enter/Space pressed inside the expanded body (e.g. the image
// preview button) must not reach the row's toggle handler — its
// preventDefault would also cancel the button's click activation.
// Other keys keep bubbling so window-level shortcuts still fire.
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") e.stopPropagation();
}}

Posted via Macroscope — UI Consistency

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

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 high effort and found 1 potential issue.

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 cb07d87. Configure here.

const rawPath = parsed.pathname;
const rawPath = parsed.hostname
? `\\\\${parsed.hostname}${parsed.pathname.replaceAll("/", "\\")}`
: parsed.pathname;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Drive file URLs become UNC

Medium Severity

file://C:/... style Windows drive URLs are treated as UNC hosts. WHATWG parses the drive letter as hostname, and both the web parseFileUrlHref UNC branch and mobile markdownImageWorkspacePath authority branch rewrite it to \\C\.... Workspace image loads then request the wrong path and fail, while file:///C:/... (three slashes) works.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit cb07d87. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant