Files
Jyotisha/docs/tasks/TASK-secondary-new-chat-intent-20260923.md
T
jesse-uxandClaude Code 8902e48468
Independent Staging Quality Gate / validate (push) Successful in 13m41s
Independent Staging Quality Gate / publish (push) Successful in 3m30s
fix(chat): honor new-chat intent from secondary pages
Create a fresh local consultation for explicit new-chat navigation and keep reserved recovery from taking over its landing. Add regression tests and record validation gaps for remote review.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-24 00:25:28 +08:00

11 KiB
Raw Blame History

TASK · 次级页「新建对话」回首页落到最近一次对话(2026-09-23)

基线

  • origin/staging = 6b53d9bb(docs: record BUG-1011 green gate and staging deployment)。
  • staging 线上 deployment.gitCommit = 018b2b48;deploy/is-docs-only-range.sh 018b2b48… 6b53d9bb… 退出 0,线上即基线代码。
  • 执行分支:codex/secondary-new-chat-intent-20260923,worktree .worktrees/secondary-new-chat-intent-20260923。
  • 与本单同期待领取的前端单:TASK-rectification-session-title-result-20260922.md(改标题算式,不碰本单四个文件);TASK-console-noise-20260916.md(app-sidebar.tsx 不在其范围)。本单无串行依赖。

事故实证

产品在真实环境复现:在 /chart、/ephemeris、/reports 任一页点侧栏「新建对话」,回到 / 后打开的是最近一次对话,不是新会话。每次都如此,不是偶发。

代码链(全部按 origin/staging 6b53d9bb):

  1. frontend/src/components/app-sidebar.tsx,AppSidebar 的 <SidebarMenuItem> 第一项:controls 存在时渲染 <SidebarMenuButton onClick={handleNewChat}>(走 onNewChat → startNewChat());controls 缺省时渲染 <SidebarMenuLink className="new-chat" href="/">。
  2. frontend/src/app/(app)/layout.tsx AppShell:controls={registration?.controls}。只有 / 的 Home() 调 registerShellControls(page.tsx 中 registerShellControls 两处),三个次级页从不注册,所以次级页的「新建对话」= 纯链接 /,URL 上没有任何"新建"意图。
  3. frontend/src/lib/chat-session-url.ts resolveBootstrapSessionSelection:search 无 c、storedReturnId 为空 → { sessionId: defaultSessionId, urlAction: "none" }。
  4. frontend/src/app/(app)/page.tsx bootstrap 内 resolveLookupBootstrap({ …, defaultSessionId: nextSessions[0].id }):默认就是列表第一条=最近更新的会话。
  5. frontend/src/lib/home-bootstrap.ts resolveStarterHomeLandingSessionId / starterHomeLandingNeedsConsultation:urlAction === "none" 时只有"最近一条是生时校正"才改落空咨询或新建;最近一条是普通咨询就直接打开它。

同一入口在 BUG-745 已被记过一次("从次级页点「新建对话」回首页要等很久"),当时修的是慢(整页刷新→客户端跳转),"回到哪"没有动。BUG-989 又把新建会话定为"第一问之前不落库、不写 ?c=",所以也不存在"先建好再带 id 回来"的路径。

根因

次级页的「新建对话」只是一条回首页链接,而首页无参数打开的落点规则是"最近一次对话"。两条规则各自都对,缺的是**"新建"这个意图在 URL 里没有表达方式**。不是启动链算错,不是缓存串会话。

