Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
263 changes: 174 additions & 89 deletions apps/web/src/components/ChatMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,20 @@ const EMPTY_MARKDOWN_SKILLS: ReadonlyArray<Pick<ServerProviderSkill, "name" | "d
const CODE_FENCE_LANGUAGE_REGEX = /(?:^|\s)language-([^\s]+)/;
const MAX_HIGHLIGHT_CACHE_ENTRIES = 500;
const MAX_HIGHLIGHT_CACHE_MEMORY_BYTES = 50 * 1024 * 1024;

interface MarkdownActionFailureContext {
readonly operation: string;
readonly target?: string;
readonly format?: "markdown" | "csv";
readonly language?: string;
readonly fenceTitle?: string;
readonly copyTarget?: string;
}

function reportMarkdownActionFailure(context: MarkdownActionFailureContext, cause: unknown): void {
console.error("[chat-markdown] action failed", context, cause);
}

const highlightedCodeCache = new LRUCache<string>(
MAX_HIGHLIGHT_CACHE_ENTRIES,
MAX_HIGHLIGHT_CACHE_MEMORY_BYTES,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
() => () => {
Expand Down Expand Up @@ -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]);

Expand All @@ -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<HTMLAnchorElement>) => {
Expand All @@ -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],
Expand Down Expand Up @@ -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 ? (
Expand Down
Loading