From c6ecb86fb419d84d8b22cef24b0545021435e5e7 Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Tue, 15 Sep 2026 19:29:41 +0800 Subject: [PATCH] fix(web): leave chat with a document load for chart, ephemeris, and reports Session URLs are written with history.pushState, so App Router router.push cannot leave the home page. Sidebar destinations now assign the document and stash the current conversation for return. --- frontend/src/components/app-sidebar.tsx | 17 +++++++++++------ .../tests/chat-navigation-a11y-contract.test.ts | 6 +++--- frontend/tests/sidebar-contract.test.ts | 12 ++++++++++++ 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/app-sidebar.tsx b/frontend/src/components/app-sidebar.tsx index 3118201c..d4a15d2e 100644 --- a/frontend/src/components/app-sidebar.tsx +++ b/frontend/src/components/app-sidebar.tsx @@ -18,9 +18,10 @@ import { UserRound, Users, } from "lucide-react"; -import { usePathname, useRouter } from "next/navigation"; +import { usePathname } from "next/navigation"; import { useEffect, useRef } from "react"; import type { Ref } from "react"; +import { persistLoginSessionReturn } from "@/lib/chat-session-url"; import { Sidebar, SidebarContent, @@ -102,7 +103,7 @@ export function AppSidebar({ sessionControls, onAccountMenuOpenChange, onNewChat, - onOpenReports, + onOpenReports: _onOpenReports, onSelectSession, onSelectChart, onAddChart, @@ -113,7 +114,6 @@ export function AppSidebar({ onOpenLogout, }: AppSidebarProps) { const { isMobile, setOpen, setOpenMobile, state, viewport } = useSidebar(); - const router = useRouter(); const pathname = usePathname(); const firstSessionRef = useRef(null); const historyHeadingRef = useRef(null); @@ -149,18 +149,23 @@ export function AppSidebar({ if (isMobile) setOpenMobile(false); } + function leaveChat(path: "/chart" | "/ephemeris" | "/reports") { + persistLoginSessionReturn(); + window.location.assign(path); + } + function handleOpenReports() { - onOpenReports(); + leaveChat("/reports"); if (isMobile) setOpenMobile(false); } function handleOpenChart() { - router.push("/chart"); + leaveChat("/chart"); if (isMobile) setOpenMobile(false); } function handleOpenEphemeris() { - router.push("/ephemeris"); + leaveChat("/ephemeris"); if (isMobile) setOpenMobile(false); } diff --git a/frontend/tests/chat-navigation-a11y-contract.test.ts b/frontend/tests/chat-navigation-a11y-contract.test.ts index f4e78b78..a49c9a2a 100644 --- a/frontend/tests/chat-navigation-a11y-contract.test.ts +++ b/frontend/tests/chat-navigation-a11y-contract.test.ts @@ -27,15 +27,15 @@ test("in-app destinations navigate client-side so the chat survives the round tr assert.match(pageSource, /const router = useRouter\(\)/); // 原值: 4 条 membershipHref 软跳 /membership - // 新值: 账单入口改 openAccountDialog("billing");报告仍 router.push - // 原因: 删除 /membership 整页,对话状态不丢 + // 新值: 账单入口改 openAccountDialog("billing");离开对话的星盘/星历/报告走 document load + // 原因: 对话用 history.pushState 写 ?c=,App Router 的 router.push 无法离开首页 assert.match(pageSource, /onOpenReports=\{\(\) => router\.push\("\/reports"\)\}/); assert.match(pageSource, /onOpenBilling=\{\(\) => openAccountDialog\("billing", \{ source: "account-menu" \}\)\}/); assert.match(pageSource, /openAccountDialog\("billing", \{ returnTarget: event\.currentTarget, source: "credits" \}\)/); assert.match(pageSource, /openAccountDialog\("billing", \{ source: "insufficient-credits" \}\)/); assert.match(pageSource, /if \(response\.status === 402\) openAccountDialog\("billing", \{ source: "insufficient-credits" \}\)/); - // And: none of them fall back to a document load. + // And: membership stays in-page; leaving chat uses a document load in the sidebar. assert.doesNotMatch(pageSource, /window\.location\.assign\("\/reports"\)/); assert.doesNotMatch(pageSource, /window\.location\.assign\(membershipHref\(/); }); diff --git a/frontend/tests/sidebar-contract.test.ts b/frontend/tests/sidebar-contract.test.ts index 261de1da..adee2e65 100644 --- a/frontend/tests/sidebar-contract.test.ts +++ b/frontend/tests/sidebar-contract.test.ts @@ -185,6 +185,18 @@ test("keeps the sidebar brand row free of a duplicate collapse trigger", () => { assert.doesNotMatch(appSidebar, /SidebarTrigger/); }); +test("leaves the chat document for chart, ephemeris, and reports", () => { + const appSidebar = readProjectFile("src/components/app-sidebar.tsx"); + // 对话首页用 history.pushState 维护 ?c=,App Router 的 router.push 无法离开。 + assert.match(appSidebar, /persistLoginSessionReturn\(\);\s*window\.location\.assign\(path\)/); + assert.match(appSidebar, /leaveChat\("\/chart"\)/); + assert.match(appSidebar, /leaveChat\("\/ephemeris"\)/); + assert.match(appSidebar, /leaveChat\("\/reports"\)/); + assert.doesNotMatch(appSidebar, /router\.push\("\/chart"\)/); + assert.doesNotMatch(appSidebar, /router\.push\("\/ephemeris"\)/); + assert.doesNotMatch(appSidebar, /useRouter/); +}); + test("uses one collapsed history action instead of icon-only session rows", () => { const appSidebar = readProjectFile("src/components/app-sidebar.tsx"); assert.match(appSidebar, /MessageSquareText/);