| ... | @@ -35,6 +35,7 @@ import React, { | ... | @@ -35,6 +35,7 @@ import React, { |
| 35 | Suspense, | 35 | Suspense, |
| 36 | type CSSProperties, | 36 | type CSSProperties, |
| 37 | type ClipboardEvent as ReactClipboardEvent, | 37 | type ClipboardEvent as ReactClipboardEvent, |
| | 38 | type KeyboardEvent as ReactKeyboardEvent, |
| 38 | type MouseEvent as ReactMouseEvent, | 39 | type MouseEvent as ReactMouseEvent, |
| 39 | isValidElement, | 40 | isValidElement, |
| 40 | use, | 41 | use, |
| ... | @@ -55,6 +56,7 @@ import remarkBreaks from "remark-breaks"; | ... | @@ -55,6 +56,7 @@ import remarkBreaks from "remark-breaks"; |
| 55 | import remarkGfm from "remark-gfm"; | 56 | import remarkGfm from "remark-gfm"; |
| 56 | import { remarkGithubAlerts } from "../markdown-github-alerts"; | 57 | import { remarkGithubAlerts } from "../markdown-github-alerts"; |
| 57 | import { renderSkillInlineMarkdownChildren } from "./chat/SkillInlineText"; | 58 | import { renderSkillInlineMarkdownChildren } from "./chat/SkillInlineText"; |
| | 59 | import { type ExpandedImagePreview } from "./chat/ExpandedImagePreview"; |
| 58 | import { CHAT_FILE_TAG_CHIP_CLASS_NAME, FileTagChipContent } from "./chat/FileTagChip"; | 60 | import { CHAT_FILE_TAG_CHIP_CLASS_NAME, FileTagChipContent } from "./chat/FileTagChip"; |
| 59 | import { PierreEntryIcon } from "./chat/PierreEntryIcon"; | 61 | import { PierreEntryIcon } from "./chat/PierreEntryIcon"; |
| 60 | import { | 62 | import { |
| ... | @@ -155,6 +157,7 @@ interface ChatMarkdownProps { | ... | @@ -155,6 +157,7 @@ interface ChatMarkdownProps { |
| 155 | /** Parse sanitized raw HTML instead of displaying its source text. */ | 157 | /** Parse sanitized raw HTML instead of displaying its source text. */ |
| 156 | parseRawHtml?: boolean; | 158 | parseRawHtml?: boolean; |
| 157 | imageBaseDir?: string | undefined; | 159 | imageBaseDir?: string | undefined; |
| | 160 | onImageExpand?: ((preview: ExpandedImagePreview) => void) | undefined; |
| 158 | } | 161 | } |
| 159 | | 162 | |
| 160 | export function canUseMarkdownFileShellActions( | 163 | export function canUseMarkdownFileShellActions( |
| ... | @@ -1089,6 +1092,31 @@ const CHAT_MARKDOWN_WORKSPACE_IMAGE_CLASS_NAME = cn( | ... | @@ -1089,6 +1092,31 @@ const CHAT_MARKDOWN_WORKSPACE_IMAGE_CLASS_NAME = cn( |
| 1089 | CHAT_MARKDOWN_WORKSPACE_IMAGE_LAYOUT_CLASS_NAME, | 1092 | CHAT_MARKDOWN_WORKSPACE_IMAGE_LAYOUT_CLASS_NAME, |
| 1090 | "rounded-lg border border-border/40", | 1093 | "rounded-lg border border-border/40", |
| 1091 | ); | 1094 | ); |
| | 1095 | const MarkdownLinkContext = React.createContext(false); |
| | 1096 | |
| | 1097 | function 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 | } |
| 1092 | | 1120 | |
| 1093 | function ChatMarkdownImageFallback(props: { | 1121 | function ChatMarkdownImageFallback(props: { |
| 1094 | readonly alt: string; | 1122 | readonly alt: string; |
| ... | @@ -1118,6 +1146,7 @@ const ChatMarkdownWorkspaceImage = memo(function ChatMarkdownWorkspaceImage(prop | ... | @@ -1118,6 +1146,7 @@ const ChatMarkdownWorkspaceImage = memo(function ChatMarkdownWorkspaceImage(prop |
| 1118 | readonly copyMarkdown: string; | 1146 | readonly copyMarkdown: string; |
| 1119 | readonly srcFragment: string; | 1147 | readonly srcFragment: string; |
| 1120 | readonly style?: CSSProperties | undefined; | 1148 | readonly style?: CSSProperties | undefined; |
| | 1149 | readonly onImageExpand?: ((preview: ExpandedImagePreview) => void) | undefined; |
| 1121 | }) { | 1150 | }) { |
| 1122 | const assetUrl = useAssetUrlState(props.threadRef.environmentId, { | 1151 | const assetUrl = useAssetUrlState(props.threadRef.environmentId, { |
| 1123 | _tag: "workspace-file", | 1152 | _tag: "workspace-file", |
| ... | @@ -1144,15 +1173,20 @@ const ChatMarkdownWorkspaceImage = memo(function ChatMarkdownWorkspaceImage(prop | ... | @@ -1144,15 +1173,20 @@ const ChatMarkdownWorkspaceImage = memo(function ChatMarkdownWorkspaceImage(prop |
| 1144 | /> | 1173 | /> |
| 1145 | ); | 1174 | ); |
| 1146 | } | 1175 | } |
| | 1176 | const src = assetUrl.url + props.srcFragment; |
| 1147 | return ( | 1177 | return ( |
| 1148 | <img | 1178 | <img |
| 1149 | src={assetUrl.url + props.srcFragment} | 1179 | src={src} |
| 1150 | alt={props.alt} | 1180 | alt={props.alt} |
| 1151 | data-markdown-copy={props.copyMarkdown} | 1181 | data-markdown-copy={props.copyMarkdown} |
| 1152 | loading="lazy" | 1182 | loading="lazy" |
| 1153 | draggable={false} | 1183 | 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 | )} |
| 1155 | style={props.style} | 1188 | style={props.style} |
| | 1189 | {...expandableMarkdownImageProps(props.onImageExpand, src, props.alt)} |
| 1156 | onError={() => setFailedUrl(assetUrl.url)} | 1190 | onError={() => setFailedUrl(assetUrl.url)} |
| 1157 | /> | 1191 | /> |
| 1158 | ); | 1192 | ); |
| ... | @@ -1694,6 +1728,7 @@ function ChatMarkdown({ | ... | @@ -1694,6 +1728,7 @@ function ChatMarkdown({ |
| 1694 | lineBreaks = false, | 1728 | lineBreaks = false, |
| 1695 | parseRawHtml = true, | 1729 | parseRawHtml = true, |
| 1696 | imageBaseDir, | 1730 | imageBaseDir, |
| | 1731 | onImageExpand, |
| 1697 | }: ChatMarkdownProps) { | 1732 | }: ChatMarkdownProps) { |
| 1698 | const { resolvedTheme } = useTheme(); | 1733 | const { resolvedTheme } = useTheme(); |
| 1699 | const createAssetUrl = useAtomQueryRunner(assetEnvironment.createUrl, { | 1734 | const createAssetUrl = useAtomQueryRunner(assetEnvironment.createUrl, { |
| ... | @@ -2084,6 +2119,7 @@ function ChatMarkdown({ | ... | @@ -2084,6 +2119,7 @@ function ChatMarkdown({ |
| 2084 | const isSameDocumentLink = href?.startsWith("#") ?? false; | 2119 | const isSameDocumentLink = href?.startsWith("#") ?? false; |
| 2085 | const onClick = props.onClick; | 2120 | const onClick = props.onClick; |
| 2086 | const canOpenInPreview = Boolean(threadRef) && isPreviewSupportedInRuntime(); | 2121 | const canOpenInPreview = Boolean(threadRef) && isPreviewSupportedInRuntime(); |
| | 2122 | const linkChildren = <MarkdownLinkContext value>{children}</MarkdownLinkContext>; |
| 2087 | const link = ( | 2123 | const link = ( |
| 2088 | <a | 2124 | <a |
| 2089 | {...props} | 2125 | {...props} |
| ... | @@ -2160,10 +2196,10 @@ function ChatMarkdown({ | ... | @@ -2160,10 +2196,10 @@ function ChatMarkdown({ |
| 2160 | > | 2196 | > |
| 2161 | {faviconHost && hastHasText(node) ? ( | 2197 | {faviconHost && hastHasText(node) ? ( |
| 2162 | <MarkdownExternalLinkContent host={faviconHost} plainText={plainHastText(node)}> | 2198 | <MarkdownExternalLinkContent host={faviconHost} plainText={plainHastText(node)}> |
| 2163 | {children} | 2199 | {linkChildren} |
| 2164 | </MarkdownExternalLinkContent> | 2200 | </MarkdownExternalLinkContent> |
| 2165 | ) : ( | 2201 | ) : ( |
| 2166 | children | 2202 | linkChildren |
| 2167 | )} | 2203 | )} |
| 2168 | </a> | 2204 | </a> |
| 2169 | ); | 2205 | ); |
| ... | @@ -2205,7 +2241,8 @@ function ChatMarkdown({ | ... | @@ -2205,7 +2241,8 @@ function ChatMarkdown({ |
| 2205 | </code> | 2241 | </code> |
| 2206 | ); | 2242 | ); |
| 2207 | }, | 2243 | }, |
| 2208 | img({ node, title, src, alt, ...props }) { | 2244 | img: function MarkdownImage({ node, title, src, alt, ...props }) { |
| | 2245 | const imageExpand = use(MarkdownLinkContext) ? undefined : onImageExpand; |
| 2209 | const localSrc = node?.properties?.dataLocalSrc; | 2246 | const localSrc = node?.properties?.dataLocalSrc; |
| 2210 | const markdownTitle = node?.properties?.dataMarkdownTitle; | 2247 | const markdownTitle = node?.properties?.dataMarkdownTitle; |
| 2211 | const authoredSrc = typeof localSrc === "string" ? localSrc : src; | 2248 | const authoredSrc = typeof localSrc === "string" ? localSrc : src; |
| ... | @@ -2225,8 +2262,13 @@ function ChatMarkdown({ | ... | @@ -2225,8 +2262,13 @@ function ChatMarkdown({ |
| 2225 | src={imageSource.uri} | 2262 | src={imageSource.uri} |
| 2226 | alt={altText} | 2263 | alt={altText} |
| 2227 | loading="lazy" | 2264 | 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 | )} |
| 2229 | style={authoredSizeStyle} | 2270 | style={authoredSizeStyle} |
| | 2271 | {...expandableMarkdownImageProps(imageExpand, imageSource.uri, altText)} |
| 2230 | /> | 2272 | /> |
| 2231 | ); | 2273 | ); |
| 2232 | } | 2274 | } |
| ... | @@ -2239,6 +2281,7 @@ function ChatMarkdown({ | ... | @@ -2239,6 +2281,7 @@ function ChatMarkdown({ |
| 2239 | copyMarkdown={copyMarkdown} | 2281 | copyMarkdown={copyMarkdown} |
| 2240 | srcFragment={markdownImageSourceFragment(classifiedSrc)} | 2282 | srcFragment={markdownImageSourceFragment(classifiedSrc)} |
| 2241 | style={authoredSizeStyle} | 2283 | style={authoredSizeStyle} |
| | 2284 | onImageExpand={imageExpand} |
| 2242 | /> | 2285 | /> |
| 2243 | ); | 2286 | ); |
| 2244 | } | 2287 | } |
| ... | @@ -2289,6 +2332,7 @@ function ChatMarkdown({ | ... | @@ -2289,6 +2332,7 @@ function ChatMarkdown({ |
| 2289 | isStreaming, | 2332 | isStreaming, |
| 2290 | markdownFileLinkMetaByHref, | 2333 | markdownFileLinkMetaByHref, |
| 2291 | onTaskListChange, | 2334 | onTaskListChange, |
| | 2335 | onImageExpand, |
| 2292 | openFileInPanel, | 2336 | openFileInPanel, |
| 2293 | openInPreferredEditor, | 2337 | openInPreferredEditor, |
| 2294 | openChangeRequestLink, | 2338 | openChangeRequestLink, |