From f71999d99c15630b673e46236cb65dce5d0ef531 Mon Sep 17 00:00:00 2001 From: stanGSA Date: Sat, 10 Oct 2026 16:02:05 +0800 Subject: [PATCH] fix(frontend): export complete persisted conversation history (#6530) * fix(frontend): export complete persisted conversation history * fix(frontend): preserve canonical export order across runs --------- Co-authored-by: stanGSA <225717802+stanGSA@users.noreply.github.com> --- README.md | 5 + frontend/src/AGENTS.md | 6 + .../components/workspace/export-trigger.tsx | 28 +++- .../components/workspace/recent-chat-list.tsx | 22 ++- frontend/src/core/threads/hooks.ts | 36 +++++ .../workspace/export-trigger.dom.test.tsx | 147 ++++++++++++++++++ 6 files changed, 231 insertions(+), 13 deletions(-) create mode 100644 frontend/tests/unit/components/workspace/export-trigger.dom.test.tsx diff --git a/README.md b/README.md index aa410b6d63..1adc6e30dd 100644 --- a/README.md +++ b/README.md @@ -802,6 +802,11 @@ Signed-in users' notification toggle, default model, conversation mode, and reas In a new chat, the submitted question stays above its streamed reasoning and tool steps while the server creates the conversation and confirms the message. +Markdown and JSON conversation exports from the chat header or sidebar read all +persisted history pages, including earlier turns outside the loaded view or +compacted model context. A failed history read stops the export instead of +downloading a partial transcript. Public demos export their bundled messages. + Capability Center groups plugins by office collaboration, documents and knowledge, search and research, business and data, and development and operations. The directory includes setup references alongside existing MCP configurations and Lark. Recommended integrations and built-in support do not imply an installed or verified connection; the Installed filter shows configured MCP entries and installed Lark only. Personal MCP connections configured in the web interface are persisted per user. diff --git a/frontend/src/AGENTS.md b/frontend/src/AGENTS.md index af94013729..7d49ff8bad 100644 --- a/frontend/src/AGENTS.md +++ b/frontend/src/AGENTS.md @@ -170,6 +170,12 @@ Array previews coalesce consecutive generated markers only at the end into one o ### Key Patterns +Conversation exports read every persisted message page through +`fetchThreadExportMessages` (200 rows/page). Run `mergeMessages` after history +deduplication to restore earliest trusted positions for identities repeated across runs. +Never export only the loaded UI window or compacted checkpoint. Fail the whole +export on a page error or non-advancing cursor; demos keep their fixture path. + - **Server Components by default**, `"use client"` only for interactive components - **Static root boundary** — `src/app/layout.tsx` must not read cookies or import chat-only KaTeX/Streamdown styles. Auth and workspace layouts own the cookie-derived diff --git a/frontend/src/components/workspace/export-trigger.tsx b/frontend/src/components/workspace/export-trigger.tsx index 66bed29a00..1ac4f13428 100644 --- a/frontend/src/components/workspace/export-trigger.tsx +++ b/frontend/src/components/workspace/export-trigger.tsx @@ -1,7 +1,7 @@ "use client"; import { Download, FileJson, FileText } from "lucide-react"; -import { useCallback } from "react"; +import { useCallback, useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; @@ -12,7 +12,9 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { useI18n } from "@/core/i18n/hooks"; +import { isStaticWebsiteOnly } from "@/core/static-mode"; import { exportThread, type ThreadExportFormat } from "@/core/threads/export"; +import { fetchThreadExportMessages } from "@/core/threads/hooks"; import type { AgentThread } from "@/core/threads/types"; import { useThread } from "./messages/context"; @@ -20,30 +22,43 @@ import { Tooltip } from "./tooltip"; export function ExportTrigger({ threadId }: { threadId: string }) { const { t } = useI18n(); - const { thread } = useThread(); + const { thread, isMock } = useThread(); + const [exporting, setExporting] = useState(false); const messages = thread.messages; const handleExport = useCallback( - (format: ThreadExportFormat) => { + async (format: ThreadExportFormat) => { if (messages.length === 0) { toast.error(t.conversation.noMessages); return; } + setExporting(true); try { + const exportMessages = + isMock || isStaticWebsiteOnly() + ? messages + : await fetchThreadExportMessages(threadId); + if (exportMessages.length === 0) { + toast.error(t.conversation.noMessages); + return; + } const agentThread = { thread_id: threadId, updated_at: new Date().toISOString(), values: thread.values, } as AgentThread; - exportThread(agentThread, messages, format); + exportThread(agentThread, exportMessages, format); toast.success(t.common.exportSuccess); - } catch { + } catch (error) { + console.error(error); toast.error(t.common.exportFailed); + } finally { + setExporting(false); } }, - [messages, thread.values, threadId, t], + [messages, thread.values, threadId, isMock, t], ); if (messages.length === 0) { @@ -56,6 +71,7 @@ export function ExportTrigger({ threadId }: { threadId: string }) {