diff --git a/apps/web/src/components/ChatMarkdown.tsx b/apps/web/src/components/ChatMarkdown.tsx index 1d8920e4812..711a545d90a 100644 --- a/apps/web/src/components/ChatMarkdown.tsx +++ b/apps/web/src/components/ChatMarkdown.tsx @@ -120,6 +120,20 @@ const EMPTY_MARKDOWN_SKILLS: ReadonlyArray( MAX_HIGHLIGHT_CACHE_ENTRIES, MAX_HIGHLIGHT_CACHE_MEMORY_BYTES, @@ -332,7 +346,9 @@ function MarkdownTable({ children, ...props }: React.ComponentProps<"table">) { copiedTimerRef.current = null; }, 1200); }) - .catch(() => undefined); + .catch((cause) => { + reportMarkdownActionFailure({ operation: "copy-table", format }, cause); + }); }, []); useEffect( @@ -529,8 +545,17 @@ function MarkdownCodeBlock({ copiedTimerRef.current = null; }, 1200); }) - .catch(() => undefined); - }, [code]); + .catch((cause) => { + reportMarkdownActionFailure( + { + operation: "copy-code-block", + language, + ...(fenceTitle ? { fenceTitle } : {}), + }, + cause, + ); + }); + }, [code, fenceTitle, language]); useEffect( () => () => { @@ -977,18 +1002,36 @@ const MarkdownFileLink = memo(function MarkdownFileLink({ }: MarkdownFileLinkProps) { const handleOpenInEditor = useCallback(() => { void (async () => { - const result = await onOpen(targetPath); - if (result._tag === "Success" || isAtomCommandInterrupted(result)) { - return; + try { + const result = await onOpen(targetPath); + if (result._tag === "Success" || isAtomCommandInterrupted(result)) { + return; + } + reportMarkdownActionFailure( + { operation: "open-file-in-editor", target: targetPath }, + result.cause, + ); + const error = squashAtomCommandFailure(result); + toastManager.add( + stackedThreadToast({ + type: "error", + title: "Unable to open file", + description: error instanceof Error ? error.message : "An error occurred.", + }), + ); + } catch (cause) { + reportMarkdownActionFailure( + { operation: "open-file-in-editor", target: targetPath }, + cause, + ); + toastManager.add( + stackedThreadToast({ + type: "error", + title: "Unable to open file", + description: cause instanceof Error ? cause.message : "An error occurred.", + }), + ); } - const error = squashAtomCommandFailure(result); - toastManager.add( - stackedThreadToast({ - type: "error", - title: "Unable to open file", - description: error instanceof Error ? error.message : "An error occurred.", - }), - ); })(); }, [onOpen, targetPath]); @@ -1005,52 +1048,77 @@ const MarkdownFileLink = memo(function MarkdownFileLink({ return; } void (async () => { - const result = await onOpenInBrowser(); - if (result._tag === "Success" || isAtomCommandInterrupted(result)) { - return; + try { + const result = await onOpenInBrowser(); + if (result._tag === "Success" || isAtomCommandInterrupted(result)) { + return; + } + reportMarkdownActionFailure( + { operation: "open-file-in-browser", target: targetPath }, + result.cause, + ); + const error = squashAtomCommandFailure(result); + toastManager.add( + stackedThreadToast({ + type: "error", + title: "Unable to open file in browser", + description: error instanceof Error ? error.message : "An error occurred.", + }), + ); + } catch (cause) { + reportMarkdownActionFailure( + { operation: "open-file-in-browser", target: targetPath }, + cause, + ); + toastManager.add( + stackedThreadToast({ + type: "error", + title: "Unable to open file in browser", + description: cause instanceof Error ? cause.message : "An error occurred.", + }), + ); } - const error = squashAtomCommandFailure(result); - toastManager.add( - stackedThreadToast({ - type: "error", - title: "Unable to open file in browser", - description: error instanceof Error ? error.message : "An error occurred.", - }), - ); })(); - }, [onOpenInBrowser]); - - const handleCopy = useCallback((value: string, title: string) => { - if (typeof window === "undefined" || !navigator.clipboard?.writeText) { - toastManager.add( - stackedThreadToast({ - type: "error", - title: `Failed to copy ${title.toLowerCase()}`, - description: "Clipboard API unavailable.", - }), - ); - return; - } + }, [onOpenInBrowser, targetPath]); - void navigator.clipboard.writeText(value).then( - () => { - toastManager.add({ - type: "success", - title: `${title} copied`, - description: value, - }); - }, - (error) => { + const handleCopy = useCallback( + (value: string, title: string) => { + if (typeof window === "undefined" || !navigator.clipboard?.writeText) { toastManager.add( stackedThreadToast({ type: "error", title: `Failed to copy ${title.toLowerCase()}`, - description: error instanceof Error ? error.message : "An error occurred.", + description: "Clipboard API unavailable.", }), ); - }, - ); - }, []); + return; + } + + void navigator.clipboard.writeText(value).then( + () => { + toastManager.add({ + type: "success", + title: `${title} copied`, + description: value, + }); + }, + (error) => { + reportMarkdownActionFailure( + { operation: "copy-file-path", target: targetPath, copyTarget: title }, + error, + ); + toastManager.add( + stackedThreadToast({ + type: "error", + title: `Failed to copy ${title.toLowerCase()}`, + description: error instanceof Error ? error.message : "An error occurred.", + }), + ); + }, + ); + }, + [targetPath], + ); const handleContextMenu = useCallback( async (event: ReactMouseEvent) => { @@ -1060,32 +1128,39 @@ const MarkdownFileLink = memo(function MarkdownFileLink({ const api = readLocalApi(); if (!api) return; - const clicked = await api.contextMenu.show( - [ - { id: "open", label: "Open in editor" }, - ...(onOpenInBrowser - ? ([{ id: "open-in-browser", label: "Open in integrated browser" }] as const) - : []), - { id: "copy-relative", label: "Copy relative path" }, - { id: "copy-full", label: "Copy full path" }, - ] as const, - { x: event.clientX, y: event.clientY }, - ); + try { + const clicked = await api.contextMenu.show( + [ + { id: "open", label: "Open in editor" }, + ...(onOpenInBrowser + ? ([{ id: "open-in-browser", label: "Open in integrated browser" }] as const) + : []), + { id: "copy-relative", label: "Copy relative path" }, + { id: "copy-full", label: "Copy full path" }, + ] as const, + { x: event.clientX, y: event.clientY }, + ); - if (clicked === "open") { - handleOpenInEditor(); - return; - } - if (clicked === "open-in-browser") { - handleOpenInBrowser(); - return; - } - if (clicked === "copy-relative") { - handleCopy(displayPath, "Relative path"); - return; - } - if (clicked === "copy-full") { - handleCopy(targetPath, "Full path"); + if (clicked === "open") { + handleOpenInEditor(); + return; + } + if (clicked === "open-in-browser") { + handleOpenInBrowser(); + return; + } + if (clicked === "copy-relative") { + handleCopy(displayPath, "Relative path"); + return; + } + if (clicked === "copy-full") { + handleCopy(targetPath, "Full path"); + } + } catch (cause) { + reportMarkdownActionFailure( + { operation: "show-file-context-menu", target: targetPath }, + cause, + ); } }, [displayPath, handleCopy, handleOpenInBrowser, handleOpenInEditor, onOpenInBrowser, targetPath], @@ -1315,22 +1390,32 @@ function ChatMarkdown({ event.stopPropagation(); const api = readLocalApi(); if (!api) return; - void api.contextMenu - .show( - [ - { id: "open-in-browser", label: "Open in integrated browser" }, - { id: "open-external", label: "Open in system browser" }, - ] as const, - { x: event.clientX, y: event.clientY }, - ) - .then((clicked) => { + void (async () => { + let operation = "show-link-context-menu"; + try { + const clicked = await api.contextMenu.show( + [ + { id: "open-in-browser", label: "Open in integrated browser" }, + { id: "open-external", label: "Open in system browser" }, + ] as const, + { x: event.clientX, y: event.clientY }, + ); if (clicked === "open-in-browser") { - void openExternalLinkInPreview(href); + operation = "open-link-in-preview"; + const result = await openExternalLinkInPreview(href); + if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { + reportMarkdownActionFailure({ operation, target: href }, result.cause); + } return; } - if (clicked === "open-external") return api.shell.openExternal(href); - }) - .catch(() => undefined); + if (clicked === "open-external") { + operation = "open-link-external"; + await api.shell.openExternal(href); + } + } catch (cause) { + reportMarkdownActionFailure({ operation, target: href }, cause); + } + })(); }} > {faviconHost ? (