决策记录

  • D1 产品 2026-09-23 确认:次级页「新建对话」必须开一个新会话,与首页按钮行为一致。
  • D2 表达方式定为 URL 意图参数:次级页链接指向 /?new=1;首页启动看到 new=1 就本地建一个空咨询会话并落到它,随后用 replaceState 把 new(连同 c)从地址栏抹掉。
  • D3 沿用 BUG-989:新会话只在本地建,不 POST /api/sessions、不写 ?c=,第一问时由既有 send() → persistSession(…, "create") 落库。isUnsavedEmptyConsultation() 已覆盖启动期用 createSession() 建的会话(page.tsx 中 starterHomeLandingNeedsConsultation 分支就是先例),不需要新的落库路径。
  • D4 优先级:new 胜过 c 与登录返回存根(storedReturnId)。带 new 时忽略两者,并清掉存根(它只服务于登录后回到原会话,用户既然点了新建,存根已过时)。
  • D5 不改的:侧栏页脚 .profile-trigger 与品牌行两个 href="/" 仍是"回首页",落点规则不变;首页自身按钮不变;BUG-989 的三条防复发不变。
  • D6 不在本单:刷新 /(无参数)仍落最近对话——这是既有设计,不是本单事故。
  • D7(执行中追加授权,2026-09-23) 独立验收发现:存在旧 reserved 咨询时,restoreConsultationRecovery() 会把新建落点改回旧咨询。产品明确选择「一并修复」,授权首页在恢复后增加 new-chat 落点保护并清除此次恢复提示,保留旧任务的 pending/recovering 能力,不取消任务、不改恢复 hook。本授权有限扩展红线 5 / T2「首页只加一行」:允许额外一个四行保护块(首页合计 +5 行、state/ref 不增),必须补动态执行真实恢复函数与首页装配段的回归测试;无 new 的恢复行为不变。
  • 执行勘误:当前品牌行是 div + span/strong,没有 D5 所述链接;保持现状,不补造入口。首页无参数还受收藏顺序及登录存根影响,本单不更改这些规则。URL 清理优先满足 T2「激活之前」时序,不拘泥于位于激活之后的 replace-clear 邻行。

硬红线

  1. / 必须保持 ○ Static:读取 new 只能在客户端(与 preview 参数、parseSessionUrlQuery 同一方式),不得引入 useSearchParams(chat-session-url.test.ts 的 "home stays a client-read query on a static route" 已锁)。
  2. 第一问之前不得 POST /api/sessions;未落库会话不得写入 ?c=;列表响应不得带 draft(BUG-989 防复发三条)。
  3. 解析与生成 new 的逻辑只在 frontend/src/lib/chat-session-url.ts 一处;app-sidebar.tsx 与 page.tsx 只调用,不得各写一份字符串拼接。
  4. 产品里只有一个侧栏组件、只读模式只少"菜单按钮、chevron、账户菜单"三样(BUG-744);不得为次级页再加第二个按钮或第二套类名。
  5. 不得改 frontend/src/app/(app)/page.tsx 之外的会话状态流;page.tsx 不得增行超过本单需要的一个分支(§6 增长冻结)。
  6. 既有断言改动逐条写"原值 / 新值 / 原因",测试总数不得低于开工时 origin/staging 实测。
  7. 不顺手修 lint warning、不升级依赖、不动迁移、不改 workflow。

任务分解

T1 · URL 意图(chat-session-url.ts)

  • 新增导出:NEW_CHAT_QUERY_KEY = "new"、newChatHref(): string(返回 /?new=1)、parseNewChatIntent(search): boolean。
  • sessionHref() 生成任何地址时都删掉 new(这样首问后 writeSessionUrl(id, "push") 与启动期 writeSessionUrl(null, "replace") 都能顺带抹掉它)。
  • resolveBootstrapSessionSelection 新增 urlAction: "new-chat":search 含 new 时直接返回 { sessionId: defaultSessionId, urlAction: "new-chat", missing: false, clearStoredReturn: true },不看 c、不看 storedReturnId。BootstrapSessionSelection.urlAction 联合类型同步加 "new-chat"。

验收:frontend/tests/chat-session-url.test.ts 新增 ≥4 条:parseNewChatIntent 对 ?new=1 / ?new= / 无参 的判定;new 同时带 c 与存根时胜出且 clearStoredReturn 为真;sessionHref("?new=1&x=1", null) 结果不含 new 但保留 x;sessionHref("?new=1", id) 结果只有 c。"session href keeps sibling query keys and can drop a dead c" 若改断言,写三栏说明。

