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
120120const CODE_FENCE_LANGUAGE_REGEX = /(?:^|\s)language-([^\s]+)/;
121121const MAX_HIGHLIGHT_CACHE_ENTRIES = 500;
122122const 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
123137const highlightedCodeCache = new LRUCache<string>(
124138 MAX_HIGHLIGHT_CACHE_ENTRIES,
125139 MAX_HIGHLIGHT_CACHE_MEMORY_BYTES,
......@@ -332,7 +346,9 @@ function MarkdownTable({ children, ...props }: React.ComponentProps<"table">) {
332346 copiedTimerRef.current = null;
333347 }, 1200);
334348 })
335 .catch(() => undefined);
349 .catch((cause) => {
350 reportMarkdownActionFailure({ operation: "copy-table", format }, cause);
351 });
336352 }, []);
337353
338354 useEffect(
......@@ -529,8 +545,17 @@ function MarkdownCodeBlock({
529545 copiedTimerRef.current = null;
530546 }, 1200);
531547 })
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]);
534559
535560 useEffect(
536561 () => () => {
......@@ -977,18 +1002,36 @@ const MarkdownFileLink = memo(function MarkdownFileLink({
9771002}: MarkdownFileLinkProps) {
9781003 const handleOpenInEditor = useCallback(() => {
9791004 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 );
9831034 }
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 );
9921035 })();
9931036 }, [onOpen, targetPath]);
9941037
......@@ -1005,52 +1048,77 @@ const MarkdownFileLink = memo(function MarkdownFileLink({
10051048 return;
10061049 }
10071050 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 );
10111080 }
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 );
10201081 })();
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]);
10341083
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) {
10441087 toastManager.add(
10451088 stackedThreadToast({
10461089 type: "error",
10471090 title: `Failed to copy ${title.toLowerCase()}`,
1048 description: error instanceof Error ? error.message : "An error occurred.",
1091 description: "Clipboard API unavailable.",
10491092 }),
10501093 );
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 );
10541122
10551123 const handleContextMenu = useCallback(
10561124 async (event: ReactMouseEvent<HTMLAnchorElement>) => {
......@@ -1060,32 +1128,39 @@ const MarkdownFileLink = memo(function MarkdownFileLink({
10601128 const api = readLocalApi();
10611129 if (!api) return;
10621130
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 );
10741143
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 );
10891164 }
10901165 },
10911166 [displayPath, handleCopy, handleOpenInBrowser, handleOpenInEditor, onOpenInBrowser, targetPath],
......@@ -1315,22 +1390,32 @@ function ChatMarkdown({
13151390 event.stopPropagation();
13161391 const api = readLocalApi();
13171392 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 );
13271403 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 }
13291409 return;
13301410 }
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 })();
13341419 }}
13351420 >
13361421 {faviconHost ? (