authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-08-30 17:48:54-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-30 17:48:54-04:00
log8f525af5afa93dff58775030d9b503d3bda36b7b
tree0bf2469f69846e4772412c1a956e1431aba006b7
parent12fe2d6d03062c5d7fc3beff168f8c4af1235b50
signature Signed by PGP key B5690EEEBB952194

fix(web): open agent images in expanded preview (#8807)

Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>

2 files changed, 51 insertions(+), 6 deletions(-)

apps/web/src/components/ChatMarkdown.tsx+50-6
......@@ -35,6 +35,7 @@ import React, {
3535 Suspense,
3636 type CSSProperties,
3737 type ClipboardEvent as ReactClipboardEvent,
38 type KeyboardEvent as ReactKeyboardEvent,
3839 type MouseEvent as ReactMouseEvent,
3940 isValidElement,
4041 use,
......@@ -55,6 +56,7 @@ import remarkBreaks from "remark-breaks";
5556import remarkGfm from "remark-gfm";
5657import { remarkGithubAlerts } from "../markdown-github-alerts";
5758import { renderSkillInlineMarkdownChildren } from "./chat/SkillInlineText";
59import { type ExpandedImagePreview } from "./chat/ExpandedImagePreview";
5860import { CHAT_FILE_TAG_CHIP_CLASS_NAME, FileTagChipContent } from "./chat/FileTagChip";
5961import { PierreEntryIcon } from "./chat/PierreEntryIcon";
6062import {
......@@ -155,6 +157,7 @@ interface ChatMarkdownProps {
155157 /** Parse sanitized raw HTML instead of displaying its source text. */
156158 parseRawHtml?: boolean;
157159 imageBaseDir?: string | undefined;
160 onImageExpand?: ((preview: ExpandedImagePreview) => void) | undefined;
158161}
159162
160163export function canUseMarkdownFileShellActions(
......@@ -1089,6 +1092,31 @@ const CHAT_MARKDOWN_WORKSPACE_IMAGE_CLASS_NAME = cn(
10891092 CHAT_MARKDOWN_WORKSPACE_IMAGE_LAYOUT_CLASS_NAME,
10901093 "rounded-lg border border-border/40",
10911094);
1095const MarkdownLinkContext = React.createContext(false);
1096
1097function expandableMarkdownImageProps(
1098 onImageExpand: ((preview: ExpandedImagePreview) => void) | undefined,
1099 src: string,
1100 alt: string,
1101) {
1102 if (!onImageExpand) return {};
1103 const previewName = alt.trim() || "image";
1104 const expand = (event: ReactMouseEvent | ReactKeyboardEvent) => {
1105 if (event.currentTarget.closest("a")) return;
1106 event.preventDefault();
1107 event.stopPropagation();
1108 onImageExpand({ images: [{ src, name: previewName }], index: 0 });
1109 };
1110 return {
1111 role: "button" as const,
1112 tabIndex: 0,
1113 "aria-label": `Preview ${previewName}`,
1114 onClick: expand,
1115 onKeyDown: (event: ReactKeyboardEvent) => {
1116 if (event.key === "Enter" || event.key === " ") expand(event);
1117 },
1118 };
1119}
10921120
10931121function ChatMarkdownImageFallback(props: {
10941122 readonly alt: string;
......@@ -1118,6 +1146,7 @@ const ChatMarkdownWorkspaceImage = memo(function ChatMarkdownWorkspaceImage(prop
11181146 readonly copyMarkdown: string;
11191147 readonly srcFragment: string;
11201148 readonly style?: CSSProperties | undefined;
1149 readonly onImageExpand?: ((preview: ExpandedImagePreview) => void) | undefined;
11211150}) {
11221151 const assetUrl = useAssetUrlState(props.threadRef.environmentId, {
11231152 _tag: "workspace-file",
......@@ -1144,15 +1173,20 @@ const ChatMarkdownWorkspaceImage = memo(function ChatMarkdownWorkspaceImage(prop
11441173 />
11451174 );
11461175 }
1176 const src = assetUrl.url + props.srcFragment;
11471177 return (
11481178 <img
1149 src={assetUrl.url + props.srcFragment}
1179 src={src}
11501180 alt={props.alt}
11511181 data-markdown-copy={props.copyMarkdown}
11521182 loading="lazy"
11531183 draggable={false}
1154 className={CHAT_MARKDOWN_WORKSPACE_IMAGE_CLASS_NAME}
1184 className={cn(
1185 CHAT_MARKDOWN_WORKSPACE_IMAGE_CLASS_NAME,
1186 props.onImageExpand && "cursor-zoom-in",
1187 )}
11551188 style={props.style}
1189 {...expandableMarkdownImageProps(props.onImageExpand, src, props.alt)}
11561190 onError={() => setFailedUrl(assetUrl.url)}
11571191 />
11581192 );
......@@ -1694,6 +1728,7 @@ function ChatMarkdown({
16941728 lineBreaks = false,
16951729 parseRawHtml = true,
16961730 imageBaseDir,
1731 onImageExpand,
16971732}: ChatMarkdownProps) {
16981733 const { resolvedTheme } = useTheme();
16991734 const createAssetUrl = useAtomQueryRunner(assetEnvironment.createUrl, {
......@@ -2084,6 +2119,7 @@ function ChatMarkdown({
20842119 const isSameDocumentLink = href?.startsWith("#") ?? false;
20852120 const onClick = props.onClick;
20862121 const canOpenInPreview = Boolean(threadRef) && isPreviewSupportedInRuntime();
2122 const linkChildren = <MarkdownLinkContext value>{children}</MarkdownLinkContext>;
20872123 const link = (
20882124 <a
20892125 {...props}
......@@ -2160,10 +2196,10 @@ function ChatMarkdown({
21602196 >
21612197 {faviconHost && hastHasText(node) ? (
21622198 <MarkdownExternalLinkContent host={faviconHost} plainText={plainHastText(node)}>
2163 {children}
2199 {linkChildren}
21642200 </MarkdownExternalLinkContent>
21652201 ) : (
2166 children
2202 linkChildren
21672203 )}
21682204 </a>
21692205 );
......@@ -2205,7 +2241,8 @@ function ChatMarkdown({
22052241 </code>
22062242 );
22072243 },
2208 img({ node, title, src, alt, ...props }) {
2244 img: function MarkdownImage({ node, title, src, alt, ...props }) {
2245 const imageExpand = use(MarkdownLinkContext) ? undefined : onImageExpand;
22092246 const localSrc = node?.properties?.dataLocalSrc;
22102247 const markdownTitle = node?.properties?.dataMarkdownTitle;
22112248 const authoredSrc = typeof localSrc === "string" ? localSrc : src;
......@@ -2225,8 +2262,13 @@ function ChatMarkdown({
22252262 src={imageSource.uri}
22262263 alt={altText}
22272264 loading="lazy"
2228 className={cn(props.className, CHAT_MARKDOWN_IMAGE_SIZE_CLASS_NAME)}
2265 className={cn(
2266 props.className,
2267 CHAT_MARKDOWN_IMAGE_SIZE_CLASS_NAME,
2268 imageExpand && "cursor-zoom-in",
2269 )}
22292270 style={authoredSizeStyle}
2271 {...expandableMarkdownImageProps(imageExpand, imageSource.uri, altText)}
22302272 />
22312273 );
22322274 }
......@@ -2239,6 +2281,7 @@ function ChatMarkdown({
22392281 copyMarkdown={copyMarkdown}
22402282 srcFragment={markdownImageSourceFragment(classifiedSrc)}
22412283 style={authoredSizeStyle}
2284 onImageExpand={imageExpand}
22422285 />
22432286 );
22442287 }
......@@ -2289,6 +2332,7 @@ function ChatMarkdown({
22892332 isStreaming,
22902333 markdownFileLinkMetaByHref,
22912334 onTaskListChange,
2335 onImageExpand,
22922336 openFileInPanel,
22932337 openInPreferredEditor,
22942338 openChangeRequestLink,
apps/web/src/components/chat/MessagesTimeline.tsx+1
......@@ -1253,6 +1253,7 @@ function AssistantTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "mess
12531253 isStreaming={Boolean(row.message.streaming)}
12541254 lineBreaks={shouldPreserveAssistantLineBreaks(messageText)}
12551255 skills={ctx.skills}
1256 onImageExpand={ctx.onImageExpand}
12561257 />
12571258 <AssistantChangedFilesSection
12581259 turnSummary={row.assistantTurnDiffSummary}