| ... | ... | @@ -120,6 +120,20 @@ const EMPTY_MARKDOWN_SKILLS: ReadonlyArray<Pick<ServerProviderSkill, "name" | "d |
| 120 | 120 | const CODE_FENCE_LANGUAGE_REGEX = /(?:^|\s)language-([^\s]+)/; |
| 121 | 121 | const MAX_HIGHLIGHT_CACHE_ENTRIES = 500; |
| 122 | 122 | const MAX_HIGHLIGHT_CACHE_MEMORY_BYTES = 50 * 1024 * 1024; |
| 123 | |
| 124 | interface MarkdownActionFailureContext { |
| 125 | readonly operation: string; |
| 126 | readonly target?: string; |
| 127 | readonly format?: "markdown" | "csv"; |
| 128 | readonly language?: string; |
| 129 | readonly fenceTitle?: string; |
| 130 | readonly copyTarget?: string; |
| 131 | } |
| 132 | |
| 133 | function reportMarkdownActionFailure(context: MarkdownActionFailureContext, cause: unknown): void { |
| 134 | console.error("[chat-markdown] action failed", context, cause); |
| 135 | } |
| 136 | |
| 123 | 137 | const highlightedCodeCache = new LRUCache<string>( |
| 124 | 138 | MAX_HIGHLIGHT_CACHE_ENTRIES, |
| 125 | 139 | MAX_HIGHLIGHT_CACHE_MEMORY_BYTES, |
| ... | ... | @@ -332,7 +346,9 @@ function MarkdownTable({ children, ...props }: React.ComponentProps<"table">) { |
| 332 | 346 | copiedTimerRef.current = null; |
| 333 | 347 | }, 1200); |
| 334 | 348 | }) |
| 335 | | .catch(() => undefined); |
| 349 | .catch((cause) => { |
| 350 | reportMarkdownActionFailure({ operation: "copy-table", format }, cause); |
| 351 | }); |
| 336 | 352 | }, []); |
| 337 | 353 | |
| 338 | 354 | useEffect( |
| ... | ... | @@ -529,8 +545,17 @@ function MarkdownCodeBlock({ |
| 529 | 545 | copiedTimerRef.current = null; |
| 530 | 546 | }, 1200); |
| 531 | 547 | }) |
| 532 | | .catch(() => undefined); |
| 533 | | }, [code]); |
| 548 | .catch((cause) => { |
| 549 | reportMarkdownActionFailure( |
| 550 | { |
| 551 | operation: "copy-code-block", |
| 552 | language, |
| 553 | ...(fenceTitle ? { fenceTitle } : {}), |
| 554 | }, |
| 555 | cause, |
| 556 | ); |
| 557 | }); |
| 558 | }, [code, fenceTitle, language]); |
| 534 | 559 | |
| 535 | 560 | useEffect( |
| 536 | 561 | () => () => { |
| ... | ... | @@ -977,18 +1002,36 @@ const MarkdownFileLink = memo(function MarkdownFileLink({ |
| 977 | 1002 | }: MarkdownFileLinkProps) { |
| 978 | 1003 | const handleOpenInEditor = useCallback(() => { |
| 979 | 1004 | void (async () => { |
| 980 | | const result = await onOpen(targetPath); |
| 981 | | if (result._tag === "Success" || isAtomCommandInterrupted(result)) { |
| 982 | | return; |
| 1005 | try { |
| 1006 | const result = await onOpen(targetPath); |
| 1007 | if (result._tag === "Success" || isAtomCommandInterrupted(result)) { |
| 1008 | return; |
| 1009 | } |
| 1010 | reportMarkdownActionFailure( |
| 1011 | { operation: "open-file-in-editor", target: targetPath }, |
| 1012 | result.cause, |
| 1013 | ); |
| 1014 | const error = squashAtomCommandFailure(result); |
| 1015 | toastManager.add( |
| 1016 | stackedThreadToast({ |
| 1017 | type: "error", |
| 1018 | title: "Unable to open file", |
| 1019 | description: error instanceof Error ? error.message : "An error occurred.", |
| 1020 | }), |
| 1021 | ); |
| 1022 | } catch (cause) { |
| 1023 | reportMarkdownActionFailure( |
| 1024 | { operation: "open-file-in-editor", target: targetPath }, |
| 1025 | cause, |
| 1026 | ); |
| 1027 | toastManager.add( |
| 1028 | stackedThreadToast({ |
| 1029 | type: "error", |
| 1030 | title: "Unable to open file", |
| 1031 | description: cause instanceof Error ? cause.message : "An error occurred.", |
| 1032 | }), |
| 1033 | ); |
| 983 | 1034 | } |
| 984 | | const error = squashAtomCommandFailure(result); |
| 985 | | toastManager.add( |
| 986 | | stackedThreadToast({ |
| 987 | | type: "error", |
| 988 | | title: "Unable to open file", |
| 989 | | description: error instanceof Error ? error.message : "An error occurred.", |
| 990 | | }), |
| 991 | | ); |
| 992 | 1035 | })(); |
| 993 | 1036 | }, [onOpen, targetPath]); |
| 994 | 1037 | |
| ... | ... | @@ -1005,52 +1048,77 @@ const MarkdownFileLink = memo(function MarkdownFileLink({ |
| 1005 | 1048 | return; |
| 1006 | 1049 | } |
| 1007 | 1050 | void (async () => { |
| 1008 | | const result = await onOpenInBrowser(); |
| 1009 | | if (result._tag === "Success" || isAtomCommandInterrupted(result)) { |
| 1010 | | return; |
| 1051 | try { |
| 1052 | const result = await onOpenInBrowser(); |
| 1053 | if (result._tag === "Success" || isAtomCommandInterrupted(result)) { |
| 1054 | return; |
| 1055 | } |
| 1056 | reportMarkdownActionFailure( |
| 1057 | { operation: "open-file-in-browser", target: targetPath }, |
| 1058 | result.cause, |
| 1059 | ); |
| 1060 | const error = squashAtomCommandFailure(result); |
| 1061 | toastManager.add( |
| 1062 | stackedThreadToast({ |
| 1063 | type: "error", |
| 1064 | title: "Unable to open file in browser", |
| 1065 | description: error instanceof Error ? error.message : "An error occurred.", |
| 1066 | }), |
| 1067 | ); |
| 1068 | } catch (cause) { |
| 1069 | reportMarkdownActionFailure( |
| 1070 | { operation: "open-file-in-browser", target: targetPath }, |
| 1071 | cause, |
| 1072 | ); |
| 1073 | toastManager.add( |
| 1074 | stackedThreadToast({ |
| 1075 | type: "error", |
| 1076 | title: "Unable to open file in browser", |
| 1077 | description: cause instanceof Error ? cause.message : "An error occurred.", |
| 1078 | }), |
| 1079 | ); |
| 1011 | 1080 | } |
| 1012 | | const error = squashAtomCommandFailure(result); |
| 1013 | | toastManager.add( |
| 1014 | | stackedThreadToast({ |
| 1015 | | type: "error", |
| 1016 | | title: "Unable to open file in browser", |
| 1017 | | description: error instanceof Error ? error.message : "An error occurred.", |
| 1018 | | }), |
| 1019 | | ); |
| 1020 | 1081 | })(); |
| 1021 | | }, [onOpenInBrowser]); |
| 1022 | | |
| 1023 | | const handleCopy = useCallback((value: string, title: string) => { |
| 1024 | | if (typeof window === "undefined" || !navigator.clipboard?.writeText) { |
| 1025 | | toastManager.add( |
| 1026 | | stackedThreadToast({ |
| 1027 | | type: "error", |
| 1028 | | title: `Failed to copy ${title.toLowerCase()}`, |
| 1029 | | description: "Clipboard API unavailable.", |
| 1030 | | }), |
| 1031 | | ); |
| 1032 | | return; |
| 1033 | | } |
| 1082 | }, [onOpenInBrowser, targetPath]); |
| 1034 | 1083 | |
| 1035 | | void navigator.clipboard.writeText(value).then( |
| 1036 | | () => { |
| 1037 | | toastManager.add({ |
| 1038 | | type: "success", |
| 1039 | | title: `${title} copied`, |
| 1040 | | description: value, |
| 1041 | | }); |
| 1042 | | }, |
| 1043 | | (error) => { |
| 1084 | const handleCopy = useCallback( |
| 1085 | (value: string, title: string) => { |
| 1086 | if (typeof window === "undefined" || !navigator.clipboard?.writeText) { |
| 1044 | 1087 | toastManager.add( |
| 1045 | 1088 | stackedThreadToast({ |
| 1046 | 1089 | type: "error", |
| 1047 | 1090 | title: `Failed to copy ${title.toLowerCase()}`, |
| 1048 | | description: error instanceof Error ? error.message : "An error occurred.", |
| 1091 | description: "Clipboard API unavailable.", |
| 1049 | 1092 | }), |
| 1050 | 1093 | ); |
| 1051 | | }, |
| 1052 | | ); |
| 1053 | | }, []); |
| 1094 | return; |
| 1095 | } |
| 1096 | |
| 1097 | void navigator.clipboard.writeText(value).then( |
| 1098 | () => { |
| 1099 | toastManager.add({ |
| 1100 | type: "success", |
| 1101 | title: `${title} copied`, |
| 1102 | description: value, |
| 1103 | }); |
| 1104 | }, |
| 1105 | (error) => { |
| 1106 | reportMarkdownActionFailure( |
| 1107 | { operation: "copy-file-path", target: targetPath, copyTarget: title }, |
| 1108 | error, |
| 1109 | ); |
| 1110 | toastManager.add( |
| 1111 | stackedThreadToast({ |
| 1112 | type: "error", |
| 1113 | title: `Failed to copy ${title.toLowerCase()}`, |
| 1114 | description: error instanceof Error ? error.message : "An error occurred.", |
| 1115 | }), |
| 1116 | ); |
| 1117 | }, |
| 1118 | ); |
| 1119 | }, |
| 1120 | [targetPath], |
| 1121 | ); |
| 1054 | 1122 | |
| 1055 | 1123 | const handleContextMenu = useCallback( |
| 1056 | 1124 | async (event: ReactMouseEvent<HTMLAnchorElement>) => { |
| ... | ... | @@ -1060,32 +1128,39 @@ const MarkdownFileLink = memo(function MarkdownFileLink({ |
| 1060 | 1128 | const api = readLocalApi(); |
| 1061 | 1129 | if (!api) return; |
| 1062 | 1130 | |
| 1063 | | const clicked = await api.contextMenu.show( |
| 1064 | | [ |
| 1065 | | { id: "open", label: "Open in editor" }, |
| 1066 | | ...(onOpenInBrowser |
| 1067 | | ? ([{ id: "open-in-browser", label: "Open in integrated browser" }] as const) |
| 1068 | | : []), |
| 1069 | | { id: "copy-relative", label: "Copy relative path" }, |
| 1070 | | { id: "copy-full", label: "Copy full path" }, |
| 1071 | | ] as const, |
| 1072 | | { x: event.clientX, y: event.clientY }, |
| 1073 | | ); |
| 1131 | try { |
| 1132 | const clicked = await api.contextMenu.show( |
| 1133 | [ |
| 1134 | { id: "open", label: "Open in editor" }, |
| 1135 | ...(onOpenInBrowser |
| 1136 | ? ([{ id: "open-in-browser", label: "Open in integrated browser" }] as const) |
| 1137 | : []), |
| 1138 | { id: "copy-relative", label: "Copy relative path" }, |
| 1139 | { id: "copy-full", label: "Copy full path" }, |
| 1140 | ] as const, |
| 1141 | { x: event.clientX, y: event.clientY }, |
| 1142 | ); |
| 1074 | 1143 | |
| 1075 | | if (clicked === "open") { |
| 1076 | | handleOpenInEditor(); |
| 1077 | | return; |
| 1078 | | } |
| 1079 | | if (clicked === "open-in-browser") { |
| 1080 | | handleOpenInBrowser(); |
| 1081 | | return; |
| 1082 | | } |
| 1083 | | if (clicked === "copy-relative") { |
| 1084 | | handleCopy(displayPath, "Relative path"); |
| 1085 | | return; |
| 1086 | | } |
| 1087 | | if (clicked === "copy-full") { |
| 1088 | | handleCopy(targetPath, "Full path"); |
| 1144 | if (clicked === "open") { |
| 1145 | handleOpenInEditor(); |
| 1146 | return; |
| 1147 | } |
| 1148 | if (clicked === "open-in-browser") { |
| 1149 | handleOpenInBrowser(); |
| 1150 | return; |
| 1151 | } |
| 1152 | if (clicked === "copy-relative") { |
| 1153 | handleCopy(displayPath, "Relative path"); |
| 1154 | return; |
| 1155 | } |
| 1156 | if (clicked === "copy-full") { |
| 1157 | handleCopy(targetPath, "Full path"); |
| 1158 | } |
| 1159 | } catch (cause) { |
| 1160 | reportMarkdownActionFailure( |
| 1161 | { operation: "show-file-context-menu", target: targetPath }, |
| 1162 | cause, |
| 1163 | ); |
| 1089 | 1164 | } |
| 1090 | 1165 | }, |
| 1091 | 1166 | [displayPath, handleCopy, handleOpenInBrowser, handleOpenInEditor, onOpenInBrowser, targetPath], |
| ... | ... | @@ -1315,22 +1390,32 @@ function ChatMarkdown({ |
| 1315 | 1390 | event.stopPropagation(); |
| 1316 | 1391 | const api = readLocalApi(); |
| 1317 | 1392 | if (!api) return; |
| 1318 | | void api.contextMenu |
| 1319 | | .show( |
| 1320 | | [ |
| 1321 | | { id: "open-in-browser", label: "Open in integrated browser" }, |
| 1322 | | { id: "open-external", label: "Open in system browser" }, |
| 1323 | | ] as const, |
| 1324 | | { x: event.clientX, y: event.clientY }, |
| 1325 | | ) |
| 1326 | | .then((clicked) => { |
| 1393 | void (async () => { |
| 1394 | let operation = "show-link-context-menu"; |
| 1395 | try { |
| 1396 | const clicked = await api.contextMenu.show( |
| 1397 | [ |
| 1398 | { id: "open-in-browser", label: "Open in integrated browser" }, |
| 1399 | { id: "open-external", label: "Open in system browser" }, |
| 1400 | ] as const, |
| 1401 | { x: event.clientX, y: event.clientY }, |
| 1402 | ); |
| 1327 | 1403 | if (clicked === "open-in-browser") { |
| 1328 | | void openExternalLinkInPreview(href); |
| 1404 | operation = "open-link-in-preview"; |
| 1405 | const result = await openExternalLinkInPreview(href); |
| 1406 | if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { |
| 1407 | reportMarkdownActionFailure({ operation, target: href }, result.cause); |
| 1408 | } |
| 1329 | 1409 | return; |
| 1330 | 1410 | } |
| 1331 | | if (clicked === "open-external") return api.shell.openExternal(href); |
| 1332 | | }) |
| 1333 | | .catch(() => undefined); |
| 1411 | if (clicked === "open-external") { |
| 1412 | operation = "open-link-external"; |
| 1413 | await api.shell.openExternal(href); |
| 1414 | } |
| 1415 | } catch (cause) { |
| 1416 | reportMarkdownActionFailure({ operation, target: href }, cause); |
| 1417 | } |
| 1418 | })(); |
| 1334 | 1419 | }} |
| 1335 | 1420 | > |
| 1336 | 1421 | {faviconHost ? ( |