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

102 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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(一处实现)。
## 开工前置命令
```bash
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**;若被他单占用则顺延并在进度记录里写明。