From 06648d91bb0eec2df339012317c60ca16b9a9457 Mon Sep 17 00:00:00 2001 From: vvzvlad Date: Thu, 18 Jun 2026 17:53:18 +0300 Subject: [PATCH] feat(ai-chat): copy agent chat as Markdown to clipboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a header button to the AI agent chat window that copies the active conversation to the clipboard as Markdown, including the request internals already persisted client-side — tool calls with their input/output, per-message token usage, and finish/error info. No new network call and no server/DB change: it serializes the already-loaded persisted message rows. - New util chat-markdown.ts (renamed from export-chat.ts): pure buildChatMarkdown() serializer reusing the tool-parts helpers so tool labels match the on-screen labels; fence() escapes embedded code fences. - ai-chat-window.tsx: Copy button (shown only for a saved chat with loaded rows) using the project useClipboard hook; toggles a check icon on success and shows the standard "Copied" notification. Drag is unaffected (startDrag ignores button clicks). - en-US: add "Copy chat" key, drop the obsolete "Export chat". --- .../public/locales/en-US/translation.json | 2 +- .../ai-chat/components/ai-chat-window.tsx | 27 ++++++----- .../{export-chat.ts => chat-markdown.ts} | 45 ++++--------------- 3 files changed, 26 insertions(+), 48 deletions(-) rename apps/client/src/features/ai-chat/utils/{export-chat.ts => chat-markdown.ts} (76%) diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index 1496e315..847b67a4 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -253,6 +253,7 @@ "Invite link": "Invite link", "Copy": "Copy", "Copy to space": "Copy to space", + "Copy chat": "Copy chat", "Copied": "Copied", "Duplicate": "Duplicate", "Select a user": "Select a user", @@ -947,7 +948,6 @@ "Try a different search term.": "Try a different search term.", "Try again": "Try again", "Untitled chat": "Untitled chat", - "Export chat": "Export chat", "You": "You", "What can I help you with?": "What can I help you with?", "Are you sure you want to revoke this {{credential}}": "Are you sure you want to revoke this {{credential}}", diff --git a/apps/client/src/features/ai-chat/components/ai-chat-window.tsx b/apps/client/src/features/ai-chat/components/ai-chat-window.tsx index ef3aabe6..c034b4c2 100644 --- a/apps/client/src/features/ai-chat/components/ai-chat-window.tsx +++ b/apps/client/src/features/ai-chat/components/ai-chat-window.tsx @@ -9,8 +9,9 @@ import { import { Group, Loader, Tooltip } from "@mantine/core"; import { IconArrowsDiagonal, + IconCheck, IconChevronDown, - IconFileExport, + IconCopy, IconGripVertical, IconMinus, IconPlus, @@ -34,7 +35,9 @@ import { } from "@/features/ai-chat/queries/ai-chat-query.ts"; import ConversationList from "@/features/ai-chat/components/conversation-list.tsx"; import ChatThread from "@/features/ai-chat/components/chat-thread.tsx"; -import { exportChatAsMarkdown } from "@/features/ai-chat/utils/export-chat.ts"; +import { buildChatMarkdown } from "@/features/ai-chat/utils/chat-markdown.ts"; +import { useClipboard } from "@/hooks/use-clipboard"; +import { notifications } from "@mantine/notifications"; import classes from "@/features/ai-chat/components/ai-chat-window.module.css"; // Default window geometry (from the GitmostAgent.jsx design). @@ -90,6 +93,7 @@ function clampGeom(g: { left: number; top: number; width: number; height: number */ export default function AiChatWindow() { const { t } = useTranslation(); + const clipboard = useClipboard({ timeout: 500 }); const queryClient = useQueryClient(); const [windowOpen, setWindowOpen] = useAtom(aiChatWindowOpenAtom); const [activeChatId, setActiveChatId] = useAtom(activeAiChatIdAtom); @@ -165,16 +169,19 @@ export default function AiChatWindow() { const canExport = !!activeChatId && !!messageRows && messageRows.length > 0; // Build a Markdown export from the already-loaded persisted rows (no network - // call) and trigger a browser download. The download dialog is the feedback. - const handleExport = useCallback(() => { + // call) and copy it to the clipboard. The "Copied" notification is the + // feedback. + const handleCopy = useCallback(() => { if (!activeChatId || !messageRows || messageRows.length === 0) return; - exportChatAsMarkdown({ + const markdown = buildChatMarkdown({ title: activeChat?.title ?? null, chatId: activeChatId, rows: messageRows, t, }); - }, [activeChatId, messageRows, activeChat, t]); + clipboard.copy(markdown); + notifications.show({ message: t("Copied") }); + }, [activeChatId, messageRows, activeChat, clipboard, t]); // When awaiting a new chat's id, adopt the most-recent chat (the list is // ordered newest-first) once it appears. @@ -334,11 +341,11 @@ export default function AiChatWindow() { )}