| ... | @@ -1952,7 +1952,7 @@ function areMarkdownFileLinkPropsEqual( | ... | @@ -1952,7 +1952,7 @@ function areMarkdownFileLinkPropsEqual( |
| 1952 | ); | 1952 | ); |
| 1953 | } | 1953 | } |
| 1954 | | 1954 | |
| 1955 | function ChatMarkdown({ | 1955 | function useChatMarkdownState({ |
| 1956 | text, | 1956 | text, |
| 1957 | cwd, | 1957 | cwd, |
| 1958 | threadRef, | 1958 | threadRef, |
| ... | @@ -1960,13 +1960,9 @@ function ChatMarkdown({ | ... | @@ -1960,13 +1960,9 @@ function ChatMarkdown({ |
| 1960 | onTaskListChange, | 1960 | onTaskListChange, |
| 1961 | isStreaming = false, | 1961 | isStreaming = false, |
| 1962 | skills = EMPTY_MARKDOWN_SKILLS, | 1962 | skills = EMPTY_MARKDOWN_SKILLS, |
| 1963 | className, | | |
| 1964 | lineBreaks = false, | | |
| 1965 | parseRawHtml = true, | | |
| 1966 | onUseArtifactTemplate, | 1963 | onUseArtifactTemplate, |
| 1967 | imageBaseDir, | 1964 | imageBaseDir, |
| 1968 | onImageExpand, | 1965 | onImageExpand, |
| 1969 | extraRemarkPlugins = EMPTY_REMARK_PLUGINS, | | |
| 1970 | }: ChatMarkdownProps) { | 1966 | }: ChatMarkdownProps) { |
| 1971 | const { resolvedTheme } = useTheme(); | 1967 | const { resolvedTheme } = useTheme(); |
| 1972 | const [localMediaPreview, setLocalMediaPreview] = useState<ExpandedImagePreview | null>(null); | 1968 | const [localMediaPreview, setLocalMediaPreview] = useState<ExpandedImagePreview | null>(null); |
| ... | @@ -2270,11 +2266,8 @@ function ChatMarkdown({ | ... | @@ -2270,11 +2266,8 @@ function ChatMarkdown({ |
| 2270 | }, | 2266 | }, |
| 2271 | [cwd, findWorkspaceBasenameMatch, revealFileInFileManager], | 2267 | [cwd, findWorkspaceBasenameMatch, revealFileInFileManager], |
| 2272 | ); | 2268 | ); |
| 2273 | /* eslint-disable react/no-unstable-nested-components -- ReactMarkdown requires component | 2269 | const fileLinkChip = useCallback( |
| 2274 | * renderers that close over this message's metadata. useMemo keeps them stable until that | 2270 | ( |
| 2275 | * metadata changes. */ | | |
| 2276 | const markdownComponents = useMemo<Components>(() => { | | |
| 2277 | const fileLinkChip = ( | | |
| 2278 | fileLinkMeta: MarkdownFileLinkMeta, | 2271 | fileLinkMeta: MarkdownFileLinkMeta, |
| 2279 | copyMarkdown: string, | 2272 | copyMarkdown: string, |
| 2280 | className?: string, | 2273 | className?: string, |
| ... | @@ -2339,433 +2332,507 @@ function ChatMarkdown({ | ... | @@ -2339,433 +2332,507 @@ function ChatMarkdown({ |
| 2339 | className={className} | 2332 | className={className} |
| 2340 | /> | 2333 | /> |
| 2341 | ); | 2334 | ); |
| 2342 | }; | 2335 | }, |
| | 2336 | [ |
| | 2337 | canUseShellActions, |
| | 2338 | fileLinkParentSuffixByPath, |
| | 2339 | openFileInPanel, |
| | 2340 | openInPreferredEditor, |
| | 2341 | openMarkdownFileInPreview, |
| | 2342 | openMarkdownMedia, |
| | 2343 | preferredEditorMenuLabel, |
| | 2344 | resolvedTheme, |
| | 2345 | revealInFileManagerLabel, |
| | 2346 | revealMarkdownFileInFileManager, |
| | 2347 | threadRef, |
| | 2348 | ], |
| | 2349 | ); |
| 2343 | | 2350 | |
| 2344 | return { | 2351 | const componentState = useMemo( |
| 2345 | div({ node, children, ...props }) { | 2352 | () => ({ |
| 2346 | const artifactTemplate = artifactTemplateFromHastProperties(node?.properties); | 2353 | cwd, |
| 2347 | if (artifactTemplate) { | 2354 | diffThemeName, |
| 2348 | return ( | 2355 | expandMedia, |
| 2349 | <CodexArtifactTemplateCard template={artifactTemplate} onUse={onUseArtifactTemplate} /> | 2356 | fileLinkChip, |
| 2350 | ); | 2357 | imageBaseDir, |
| 2351 | } | 2358 | inlineCodeFileLinkMetaByText, |
| 2352 | return <div {...props}>{children}</div>; | 2359 | isStreaming, |
| 2353 | }, | 2360 | linkTargetPreference, |
| 2354 | p({ node: _node, children, ...props }) { | 2361 | markdownFileLinkMetaByHref, |
| 2355 | return <p {...props}>{renderSkillInlineMarkdownChildren(children, skills)}</p>; | 2362 | onTaskListChange, |
| 2356 | }, | 2363 | onUseArtifactTemplate, |
| 2357 | blockquote({ node: _node, children, ...props }) { | 2364 | openChangeRequestLink, |
| 2358 | const alert = | 2365 | openExternalLinkInPreview, |
| 2359 | GITHUB_ALERT_PRESENTATIONS[ | 2366 | openMarkdownMedia, |
| 2360 | String((props as Record<string, unknown>)["data-alert"] ?? "") | 2367 | resolveThreadPullRequest, |
| 2361 | ]; | 2368 | resolvedTheme, |
| 2362 | if (!alert) { | 2369 | skills, |
| 2363 | return <blockquote {...props}>{children}</blockquote>; | 2370 | text, |
| 2364 | } | 2371 | threadRef, |
| 2365 | // Not a <blockquote>: the stylesheet mutes those, and an alert's body is ordinary | 2372 | updateThreadPullRequestLink, |
| 2366 | // text under a colored title — which is how the host renders it. | 2373 | }), |
| 2367 | return ( | 2374 | [ |
| 2368 | <div role="note" className={cn("my-1 border-l-2 pl-3", alert.borderClassName)}> | 2375 | cwd, |
| 2369 | <p className={cn("flex items-center gap-1.5 font-medium", alert.titleClassName)}> | 2376 | diffThemeName, |
| 2370 | <alert.Icon aria-hidden className="size-3.5 shrink-0" /> | 2377 | expandMedia, |
| 2371 | {alert.label} | 2378 | fileLinkChip, |
| 2372 | </p> | 2379 | imageBaseDir, |
| 2373 | {children} | 2380 | inlineCodeFileLinkMetaByText, |
| 2374 | </div> | 2381 | isStreaming, |
| 2375 | ); | 2382 | linkTargetPreference, |
| 2376 | }, | 2383 | markdownFileLinkMetaByHref, |
| 2377 | ol({ node, start, style, ...props }) { | 2384 | onTaskListChange, |
| 2378 | const itemCount = | 2385 | onUseArtifactTemplate, |
| 2379 | node?.children?.filter((child) => child.type === "element" && child.tagName === "li") | 2386 | openChangeRequestLink, |
| 2380 | .length ?? 0; | 2387 | openExternalLinkInPreview, |
| 2381 | const gutterStyle = orderedListGutterStyle(itemCount, start); | 2388 | openMarkdownMedia, |
| 2382 | return ( | 2389 | resolveThreadPullRequest, |
| 2383 | <ol {...props} start={start} style={gutterStyle ? { ...style, ...gutterStyle } : style} /> | 2390 | resolvedTheme, |
| 2384 | ); | 2391 | skills, |
| 2385 | }, | 2392 | text, |
| 2386 | li({ node, children, ...props }) { | 2393 | threadRef, |
| 2387 | const listItemStart = node?.position?.start.offset; | 2394 | updateThreadPullRequestLink, |
| 2388 | const markerOffset = | 2395 | ], |
| 2389 | typeof listItemStart === "number" ? findTaskListMarkerOffset(text, listItemStart) : null; | 2396 | ); |
| 2390 | return ( | 2397 | return { |
| 2391 | <li {...props} data-task-marker-offset={markerOffset ?? undefined}> | 2398 | componentState, |
| 2392 | {renderSkillInlineMarkdownChildren(children, skills)} | 2399 | handleCopy, |
| 2393 | </li> | 2400 | markdownUrlTransform, |
| 2394 | ); | 2401 | localMediaPreview, |
| 2395 | }, | 2402 | setLocalMediaPreview, |
| 2396 | input({ node: _node, type, checked, disabled: _disabled, ...props }) { | 2403 | }; |
| 2397 | if (type !== "checkbox" || !onTaskListChange) { | 2404 | } |
| 2398 | return ( | 2405 | |
| 2399 | <input | 2406 | const ChatMarkdownRendererContext = React.createContext< |
| 2400 | {...props} | 2407 | ReturnType<typeof useChatMarkdownState>["componentState"] |
| 2401 | type={type} | 2408 | >(null!); |
| 2402 | checked={checked} | 2409 | |
| 2403 | disabled={_disabled} | 2410 | // Keep component types stable when streaming changes the message state. |
| 2404 | readOnly={type === "checkbox"} | 2411 | const CHAT_MARKDOWN_COMPONENTS = { |
| 2405 | /> | 2412 | div: function MarkdownDiv({ node, children, ...props }) { |
| 2406 | ); | 2413 | const { onUseArtifactTemplate } = use(ChatMarkdownRendererContext); |
| 2407 | } | 2414 | const artifactTemplate = artifactTemplateFromHastProperties(node?.properties); |
| 2408 | return ( | 2415 | if (artifactTemplate) { |
| 2409 | <input | 2416 | return ( |
| 2410 | {...props} | 2417 | <CodexArtifactTemplateCard template={artifactTemplate} onUse={onUseArtifactTemplate} /> |
| 2411 | type="checkbox" | 2418 | ); |
| 2412 | name="markdown-task" | 2419 | } |
| 2413 | aria-label="Toggle task" | 2420 | return <div {...props}>{children}</div>; |
| 2414 | checked={checked} | 2421 | }, |
| 2415 | onChange={(event) => { | 2422 | p: function MarkdownParagraph({ node: _node, children, ...props }) { |
| 2416 | const markerOffset = Number( | 2423 | const { skills } = use(ChatMarkdownRendererContext); |
| 2417 | event.currentTarget.closest("li")?.dataset.taskMarkerOffset, | 2424 | return <p {...props}>{renderSkillInlineMarkdownChildren(children, skills)}</p>; |
| | 2425 | }, |
| | 2426 | blockquote: function MarkdownBlockquote({ node: _node, children, ...props }) { |
| | 2427 | const alert = |
| | 2428 | GITHUB_ALERT_PRESENTATIONS[String((props as Record<string, unknown>)["data-alert"] ?? "")]; |
| | 2429 | if (!alert) { |
| | 2430 | return <blockquote {...props}>{children}</blockquote>; |
| | 2431 | } |
| | 2432 | // Not a <blockquote>: the stylesheet mutes those, and an alert's body is ordinary |
| | 2433 | // text under a colored title — which is how the host renders it. |
| | 2434 | return ( |
| | 2435 | <div role="note" className={cn("my-1 border-l-2 pl-3", alert.borderClassName)}> |
| | 2436 | <p className={cn("flex items-center gap-1.5 font-medium", alert.titleClassName)}> |
| | 2437 | <alert.Icon aria-hidden className="size-3.5 shrink-0" /> |
| | 2438 | {alert.label} |
| | 2439 | </p> |
| | 2440 | {children} |
| | 2441 | </div> |
| | 2442 | ); |
| | 2443 | }, |
| | 2444 | ol: function MarkdownOrderedList({ node, start, style, ...props }) { |
| | 2445 | const itemCount = |
| | 2446 | node?.children?.filter((child) => child.type === "element" && child.tagName === "li") |
| | 2447 | .length ?? 0; |
| | 2448 | const gutterStyle = orderedListGutterStyle(itemCount, start); |
| | 2449 | return ( |
| | 2450 | <ol {...props} start={start} style={gutterStyle ? { ...style, ...gutterStyle } : style} /> |
| | 2451 | ); |
| | 2452 | }, |
| | 2453 | li: function MarkdownListItem({ node, children, ...props }) { |
| | 2454 | const { text, skills } = use(ChatMarkdownRendererContext); |
| | 2455 | const listItemStart = node?.position?.start.offset; |
| | 2456 | const markerOffset = |
| | 2457 | typeof listItemStart === "number" ? findTaskListMarkerOffset(text, listItemStart) : null; |
| | 2458 | return ( |
| | 2459 | <li {...props} data-task-marker-offset={markerOffset ?? undefined}> |
| | 2460 | {renderSkillInlineMarkdownChildren(children, skills)} |
| | 2461 | </li> |
| | 2462 | ); |
| | 2463 | }, |
| | 2464 | input: function MarkdownInput({ node: _node, type, checked, disabled: _disabled, ...props }) { |
| | 2465 | const { onTaskListChange } = use(ChatMarkdownRendererContext); |
| | 2466 | if (type !== "checkbox" || !onTaskListChange) { |
| | 2467 | return ( |
| | 2468 | <input |
| | 2469 | {...props} |
| | 2470 | type={type} |
| | 2471 | checked={checked} |
| | 2472 | disabled={_disabled} |
| | 2473 | readOnly={type === "checkbox"} |
| | 2474 | /> |
| | 2475 | ); |
| | 2476 | } |
| | 2477 | return ( |
| | 2478 | <input |
| | 2479 | {...props} |
| | 2480 | type="checkbox" |
| | 2481 | name="markdown-task" |
| | 2482 | aria-label="Toggle task" |
| | 2483 | checked={checked} |
| | 2484 | onChange={(event) => { |
| | 2485 | const markerOffset = Number(event.currentTarget.closest("li")?.dataset.taskMarkerOffset); |
| | 2486 | if (!Number.isSafeInteger(markerOffset)) return; |
| | 2487 | onTaskListChange({ markerOffset, checked: event.currentTarget.checked }); |
| | 2488 | }} |
| | 2489 | /> |
| | 2490 | ); |
| | 2491 | }, |
| | 2492 | a: function MarkdownAnchor({ node, href, children, title: _title, ...props }) { |
| | 2493 | const { |
| | 2494 | cwd, |
| | 2495 | imageBaseDir, |
| | 2496 | markdownFileLinkMetaByHref, |
| | 2497 | threadRef, |
| | 2498 | openMarkdownMedia, |
| | 2499 | openChangeRequestLink, |
| | 2500 | linkTargetPreference, |
| | 2501 | openExternalLinkInPreview, |
| | 2502 | resolveThreadPullRequest, |
| | 2503 | updateThreadPullRequestLink, |
| | 2504 | fileLinkChip, |
| | 2505 | } = use(ChatMarkdownRendererContext); |
| | 2506 | const citation = href ? parseAssistantCitationHref(href) : null; |
| | 2507 | if (citation) return <AssistantCitationChip citation={citation} />; |
| | 2508 | const normalizedHref = href ? normalizeMarkdownLinkHrefKey(href) : ""; |
| | 2509 | const fileLinkMeta = normalizedHref |
| | 2510 | ? (markdownFileLinkMetaByHref.get(normalizedHref) ?? |
| | 2511 | resolveMarkdownFileLinkMeta(normalizedHref, cwd, imageBaseDir ?? cwd)) |
| | 2512 | : null; |
| | 2513 | if (!fileLinkMeta) { |
| | 2514 | const faviconHost = resolveExternalWebLinkHost(href); |
| | 2515 | const pullRequestAutolink = String( |
| | 2516 | (props as Record<string, unknown>)["data-pull-request-autolink"] ?? "", |
| | 2517 | ); |
| | 2518 | const pullRequestCopy = |
| | 2519 | pullRequestAutolink === "commit" |
| | 2520 | ? /\/commit\/([0-9a-f]{40})$/iu.exec(href ?? "")?.[1] |
| | 2521 | : pullRequestAutolink === "reference" |
| | 2522 | ? plainHastText(node) |
| | 2523 | : undefined; |
| | 2524 | const isPullRequestAutolink = pullRequestCopy !== undefined; |
| | 2525 | const isSameDocumentLink = href?.startsWith("#") ?? false; |
| | 2526 | const onClick = props.onClick; |
| | 2527 | const canOpenInPreview = Boolean(threadRef) && isPreviewSupportedInRuntime(); |
| | 2528 | const linkChildren = <MarkdownLinkContext value>{children}</MarkdownLinkContext>; |
| | 2529 | const link = ( |
| | 2530 | <a |
| | 2531 | {...props} |
| | 2532 | className={cn(props.className, pullRequestAutolink === "commit" && "font-mono")} |
| | 2533 | data-markdown-copy={pullRequestCopy} |
| | 2534 | href={href} |
| | 2535 | target={isSameDocumentLink ? undefined : "_blank"} |
| | 2536 | rel={isSameDocumentLink ? undefined : "noopener noreferrer"} |
| | 2537 | onClick={(event) => { |
| | 2538 | onClick?.(event); |
| | 2539 | if (isSameDocumentLink && href) { |
| | 2540 | handleMarkdownFragmentClick(event, href); |
| | 2541 | return; |
| | 2542 | } |
| | 2543 | if ( |
| | 2544 | href && |
| | 2545 | faviconHost !== null && |
| | 2546 | mediaKindFromPath(href) !== null && |
| | 2547 | !event.defaultPrevented && |
| | 2548 | !event.metaKey && |
| | 2549 | !event.ctrlKey && |
| | 2550 | !event.shiftKey && |
| | 2551 | !event.altKey |
| | 2552 | ) { |
| | 2553 | event.preventDefault(); |
| | 2554 | event.stopPropagation(); |
| | 2555 | openMarkdownMedia(href); |
| | 2556 | return; |
| | 2557 | } |
| | 2558 | // A link to a change request in a workspace project opens beside the |
| | 2559 | // conversation instead of in a browser: it is the thing being talked about, and |
| | 2560 | // the panel it opens offers the browser as one of its actions. |
| | 2561 | if (!href || openChangeRequestLink(event, href)) return; |
| | 2562 | // Anything else follows the "Open links in" setting. The system browser |
| | 2563 | // keeps the `_blank` the shell already handles; the in-app browser needs |
| | 2564 | // the click intercepted here. A modifier click is the way out of the |
| | 2565 | // in-app default, so it is left to the shell too. |
| | 2566 | if ( |
| | 2567 | event.defaultPrevented || |
| | 2568 | resolveLinkTarget({ |
| | 2569 | url: href, |
| | 2570 | event, |
| | 2571 | preference: linkTargetPreference, |
| | 2572 | canOpenInApp: canOpenInPreview, |
| | 2573 | }) !== "app" |
| | 2574 | ) { |
| | 2575 | return; |
| | 2576 | } |
| | 2577 | event.preventDefault(); |
| | 2578 | event.stopPropagation(); |
| | 2579 | // The click was taken from the shell, so an in-app open that fails |
| | 2580 | // hands the link to the system browser instead of dropping it. |
| | 2581 | void openExternalLinkInPreview(href).then((result) => { |
| | 2582 | if (result._tag === "Success" || isAtomCommandInterrupted(result)) return; |
| | 2583 | reportMarkdownActionFailure( |
| | 2584 | { operation: "open-link-in-preview", target: href }, |
| | 2585 | result.cause, |
| 2418 | ); | 2586 | ); |
| 2419 | if (!Number.isSafeInteger(markerOffset)) return; | 2587 | void readLocalApi()?.shell.openExternal(href); |
| 2420 | onTaskListChange({ markerOffset, checked: event.currentTarget.checked }); | 2588 | }); |
| 2421 | }} | 2589 | }} |
| 2422 | /> | 2590 | onContextMenu={(event) => { |
| 2423 | ); | 2591 | if (!href || !faviconHost) return; |
| 2424 | }, | 2592 | event.preventDefault(); |
| 2425 | a({ node, href, children, title: _title, ...props }) { | 2593 | event.stopPropagation(); |
| 2426 | const citation = href ? parseAssistantCitationHref(href) : null; | 2594 | const api = readLocalApi(); |
| 2427 | if (citation) return <AssistantCitationChip citation={citation} />; | 2595 | if (!api) return; |
| 2428 | const normalizedHref = href ? normalizeMarkdownLinkHrefKey(href) : ""; | 2596 | const pullRequest = resolveThreadPullRequest(href); |
| 2429 | const fileLinkMeta = normalizedHref | 2597 | const currentPullRequest = |
| 2430 | ? (markdownFileLinkMetaByHref.get(normalizedHref) ?? | 2598 | threadRef === undefined ? null : readThreadShell(threadRef)?.linkedPullRequest; |
| 2431 | resolveMarkdownFileLinkMeta(normalizedHref, cwd, imageBaseDir ?? cwd)) | 2599 | const threadLinkAction = |
| 2432 | : null; | 2600 | currentPullRequest != null && matchesLinkedPullRequestUrl(currentPullRequest, href) |
| 2433 | if (!fileLinkMeta) { | 2601 | ? "unlink-from-thread" |
| 2434 | const faviconHost = resolveExternalWebLinkHost(href); | 2602 | : pullRequest === null |
| 2435 | const pullRequestAutolink = String( | 2603 | ? undefined |
| 2436 | (props as Record<string, unknown>)["data-pull-request-autolink"] ?? "", | 2604 | : "link-to-thread"; |
| 2437 | ); | 2605 | void showExternalLinkContextMenu({ |
| 2438 | const pullRequestCopy = | 2606 | href, |
| 2439 | pullRequestAutolink === "commit" | 2607 | canOpenInPreview, |
| 2440 | ? /\/commit\/([0-9a-f]{40})$/iu.exec(href ?? "")?.[1] | 2608 | threadLinkAction, |
| 2441 | : pullRequestAutolink === "reference" | 2609 | position: { x: event.clientX, y: event.clientY }, |
| 2442 | ? plainHastText(node) | 2610 | showContextMenu: (items, position) => api.contextMenu.show(items, position), |
| 2443 | : undefined; | 2611 | openInPreview: async (target) => { |
| 2444 | const isPullRequestAutolink = pullRequestCopy !== undefined; | 2612 | const result = await openExternalLinkInPreview(target); |
| 2445 | const isSameDocumentLink = href?.startsWith("#") ?? false; | 2613 | if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { |
| 2446 | const onClick = props.onClick; | 2614 | reportMarkdownActionFailure( |
| 2447 | const canOpenInPreview = Boolean(threadRef) && isPreviewSupportedInRuntime(); | 2615 | { operation: "open-link-in-preview", target }, |
| 2448 | const linkChildren = <MarkdownLinkContext value>{children}</MarkdownLinkContext>; | 2616 | result.cause, |
| 2449 | const link = ( | 2617 | ); |
| 2450 | <a | | |
| 2451 | {...props} | | |
| 2452 | className={cn(props.className, pullRequestAutolink === "commit" && "font-mono")} | | |
| 2453 | data-markdown-copy={pullRequestCopy} | | |
| 2454 | href={href} | | |
| 2455 | target={isSameDocumentLink ? undefined : "_blank"} | | |
| 2456 | rel={isSameDocumentLink ? undefined : "noopener noreferrer"} | | |
| 2457 | onClick={(event) => { | | |
| 2458 | onClick?.(event); | | |
| 2459 | if (isSameDocumentLink && href) { | | |
| 2460 | handleMarkdownFragmentClick(event, href); | | |
| 2461 | return; | | |
| 2462 | } | | |
| 2463 | if ( | | |
| 2464 | href && | | |
| 2465 | faviconHost !== null && | | |
| 2466 | mediaKindFromPath(href) !== null && | | |
| 2467 | !event.defaultPrevented && | | |
| 2468 | !event.metaKey && | | |
| 2469 | !event.ctrlKey && | | |
| 2470 | !event.shiftKey && | | |
| 2471 | !event.altKey | | |
| 2472 | ) { | | |
| 2473 | event.preventDefault(); | | |
| 2474 | event.stopPropagation(); | | |
| 2475 | openMarkdownMedia(href); | | |
| 2476 | return; | | |
| 2477 | } | 2618 | } |
| 2478 | // A link to a change request in a workspace project opens beside the | 2619 | }, |
| 2479 | // conversation instead of in a browser: it is the thing being talked about, and | 2620 | openExternal: (target) => api.shell.openExternal(target), |
| 2480 | // the panel it opens offers the browser as one of its actions. | 2621 | copyLink: (target) => writeTextToClipboard(target, "link"), |
| 2481 | if (!href || openChangeRequestLink(event, href)) return; | 2622 | updateThreadLink: updateThreadPullRequestLink, |
| 2482 | // Anything else follows the "Open links in" setting. The system browser | 2623 | reportFailure: (operation, cause) => { |
| 2483 | // keeps the `_blank` the shell already handles; the in-app browser needs | 2624 | reportMarkdownActionFailure({ operation, target: href }, cause); |
| 2484 | // the click intercepted here. A modifier click is the way out of the | | |
| 2485 | // in-app default, so it is left to the shell too. | | |
| 2486 | if ( | 2625 | if ( |
| 2487 | event.defaultPrevented || | 2626 | operation === "link-pull-request-to-thread" || |
| 2488 | resolveLinkTarget({ | 2627 | operation === "unlink-pull-request-from-thread" |
| 2489 | url: href, | | |
| 2490 | event, | | |
| 2491 | preference: linkTargetPreference, | | |
| 2492 | canOpenInApp: canOpenInPreview, | | |
| 2493 | }) !== "app" | | |
| 2494 | ) { | 2628 | ) { |
| 2495 | return; | 2629 | toastManager.add( |
| 2496 | } | 2630 | stackedThreadToast({ |
| 2497 | event.preventDefault(); | 2631 | type: "error", |
| 2498 | event.stopPropagation(); | 2632 | title: |
| 2499 | // The click was taken from the shell, so an in-app open that fails | 2633 | operation === "link-pull-request-to-thread" |
| 2500 | // hands the link to the system browser instead of dropping it. | 2634 | ? "Unable to link pull request" |
| 2501 | void openExternalLinkInPreview(href).then((result) => { | 2635 | : "Unable to unlink pull request", |
| 2502 | if (result._tag === "Success" || isAtomCommandInterrupted(result)) return; | 2636 | description: cause instanceof Error ? cause.message : "The request failed.", |
| 2503 | reportMarkdownActionFailure( | 2637 | }), |
| 2504 | { operation: "open-link-in-preview", target: href }, | | |
| 2505 | result.cause, | | |
| 2506 | ); | 2638 | ); |
| 2507 | void readLocalApi()?.shell.openExternal(href); | 2639 | } |
| 2508 | }); | 2640 | }, |
| 2509 | }} | 2641 | }); |
| 2510 | onContextMenu={(event) => { | 2642 | }} |
| 2511 | if (!href || !faviconHost) return; | 2643 | > |
| 2512 | event.preventDefault(); | 2644 | {faviconHost && hastHasText(node) && !isPullRequestAutolink ? ( |
| 2513 | event.stopPropagation(); | 2645 | <MarkdownExternalLinkContent host={faviconHost} plainText={plainHastText(node)}> |
| 2514 | const api = readLocalApi(); | 2646 | {linkChildren} |
| 2515 | if (!api) return; | 2647 | </MarkdownExternalLinkContent> |
| 2516 | const pullRequest = resolveThreadPullRequest(href); | 2648 | ) : ( |
| 2517 | const currentPullRequest = | 2649 | linkChildren |
| 2518 | threadRef === undefined ? null : readThreadShell(threadRef)?.linkedPullRequest; | 2650 | )} |
| 2519 | const threadLinkAction = | 2651 | </a> |
| 2520 | currentPullRequest != null && | 2652 | ); |
| 2521 | matchesLinkedPullRequestUrl(currentPullRequest, href) | 2653 | if (!faviconHost || !href) { |
| 2522 | ? "unlink-from-thread" | 2654 | return link; |
| 2523 | : pullRequest === null | 2655 | } |
| 2524 | ? undefined | 2656 | return ( |
| 2525 | : "link-to-thread"; | 2657 | <Tooltip> |
| 2526 | void showExternalLinkContextMenu({ | 2658 | <TooltipTrigger render={link} /> |
| 2527 | href, | 2659 | <TooltipPopup |
| 2528 | canOpenInPreview, | 2660 | side="top" |
| 2529 | threadLinkAction, | 2661 | className="max-w-[min(36rem,calc(100vw-2rem))] whitespace-normal leading-tight wrap-anywhere" |
| 2530 | position: { x: event.clientX, y: event.clientY }, | 2662 | > |
| 2531 | showContextMenu: (items, position) => api.contextMenu.show(items, position), | 2663 | {href} |
| 2532 | openInPreview: async (target) => { | 2664 | </TooltipPopup> |
| 2533 | const result = await openExternalLinkInPreview(target); | 2665 | </Tooltip> |
| 2534 | if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { | 2666 | ); |
| 2535 | reportMarkdownActionFailure( | 2667 | } |
| 2536 | { operation: "open-link-in-preview", target }, | | |
| 2537 | result.cause, | | |
| 2538 | ); | | |
| 2539 | } | | |
| 2540 | }, | | |
| 2541 | openExternal: (target) => api.shell.openExternal(target), | | |
| 2542 | copyLink: (target) => writeTextToClipboard(target, "link"), | | |
| 2543 | updateThreadLink: updateThreadPullRequestLink, | | |
| 2544 | reportFailure: (operation, cause) => { | | |
| 2545 | reportMarkdownActionFailure({ operation, target: href }, cause); | | |
| 2546 | if ( | | |
| 2547 | operation === "link-pull-request-to-thread" || | | |
| 2548 | operation === "unlink-pull-request-from-thread" | | |
| 2549 | ) { | | |
| 2550 | toastManager.add( | | |
| 2551 | stackedThreadToast({ | | |
| 2552 | type: "error", | | |
| 2553 | title: | | |
| 2554 | operation === "link-pull-request-to-thread" | | |
| 2555 | ? "Unable to link pull request" | | |
| 2556 | : "Unable to unlink pull request", | | |
| 2557 | description: | | |
| 2558 | cause instanceof Error ? cause.message : "The request failed.", | | |
| 2559 | }), | | |
| 2560 | ); | | |
| 2561 | } | | |
| 2562 | }, | | |
| 2563 | }); | | |
| 2564 | }} | | |
| 2565 | > | | |
| 2566 | {faviconHost && hastHasText(node) && !isPullRequestAutolink ? ( | | |
| 2567 | <MarkdownExternalLinkContent host={faviconHost} plainText={plainHastText(node)}> | | |
| 2568 | {linkChildren} | | |
| 2569 | </MarkdownExternalLinkContent> | | |
| 2570 | ) : ( | | |
| 2571 | linkChildren | | |
| 2572 | )} | | |
| 2573 | </a> | | |
| 2574 | ); | | |
| 2575 | if (!faviconHost || !href) { | | |
| 2576 | return link; | | |
| 2577 | } | | |
| 2578 | return ( | | |
| 2579 | <Tooltip> | | |
| 2580 | <TooltipTrigger render={link} /> | | |
| 2581 | <TooltipPopup | | |
| 2582 | side="top" | | |
| 2583 | className="max-w-[min(36rem,calc(100vw-2rem))] whitespace-normal leading-tight wrap-anywhere" | | |
| 2584 | > | | |
| 2585 | {href} | | |
| 2586 | </TooltipPopup> | | |
| 2587 | </Tooltip> | | |
| 2588 | ); | | |
| 2589 | } | | |
| 2590 | | 2668 | |
| | 2669 | return fileLinkChip( |
| | 2670 | fileLinkMeta, |
| | 2671 | `[${fileLinkMeta.basename}](${normalizedHref})`, |
| | 2672 | props.className, |
| | 2673 | normalizedHref, |
| | 2674 | ); |
| | 2675 | }, |
| | 2676 | code: function MarkdownCode({ node, children, className, ...props }) { |
| | 2677 | const { cwd, imageBaseDir, inlineCodeFileLinkMetaByText, fileLinkChip } = use( |
| | 2678 | ChatMarkdownRendererContext, |
| | 2679 | ); |
| | 2680 | if (node?.properties?.dataInlineCode != null) { |
| | 2681 | const codeText = nodeToPlainText(children); |
| | 2682 | const fileLinkMeta = |
| | 2683 | inlineCodeFileLinkMetaByText.get(codeText.trim()) ?? |
| | 2684 | resolveInlineCodeFileLinkMeta(codeText, cwd, imageBaseDir ?? cwd); |
| | 2685 | if (fileLinkMeta) { |
| 2591 | return fileLinkChip( | 2686 | return fileLinkChip( |
| 2592 | fileLinkMeta, | 2687 | fileLinkMeta, |
| 2593 | `[${fileLinkMeta.basename}](${normalizedHref})`, | 2688 | `\`${codeText}\``, |
| 2594 | props.className, | 2689 | undefined, |
| 2595 | normalizedHref, | 2690 | inlineCodeFilePathCandidate(codeText) ?? codeText.trim(), |
| 2596 | ); | 2691 | ); |
| 2597 | }, | 2692 | } |
| 2598 | code({ node, children, className, ...props }) { | 2693 | } |
| 2599 | if (node?.properties?.dataInlineCode != null) { | 2694 | return ( |
| 2600 | const codeText = nodeToPlainText(children); | 2695 | <code {...props} className={className}> |
| 2601 | const fileLinkMeta = | 2696 | {children} |
| 2602 | inlineCodeFileLinkMetaByText.get(codeText.trim()) ?? | 2697 | </code> |
| 2603 | resolveInlineCodeFileLinkMeta(codeText, cwd, imageBaseDir ?? cwd); | 2698 | ); |
| 2604 | if (fileLinkMeta) { | 2699 | }, |
| 2605 | return fileLinkChip( | 2700 | img: function MarkdownImage({ node, title, src, alt, ...props }) { |
| 2606 | fileLinkMeta, | 2701 | const { expandMedia, cwd, imageBaseDir, threadRef } = use(ChatMarkdownRendererContext); |
| 2607 | `\`${codeText}\``, | 2702 | const imageExpand = use(MarkdownLinkContext) ? undefined : expandMedia; |
| 2608 | undefined, | 2703 | const localSrc = node?.properties?.dataLocalSrc; |
| 2609 | inlineCodeFilePathCandidate(codeText) ?? codeText.trim(), | 2704 | const markdownTitle = node?.properties?.dataMarkdownTitle; |
| 2610 | ); | 2705 | const authoredSrc = typeof localSrc === "string" ? localSrc : src; |
| 2611 | } | 2706 | const authoredTitle = typeof markdownTitle === "string" ? markdownTitle : title; |
| 2612 | } | 2707 | const srcString = |
| | 2708 | typeof authoredSrc === "string" ? normalizeMarkdownLinkDestination(authoredSrc) : ""; |
| | 2709 | const classifiedSrc = |
| | 2710 | typeof localSrc === "string" ? srcString.replaceAll("\\", "/") : srcString; |
| | 2711 | const altText = alt ?? ""; |
| | 2712 | const copyMarkdown = markdownImageCopy(altText, srcString, authoredTitle); |
| | 2713 | const authoredSizeStyle = authoredImageSizeStyle(props.width, props.height); |
| | 2714 | const imageSource = classifyMarkdownImageSource(classifiedSrc, imageBaseDir ?? cwd); |
| | 2715 | const kind = mediaKindFromPath(classifiedSrc) ?? "image"; |
| | 2716 | if (imageSource._tag === "Direct") { |
| | 2717 | const mediaSrc = resolveProtocolRelativeMediaUrl(imageSource.uri); |
| | 2718 | const originalUrl = |
| | 2719 | resolveExternalWebLinkHost(imageSource.uri) !== null ? imageSource.uri : undefined; |
| | 2720 | const reference = mediaUrlReference(imageSource.uri); |
| | 2721 | const actionsSource: MediaActionSource = { |
| | 2722 | kind, |
| | 2723 | name: altText || kind, |
| | 2724 | src: mediaSrc, |
| | 2725 | ...(reference ? { reference } : {}), |
| | 2726 | }; |
| | 2727 | if (kind === "video") { |
| 2613 | return ( | 2728 | return ( |
| 2614 | <code {...props} className={className}> | 2729 | <ChatMarkdownVideo |
| 2615 | {children} | 2730 | src={mediaSrc} |
| 2616 | </code> | 2731 | alt={altText} |
| | 2732 | copyMarkdown={copyMarkdown} |
| | 2733 | originalUrl={originalUrl} |
| | 2734 | style={authoredSizeStyle} |
| | 2735 | actionsSource={actionsSource} |
| | 2736 | /> |
| 2617 | ); | 2737 | ); |
| 2618 | }, | 2738 | } |
| 2619 | img: function MarkdownImage({ node, title, src, alt, ...props }) { | 2739 | return ( |
| 2620 | const imageExpand = use(MarkdownLinkContext) ? undefined : expandMedia; | 2740 | <MediaActions source={actionsSource}> |
| 2621 | const localSrc = node?.properties?.dataLocalSrc; | 2741 | <img |
| 2622 | const markdownTitle = node?.properties?.dataMarkdownTitle; | 2742 | {...props} |
| 2623 | const authoredSrc = typeof localSrc === "string" ? localSrc : src; | 2743 | src={mediaSrc} |
| 2624 | const authoredTitle = typeof markdownTitle === "string" ? markdownTitle : title; | 2744 | alt={altText} |
| 2625 | const srcString = | 2745 | loading="lazy" |
| 2626 | typeof authoredSrc === "string" ? normalizeMarkdownLinkDestination(authoredSrc) : ""; | 2746 | className={cn( |
| 2627 | const classifiedSrc = | 2747 | props.className, |
| 2628 | typeof localSrc === "string" ? srcString.replaceAll("\\", "/") : srcString; | 2748 | CHAT_MARKDOWN_IMAGE_SIZE_CLASS_NAME, |
| 2629 | const altText = alt ?? ""; | 2749 | imageExpand && "cursor-zoom-in", |
| 2630 | const copyMarkdown = markdownImageCopy(altText, srcString, authoredTitle); | 2750 | )} |
| 2631 | const authoredSizeStyle = authoredImageSizeStyle(props.width, props.height); | 2751 | style={authoredSizeStyle} |
| 2632 | const imageSource = classifyMarkdownImageSource(classifiedSrc, imageBaseDir ?? cwd); | 2752 | {...expandableMarkdownImageProps( |
| 2633 | const kind = mediaKindFromPath(classifiedSrc) ?? "image"; | 2753 | imageExpand, |
| 2634 | if (imageSource._tag === "Direct") { | 2754 | mediaSrc, |
| 2635 | const mediaSrc = resolveProtocolRelativeMediaUrl(imageSource.uri); | 2755 | altText, |
| 2636 | const originalUrl = | 2756 | originalUrl, |
| 2637 | resolveExternalWebLinkHost(imageSource.uri) !== null ? imageSource.uri : undefined; | 2757 | actionsSource, |
| 2638 | const reference = mediaUrlReference(imageSource.uri); | 2758 | )} |
| 2639 | const actionsSource: MediaActionSource = { | 2759 | /> |
| 2640 | kind, | 2760 | </MediaActions> |
| 2641 | name: altText || kind, | 2761 | ); |
| 2642 | src: mediaSrc, | 2762 | } |
| 2643 | ...(reference ? { reference } : {}), | 2763 | if (imageSource._tag === "WorkspaceFile" && threadRef) { |
| 2644 | }; | 2764 | return ( |
| 2645 | if (kind === "video") { | 2765 | <ChatMarkdownAssetImage |
| 2646 | return ( | 2766 | environmentId={threadRef.environmentId} |
| 2647 | <ChatMarkdownVideo | 2767 | resource={{ |
| 2648 | src={mediaSrc} | 2768 | _tag: "media-file", |
| 2649 | alt={altText} | 2769 | threadId: threadRef.threadId, |
| 2650 | copyMarkdown={copyMarkdown} | 2770 | path: imageSource.path, |
| 2651 | originalUrl={originalUrl} | 2771 | }} |
| 2652 | style={authoredSizeStyle} | 2772 | alt={altText} |
| 2653 | actionsSource={actionsSource} | 2773 | kind={kind} |
| 2654 | /> | 2774 | copyMarkdown={copyMarkdown} |
| 2655 | ); | 2775 | srcFragment={markdownImageSourceFragment(classifiedSrc)} |
| 2656 | } | 2776 | style={authoredSizeStyle} |
| 2657 | return ( | 2777 | workspaceRoot={cwd} |
| 2658 | <MediaActions source={actionsSource}> | 2778 | onImageExpand={imageExpand} |
| 2659 | <img | 2779 | /> |
| 2660 | {...props} | 2780 | ); |
| 2661 | src={mediaSrc} | 2781 | } |
| 2662 | alt={altText} | 2782 | return <ChatMarkdownImageFallback alt={altText} copyMarkdown={copyMarkdown} kind={kind} />; |
| 2663 | loading="lazy" | 2783 | }, |
| 2664 | className={cn( | 2784 | table: function MarkdownTableRenderer({ node: _node, ...props }) { |
| 2665 | props.className, | 2785 | return <MarkdownTable {...props} />; |
| 2666 | CHAT_MARKDOWN_IMAGE_SIZE_CLASS_NAME, | 2786 | }, |
| 2667 | imageExpand && "cursor-zoom-in", | 2787 | details: function MarkdownDetailsRenderer({ node: _node, children, open: detailsOpen }) { |
| 2668 | )} | 2788 | return <MarkdownDetails open={detailsOpen}>{children}</MarkdownDetails>; |
| 2669 | style={authoredSizeStyle} | 2789 | }, |
| 2670 | {...expandableMarkdownImageProps( | 2790 | pre: function MarkdownPre({ node, children, ...props }) { |
| 2671 | imageExpand, | 2791 | const { resolvedTheme, diffThemeName, isStreaming } = use(ChatMarkdownRendererContext); |
| 2672 | mediaSrc, | 2792 | const codeBlock = extractCodeBlock(children); |
| 2673 | altText, | 2793 | if (!codeBlock) { |
| 2674 | originalUrl, | 2794 | return <pre {...props}>{children}</pre>; |
| 2675 | actionsSource, | 2795 | } |
| 2676 | )} | | |
| 2677 | /> | | |
| 2678 | </MediaActions> | | |
| 2679 | ); | | |
| 2680 | } | | |
| 2681 | if (imageSource._tag === "WorkspaceFile" && threadRef) { | | |
| 2682 | return ( | | |
| 2683 | <ChatMarkdownAssetImage | | |
| 2684 | environmentId={threadRef.environmentId} | | |
| 2685 | resource={{ | | |
| 2686 | _tag: "media-file", | | |
| 2687 | threadId: threadRef.threadId, | | |
| 2688 | path: imageSource.path, | | |
| 2689 | }} | | |
| 2690 | alt={altText} | | |
| 2691 | kind={kind} | | |
| 2692 | copyMarkdown={copyMarkdown} | | |
| 2693 | srcFragment={markdownImageSourceFragment(classifiedSrc)} | | |
| 2694 | style={authoredSizeStyle} | | |
| 2695 | workspaceRoot={cwd} | | |
| 2696 | onImageExpand={imageExpand} | | |
| 2697 | /> | | |
| 2698 | ); | | |
| 2699 | } | | |
| 2700 | return <ChatMarkdownImageFallback alt={altText} copyMarkdown={copyMarkdown} kind={kind} />; | | |
| 2701 | }, | | |
| 2702 | table({ node: _node, ...props }) { | | |
| 2703 | return <MarkdownTable {...props} />; | | |
| 2704 | }, | | |
| 2705 | details({ node: _node, children, open: detailsOpen }) { | | |
| 2706 | return <MarkdownDetails open={detailsOpen}>{children}</MarkdownDetails>; | | |
| 2707 | }, | | |
| 2708 | pre({ node, children, ...props }) { | | |
| 2709 | const codeBlock = extractCodeBlock(children); | | |
| 2710 | if (!codeBlock) { | | |
| 2711 | return <pre {...props}>{children}</pre>; | | |
| 2712 | } | | |
| 2713 | | 2796 | |
| 2714 | const language = extractFenceLanguage(codeBlock.className); | 2797 | const language = extractFenceLanguage(codeBlock.className); |
| 2715 | const fenceTitle = extractFenceTitle(extractPreCodeMeta(node)); | 2798 | const fenceTitle = extractFenceTitle(extractPreCodeMeta(node)); |
| 2716 | return ( | 2799 | return ( |
| 2717 | <MarkdownCodeBlock | 2800 | <MarkdownCodeBlock |
| 2718 | code={codeBlock.code} | 2801 | code={codeBlock.code} |
| 2719 | language={language} | 2802 | language={language} |
| 2720 | fenceTitle={fenceTitle} | 2803 | fenceTitle={fenceTitle} |
| 2721 | theme={resolvedTheme} | 2804 | theme={resolvedTheme} |
| 2722 | > | 2805 | > |
| 2723 | <RenderErrorBoundary fallback={<pre {...props}>{children}</pre>}> | 2806 | <RenderErrorBoundary fallback={<pre {...props}>{children}</pre>}> |
| 2724 | <Suspense fallback={<pre {...props}>{children}</pre>}> | 2807 | <Suspense fallback={<pre {...props}>{children}</pre>}> |
| 2725 | <SuspenseShikiCodeBlock | 2808 | <SuspenseShikiCodeBlock |
| 2726 | className={codeBlock.className} | 2809 | className={codeBlock.className} |
| 2727 | code={codeBlock.code} | 2810 | code={codeBlock.code} |
| 2728 | themeName={diffThemeName} | 2811 | themeName={diffThemeName} |
| 2729 | isStreaming={isStreaming} | 2812 | isStreaming={isStreaming} |
| 2730 | /> | 2813 | /> |
| 2731 | </Suspense> | 2814 | </Suspense> |
| 2732 | </RenderErrorBoundary> | 2815 | </RenderErrorBoundary> |
| 2733 | </MarkdownCodeBlock> | 2816 | </MarkdownCodeBlock> |
| 2734 | ); | 2817 | ); |
| 2735 | }, | 2818 | }, |
| 2736 | }; | 2819 | } satisfies Components; |
| 2737 | }, [ | | |
| 2738 | canUseShellActions, | | |
| 2739 | cwd, | | |
| 2740 | diffThemeName, | | |
| 2741 | fileLinkParentSuffixByPath, | | |
| 2742 | inlineCodeFileLinkMetaByText, | | |
| 2743 | imageBaseDir, | | |
| 2744 | isStreaming, | | |
| 2745 | linkTargetPreference, | | |
| 2746 | markdownFileLinkMetaByHref, | | |
| 2747 | onTaskListChange, | | |
| 2748 | onUseArtifactTemplate, | | |
| 2749 | onImageExpand, | | |
| 2750 | expandMedia, | | |
| 2751 | openMarkdownMedia, | | |
| 2752 | openFileInPanel, | | |
| 2753 | openInPreferredEditor, | | |
| 2754 | openChangeRequestLink, | | |
| 2755 | openExternalLinkInPreview, | | |
| 2756 | openMarkdownFileInPreview, | | |
| 2757 | preferredEditorMenuLabel, | | |
| 2758 | resolveThreadPullRequest, | | |
| 2759 | resolvedTheme, | | |
| 2760 | revealMarkdownFileInFileManager, | | |
| 2761 | revealInFileManagerLabel, | | |
| 2762 | skills, | | |
| 2763 | text, | | |
| 2764 | threadRef, | | |
| 2765 | updateThreadPullRequestLink, | | |
| 2766 | ]); | | |
| 2767 | /* eslint-enable react/no-unstable-nested-components */ | | |
| 2768 | | 2820 | |
| | 2821 | function ChatMarkdown({ |
| | 2822 | text, |
| | 2823 | className, |
| | 2824 | lineBreaks = false, |
| | 2825 | parseRawHtml = true, |
| | 2826 | extraRemarkPlugins = EMPTY_REMARK_PLUGINS, |
| | 2827 | ...props |
| | 2828 | }: ChatMarkdownProps) { |
| | 2829 | const { |
| | 2830 | componentState, |
| | 2831 | handleCopy, |
| | 2832 | markdownUrlTransform, |
| | 2833 | localMediaPreview, |
| | 2834 | setLocalMediaPreview, |
| | 2835 | } = useChatMarkdownState({ text, ...props }); |
| 2769 | const remarkPlugins = useMemo( | 2836 | const remarkPlugins = useMemo( |
| 2770 | () => [ | 2837 | () => [ |
| 2771 | ...(lineBreaks ? CHAT_MARKDOWN_REMARK_PLUGINS_WITH_BREAKS : CHAT_MARKDOWN_REMARK_PLUGINS), | 2838 | ...(lineBreaks ? CHAT_MARKDOWN_REMARK_PLUGINS_WITH_BREAKS : CHAT_MARKDOWN_REMARK_PLUGINS), |
| ... | @@ -2785,15 +2852,17 @@ function ChatMarkdown({ | ... | @@ -2785,15 +2852,17 @@ function ChatMarkdown({ |
| 2785 | )} | 2852 | )} |
| 2786 | onCopy={handleCopy} | 2853 | onCopy={handleCopy} |
| 2787 | > | 2854 | > |
| 2788 | <ReactMarkdown | 2855 | <ChatMarkdownRendererContext value={componentState}> |
| 2789 | remarkPlugins={remarkPlugins} | 2856 | <ReactMarkdown |
| 2790 | rehypePlugins={parseRawHtml ? CHAT_MARKDOWN_REHYPE_PLUGINS : undefined} | 2857 | remarkPlugins={remarkPlugins} |
| 2791 | skipHtml={false} | 2858 | rehypePlugins={parseRawHtml ? CHAT_MARKDOWN_REHYPE_PLUGINS : undefined} |
| 2792 | components={markdownComponents} | 2859 | skipHtml={false} |
| 2793 | urlTransform={markdownUrlTransform} | 2860 | components={CHAT_MARKDOWN_COMPONENTS} |
| 2794 | > | 2861 | urlTransform={markdownUrlTransform} |
| 2795 | {text} | 2862 | > |
| 2796 | </ReactMarkdown> | 2863 | {text} |
| | 2864 | </ReactMarkdown> |
| | 2865 | </ChatMarkdownRendererContext> |
| 2797 | {localMediaPreview ? ( | 2866 | {localMediaPreview ? ( |
| 2798 | <ExpandedImageDialog | 2867 | <ExpandedImageDialog |
| 2799 | preview={localMediaPreview} | 2868 | preview={localMediaPreview} |