Files
Jyotisha/docs/tasks/TASK-sidebar-unify-20260916.md
T

14 KiB
Raw Blame History

TASK · 侧栏统一成一个组件 + 次级页共享外壳 + 跳转不再整页刷新(2026-09-16)

分支:codex/sidebar-unify-20260916。纯前端,不动数据库、不动 Skill、不动 Python。

基线 commit

origin/staging = cfb41daffeat(rectification): 出卡加精度门槛,补经历改成系统点名)。本单涉及的文件自 317e9f18 起没有变化,进度记录里按开工时的 git rev-parse origin/staging 重新写一次。

事故实证(行号按符号定位,会漂)

产品负责人在 staging 真机上的三条观察,逐条对到代码:

  1. 首页与三个次级页的侧栏长得不一样。
    • /AppSidebarfrontend/src/components/app-sidebar.tsx),会话行是 SidebarSessionRowsidebar-session-row.tsx.session-row > .session-main + 菜单按钮 + 副标题),页脚是 .profile-trigger56px、带 chevron、打开账户菜单)。
    • /chart/ephemeris/reports/reports/[reportId]AppNavRailapp-nav-rail.tsx),会话行在 renderRow 里是 <Link className="session-row nav-rail-row"> 直接套 .session-title没有 .session-main 这一层:丢了 padding: var(--space-2)min-height: 44px、当前项 2px 左标记(globals.css.session-main[data-active="true"]::before),而 .session-rowgrid-template-columns: minmax(0, 1fr) 44px 还给不存在的菜单按钮留着一列 44px 空位。页脚是 .nav-rail-identity(44px、flex、文字「N 点」)。
    • 两个组件共用一份 globals.css,所以「四个导航项」本身一致(.new-chat 强调色、.report-nav-button 文字色,DESIGN.md §Scarce 第 1 条写明这是设计);分叉全在会话行与页脚。
    • SidebarProviderui/sidebar.tsxdefaultOpen = true)不存 cookie / localStorage,折叠状态只活在内存里。
  2. 每次进次级页都重新读会话列表。
    • app-sidebar.tsxleaveChat()window.location.assign(path):首页 → 次级页是整页刷新React 树与内存全部清零。
    • 三个次级页各自在组件内部渲染 SecondaryShellchart-page-view.tsxephemeris-page.tsxpersonal-report-center.tsxpersonal-report-page.tsx 共 8 处调用),每个 SecondaryShell 挂一份 SidebarProvider + AppNavRailapp/ 下没有把这四个路由包起来的 layout,所以次级页之间即便是客户端跳转,外壳也整个重挂,useNavRail()hooks/use-nav-rail.ts)的 effect 重新 GET /api/sessions?limit=40 + GET /api/account,且无任何缓存。
  3. 从次级页回「新建对话」等很久。
    • AppNavRail 里「新建对话」是 <Link href="/">,客户端跳转本身没问题;慢在 Home() 的启动链每次都从零跑:page.tsxloadCloudData()Promise.all(账户, 模型目录, 会话列表)fetchActiveConsultationStatusresolveLookupBootstrap → 当前会话 fetchSessionDetailsetBootstrapPhase("prepare") → 「准备」阶段并行拉 /api/rectification/cases/entry-summaryPOST /api/daily-starlanguagebootstrapRevealDelayMslib/home-bootstrap.ts,上限 BOOTSTRAP_PREPARE_TIMEOUT_MS = 4000)→ setHydrated(true) 揭幕。串行至少 4 次往返。
    • 2026-09-16 从验收机测 staging:接口单次 TTFB 0.630.90 s/ 首屏脚本 23 个 gzip 604 KB/chart 17 个 gzip 283 KB,从次级页回 / 要新下载 8 个共 gzip 339 KB<Link> 默认会预取,但未在浏览器里证实)。

根因

  • 样式分叉:TASK-cend-surfaces-claude-alignment-20260916 D9 决定次级页侧栏只读,T4.1 为了不把 Home() 的会话管理层上提而另写了第二个组件。D9 只说「不带操作」,没说「长得不一样」;分叉是实现手段的副作用,不是设计。
  • 重复读取:没有共享 layout、没有缓存、首页出去是整页刷新,三件事叠加。
  • 回首页慢:启动链串行且不复用侧栏已经拿到的账户与会话列表;「准备」阶段两个请求每次现拉。

