import { buildFileLinkParentSuffixByPath, fileLinkLabel } from "@t3tools/shared/fileLinks"; import { isWindowsDrivePathHref, normalizeMarkdownLinkDestination, extractInlineCodeSpans, extractMarkdownLinkHrefs, inlineCodeFilePathCandidate, } from "@t3tools/shared/markdownLinks"; import { isHostPath, resolveWorkspaceFilePath } from "@t3tools/shared/path"; import { usePullRequestLinking } from "~/hooks/usePullRequestLinking"; import { AuthFilesystemReadScope, AuthOrchestrationOperateScope, SOURCE_CONTROL_MEDIA_ASSET, } from "@t3tools/contracts"; import { CHAT_MARKDOWN_REMARK_PLUGINS, CHAT_MARKDOWN_REMARK_PLUGINS_WITH_BREAKS, CHAT_MARKDOWN_REHYPE_PLUGINS, } from "@t3tools/shared/markdownPipeline"; import { useAtomValue } from "@effect/atom-react"; import { COMPOSER_CONTEXT_CLIPBOARD_MIME, encodeComposerContextClipboardHtml, } from "@t3tools/shared/composerContextClipboard"; import { FileSpreadsheetIcon, FileTextIcon, GlobeIcon, ImageIcon, InfoIcon, LightbulbIcon, MailIcon, MessageSquareIcon, MessageSquareWarningIcon, OctagonAlertIcon, PresentationIcon, SparklesIcon, TriangleAlertIcon, type LucideIcon, } from "lucide-react"; import { AssetWorkspaceAssetNotFoundError, AuthPreviewOperateScope, type EditorId, type EnvironmentId, type ServerConfig, type ScopedThreadRef, type ServerProviderSkill, type ThreadPullRequestKey, } from "@t3tools/contracts"; import { isAtomCommandInterrupted, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import { codexArtifactTemplatePresentationLabel, type CodexArtifactTemplate, type CodexArtifactTemplateKind, } from "@t3tools/shared/codexArtifactTemplates"; import { classifyMarkdownImageSource, markdownImageSourceFragment, } from "@t3tools/client-runtime/markdown-images"; import { mediaUrlReference } from "@t3tools/client-runtime/media-reference"; import { mediaKindFromPath, mediaMimeTypeFromExtension } from "@t3tools/shared/mediaPreview"; import { InlineAudioPreview } from "./files/InlineAudioPreview"; import { HostImageUrlContext } from "./chat/hostImageUrl"; import * as Cause from "effect/Cause"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import { AsyncResult, Atom } from "effect/reactivity"; import React, { Suspense, type ComponentProps, type ClipboardEvent as ReactClipboardEvent, type MouseEvent as ReactMouseEvent, use, memo, useLayoutEffect, useRef, useState, type ReactNode, } from "react"; import type { Components, ExtraProps as ReactMarkdownExtraProps, Options as ReactMarkdownOptions, } from "react-markdown"; import ReactMarkdown from "react-markdown"; import { createIncrementalMarkdownPlugin } from "../markdown-incremental"; import { defaultUrlTransform } from "react-markdown"; import { parseAssistantCitationHref } from "@t3tools/shared/assistantCitations"; import { parseComposerContextHref } from "@t3tools/shared/composerContextReferences"; import { parseThreadLinkHref } from "@t3tools/shared/threadLinks"; import { AssistantCitationChip } from "./chat/AssistantCitationChip"; import { MarkdownThreadLink } from "./chat/MarkdownThreadLink"; import { mathKind } from "@t3tools/shared/markdownMath"; import remarkMath from "remark-math"; import { artifactTemplateFromHastProperties, renderCodexInlineDirectivesAsMarkdown, } from "@t3tools/shared/codexMarkdownDirectives"; import { renderSkillInlineMarkdownChildren } from "./chat/SkillInlineText"; import { MarkdownMath } from "./chat/MarkdownMath"; import { resolveMarkdownMediaPreview, type ExpandedImagePreview, } from "./chat/ExpandedImagePreview"; import { ExpandedImageDialog } from "./chat/ExpandedImageDialog"; import { markdownImageGallery } from "./chat/markdownImageGallery"; import { type MediaActionSource } from "./media/MediaActions"; import { resolveProtocolRelativeMediaUrl } from "./media/mediaContent"; import { revealInFileExplorerLabelForKind, revealInFileExplorerLabelForOs, } from "./preview/fileExplorerLabel"; import { resolveExternalWebLinkHost, showExternalLinkContextMenu, } from "./chat/externalLinkContextMenu"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; import { Button } from "./ui/button"; import { stackedThreadToast, toastManager } from "./ui/toast"; import { recordVisitForThread } from "../browserHistoryStore"; import { PreferredEditorEnvironmentRequiredError, useOpenInPreferredEditor, usePreferredEditor, } from "../editorPreferences"; import { openInEditorMenuLabel } from "../editorLabels"; import { resolveDiffThemeName } from "../lib/diffRendering"; import { RenderErrorBoundary } from "./RenderErrorBoundary"; import { useRenderedAppearance } from "../hooks/useTheme"; import { useClientSettings } from "../hooks/useSettings"; import { chatMarkdownClipboardPayload } from "../markdown-clipboard"; import { resolveInlineCodeFileLinkMeta, resolveMarkdownFileLinkMeta, rewriteMarkdownFileUriHref, type MarkdownFileLinkMeta, } from "../markdown-links"; import { markdownFileCopyPaths, markdownFileEditorTarget, resolveMarkdownFileHostPath, } from "../markdownFileHostPath"; import { isMarkdownFileLinkLabel } from "@t3tools/shared/markdownLinks"; import { isElectron } from "../env"; import { readLocalApi } from "../localApi"; import { cn } from "../lib/utils"; import { resolveRemoteOpenState, useRemoteOpenResolution } from "../remoteOpen"; import { useRightPanelStore } from "../rightPanelStore"; import { readThreadShell, useProjects } from "../state/entities"; import { serverEnvironment } from "../state/server"; import { shellEnvironment } from "../state/shell"; import { assetEnvironment } from "../state/assets"; import { readEnvironmentScope, readPreparedConnection, useEnvironmentScope, } from "../state/session"; import { previewEnvironment } from "../state/preview"; import { useAtomCommand } from "../state/use-atom-command"; import { useAtomQueryRunner } from "../state/use-atom-query-runner"; import { projectEnvironment } from "../state/projects"; import { filesystemEnvironment } from "../state/filesystem"; import { claimWorkspaceBasenameLookup, needsWorkspaceBasenameLookup, pickStrippedWorkspaceMatch, pickWorkspaceBasenameMatch, stripRepeatedWorkspacePrefix, WORKSPACE_BASENAME_LOOKUP_LIMIT, } from "@t3tools/client-runtime/workspace-basename-lookup"; import { resolvePullRequestPreviewTarget } from "@t3tools/client-runtime/change-request-links"; import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; import { useOpenChangeRequestLink } from "~/lib/openPullRequestLink"; import { useOpenLink } from "../browser/useOpenLink"; import { writeTextToClipboard } from "../hooks/useCopyToClipboard"; import { isPreviewAvailableFor } from "../browser/previewRuntime"; import { isBrowserPreviewFile, openFileInPreview, openUrlInPreview, BrowserPreviewUnavailableError, BrowserSettingsReadError, } from "../browser/openFileInPreview"; import { resolveLinkTarget } from "../browser/browserLinkTarget"; import { PullRequestLinkPreview } from "./pullRequest/PullRequestLinkPreview"; import { MarkdownCodeBlock, SuspenseShikiCodeBlock } from "./chat/MarkdownCodeBlock"; import { MarkdownMermaidCodeBlock } from "./chat/MarkdownMermaidCodeBlock"; import { MarkdownTable } from "./chat/MarkdownTable"; import { MarkdownDetails } from "./chat/MarkdownDetails"; import { authoredImageSizeStyle, ChatMarkdownAssetImage, ChatMarkdownImage, ChatMarkdownImageFallback, ChatMarkdownVideo, MarkdownLinkContext, markdownImageCopy, } from "./chat/ChatMarkdownMedia"; import { MarkdownExternalLinkContent } from "./chat/MarkdownExternalLink"; import { MarkdownFileLink } from "./chat/MarkdownFileLink"; import { canUseMarkdownFileShellActions } from "./chat/MarkdownFileLink.logic"; import { handleMarkdownFragmentClick, rehypeHeadingIds } from "./chat/markdownFragments"; import { rehypeTextDirection } from "./chat/markdownTextDirection"; import { hastHasText, hastPlainTextDeep, plainHastText } from "./chat/markdownHast.logic"; import { extractCodeBlock, extractFenceLanguage, extractFenceTitle, extractPreCodeMeta, isClosedCodeFence, nodeToPlainText, } from "./chat/MarkdownCodeBlock.logic"; import { reportMarkdownActionFailure } from "./chat/markdownActionFailure"; const isAssetNotFoundError = Schema.is(AssetWorkspaceAssetNotFoundError); interface ChatMarkdownProps { /** Compact markdown transforms remove blocks before rendering inside a label. */ inline?: boolean; text: string; cwd: string | undefined; threadRef?: ScopedThreadRef | undefined; pullRequestPanelRef?: ScopedThreadRef | undefined; environmentId?: EnvironmentId | undefined; onTaskListChange?: ((input: { markerOffset: number; checked: boolean }) => void) | undefined; isStreaming?: boolean; skills?: ReadonlyArray; className?: string; lineBreaks?: boolean; parseRawHtml?: boolean; onUseArtifactTemplate?: ((template: CodexArtifactTemplate) => void) | undefined; onRunShellCommand?: ((command: string) => void) | undefined; imageBaseDir?: string | undefined; workspaceMutationId?: string | null | undefined; onImageExpand?: ((preview: ExpandedImagePreview) => void) | undefined; extraRemarkPlugins?: NonNullable; renderContextReference?: ((reference: ChatMarkdownContextReference) => ReactNode) | undefined; /** Loads media a source control host keeps behind a credential through the server, which holds `cwd`'s; without it a private repository's uploads load unauthenticated and 404. */ sourceControlMedia?: boolean | undefined; headingLevelOffset?: number | undefined; } export interface ChatMarkdownContextReference { kind: string; contextId: string; label: string; } const EMPTY_MARKDOWN_SKILLS: ReadonlyArray = []; const EMPTY_REMARK_PLUGINS: NonNullable = []; const ARTIFACT_TEMPLATE_ICON_BY_KIND = { document: FileTextIcon, presentation: PresentationIcon, spreadsheet: FileSpreadsheetIcon, site: GlobeIcon, "google-docs": FileTextIcon, "google-slides": PresentationIcon, "google-sheets": FileSpreadsheetIcon, image: ImageIcon, email: MailIcon, slack: MessageSquareIcon, } satisfies Record; function CodexArtifactTemplateCard(props: { readonly template: CodexArtifactTemplate; readonly onUse?: ((template: CodexArtifactTemplate) => void) | undefined; }) { const Icon = ARTIFACT_TEMPLATE_ICON_BY_KIND[props.template.artifactKind]; const presentationLabel = codexArtifactTemplatePresentationLabel(props.template.artifactKind); return (
{props.template.displayName}
{presentationLabel}
{props.onUse ? ( ) : null}
); } const WINDOWS_DRIVE_PATH_REGEX = /^[A-Za-z]:[\\/]/; function findTaskListMarkerOffset(markdown: string, listItemStart: number): number | null { const firstLineEnd = markdown.indexOf("\n", listItemStart); const firstLine = markdown.slice( listItemStart, firstLineEnd === -1 ? markdown.length : firstLineEnd, ); const match = firstLine.match(/^(?:\s*(?:[-+*]|\d+[.)])\s+)(\[[ xX]\])/); if (!match?.[1]) return null; return listItemStart + firstLine.indexOf(match[1]); } function orderedListGutterStyle( itemCount: number, start: unknown, ): { "--list-gutter": string } | undefined { const parsedStart = Number.parseInt(String(start ?? 1), 10); const firstNumber = Number.isNaN(parsedStart) ? 1 : parsedStart; const lastNumber = firstNumber + Math.max(itemCount - 1, 0); const markerWidth = Math.max(String(firstNumber).length, String(lastNumber).length); if (markerWidth <= 1) return undefined; return { "--list-gutter": `${markerWidth + 2}ch` }; } const GITHUB_ALERT_PRESENTATIONS: Record< string, { label: string; Icon: typeof InfoIcon; borderClassName: string; titleClassName: string } > = { note: { label: "Note", Icon: InfoIcon, borderClassName: "border-blue-500/70", titleClassName: "text-blue-600 dark:text-blue-400", }, tip: { label: "Tip", Icon: LightbulbIcon, borderClassName: "border-emerald-500/70", titleClassName: "text-emerald-600 dark:text-emerald-400", }, important: { label: "Important", Icon: MessageSquareWarningIcon, borderClassName: "border-purple-500/70", titleClassName: "text-purple-600 dark:text-purple-400", }, warning: { label: "Warning", Icon: TriangleAlertIcon, borderClassName: "border-amber-500/70", titleClassName: "text-amber-600 dark:text-amber-500", }, caution: { label: "Caution", Icon: OctagonAlertIcon, borderClassName: "border-red-500/70", titleClassName: "text-red-600 dark:text-red-400", }, }; function normalizeMarkdownLinkHrefKey(href: string): string { const normalizedHref = normalizeMarkdownLinkDestination(href); const rewrittenHref = rewriteMarkdownFileUriHref(normalizedHref) ?? normalizedHref; return WINDOWS_DRIVE_PATH_REGEX.test(rewrittenHref) ? rewrittenHref.replaceAll("\\", "/") : rewrittenHref; } // Heading ids are added after sanitizing, which would prefix them a second time. const CHAT_MARKDOWN_RENDER_REHYPE_PLUGINS = [ ...CHAT_MARKDOWN_REHYPE_PLUGINS, rehypeHeadingIds, rehypeTextDirection, ] satisfies NonNullable; const CHAT_MARKDOWN_LITERAL_HTML_REHYPE_PLUGINS = [ rehypeHeadingIds, rehypeTextDirection, ] satisfies NonNullable; const NO_MARKDOWN_EDITORS: ReadonlyArray = []; type MarkdownServerConfigFields = ReturnType; function markdownServerConfigFields(config: ServerConfig | null) { return { pullRequestsEnabled: config !== null, availableEditors: config?.availableEditors ?? NO_MARKDOWN_EDITORS, revealInFileManagerLabel: config?.shellRevealInFileManager === true && config.availableEditors.includes("file-manager") ? config.shellRevealInFileManagerKind === undefined ? revealInFileExplorerLabelForOs(config.environment.platform.os) : revealInFileExplorerLabelForKind(config.shellRevealInFileManagerKind) : undefined, }; } function markdownServerConfigFieldsEqual( previous: MarkdownServerConfigFields, next: MarkdownServerConfigFields, ): boolean { return ( previous.pullRequestsEnabled === next.pullRequestsEnabled && previous.revealInFileManagerLabel === next.revealInFileManagerLabel && previous.availableEditors.length === next.availableEditors.length && previous.availableEditors.every((editor, index) => editor === next.availableEditors[index]) ); } // Compare only markdown config fields to avoid rerendering replies on every provider status push. const markdownServerConfigFieldsAtom = Atom.family((environmentId: EnvironmentId | null) => Atom.make((get) => { const next = markdownServerConfigFields(get(serverEnvironment.configValueAtom(environmentId))); const previous = Option.getOrNull(get.self()); return previous !== null && markdownServerConfigFieldsEqual(previous, next) ? previous : next; }).pipe(Atom.withLabel("markdown-server-config-fields")), ); function useChatMarkdownState({ text, inline = false, cwd, threadRef, pullRequestPanelRef, environmentId: explicitEnvironmentId, onTaskListChange, isStreaming = false, skills = EMPTY_MARKDOWN_SKILLS, onUseArtifactTemplate, onRunShellCommand, imageBaseDir, workspaceMutationId, onImageExpand, renderContextReference, headingLevelOffset = 0, sourceControlMedia = false, }: ChatMarkdownProps) { const resolvedTheme = useRenderedAppearance(); const markdownRef = useRef(null); const mediaRequestId = useRef(0); const mediaScopeKey = [ threadRef?.environmentId, threadRef?.threadId, explicitEnvironmentId, cwd, imageBaseDir, ].join("\u0000"); // Hide requests from an old scope even if they resolve before effect cleanup. const [localMedia, setLocalMedia] = useState<{ scope: string; preview: ExpandedImagePreview; } | null>(null); const localMediaPreview = localMedia?.scope === mediaScopeKey ? localMedia.preview : null; const closeLocalMediaPreview = () => setLocalMedia(null); const expandMedia = onImageExpand ?? ((preview: ExpandedImagePreview) => setLocalMedia({ scope: mediaScopeKey, preview })); const [mediaScope, setMediaScope] = useState(mediaScopeKey); if (mediaScope !== mediaScopeKey) { setMediaScope(mediaScopeKey); setLocalMedia(null); } // Layout cleanup blocks stale requests from reaching onImageExpand before paint. // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- layout cleanup invalidates media requests still loading for the old scope before paint useLayoutEffect( () => () => { mediaRequestId.current += 1; }, // oxlint-disable-next-line react/exhaustive-effect-dependencies -- Leaving the scope invalidates media requests still loading for it. [mediaScopeKey], ); const createAssetUrl = useAtomQueryRunner(assetEnvironment.createUrl, { reportFailure: false, refresh: true, }); const searchProjectEntries = useAtomQueryRunner(projectEnvironment.searchEntries, { reportFailure: false, }); const getFilesystemMetadata = useAtomQueryRunner(filesystemEnvironment.getMetadata, { reportFailure: false, refresh: true, }); const openPreview = useAtomCommand(previewEnvironment.open, { reportFailure: false, }); const pullRequestLinking = usePullRequestLinking(threadRef?.environmentId); const environmentId = threadRef?.environmentId ?? explicitEnvironmentId ?? null; const canOperateHost = useEnvironmentScope(environmentId, AuthOrchestrationOperateScope); const canOperatePreview = useEnvironmentScope(environmentId, AuthPreviewOperateScope); const remoteOpen = useRemoteOpenResolution(environmentId); const canUseShellActions = canOperateHost && canUseMarkdownFileShellActions(environmentId, remoteOpen.state.mode, remoteOpen.isResolved); const openMarkdownMedia = ( source: string, resolvedFilePath?: string, clickedImage?: HTMLImageElement | null, ) => { const requestId = ++mediaRequestId.current; void resolveMarkdownMediaPreview({ source, resolvedFilePath, cwd, threadRef, // Read the connection on click; reconnect replaces it, and subscriptions would refresh every markdown // context. httpBaseUrl: environmentId === null ? undefined : readPreparedConnection(environmentId)?.httpBaseUrl, createAssetUrl, onOpenFile: threadRef ? (path) => useRightPanelStore.getState().openFile(threadRef, path) : undefined, }).then( (preview) => { if (preview && mediaRequestId.current === requestId) { const selected = preview.images[preview.index]; expandMedia( selected && selected.type !== "video" && markdownRef.current ? markdownImageGallery(clickedImage ?? markdownRef.current, selected) : preview, ); } }, (error: unknown) => { if (mediaRequestId.current !== requestId) return; let description = error instanceof Error ? error.message : "The file could not be loaded. It may have been moved or deleted."; if ( isAssetNotFoundError(error) && error.resource._tag === "media-file" && environmentId !== null ) { // The connection may resolve while the media lookup is pending. const target = readPreparedConnection(environmentId)?.target ?? null; if ( target !== null && resolveRemoteOpenState({ target, sshTarget: null, remoteOpenTargets: undefined, isDesktopRenderer: window.desktopBridge !== undefined, }).mode !== "local-exec" ) { description = `The file was not found on ${target.label} at ${error.checkedPath ?? error.resource.path}.`; } } toastManager.add( stackedThreadToast({ type: "error", title: "Media unavailable", description, }), ); }, ); }; const { pullRequestsEnabled, availableEditors, revealInFileManagerLabel } = useAtomValue( markdownServerConfigFieldsAtom(environmentId), ); const projects = useProjects(); const [preferredEditor] = usePreferredEditor(availableEditors); const preferredEditorMenuLabel = openInEditorMenuLabel(preferredEditor); const openInPreferredEditor = useOpenInPreferredEditor(environmentId, availableEditors); const openInEditor = useAtomCommand(shellEnvironment.openInEditor, { reportFailure: false, }); const revealFileInFileManager = (filePath: string) => { if (environmentId === null) { return Promise.resolve( AsyncResult.failure( Cause.fail(new PreferredEditorEnvironmentRequiredError({ targetPath: filePath })), ), ); } if (!readEnvironmentScope(environmentId, AuthOrchestrationOperateScope)) { return Promise.resolve( AsyncResult.failure( Cause.fail(new Error("This connection cannot reveal files on this environment.")), ), ); } return openInEditor({ environmentId, input: { cwd: filePath, editor: "file-manager", reveal: true }, }); }; const diffThemeName = resolveDiffThemeName(resolvedTheme); const markdownFileLinkMetaByHref = (() => { const metaByHref = new Map< string, NonNullable> >(); for (const href of extractMarkdownLinkHrefs(renderCodexInlineDirectivesAsMarkdown(text))) { if (parseComposerContextHref(href)) continue; const normalizedHref = normalizeMarkdownLinkHrefKey(href); if (metaByHref.has(normalizedHref)) continue; const meta = resolveMarkdownFileLinkMeta(normalizedHref, cwd, imageBaseDir ?? cwd); if (meta) { metaByHref.set(normalizedHref, meta); } } return metaByHref; })(); const inlineCodeFileLinkMetaByText = (() => { const metaByText = new Map(); for (const span of extractInlineCodeSpans(text)) { if (metaByText.has(span)) continue; const meta = resolveInlineCodeFileLinkMeta(span, cwd, imageBaseDir ?? cwd); if (meta) { metaByText.set(span, meta); } } return metaByText; })(); const fileLinkParentSuffixByPath = (() => { const filePaths = [ ...[...markdownFileLinkMetaByHref.values()].map((meta) => meta.filePath), ...[...inlineCodeFileLinkMetaByText.values()].map((meta) => meta.filePath), ]; return buildFileLinkParentSuffixByPath(filePaths); })(); const markdownUrlTransform = (href: string) => { if (parseAssistantCitationHref(href)) return href; if (parseComposerContextHref(href)) return href; if (parseThreadLinkHref(href)) return href; if (isWindowsDrivePathHref(href)) return href; return rewriteMarkdownFileUriHref(href) ?? defaultUrlTransform(href); }; const handleCopy = (event: ReactClipboardEvent) => { const selection = window.getSelection(); if (!selection || selection.isCollapsed || !event.clipboardData) return; const payload = chatMarkdownClipboardPayload(selection); if (!payload) return; event.preventDefault(); event.clipboardData.setData("text/plain", payload.text); const fragment = event.clipboardData.getData(COMPOSER_CONTEXT_CLIPBOARD_MIME); event.clipboardData.setData( "text/html", fragment ? encodeComposerContextClipboardHtml(payload.text, fragment, payload.html) : payload.html, ); }; const openChangeRequestLink = useOpenChangeRequestLink(threadRef, pullRequestPanelRef); const openDeferredMarkdownLink = useOpenLink(threadRef); // Subscribe so hydrated settings apply before the anchor synchronously decides whether to intercept _blank. const linkTargetPreference = useClientSettings((settings) => settings.browserLinkTarget); const mathEnabled = useClientSettings((settings) => settings.mathRenderingEnabled); const resolveThreadPullRequest = ( href: string, ): (ThreadPullRequestKey & { readonly url: string }) | null => { if ( threadRef === undefined || readThreadShell(threadRef) === null || !pullRequestLinking.canLink(href) ) return null; const parsed = sourceControlClients.parseChangeRequestUrl(href)?.link; return parsed === undefined ? null : { ...parsed, url: href }; }; const linkedThreadPullRequestFor = (href: string) => { if (threadRef === undefined || !pullRequestLinking.isLinked(readThreadShell(threadRef), href)) return null; const parsed = sourceControlClients.parseChangeRequestUrl(href)?.link; return parsed === undefined ? null : { ...parsed, url: href }; }; const updateThreadPullRequestLink = async (href: string, linked: boolean) => { if (threadRef === undefined || (!linked && linkedThreadPullRequestFor(href) === null)) return; if (!readEnvironmentScope(threadRef.environmentId, AuthOrchestrationOperateScope)) return; await pullRequestLinking.changeLink(threadRef, href, linked); }; const openExternalLinkInPreview = (url: string) => { if (!threadRef || !canOperatePreview) { return Promise.resolve( AsyncResult.failure( Cause.fail( new BrowserPreviewUnavailableError({ message: "Preview access is unavailable for this client.", }), ), ), ); } return openUrlInPreview({ threadRef, url, openPreview }).then((result) => { if (result._tag === "Success") recordVisitForThread(threadRef, url); else if (!isAtomCommandInterrupted(result)) { const error = squashAtomCommandFailure(result); if (error instanceof BrowserSettingsReadError) { toastManager.add( stackedThreadToast({ type: "error", title: "Unable to open link in browser", description: error.message, }), ); } } return result; }); }; const openMarkdownFileInPreview = (path: string) => { const preparedConnection = threadRef ? readPreparedConnection(threadRef.environmentId) : null; if (!threadRef || !canOperatePreview || preparedConnection === null) { return Promise.resolve( AsyncResult.failure( Cause.fail( new BrowserPreviewUnavailableError({ message: "Environment is not connected.", }), ), ), ); } return openFileInPreview({ threadRef, filePath: path, workspaceRoot: cwd, httpBaseUrl: preparedConnection.httpBaseUrl, createAssetUrl, openPreview, }); }; const findWorkspaceFileMatch = async (fileLinkMeta: MarkdownFileLinkMeta) => { const workspaceRelativePath = fileLinkMeta.workspaceLookupPath; if ( !workspaceRelativePath || !cwd || environmentId === null || !readEnvironmentScope(environmentId, AuthFilesystemReadScope) ) { return null; } const isBasename = needsWorkspaceBasenameLookup(workspaceRelativePath); const strippedPath = isBasename ? null : stripRepeatedWorkspacePrefix(workspaceRelativePath, cwd); if (!isBasename && strippedPath === null) return null; const result = await searchProjectEntries({ environmentId, input: { cwd, query: strippedPath ?? workspaceRelativePath, limit: WORKSPACE_BASENAME_LOOKUP_LIMIT, kind: "file", }, }); if (result._tag !== "Success") return null; if (strippedPath === null) { return pickWorkspaceBasenameMatch(workspaceRelativePath, result.value.entries); } const match = pickStrippedWorkspaceMatch(strippedPath, cwd, result.value.entries); if (match === null) return null; // Search can be truncated or still scanning, so it cannot rule out the // literal path. Check the host before redirecting to the stripped match. const metadata = await getFilesystemMetadata({ environmentId, input: { paths: [resolveWorkspaceFilePath(workspaceRelativePath, cwd)] }, }); return metadata._tag === "Success" && metadata.value.entries[0] === null ? match : null; }; const openFileInPanel = (fileLinkMeta: MarkdownFileLinkMeta, panelPath: string) => { if (!threadRef) return; // Every open supersedes pending lookups, including synchronous opens. const isLatestLookup = claimWorkspaceBasenameLookup(); const openAt = (path: string) => useRightPanelStore.getState().openFile(threadRef, path, fileLinkMeta.line); if ( fileLinkMeta.workspaceLookupPath === null || !cwd || (!needsWorkspaceBasenameLookup(panelPath) && stripRepeatedWorkspacePrefix(panelPath, cwd) === null) ) { openAt(panelPath); return; } void (async () => { const match = await findWorkspaceFileMatch(fileLinkMeta); if (!isLatestLookup()) return; openAt(match ?? panelPath); })(); }; const fileLinkChip = ( fileLinkMeta: MarkdownFileLinkMeta, copyMarkdown: string, mediaSource?: string, ) => { const resolveHostPath = () => resolveMarkdownFileHostPath({ meta: fileLinkMeta, cwd, findWorkspaceMatch: () => findWorkspaceFileMatch(fileLinkMeta), }); const mediaPath = mediaSource ?? fileLinkMeta.filePath; const canPreviewMedia = mediaMimeTypeFromExtension( fileLinkMeta.basename.slice(fileLinkMeta.basename.lastIndexOf(".")), ) !== null; const panelPath = fileLinkMeta.workspaceRelativePath ?? (!canPreviewMedia && isHostPath(fileLinkMeta.filePath) ? fileLinkMeta.filePath : null); return ( openInPreferredEditor( markdownFileEditorTarget(fileLinkMeta, await resolveHostPath()), ) : undefined } resolveCopyPaths={async () => markdownFileCopyPaths(fileLinkMeta, await resolveHostPath(), cwd) } onOpenInPanel={(path) => openFileInPanel(fileLinkMeta, path)} onOpenMedia={ threadRef && canPreviewMedia ? () => openMarkdownMedia(mediaPath, fileLinkMeta.filePath) : undefined } openInEditorMenuLabel={preferredEditorMenuLabel} onReveal={ canUseShellActions && revealInFileManagerLabel !== undefined ? async () => revealFileInFileManager((await resolveHostPath()).absolutePath) : undefined } revealLabel={revealInFileManagerLabel} onOpenInBrowser={ threadRef && canOperatePreview && isPreviewAvailableFor(threadRef.environmentId) && isBrowserPreviewFile(fileLinkMeta.filePath) ? () => openMarkdownFileInPreview(fileLinkMeta.filePath) : undefined } /> ); }; const componentState = { canOperateHost, canOperatePreview, cwd, diffThemeName, environmentId, expandMedia, fileLinkChip, sourceControlMedia, renderContextReference, headingLevelOffset, imageBaseDir, workspaceMutationId, inlineCodeFileLinkMetaByText, inline, mathEnabled, isStreaming, linkTargetPreference, markdownFileLinkMetaByHref, onTaskListChange, onUseArtifactTemplate, onRunShellCommand, openChangeRequestLink, openDeferredMarkdownLink, openExternalLinkInPreview, openMarkdownMedia, projects, linkedThreadPullRequestFor, resolveThreadPullRequest, resolvedTheme, pullRequestsEnabled, skills, threadRef, updateThreadPullRequestLink, }; return { componentState, handleCopy, markdownRef, markdownUrlTransform, localMediaPreview, closeLocalMediaPreview, }; } const ChatMarkdownRendererContext = React.createContext< ReturnType["componentState"] >(null!); // Isolate streamed source text from the renderer context; only offset-based renderers need it. const ChatMarkdownTextContext = React.createContext(""); function markdownHeadingRenderer(level: 1 | 2 | 3 | 4 | 5 | 6) { const Tag = `h${level}` as const; return function MarkdownHeading({ node: _node, ...props }: ComponentProps & ReactMarkdownExtraProps) { const { headingLevelOffset } = use(ChatMarkdownRendererContext); return ( 0 ? Math.min(level + headingLevelOffset, 6) : undefined} /> ); }; } // Native word selection stops at hyphens and dots, so a double-click takes the whole span. function selectInlineCode(event: ReactMouseEvent) { window.getSelection()?.selectAllChildren(event.currentTarget); } const CHAT_MARKDOWN_COMPONENTS = { h1: markdownHeadingRenderer(1), h2: markdownHeadingRenderer(2), h3: markdownHeadingRenderer(3), h4: markdownHeadingRenderer(4), h5: markdownHeadingRenderer(5), h6: markdownHeadingRenderer(6), div: function MarkdownDiv({ node, children, ...props }) { const { onUseArtifactTemplate } = use(ChatMarkdownRendererContext); const artifactTemplate = artifactTemplateFromHastProperties(node?.properties); if (artifactTemplate) { return ( ); } return
{children}
; }, p: function MarkdownParagraph({ node: _node, children, ...props }) { const { skills, threadRef } = use(ChatMarkdownRendererContext); return

{renderSkillInlineMarkdownChildren(children, skills, threadRef)}

; }, blockquote: function MarkdownBlockquote({ node: _node, children, ...props }) { const alert = GITHUB_ALERT_PRESENTATIONS[String((props as Record)["data-alert"] ?? "")]; if (!alert) { return
{children}
; } return (

{alert.label}

{children}
); }, ol: function MarkdownOrderedList({ node, start, style, ...props }) { const itemCount = node?.children?.filter((child) => child.type === "element" && child.tagName === "li") .length ?? 0; const gutterStyle = orderedListGutterStyle(itemCount, start); return (
    ); }, li: function MarkdownListItem({ node, children, ...props }) { const { skills, threadRef } = use(ChatMarkdownRendererContext); // Resolve checkbox markers by offset to avoid rerendering list items on every streamed token. const isTaskItem = String(props.className ?? "").includes("task-list-item"); return (
  1. {renderSkillInlineMarkdownChildren(children, skills, threadRef)}
  2. ); }, input: function MarkdownInput({ node: _node, type, checked, disabled: _disabled, ...props }) { const { onTaskListChange } = use(ChatMarkdownRendererContext); const text = use(ChatMarkdownTextContext); if (type !== "checkbox" || !onTaskListChange) { return ( ); } return ( { const listItemStart = Number(event.currentTarget.closest("li")?.dataset.listItemOffset); if (!Number.isSafeInteger(listItemStart)) return; const markerOffset = findTaskListMarkerOffset(text, listItemStart); if (markerOffset === null) return; onTaskListChange({ markerOffset, checked: event.currentTarget.checked }); }} /> ); }, a: function MarkdownAnchor({ node, href, children, title: _title, ...props }) { const { canOperateHost, canOperatePreview, cwd, environmentId, imageBaseDir, markdownFileLinkMetaByHref, threadRef, openMarkdownMedia, openChangeRequestLink, openDeferredMarkdownLink, linkTargetPreference, openExternalLinkInPreview, projects, linkedThreadPullRequestFor, resolveThreadPullRequest, pullRequestsEnabled, updateThreadPullRequestLink, fileLinkChip, renderContextReference, } = use(ChatMarkdownRendererContext); const text = use(ChatMarkdownTextContext); const citation = href ? parseAssistantCitationHref(href) : null; if (citation) return ; const threadLink = href ? parseThreadLinkHref(href) : null; if (threadLink) { const written = hastPlainTextDeep(node); // An autolink's text is its own URL, which is no label. const label = written && parseThreadLinkHref(written) === null ? written : null; return environmentId ? ( ) : ( {label ?? "Thread"} ); } const contextReference = href ? parseComposerContextHref(href) : null; if (contextReference) { const label = hastPlainTextDeep(node) || contextReference.contextId; return renderContextReference ? ( renderContextReference({ ...contextReference, label }) ) : ( {label} ); } const normalizedHref = href ? normalizeMarkdownLinkHrefKey(href) : ""; const fileLinkMeta = normalizedHref ? (markdownFileLinkMetaByHref.get(normalizedHref) ?? resolveMarkdownFileLinkMeta(normalizedHref, cwd, imageBaseDir ?? cwd)) : null; if (!fileLinkMeta) { const faviconHost = resolveExternalWebLinkHost(href); const hasText = hastHasText(node); const pullRequestAutolink = String( (props as Record)["data-pull-request-autolink"] ?? "", ); const pullRequestCopy = pullRequestAutolink === "commit" ? /\/commit\/([0-9a-f]{7,40})$/iu.exec(href ?? "")?.[1] : pullRequestAutolink === "reference" || pullRequestAutolink === "mention" ? plainHastText(node) : undefined; const isPullRequestAutolink = pullRequestCopy !== undefined; const confirmBeforeOpen = pullRequestAutolink === "reference"; const pullRequestCandidateUrl = confirmBeforeOpen && href ? sourceControlClients.changeRequestUrlFromReferenceAutolink(href) : href; const pullRequestPreviewTarget = pullRequestCandidateUrl ? resolvePullRequestPreviewTarget({ environmentId, projects, pullRequestsEnabled, url: pullRequestCandidateUrl, }) : null; const isSameDocumentLink = href?.startsWith("#") ?? false; const onClick = props.onClick; const canOpenInPreview = canOperatePreview && Boolean(threadRef && isPreviewAvailableFor(threadRef.environmentId)); const linkChildren = {children}; const link = ( { onClick?.(event); if (isSameDocumentLink && href) { handleMarkdownFragmentClick(event, href); return; } if ( href && faviconHost !== null && mediaKindFromPath(href) !== null && mediaKindFromPath(href) !== "audio" && !event.defaultPrevented && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey ) { event.preventDefault(); event.stopPropagation(); openMarkdownMedia( href, undefined, event.target instanceof HTMLImageElement ? event.target : event.currentTarget.querySelector("img"), ); return; } if ( !href || openChangeRequestLink(event, href, undefined, environmentId ?? undefined) ) { return; } if (event.defaultPrevented) return; if ( resolveLinkTarget({ url: href, event, preference: linkTargetPreference, canOpenInApp: canOpenInPreview, }) !== "app" ) { // Open desktop Alt-clicks before Chromium downloads them so link routers can read the held key. if (isElectron && event.altKey) { event.preventDefault(); event.stopPropagation(); void readLocalApi() ?.shell.openExternal(href) .catch((cause) => reportMarkdownActionFailure( { operation: "open-link-external", target: href }, cause, ), ); } return; } event.preventDefault(); event.stopPropagation(); void openExternalLinkInPreview(href).then((result) => { if (result._tag === "Success" || isAtomCommandInterrupted(result)) return; reportMarkdownActionFailure( { operation: "open-link-in-preview", target: href }, result.cause, ); if (squashAtomCommandFailure(result) instanceof BrowserSettingsReadError) return; void readLocalApi()?.shell.openExternal(href); }); }} onContextMenu={(event) => { if (!href || !faviconHost) return; event.preventDefault(); event.stopPropagation(); const api = readLocalApi(); if (!api) return; const threadLinkAction = !canOperateHost ? undefined : linkedThreadPullRequestFor(href) !== null ? "unlink-from-thread" : resolveThreadPullRequest(href) === null ? undefined : "link-to-thread"; void showExternalLinkContextMenu({ href, canOpenInPreview, threadLinkAction, position: { x: event.clientX, y: event.clientY }, showContextMenu: (items, position) => api.contextMenu.show(items, position), openInPreview: async (target) => { const result = await openExternalLinkInPreview(target); if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { reportMarkdownActionFailure( { operation: "open-link-in-preview", target }, result.cause, ); } }, openExternal: (target) => api.shell.openExternal(target), copyLink: (target) => writeTextToClipboard(target, "link"), updateThreadLink: updateThreadPullRequestLink, reportFailure: (operation, cause) => { reportMarkdownActionFailure({ operation, target: href }, cause); if ( operation === "link-pull-request-to-thread" || operation === "unlink-pull-request-from-thread" ) { toastManager.add( stackedThreadToast({ type: "error", title: operation === "link-pull-request-to-thread" ? "Unable to link pull request" : "Unable to unlink pull request", description: cause instanceof Error ? cause.message : "The request failed.", }), ); } }, }); }} > {faviconHost && hasText && !isPullRequestAutolink ? ( {linkChildren} ) : ( linkChildren )} ); if (!faviconHost || !href) { return link; } if (pullRequestPreviewTarget !== null) { return ( openChangeRequestLink( { metaKey: false, ctrlKey: false, preventDefault: () => undefined, stopPropagation: () => undefined, }, targetUrl, undefined, environmentId ?? undefined, ) } onOpenFallback={openDeferredMarkdownLink} /> ); } return ( {href} ); } const label = nodeToPlainText(children); const start = node?.position?.start.offset; const end = node?.position?.end.offset; const source = start !== undefined && end !== undefined ? text.slice(start, end) : ""; const copyMarkdown = source.startsWith("[") && source.includes("](") ? source : `[${(label || fileLinkMeta.basename).replace(/[\\[\]]/g, "\\$&")}](${normalizedHref})`; const chip = fileLinkChip(fileLinkMeta, copyMarkdown, normalizedHref); return isMarkdownFileLinkLabel(label, normalizedHref) ? ( chip ) : ( {children} {chip} ); }, code: function MarkdownCode({ node, children, className, ...props }) { const { cwd, imageBaseDir, inlineCodeFileLinkMetaByText, fileLinkChip, mathEnabled, inline } = use(ChatMarkdownRendererContext); const math = mathEnabled ? mathKind(className) : null; if (math) { return ( ); } if (node?.properties?.dataInlineCode != null) { const codeText = nodeToPlainText(children); const fileLinkMeta = inlineCodeFileLinkMetaByText.get(codeText.trim()) ?? resolveInlineCodeFileLinkMeta(codeText, cwd, imageBaseDir ?? cwd); if (fileLinkMeta) { return fileLinkChip( fileLinkMeta, `\`${codeText}\``, inlineCodeFilePathCandidate(codeText) ?? codeText.trim(), ); } return ( {children} ); } return ( {children} ); }, img: function MarkdownImage({ node, title, src, alt, ...props }) { const { expandMedia, cwd, environmentId, sourceControlMedia, imageBaseDir, threadRef, workspaceMutationId, renderContextReference, fileLinkChip, } = use(ChatMarkdownRendererContext); const imageExpand = use(MarkdownLinkContext) ? undefined : expandMedia; const contextReference = typeof src === "string" ? parseComposerContextHref(src) : null; if (contextReference) { const label = alt || contextReference.contextId; return renderContextReference ? ( renderContextReference({ ...contextReference, label }) ) : ( {label} ); } const localSrc = node?.properties?.dataLocalSrc; const markdownTitle = node?.properties?.dataMarkdownTitle; const standalone = node?.properties?.dataStandalone === true; const authoredSrc = typeof localSrc === "string" ? localSrc : src; const authoredTitle = typeof markdownTitle === "string" ? markdownTitle : title; const srcString = typeof authoredSrc === "string" ? normalizeMarkdownLinkDestination(authoredSrc) : ""; const classifiedSrc = typeof localSrc === "string" ? srcString.replaceAll("\\", "/") : srcString; const altText = alt ?? ""; const copyMarkdown = markdownImageCopy(altText, srcString, authoredTitle); const { className, style: _style, width, height, ...imageProps } = props; const authoredSizeStyle = authoredImageSizeStyle(width, height); const imageSource = classifyMarkdownImageSource(classifiedSrc, imageBaseDir ?? cwd); const mediaKind = mediaKindFromPath(classifiedSrc); if (mediaKind === "audio") { const filePath = imageSource._tag === "WorkspaceFile" ? imageSource.path : null; const name = (filePath ?? classifiedSrc.split(/[?#]/, 1)[0] ?? "").split(/[\\/]/).at(-1) || altText || "Audio"; if (imageSource._tag !== "Direct" && !(filePath && threadRef)) { return ; } return ( useRightPanelStore.getState().openFile(threadRef, filePath), } : imageSource._tag === "Direct" ? { src: resolveProtocolRelativeMediaUrl(imageSource.uri) } : {})} /> ); } if (mediaKind === null && imageSource._tag === "WorkspaceFile") { const fileLinkMeta = resolveMarkdownFileLinkMeta(srcString, cwd, imageBaseDir ?? cwd); if (fileLinkMeta) return fileLinkChip(fileLinkMeta, copyMarkdown, srcString); } const kind = mediaKind ?? "image"; const directUri = imageSource._tag === "Direct" ? imageSource.uri : null; const hostMediaUrl = directUri === null ? null : (sourceControlClients.mediaUrl(resolveProtocolRelativeMediaUrl(directUri))?.fetchUrl ?? null); if ( sourceControlMedia && cwd !== undefined && environmentId !== null && directUri !== null && hostMediaUrl !== null ) { return ( ); } if (imageSource._tag === "Direct") { const mediaSrc = resolveProtocolRelativeMediaUrl(imageSource.uri); const originalUrl = resolveExternalWebLinkHost(imageSource.uri) !== null ? imageSource.uri : undefined; const reference = mediaUrlReference(imageSource.uri); const actionsSource: MediaActionSource = { kind, name: altText || kind, src: mediaSrc, ...(reference ? { reference } : {}), }; if (kind === "video") { return ( ); } return ( ); } const hostImageUrl = use(HostImageUrlContext); if (imageSource._tag === "WorkspaceFile" && kind === "image" && hostImageUrl !== null) { const hostSrc = hostImageUrl(imageSource.path); return ( ); } if (imageSource._tag === "WorkspaceFile" && threadRef) { return ( ); } return ; }, table: function MarkdownTableRenderer({ node: _node, ...props }) { return ; }, details: function MarkdownDetailsRenderer({ node: _node, children, open: detailsOpen }) { return {children}; }, pre: function MarkdownPre({ node, children, ...props }) { const { resolvedTheme, diffThemeName, expandMedia, isStreaming, onRunShellCommand, inline, mathEnabled, } = use(ChatMarkdownRendererContext); const text = use(ChatMarkdownTextContext); const codeBlock = extractCodeBlock(children); if (!codeBlock) { return
    {children}
    ; } const language = extractFenceLanguage(codeBlock.className); if ( mathEnabled && language === "math" && (!isStreaming || mathKind(codeBlock.className) === "display" || isClosedCodeFence(node, text, codeBlock.code)) ) { return ; } const fenceTitle = extractFenceTitle(extractPreCodeMeta(node)); const highlightedCode = ( {children}} > {children} } > ); if (language === "mermaid") { return ( expandMedia({ images: [{ src, name: "Mermaid diagram" }], index: 0 })} > {highlightedCode} ); } return ( {highlightedCode} ); }, } satisfies Components; function ChatMarkdown({ text, className, inline = false, lineBreaks = false, parseRawHtml = true, extraRemarkPlugins = EMPTY_REMARK_PLUGINS, ...props }: ChatMarkdownProps) { const { componentState, handleCopy, markdownRef, markdownUrlTransform, localMediaPreview, closeLocalMediaPreview, } = useChatMarkdownState({ text, inline, ...props }); const incrementalParsing = props.isStreaming === true && extraRemarkPlugins.length === 0 && /(?:^|\n) {0,3}(?:`{3}|~{3})/.test(text); const Root = inline ? "span" : "div"; const bindMarkdownRef = (node: HTMLElement | null) => { markdownRef.current = node; }; const remarkPlugins = [ ...(lineBreaks ? CHAT_MARKDOWN_REMARK_PLUGINS_WITH_BREAKS : CHAT_MARKDOWN_REMARK_PLUGINS), ...extraRemarkPlugins, ...(incrementalParsing ? [createIncrementalMarkdownPlugin()] : []), ]; if (componentState.mathEnabled) remarkPlugins.unshift([remarkMath, { singleDollarTextMath: true }]); return ( {text} {localMediaPreview ? ( ) : null} ); } export default memo(ChatMarkdown);