From e52261fc3da3452c4e691cf71d61213ab56da1bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Wed, 29 Jul 2026 22:25:42 +0800 Subject: [PATCH] fix: preserve full chat embed mode navigation --- .../workspace/general-qa-sidebar.tsx | 38 ++++++++++++++----- .../page-layout/chats-iframe-route.test.mjs | 22 +++++++++++ .../core/page-layout/chats-iframe-route.ts | 20 ++++++++++ 3 files changed, 71 insertions(+), 9 deletions(-) diff --git a/frontend-web/src/components/workspace/general-qa-sidebar.tsx b/frontend-web/src/components/workspace/general-qa-sidebar.tsx index 23bdfbab..51f47ad8 100644 --- a/frontend-web/src/components/workspace/general-qa-sidebar.tsx +++ b/frontend-web/src/components/workspace/general-qa-sidebar.tsx @@ -83,6 +83,7 @@ import { sortThreadsPinnedFirst, titleOfThread, } from "@/core/threads/utils"; +import { buildFullChatNavigationPath } from "@/core/page-layout/chats-iframe-route"; import { env } from "@/env"; import { desensitize } from "@/lib/desensitize"; import { isIMEComposing } from "@/lib/ime"; @@ -185,15 +186,24 @@ export function GeneralQaSidebar() { // 当前嵌入的智能体(URL 上的 ?agent=):用于高亮对应卡片。 const searchParams = useSearchParams(); const activeAgentId = searchParams.get("agent"); + const currentSearch = searchParams.toString(); + const newChatHref = buildFullChatNavigationPath(NEW_CHAT_PATH, currentSearch); + const moreAgentsHref = buildFullChatNavigationPath(MORE_AGENTS_HREF, currentSearch); + const moreChatsHref = buildFullChatNavigationPath(MORE_CHATS_HREF, currentSearch); // 点击常用智能体 = 在通用问答右侧**原地嵌入**该智能体的对话页:停留在 /chats/new, // 仅给 URL 加 ?agent=,由 NewChatContent 把右侧问答区换成 AgentChatPage。 // 不跳转独立页面、左侧栏不变。每次点击都重开一个干净会话(NewChatContent 的 key)。 const handleSelectAgent = useCallback( (agentId: string) => { - navigate(`/page/workspace/chats/new?agent=${encodeURIComponent(agentId)}`); + navigate( + buildFullChatNavigationPath( + `${NEW_CHAT_PATH}?agent=${encodeURIComponent(agentId)}`, + currentSearch, + ), + ); }, - [navigate], + [currentSearch, navigate], ); // ── 最近的对话取数 + 搜索 ── @@ -241,7 +251,7 @@ export function GeneralQaSidebar() { const handleNewChatClick = (event: React.MouseEvent) => { if (event.metaKey || event.ctrlKey || event.shiftKey || event.button === 1) return; event.preventDefault(); - navigate(NEW_CHAT_PATH, { replace: true, state: { resetAt: Date.now() } }); + navigate(newChatHref, { replace: true, state: { resetAt: Date.now() } }); }; const handleDelete = useCallback( @@ -257,10 +267,17 @@ export function GeneralQaSidebar() { nextThreadPath = listPathOfThread(threads[threadIndex - 1]!); } } - void router.push(nextThreadPath); + void router.push(buildFullChatNavigationPath(nextThreadPath, currentSearch)); } }, - [agentIdFromPath, deleteThread, router, threadIdFromPath, threads], + [ + agentIdFromPath, + currentSearch, + deleteThread, + router, + threadIdFromPath, + threads, + ], ); const handleRenameClick = useCallback((threadId: string, currentTitle: string) => { @@ -314,7 +331,7 @@ export function GeneralQaSidebar() { 底色用「主色混入透明」=主色低透明度叠在当前侧栏底色上,浅/红/蓝/深蓝主题都自适应 (不要混 white,否则深蓝主题下会变成浅色块)。 */} {t.common.more} @@ -454,7 +471,10 @@ export function GeneralQaSidebar() { ) : ( filteredThreads.map((thread) => { - const href = listPathOfThread(thread); + const href = buildFullChatNavigationPath( + listPathOfThread(thread), + currentSearch, + ); const isActive = href === pathname; const isBoot = isBootstrapThread(thread); const pinned = isThreadPinned(thread); @@ -576,7 +596,7 @@ export function GeneralQaSidebar() { )} {!query && ( 更多最近对话 diff --git a/frontend-web/src/core/page-layout/chats-iframe-route.test.mjs b/frontend-web/src/core/page-layout/chats-iframe-route.test.mjs index 26692be0..313d53b0 100644 --- a/frontend-web/src/core/page-layout/chats-iframe-route.test.mjs +++ b/frontend-web/src/core/page-layout/chats-iframe-route.test.mjs @@ -3,6 +3,7 @@ import test from "node:test"; import { buildStandardChatRedirectPath, + buildFullChatNavigationPath, buildEmbeddedIframeRedirectPath, buildChatsIframeRedirectPath, buildChatsIframeThreadPath, @@ -280,6 +281,27 @@ test("builds a standard full-chat URL without leaking the legacy marker", () => ); }); +test("keeps full-chat presentation flags when the sidebar opens an agent or thread", () => { + assert.equal( + buildFullChatNavigationPath( + "/page/workspace/chats/new?agent=agent-1", + "?hideChrome=1&chatMode=full", + ), + "/page/workspace/chats/new?agent=agent-1&hideChrome=1&chatMode=full", + ); + assert.equal( + buildFullChatNavigationPath( + "/page/workspace/chats/thread-123", + "?embed=1&theme=dark-blue&chatMode=full", + ), + "/page/workspace/chats/thread-123?embed=1&theme=dark-blue&chatMode=full", + ); + assert.equal( + buildFullChatNavigationPath("/page/workspace/chats/new", ""), + "/page/workspace/chats/new", + ); +}); + test("preserves explicit Brainy iframe mode and theme across redirects", () => { assert.equal( buildChatsIframeRedirectPath("?chatMode=iframe&theme=dark-blue&ticket=secret"), diff --git a/frontend-web/src/core/page-layout/chats-iframe-route.ts b/frontend-web/src/core/page-layout/chats-iframe-route.ts index 244c35cf..07cd0bb9 100644 --- a/frontend-web/src/core/page-layout/chats-iframe-route.ts +++ b/frontend-web/src/core/page-layout/chats-iframe-route.ts @@ -107,6 +107,26 @@ export function buildStandardChatRedirectPath( return query ? `${pathname}?${query}` : pathname; } +/** + * Keep an explicitly selected standard-chat presentation while navigating + * between `/chats/new` and a concrete chat thread. Without this, a click in + * the full-chat sidebar drops `hideChrome=1&chatMode=full`; an iframe host + * then looks like the legacy bare integration and is incorrectly redirected + * to the private AgentSelector workspace. + * + * This deliberately whitelists presentation flags through + * `buildStandardChatRedirectPath` instead of copying arbitrary login/query + * values into user-facing chat URLs. + */ +export function buildFullChatNavigationPath( + destinationPath: string, + search: string, +): string { + return hasIframePresentationOverride(search) || isExplicitFullChatMode(search) + ? buildStandardChatRedirectPath(search, destinationPath) + : destinationPath; +} + /** * Preserve only the presentation parameters consumed by the iframe workspace. * Login credentials must never be copied into the post-login address bar.