fix(web-ui): preview conversation images on click - #3184
Merged
Merged
Conversation
Conversation images were only visible at inline size. An image in a markdown message, an image inside an MCP tool result and a composer attachment thumbnail had no full-size view; the only surface with one was the sent user-message gallery, which keeps its own overlay. Lift the markdown renderer's overlay into a shared ImageLightbox and let the surface that resolved the image bytes own it, so the markdown renderer, the MCP tool card and the composer attachment each mount one instance and pass their own resolved source: a data: URL for a local file read through the host transport, or the http(s) URL the message already carries. A preview click keeps the owner's behaviour: an image wrapped in a link or a file link still follows the link, and the tool-card toggle is not hit. An open preview closes when the active surface changes, so bytes that belong to the previous surface cannot stay on screen, and only data:image and http(s) sources become click targets, so the loading placeholder and the failed-read fallback never advertise an empty view. Co-authored-by: bitfun-ai <318544290+bitfun-ai@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
An image inside a conversation could only be seen at the size the surface gave it: a Markdown image in a message, an image returned by an MCP tool, and a composer attachment thumbnail had no full-size view. The only surface that already had one was the sent user-message gallery, which keeps its own overlay.
The Markdown renderer's overlay is lifted into a shared
ImageLightbox(src/web-ui/src/shared/ui/) and mounted by the surface that resolved the image bytes, so each of the three surfaces passes its own source: adata:URL for a local file read through the host transport, or thehttp(s)URL the message already carries. Markdown images, tool result images and attachment thumbnails are now click targets that open that overlay.Type and Areas
Type: bug fix (UI/UX)
Areas: web UI
Motivation / Impact
Before this change the preview affordance existed only inside the sent user-message gallery, so clicking a rendered image in the conversation, in an MCP tool result, or on a composer chip did nothing. Users had to open the file externally to read anything that was not legible inline.
After this change all three surfaces open the same overlay with the bytes they already resolved, and the click keeps the owner's behaviour instead of stealing it:
data:imageandhttp(s)sources become click targets, so the loading placeholder and the failed-read fallback never advertise an empty view.No API, persisted shape, or setting changes.
Verification
Automated (all after rebasing onto
upstream/mainf59f6279b):pnpm --dir src/web-ui run test:run src/flow_chat/tool-cards/MCPToolDisplay.test.tsx src/flow_chat/components/ChatInputImagePreview.test.tsx src/flow_chat/components/ChatInputAttachments.test.tsx src/infrastructure/markdown src/infrastructure/appearance/registry/AppearanceRegistry.test.ts-> 9 files / 94 tests passed. The new cases cover: a tool-result image opens the overlay withdata:image/png;base64,...and closes from the close control without collapsing the card; an attachment thumbnail opens the overlay and closes on demand.pnpm run appearance:contract-audit-> passed (242 surfaces, 2963 DOM contracts; only the pre-existing shared-style owner warnings).pnpm run i18n:audit-> passed, 0 warnings (imageLightbox.labeladded to en-US / zh-CN / zh-TW).pnpm run theme:color-audit:all-> all 23 selected surfaces passed;pnpm run typography:auditandpnpm run theme:visual-contractpassed.pnpm --dir src/web-ui exec tsc --noEmit-> clean;eslinton the changed source files -> 0 errors.Manual checks: no click-through was performed in a GUI in this environment; the three surfaces are covered by the jsdom tests above, which assert the overlay opens with the resolved
srcand closes on demand.Remote scenarios: exercised for the local workspace only. The composer thumbnail path resolves path-only images through
workspaceAPI.readFileContent(host transport) rather than a local asset URL, and that path is asserted in tests; SSH remote workspace, Peer Device Mode and Detached Dispatch were not exercised end to end.Reviewer Notes
<img>is declared (image-lightbox.image); the scrim and close control are design-systemdialogchrome thatImageLightbox.scssrestyles. Customdata-openbitfun-*attributes are deliberately not passed throughoverlayProps, becauseDialog's own attributes would override them.UserMessageItem) keeps its own overlay; refactoring it onto the shared component is out of scope for this PR.i18nService, andMCPToolDisplay.test.tsxneeded an@/infrastructure/i18nmock, thetrace/debuglogger levels and amatchMediastub withaddEventListener.Checklist