T2 · 首页落点(home-bootstrap.ts + page.tsx)

  • starterHomeLandingNeedsConsultation(…, "new-chat") 恒返回 true;resolveStarterHomeLandingSessionId 对 "new-chat" 返回传入的 selectedId(随后会被新建会话覆盖)。LandingUrlAction 类型同步。
  • page.tsx bootstrap 内复用现有 starterHomeLandingNeedsConsultation 分支建会话(不要再写一段 createSession 拼装),urlAction === "new-chat" 时在 setActiveSessionId 之前调 writeSessionUrl(null, "replace") 抹掉 new(该分支在 bootstrapSelection.urlAction === "replace-clear" 那一行旁边加,最多 1 行)。
  • 这条路径不得触发 SESSION_MISSING_NOTICE(missing: false)。

验收:frontend/tests/home-bootstrap-reveal.test.ts(或同目录新文件,纯函数测试)新增 ≥2 条:最近一条是普通咨询、urlAction="new-chat" 时 starterHomeLandingNeedsConsultation 为真;urlAction="none" 时行为与基线相同(回归锁)。

T3 · 次级页链接(app-sidebar.tsx)

  • 只读模式那条 <SidebarMenuLink className="new-chat" href="/"> 改为 href={newChatHref()}。页脚 .profile-trigger 与品牌行 AppLink href="/" 不改。

验收:frontend/tests/sidebar-contract.test.ts 在只读模式那条测试里加断言:.new-chat 只读链接的 href 引用 newChatHref(不是字面量 /),页脚仍是 href="/";grep -n '"/?new=1"' frontend/src 只命中 chat-session-url.ts 一处。

T4 · 记录

  • docs/BUG_HISTORY.md 新增 BUG-1015(状态 resolved,现象、触发条件、根因、修复、验证、防复发;关联 BUG-745、BUG-744、BUG-989;复发自:无)。
  • CHANGELOG.md 一条:星盘 / 星历 / 我的报告页的「新建对话」现在真的新建。Skill 版本不 bump。
  • docs/tasks/PROGRESS-secondary-new-chat-intent-20260923.md:测试数字(开工基线 / 完成后)、既有断言三栏表、环境缺口。
  • docs/testing/secondary-new-chat-20260923.md 真机清单:① /chart 点「新建对话」→ 首页是空会话、地址栏是 /(无 ?new、无 ?c)、侧栏列表没有多出一行;② 在该空会话发第一问 → 地址栏变 /?c=<id>、列表顶部出现新会话;③ 在 ① 之后直接刷新 → 落最近对话(D6,预期行为,用来确认没有重复建会话);④ /reports/<id> 页同 ①;⑤ 移动端抽屉内同 ①,抽屉关闭。

让步顺序

  1. T2 若发现 resolveStarterHomeLandingSessionId 加分支会让 home-bootstrap.ts 现有测试语义混乱,可把 "new-chat" 的判断留在 starterHomeLandingNeedsConsultation 一处,resolveStarterHomeLandingSessionId 原样透传——写进进度记录。
  2. sessionHref() 删 new 若影响 frontend/tests/chat-session-url.test.ts 的既有 sibling 断言,允许改该断言(三栏说明),不允许改成"保留 new"。
  3. 不得让步:D3(不落库、不写 ?c=)、红线 1(Static)、红线 3(一处实现)。

开工前置命令

git fetch origin --prune
git worktree add -b codex/secondary-new-chat-intent-20260923 .worktrees/secondary-new-chat-intent-20260923 origin/staging
cd .worktrees/secondary-new-chat-intent-20260923/frontend
./node_modules/.bin/tsc --noEmit
npm run lint
npm test 2>&1 | tail -5          # 记下开工时的总数与失败清单

完成后:tsc --noEmit 0 错;npm run lint 0 error;npm test 失败清单与开工基线逐条一致、总数 ≥ 基线 + 新增;next build 后 / 仍 ○ Static,首屏 gzip ±2%。纯前端改动,不要求 pre_work_check.py。

BUG 编号起点

开工时核对 docs/BUG_HISTORY.md 最大号(写单时为 BUG-1014),本单用 BUG-1015;若被他单占用则顺延并在进度记录里写明。