docs(tasks): brief for secondary-page new-chat landing on last session

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
Jesse_Chen
2026-09-23 19:59:24 +08:00
co-authored by Claude Fable 5.1
parent 6b53d9bb5b
commit 1c95eb3979
2 changed files with 100 additions and 0 deletions
+1
View File
@@ -117,6 +117,7 @@
| 任务书 | 进度 | 主题 | 状态 | 落点 |
| --- | --- | --- | --- | --- |
| `TASK-secondary-new-chat-intent-20260923.md` | — | **次级页「新建对话」落到最近一次对话**:`/chart` `/ephemeris` `/reports` 的侧栏没有 `controls`,「新建对话」只是 `href="/"` 的回首页链接;首页无参启动的落点是 `nextSessions[0]`(最近更新那条),只有最近一条是校正会话才改落空咨询。BUG-745 修过同一入口的「慢」,没修「回到哪」。产品 09-23 拍板:链接改 `/?new=1`,首页看到 `new` 就本地建空咨询并 `replaceState` 抹掉参数;`new` 胜过 `c` 与登录返回存根;沿用 BUG-989 不落库不写 `?c=`;页脚与品牌行的 `/` 不改;刷新无参 `/` 仍落最近对话属既有设计。红线:`/` 保持 Static、不得用 `useSearchParams`;解析与生成只在 `chat-session-url.ts` 一处。BUG-1015 | 待领取 | — |
| `TASK-chat-message-authority-20260901.md` | — | 消息服务端权威化 | 已验收 | `b6989c3e`(BUG-464) |
| `TASK-session-url-20260901.md` | — | 会话 URL 化 | 已验收 | `924f4202` |
| `TASK-cloud-truth-convergence-20260901.md` | — | 本地/云端双份真相收敛 | 已验收 | `ce6a8a7e`(BUG-466) |
@@ -0,0 +1,99 @@
# 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** 不在本单:刷新 `/`(无参数)仍落最近对话——这是既有设计,不是本单事故。
## 硬红线
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**;若被他单占用则顺延并在进度记录里写明。