决策记录(产品负责人 2026-09-16 拍板)

  • D1|侧栏只保留一个组件。 AppSidebar 收编 AppNavRail:会话操作回调与账户菜单改成可选,不传就渲染成只读(行是纯链接、页脚是去 / 的链接)。app-nav-rail.tsx 删除。D9「次级页不带写操作」维持,只是实现方式从「第二个组件」改成「同一组件的只读模式」。
  • D2|四个次级路由共享一个外壳实例。 用路由组 app/(secondary)/ 的 layout 挂 SidebarProvider + AppSidebar,页面只保留 46px 顶栏与正文。路由之间跳转不重挂侧栏、不重拉列表。
  • D3|首页进次级页改客户端跳转。 leaveChat()window.location.assign 换成 <Link>(保留 persistLoginSessionReturn())。/login 维持硬跳转。
  • D4|会话列表与账户在内存里缓存,先用旧数据再后台刷新。 缓存是模块级、按账户 id 键、同一标签页内有效、过期时间 60 s;/ 上的会话写操作(新建 / 重命名 / 删除 / 归档 / 收藏)成功后写穿缓存或使其失效。不落 localStorage。
  • D5|折叠状态跨页记住。 存 cookieshadcn 的 sidebar_state 惯例)或 localStorage 二选一,进度记录写明选哪个;移动端抽屉状态不记。
  • D6|本单不动 Home() 的启动链。 回首页的等待另开一单(TASK-home-bootstrap-reuse),前提是本单先落地让内存缓存能活过跳转。本单只要求「新建对话」链接保持 <Link> 且预取不被关掉。

硬红线

  1. ./node_modules/.bin/tsc --noEmit 0 错;npm run lint 0 error
  2. 测试总数不得低于开工时 origin/staging 的实测;改既有断言必须写「原值 / 新值 / 原因」三栏。tests/sidebar-contract.test.ts43 条)与 tests/sidebar-state.test.ts(5 条)是侧栏 CSS 与状态合同,不得删条。
  3. next build/ 保持 ○ Static/chart/ephemeris 的渲染标记改前改后不变;/reports/reports/[reportId] 维持 force-dynamic。路由组不得改变任何 URL。四个路由的标记逐个列进进度记录。
  4. / 首屏 gzip ±2%(口径沿用 PROGRESS-cend-ui-r1-20260916.md.next/static 下 css chunk gzip 合计,另附 / HTML 引用的 js chunk gzip 合计)。次级页体积改前改后列出。
  5. frontend/src/app/page.tsx 不得增长;Home()useState / useRef 数不得增长(tests/home-shell-growth-contract.test.ts)。本单对 page.tsx 的改动只允许:传给 AppSidebar 的 props 变化、写操作后调用缓存失效。
  6. 次级页上的侧栏不得发起任何写操作接口(D9 维持)。只读模式下 AppSidebar 不得 import page.tsx 的任何 hook。
  7. 揭幕后不得出现 spinner / 骨架 /「正在加载」。缓存未命中时侧栏显示现有静态空态文案(sidebar-empty),不是骨架。
  8. 不改 useConversationScrollAnchorChatComposer;不动 BUG-698 的 @supports (height: 1dvh) 写法;不动报告盘面 grid。
  9. 改 UI 的同一提交内更新 frontend/DESIGN.md(「The rail is read-only」一节改写成「只读模式」,删掉 .nav-rail-* 的描述)。
  10. 不顺手升级依赖、不顺手修不在本单里的 warning。

任务分解

T1 · AppSidebar 收编 AppNavRailD1

  • AppSidebarProps 拆成两组:sessions / activeSessionId / account / pathname(两边都要)与 controls?: { sessionControls, onNewChat, onSelectSession, onAccountMenuOpenChange, accountTriggerRef, onOpenProfile … }(只有 / 传)。controls 缺省时:
    • 四个导航项:新建对话是 <Link href="/">,三个页面项是 <Link> 并按 pathnamedata-active(沿用 report-nav-button 现有选中态)。
    • 会话行:仍渲染 SidebarSessionRow同一套标记.session-row > .session-main),但 .session-main<Link href={sessionHref("", id)}>,不渲染菜单按钮;grid-template-columns 在只读模式下不留 44px 空列(加 data-readonly 属性切换,不新造类名)。
    • 页脚:同一 .profile-trigger 标记,渲染成 <Link href="/">,不带 chevron、不带菜单。
  • 删除 app-nav-rail.tsxglobals.css.nav-rail-row / .nav-rail-identity 段。
  • use-nav-rail.ts 改名为 use-sidebar-data.ts(或等价),加 D4 的缓存(见 T3)。

验收标准

  • grep -rn "AppNavRail\|nav-rail" frontend/src 为 0。
  • //chart 的侧栏 DOM 结构除了「菜单按钮 / chevron / 账户菜单」三处之外逐节点相同;进度记录贴两页 document.querySelector('[data-sidebar="sidebar"]').outerHTML 的 diff(脱敏会话标题)。
  • 只读模式下 document.querySelectorAll('[data-sidebar="sidebar"] button') 只剩折叠触发器。
  • sidebar-contract.test.ts 全绿,不删条;新增断言:只读模式会话行有 .session-main、无 .session-menu-trigger、无 44px 空列。

T2 · 路由组 layout 承载外壳(D2)

  • 新建 app/(secondary)/layout.tsxclient component):SidebarProvider + AppSidebar(只读) + SidebarInsetchildren 是页面。
  • app/chartapp/ephemerisapp/reports 移进 app/(secondary)/page.tsx 内容与 metadatadynamic 声明原样保留。
  • SecondaryShell 拆成只剩顶栏的 SecondaryHeader({ title, note, actions })8 处调用改名;SidebarTrigger 仍在顶栏里(provider 由 layout 提供)。
  • ../site-styles 的 import 移到 layout 一处。

