authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 15:14:41-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 15:14:41-07:00
logaab72f10c1227b7bbf9cce76644e374e275a4e1e
treeb11beed31360f56d4b1f782adddb440c6bb4a813
parenta76b7bbfb5ad1e87714d85516e68bda594f9f830
signature Signed by PGP key B5690EEEBB952194

[codex] Report markdown interaction failures (#3355)

Co-authored-by: codex <codex@users.noreply.github.com>

1 files changed, 174 insertions(+), 89 deletions(-)

apps/web/src/components/ChatMarkdown.tsx+174-89
...@@ -120,6 +120,20 @@ const EMPTY_MARKDOWN_SKILLS: ReadonlyArray<Pick<ServerProviderSkill, "name" | "d...@@ -120,6 +120,20 @@ const EMPTY_MARKDOWN_SKILLS: ReadonlyArray<Pick<ServerProviderSkill, "name" | "d
120const CODE_FENCE_LANGUAGE_REGEX = /(?:^|\s)language-([^\s]+)/;120const CODE_FENCE_LANGUAGE_REGEX = /(?:^|\s)language-([^\s]+)/;
121const MAX_HIGHLIGHT_CACHE_ENTRIES = 500;121const MAX_HIGHLIGHT_CACHE_ENTRIES = 500;
122const MAX_HIGHLIGHT_CACHE_MEMORY_BYTES = 50 * 1024 * 1024;122const MAX_HIGHLIGHT_CACHE_MEMORY_BYTES = 50 * 1024 * 1024;
123
124interface 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
133function reportMarkdownActionFailure(context: MarkdownActionFailureContext, cause: unknown): void {
134 console.error("[chat-markdown] action failed", context, cause);
135}
136
123const highlightedCodeCache = new LRUCache<string>(137const highlightedCodeCache = new LRUCache<string>(
124 MAX_HIGHLIGHT_CACHE_ENTRIES,138 MAX_HIGHLIGHT_CACHE_ENTRIES,
125 MAX_HIGHLIGHT_CACHE_MEMORY_BYTES,139 MAX_HIGHLIGHT_CACHE_MEMORY_BYTES,
...@@ -332,7 +346,9 @@ function MarkdownTable({ children, ...props }: React.ComponentProps<"table">) {...@@ -332,7 +346,9 @@ function MarkdownTable({ children, ...props }: React.ComponentProps<"table">) {
332 copiedTimerRef.current = null;346 copiedTimerRef.current = null;
333 }, 1200);347 }, 1200);
334 })348 })
335 .catch(() => undefined);349 .catch((cause) => {
350 reportMarkdownActionFailure({ operation: "copy-table", format }, cause);
351 });
336 }, []);352 }, []);
337353
338 useEffect(354 useEffect(
...@@ -529,8 +545,17 @@ function MarkdownCodeBlock({...@@ -529,8 +545,17 @@ function MarkdownCodeBlock({
529 copiedTimerRef.current = null;545 copiedTimerRef.current = null;
530 }, 1200);546 }, 1200);
531 })547 })
532 .catch(() => undefined);548 .catch((cause) => {
533 }, [code]);549 reportMarkdownActionFailure(
550 {
551 operation: "copy-code-block",
552 language,
553 ...(fenceTitle ? { fenceTitle } : {}),
554 },
555 cause,
556 );
557 });
558 }, [code, fenceTitle, language]);
534559
535 useEffect(560 useEffect(
536 () => () => {561 () => () => {
...@@ -977,18 +1002,36 @@ const MarkdownFileLink = memo(function MarkdownFileLink({...@@ -977,18 +1002,36 @@ const MarkdownFileLink = memo(function MarkdownFileLink({
977}: MarkdownFileLinkProps) {1002}: MarkdownFileLinkProps) {
978 const handleOpenInEditor = useCallback(() => {1003 const handleOpenInEditor = useCallback(() => {
979 void (async () => {1004 void (async () => {
980 const result = await onOpen(targetPath);1005 try {
981 if (result._tag === "Success" || isAtomCommandInterrupted(result)) {1006 const result = await onOpen(targetPath);
982 return;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 }, [onOpen, targetPath]);1036 }, [onOpen, targetPath]);
9941037
...@@ -1005,52 +1048,77 @@ const MarkdownFileLink = memo(function MarkdownFileLink({...@@ -1005,52 +1048,77 @@ const MarkdownFileLink = memo(function MarkdownFileLink({
1005 return;1048 return;
1006 }1049 }
1007 void (async () => {1050 void (async () => {
1008 const result = await onOpenInBrowser();1051 try {
1009 if (result._tag === "Success" || isAtomCommandInterrupted(result)) {1052 const result = await onOpenInBrowser();
1010 return;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]);1082 }, [onOpenInBrowser, targetPath]);
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 }
10341083
1035 void navigator.clipboard.writeText(value).then(1084 const handleCopy = useCallback(
1036 () => {1085 (value: string, title: string) => {
1037 toastManager.add({1086 if (typeof window === "undefined" || !navigator.clipboard?.writeText) {
1038 type: "success",
1039 title: `${title} copied`,
1040 description: value,
1041 });
1042 },
1043 (error) => {
1044 toastManager.add(1087 toastManager.add(
1045 stackedThreadToast({1088 stackedThreadToast({
1046 type: "error",1089 type: "error",
1047 title: `Failed to copy ${title.toLowerCase()}`,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 },1094 return;
1052 );1095 }
1053 }, []);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 );
10541122
1055 const handleContextMenu = useCallback(1123 const handleContextMenu = useCallback(
1056 async (event: ReactMouseEvent<HTMLAnchorElement>) => {1124 async (event: ReactMouseEvent<HTMLAnchorElement>) => {
...@@ -1060,32 +1128,39 @@ const MarkdownFileLink = memo(function MarkdownFileLink({...@@ -1060,32 +1128,39 @@ const MarkdownFileLink = memo(function MarkdownFileLink({
1060 const api = readLocalApi();1128 const api = readLocalApi();
1061 if (!api) return;1129 if (!api) return;
10621130
1063 const clicked = await api.contextMenu.show(1131 try {
1064 [1132 const clicked = await api.contextMenu.show(
1065 { id: "open", label: "Open in editor" },1133 [
1066 ...(onOpenInBrowser1134 { id: "open", label: "Open in editor" },
1067 ? ([{ id: "open-in-browser", label: "Open in integrated browser" }] as const)1135 ...(onOpenInBrowser
1068 : []),1136 ? ([{ id: "open-in-browser", label: "Open in integrated browser" }] as const)
1069 { id: "copy-relative", label: "Copy relative path" },1137 : []),
1070 { id: "copy-full", label: "Copy full path" },1138 { id: "copy-relative", label: "Copy relative path" },
1071 ] as const,1139 { id: "copy-full", label: "Copy full path" },
1072 { x: event.clientX, y: event.clientY },1140 ] as const,
1073 );1141 { x: event.clientX, y: event.clientY },
1142 );
10741143
1075 if (clicked === "open") {1144 if (clicked === "open") {
1076 handleOpenInEditor();1145 handleOpenInEditor();
1077 return;1146 return;
1078 }1147 }
1079 if (clicked === "open-in-browser") {1148 if (clicked === "open-in-browser") {
1080 handleOpenInBrowser();1149 handleOpenInBrowser();
1081 return;1150 return;
1082 }1151 }
1083 if (clicked === "copy-relative") {1152 if (clicked === "copy-relative") {
1084 handleCopy(displayPath, "Relative path");1153 handleCopy(displayPath, "Relative path");
1085 return;1154 return;
1086 }1155 }
1087 if (clicked === "copy-full") {1156 if (clicked === "copy-full") {
1088 handleCopy(targetPath, "Full path");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 [displayPath, handleCopy, handleOpenInBrowser, handleOpenInEditor, onOpenInBrowser, targetPath],1166 [displayPath, handleCopy, handleOpenInBrowser, handleOpenInEditor, onOpenInBrowser, targetPath],
...@@ -1315,22 +1390,32 @@ function ChatMarkdown({...@@ -1315,22 +1390,32 @@ function ChatMarkdown({
1315 event.stopPropagation();1390 event.stopPropagation();
1316 const api = readLocalApi();1391 const api = readLocalApi();
1317 if (!api) return;1392 if (!api) return;
1318 void api.contextMenu1393 void (async () => {
1319 .show(1394 let operation = "show-link-context-menu";
1320 [1395 try {
1321 { id: "open-in-browser", label: "Open in integrated browser" },1396 const clicked = await api.contextMenu.show(
1322 { id: "open-external", label: "Open in system browser" },1397 [
1323 ] as const,1398 { id: "open-in-browser", label: "Open in integrated browser" },
1324 { x: event.clientX, y: event.clientY },1399 { id: "open-external", label: "Open in system browser" },
1325 )1400 ] as const,
1326 .then((clicked) => {1401 { x: event.clientX, y: event.clientY },
1402 );
1327 if (clicked === "open-in-browser") {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 return;1409 return;
1330 }1410 }
1331 if (clicked === "open-external") return api.shell.openExternal(href);1411 if (clicked === "open-external") {
1332 })1412 operation = "open-link-external";
1333 .catch(() => undefined);1413 await api.shell.openExternal(href);
1414 }
1415 } catch (cause) {
1416 reportMarkdownActionFailure({ operation, target: href }, cause);
1417 }
1418 })();
1334 }}1419 }}
1335 >1420 >
1336 {faviconHost ? (1421 {faviconHost ? (