diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json
index 59a498e5..1496e315 100644
--- a/apps/client/public/locales/en-US/translation.json
+++ b/apps/client/public/locales/en-US/translation.json
@@ -947,6 +947,8 @@
"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}}",
"Automatically provision users and groups from your identity provider via SCIM.": "Automatically provision users and groups from your identity provider via SCIM.",
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 1e4b0ed8..ef3aabe6 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
@@ -10,6 +10,7 @@ import { Group, Loader, Tooltip } from "@mantine/core";
import {
IconArrowsDiagonal,
IconChevronDown,
+ IconFileExport,
IconGripVertical,
IconMinus,
IconPlus,
@@ -33,6 +34,7 @@ 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 classes from "@/features/ai-chat/components/ai-chat-window.module.css";
// Default window geometry (from the GitmostAgent.jsx design).
@@ -154,6 +156,26 @@ export default function AiChatWindow() {
queryClient.invalidateQueries({ queryKey: AI_CHATS_RQ_KEY });
}, [activeChatId, queryClient]);
+ // The active chat object (for its title) and an export gate: only enable the
+ // export button when an existing chat with loaded persisted rows is active.
+ const activeChat = useMemo(
+ () => chats?.items?.find((c) => c.id === activeChatId) ?? null,
+ [chats, activeChatId],
+ );
+ 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(() => {
+ if (!activeChatId || !messageRows || messageRows.length === 0) return;
+ exportChatAsMarkdown({
+ title: activeChat?.title ?? null,
+ chatId: activeChatId,
+ rows: messageRows,
+ t,
+ });
+ }, [activeChatId, messageRows, activeChat, t]);
+
// When awaiting a new chat's id, adopt the most-recent chat (the list is
// ordered newest-first) once it appears.
useEffect(() => {
@@ -308,6 +330,17 @@ export default function AiChatWindow() {