验收标准

  • 四个 URL 不变;next build 路由表四个标记与开工时一致(红线 3)。
  • /chart/ephemeris/reports 之间跳转,Network 面板里没有 /api/sessions/api/account 请求(缓存未过期时);这条做不了自动化就写进 docs/testing/ 清单,并用 tests/ 里的渲染测试证明 layout 只挂一次(记录 useSidebarData 的 effect 调用次数)。
  • /reports/[reportId] 的打印样式(media="print" 隐藏侧栏,见 DESIGN.md 「Print inside the shell」)在 layout 化之后仍生效,进度记录写明验证方式。

T3 · 列表与账户缓存 + 首页写穿(D4)

  • 模块级 Map<accountId, { sessions, account, fetchedAt }>useSidebarData() 先同步读缓存渲染,再后台刷新;60 s 内不重拉。
  • /useSessionManagement 的新建 / 重命名 / 删除 / 归档 / 收藏成功回调里调用 invalidateSidebarCache()(或直接写穿)。这是本单唯一允许碰 page.tsx / use-session-management.ts 的地方。
  • 401 时清缓存并按现有 signedOut 分支降级。

验收标准

  • 单测:命中缓存时不发 fetch;过期后发一次;写操作后下一次进次级页拿到的是新标题 / 新顺序(用 vi.useFakeTimers 推时间)。
  • / 重命名一个会话 → 进 /chart,侧栏立刻显示新标题(真机清单)。

T4 · 首页进次级页改客户端跳转(D3)

  • app-sidebar.tsxleaveChat() 删除,三个页面项改 <Link>onClick 里保留 persistLoginSessionReturn() 与移动端关抽屉。
  • onOpenReports 这个已经没人用的 prop(当前解构成 _onOpenReports)一起删。

验收标准

  • 点星盘 / 星历 / 我的报告不再整页刷新(Network 里没有 document 请求;真机清单)。
  • /login 仍是硬跳转(tests/chat-session-url.test.ts 补一条 persistLoginSessionReturn 在跳转前被调用的断言)。

T5 · 折叠状态持久化(D5

  • SidebarProvider 读写 cookie sidebar_state(或 localStorage,二选一写明),defaultOpen 由 layout / page.tsx 从存储读;SSR 首帧与客户端一致,不得闪一下。
  • tests/sidebar-state.test.ts 补两条:收起后重挂仍收起;移动端不受影响。

验收标准

  • / 收起侧栏 → 进 /chart,仍收起;反向同理。

T6 · 文档与清单

  • frontend/DESIGN.md:侧栏一节改成「一个组件、两种模式」,写明只读模式少哪三样。
  • docs/testing/sidebar-unify-20260916.md:真机清单(跳转无 document 请求、跨页无 /api/sessions、重命名后立刻可见、折叠状态跨页、打印隐藏侧栏、移动端抽屉)。
  • docs/BUG_HISTORY.md:BUG-744 侧栏两套标记结构导致次级页会话行 / 页脚样式分叉;BUG-745 首页进次级页整页刷新 + 次级页外壳逐页重挂导致每次跳转重拉会话列表;BUG-746 折叠状态不持久。三条都要有针对性回归测试才能写 resolved
  • CHANGELOG.md 一条。

让步顺序

  1. T5 折叠持久化如果 SSR 首帧对不齐(闪一下),可以退到只在客户端 useEffect 后同步,进度记录写明并保留 docs/testing/ 条目。
  2. T3 的写穿如果撞 page.tsx 增长门禁,退到「写操作后只失效、不写穿」。
  3. T2 若 /reports/[reportId] 的打印样式在 layout 化后失效且一时修不好,先让 [reportId] 留在路由组外、保留一份自己的外壳,其余三页照做,并在进度记录标 blocked
  4. 不得让步的:T1(一个组件)、T4(不整页刷新)、红线 5 / 6。

开工前置命令

git -C /workspace/Jyotisha status -sb | head -1
git -C /workspace/Jyotisha fetch origin --prune
git -C /workspace/Jyotisha worktree add -b codex/sidebar-unify-20260916 .worktrees/sidebar-unify-20260916 origin/staging
cd /workspace/Jyotisha/.worktrees/sidebar-unify-20260916/frontend && npm ci
./node_modules/.bin/tsc --noEmit && npm run lint && npm test 2>&1 | tail -5   # 记基线:测试总数、fail 清单
npm run build 2>&1 | grep -E "○|ƒ|λ" | grep -E "^\s*[○ƒλ] /(chart|ephemeris|reports|$)"   # 记四个路由标记

同一时间若有别的单在改 frontend/src/app/page.tsxglobals.css(当前状态板上:TASK-home-state-lowering-batch2-20260916 待领取),本单后于它合入;本单对这两个文件的改动都很小,rebase 即可。

BUG 编号起点

开工时核对 docs/BUG_HISTORY.md 最大号;本单写作时最大号为 BUG-743,本单从 BUG-744 起。