Files
Jyotisha/docs/tasks/TASK-rectification-ux-20260902.md
T
Jesse_Chen 8db71aaf81 docs: product-level README, AGENTS.md split into code/reading parts, add CLAUDE.md, move task briefs to docs/tasks
- README.md is now the product/repo front door (architecture, repo map,
  local dev, test tiers, delivery flow, doc map). Engine positioning,
  VedAstro/Codex setup and the oracle/benchmark command reference move
  verbatim to docs/engine/README.md, docs/engine/vedastro-gateway.md and
  docs/benchmark/README.md. Capability badges realigned with the registry
  (91/78/8/0); tests/test_readme_badges.py was red on staging.
- AGENTS.md: Part A (environment truth, delivery, worktrees, record
  placement, bug workflow, growth freeze, frontend red lines, privacy,
  pre-work check, test tiers) and Part B (reading-rigor constraints).
  GitHub issue-tracker/triage boilerplate removed: GitHub is a read-only
  mirror. All strings locked by tests/ are preserved.
- CLAUDE.md added: roles, three working modes, task-brief sections,
  acceptance criteria, session discipline; imports AGENTS.md.
- 50 tracked TASK-*/PROGRESS-* files and 3 never-committed briefs move to
  docs/tasks/ with an index; REPO_LAYOUT.md merged into README.

Docs-only change (no gated path touched).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
2026-09-03 06:56:06 +00:00

197 lines
22 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.
# 任务书 · 生时校正会话面:消除空白假死与交互摩擦(2026-09-02)
基线:**`codex/streaming-ux-20260901`HEAD `c846c44a`)合入后的 `origin/staging`**。本轮改的文件与那条分支高度重叠(`rectification-agentic-chat.tsx``page.tsx``globals.css``rectification-agentic-entry.test.ts`),**必须在它合入之后开工**。
## 2026-09-03 重启说明(覆盖上面的基线与部分条目)
第一次执行(分支 `codex/rectification-ux-20260902` @ `fec000f7`,基于 `4dc0c8c7`)已完成全部条目并经验收,但 staging 随后被其它会话推进 46 个提交(校正问题内嵌、选择卡、采用流、collect 退出等一批 `fix(rectification)`),`rectification-agentic-chat.tsx` 被重写 600+ 行,老分支与之有 5 个文件的内容冲突,**不再 rebase,改为在新基线上重做**。老分支只作参考(`git show fec000f7:<path>` 读,`git diff 4dc0c8c7..fec000f7` 看思路),不要 checkout 它。
- **新基线**`origin/staging` @ `8e214e29` 或更新;新分支 `codex/rectification-ux-20260903`
- **BUG 编号从 505 起**staging 已到 504,开工再确认)。
- **已被 staging 解决、本轮删除的条目**:0.3 的"问题槽在对话流内"——`d9404976` 已把每道题内嵌进 assistant 消息,`.rectification-question-slot` 已不存在;不要复活问题槽。0.3 只剩"没有等待快照这一态"(随 0.2 自然成立)。
- **部分被解决、只补差额**:1.1 选择卡——`35688015` 已把选中态收进按钮、`d9404976` 加了 `variant="embedded"`;本轮只补 pending 时卡内的 `InlineSpinner` + shimmer「正在记录…」一行,以及选中项的 `Check` 图标(若 staging 已有则跳过并写明)。
- **接口变了、按新结构重做**`send` 现在经 `beginLiveRun(label)` / `rememberLiveActivity(label, tool)` 管 live 行文案;0.5 的 `continuation`(复用同一条 live 行、跳过 busy 守卫)要接进这两个助手,而不是照抄老分支的 `rectificationInitialLiveLabel``loadCaseSnapshot` 现在**返回** snapshot`submitStructuredChoice` 用返回值判断 `willContinue`),保留这个返回。`choiceContinuationPending` + effect 在 staging 仍在,照任务书删除。
- **已删除、不得复活**`onStartConsultation` / consult handoff 按钮(`e8c98c37` 产品决定删除)。0.6 空态的按钮只有「开始提问」。
- **仍在 staging 上、本轮照做**`page.tsx``-ready/-loading` key`rectificationCardLabel` 无 loading 分支;「等待服务端更新」两条文案(`showMissingQuestion` / `showUnavailableQuestion` 现在锚在内嵌问题上,四态判定改到那里);盘面首态文案与收窄;开场 live 文案;402 提示;1.2 用户选择回显(核对 staging 现在怎么渲染持久化的结构化选择 turn,若已回显则跳过并写明)。
- **深链 / 刷新 / popstate 对齐**(并入 0.2):staging 已有两阶段揭幕(`bootstrapPhase: "account" → "prepare"``page.tsx` 约 :529–:663)。启动时选中的会话若是校正会话,把 `openRectificationSession` 的 hydration 并入 `prepare` 阶段、揭幕前完成;popstate 回到校正会话时,hydration 完成前不切 `activeSessionId`。4 秒上限常量若 unified-loading 已导出则复用,只留一个。
- **老分支里可以直接搬的**(预计无冲突,逐个 `git show fec000f7:<path>` 对照):`src/lib/rectification-surface-state.ts` 与其 `tests/rectification-surface-state.test.ts`(hydration、四态纯函数、文案常量)、`use-session-management.ts` 的 deferred switch、`starter-home.tsx` / `sidebar-session-row.tsx` 的打开中反馈、`rectification-board.tsx` 首态、DESIGN.md 「Rectification surface」条目(按新状态集合修订)、walkthrough 第 8 节。`tests/rectification-surface-contract.test.ts` 的源码锁要按新结构重写。
## 与同日其它任务书的关系(先读)
| 任务书 | 关系 | 结论 |
| --- | --- | --- |
| `TASK-unified-loading-20260902.md` | 产品裁决:揭幕后不得再出现 spinner / 骨架 / "正在加载"文案,流式生成中除外;且"与其它改 `page.tsx` 的轮次不得并行" | 本轮**遵守同一裁决**:进入校正面的等待全部提前到切换之前(并行拉完再一次揭幕,见 0.2),面板内不设加载态;剩余等待都是生成中(timeline live 行)。两轮都改 `page.tsx`**串行执行**streaming-ux 合入 → 本轮 → unified-loading(本轮对 `page.tsx` 只有两处小改,先做冲突面小)。 |
| `TASK-rectification-walkthrough-polish-20260902.md` | 服务端抛光。其 **B.2**(流结束后前端立即刷新快照)与本轮 0.4 重复;其 **D.2**(问题槽必须在对话流内)与本轮问题槽改动同文件 | B.2 由本轮 0.4 承担,polish 执行方只做服务端 emit(若选 `question.ready` 事件,本轮 0.4 直接消费它);D.2 的 UI 部分并入本轮 0.3。两轮同改 `rectification-agentic-chat.tsx`polish 以服务端为主,**polish 先合入**,本轮 rebase。 |
用户反馈原话:"动画加载的过程中还有一段时间是空白状态,也没有加载也没有状态,导致用户以为页面卡了;交互也不是很友好。"下面每一条空白都对着代码找到了成因。**先读完「硬红线」再动手。**
---
## 事故实证:六段空白 + 一个死角
行号基于 `c846c44a`,按符号定位。
### 空白 1 · 首页卡片点下去没有任何反馈
`starter-home.tsx` 生时校正卡:`rectificationLoading` 期间只把按钮 `disabled`,文案、图标、光标都不变。`use-rectification-surface.ts` `openRectificationCase` 要等 `/api/rectification/cases/open`(鉴权 + RPC `open_agentic_rectification_case_v2` + profile 加载 + 时区解析)返回才切会话。2 vCPU 的生产机上这一步以秒计,用户看到的是"点了没反应"。
### 空白 2 · 从侧栏点开已有校正会话:先闪普通对话、再整块空白、再重挂
`use-session-management.ts` `selectSession`:先 `setActiveSessionId`,再 `void openRectificationSession(id)``page.tsx``rectificationSurfaceOpen = activeRectificationSession && activeSession.id === rectificationSessionId`——在 open 返回前是 **false**,于是这一帧渲染的是普通 `ChatTranscript`(用镜像的 `session.messages`)。open 返回后 `setRectificationTurns([])`、面板以 `key=…-loading` 挂载,`initialTurns=[]`**整块空白**(没有任何 loading 文案),直到 `refreshRectificationCase` 拉回 turnskey 翻成 `-ready`**整个面板卸载重挂**。三段画面:普通对话 → 空白 → 校正面板。
### 空白 3 · 新建校正:第一轮回答结束时整个面板重挂一次
同一个 key:新 case 挂载时 turns 为空(`-loading`),开场轮 `run.completed``onCompleted``refreshRectificationCase` → turns > 0 → key 变 `-ready`**重挂**。用户刚读完第一条引导,画面闪一下、滚动归零、时间线开合状态丢失、消息从内存态换成持久化态(trace 只剩回执)。如果用户在这一拍已经开始输入第二轮,重挂会丢掉那次流(`runAbort` 不在卸载时中止,`setMessages` 落到已卸载的实例)。`tests/rectification-agentic-entry.test.ts` :79、:190 两处正则**锁的正是这个 key 写法**。
### 空白 4 · 恢复会话后,问题槽在快照回来前什么都不显示
`rectification-agentic-chat.tsx``caseSnapshotLoaded` 为 false 时 `showLiveChoiceCard` / `showMissingQuestion` / `showUnavailableQuestion` 全为 false`.rectification-question-slot` 是空的。用户看到历史消息但没有可做的事,不知道要等。
### 空白 5 · 选择题点下去之后有两段缝、一次闪卡
`submitStructuredChoice`:760:860):
1. 追加一条 thinking 行「正在记录本次选择…」(好)。
2. `fetch` 返回 → `await loadCaseSnapshot()`**`willContinue` 分支把这条 thinking 行删掉**,置 `choiceContinuationPending``finally setPending(false)`
3. 下一次 effect 才 `send("read_only")` → 再追加一条新的 thinking 行「正在处理…」并 `setPending(true)`
2→3 之间至少有一帧:没有任何 live 行,且 `busy=false` + 快照刚装进的新 `choiceCard``showLiveChoiceCard` 为 true → **下一题的卡片闪现一帧又消失**。然后 read_only 的回答流完 → `run.completed``await loadCaseSnapshot()`(这次在 `finally` 之前,busy 仍为 true,没缝)→ 卡片出现。
### 空白 6 · 采用候选时间:只有按钮文案变了
`acceptCandidate``setAcceptingCandidateId` + `setPending(true)`transcript 里不出现任何 live 行;POST 完成 → `await loadCaseSnapshot()``choiceContinuationPending` → 再走空白 5 的 effect 路径。用户盯着一个变灰的按钮「正在采用…」等好几秒,页面其它部分静止。
### 死角 · 空 turns 的已有会话永远空白
服务端 `open_agentic_rectification_case_v2``resumed` 一律返回 `should_start_opening=false`(迁移 `20260812010000_agentic_rectification_v9_runtime.sql` :427/:459/:480)。若一个 case 的开场轮当时失败或未持久化(turns 为空),从侧栏再进来:`initialTurns=[]``!shouldStartOpening` → 面板挂载后**什么都不发生、什么都不显示**,composer 可用但用户不知道要先说什么。没有任何 CTA。
### 交互摩擦(不是空白,但用户说"不友好"的来源)
- **两条开发者文案**:「当前没有可回答的问题,正在等待服务端更新。」「当前问题暂时无法显示,请等待服务端更新。」——没有动作、没有时限、用户不知道等多久。
- **选择题选中无确认感**`.rectification-choice-card.is-pending` 只改 `cursor: wait`;选中项没有对勾,卡片里没有进度。
- **用户的选择不回显**:答过的卡贴在 assistant 消息下(`choiceAttachment`),transcript 里没有一条"我选了 B"的用户气泡;服务端已经返回 `userMessage``applied.userDisplay`)但客户端没用;持久化 turn 里的结构化选择又被 `isStructuredChoiceUserText` 过滤掉。刷新前后都看不到自己答了什么。
- **右侧盘面首态是一整块空面板**:桌面端 `minmax(18rem, 22.5rem)` 的面板,第一阶段只有一句「补充经历后,这里会显示当前本命宫位和换升时刻。」;移动端 peek 是「当前盘面 · 补充经历后会在这里更新」。用户填过出生时间,面板却像没数据。
- **开场 live 行文案是通用的「正在处理…」**,第一次进入的用户不知道系统在做什么。
- **停止**`send``catch` 已区分 `AbortError`,但请核对 aborted 分支落地的文案不是「生时校正暂时不可用,请稍后再试。」(当前 :700 附近的通用兜底)。
---
## 硬红线
1. **不改服务端语义、不改 SQL。** `should_start_opening``choice.applied` 的返回、`awaitTurnExitBeforeResponse` 都不动。死角修复用已有的 `send("opening")`(服务端已抑制重复 opening`rectification-agentic-entry.test.ts` :190 锁着这条性质)。
2. **只用上一轮统一好的那一套活动 UI**live 行 = `ConsultationRunTimeline` 的 queued/live 行(`InlineSpinner` + shimmer 文案),不得新造第二种 spinner、骨架屏或呼吸动画。§9 等待词汇表不扩表。
3. **不得手写 `useCallback` / `useMemo`**`rectification-agentic-chat.tsx` 既有的不删不加。
4. **不得修改既有测试断言**,除非它锁的正是缺陷本身(本轮明确允许:`rectification-agentic-entry.test.ts` :79/:190 的 `-ready/-loading` key 锁、任何锁「等待服务端更新」文案的断言);改时在断言上方注释原值与错因,PROGRESS 单列。
5. `./node_modules/.bin/tsc --noEmit` 通过(不要 `npx tsc`);`next build` 通过;测试数不低于基线、失败清单逐条比对无新增。
6. 浅色/深色/`prefers-reduced-motion` 三套都验。
7. 不改 `.gitea/workflows/**`;不在脏工作树切分支;不自行把 staging 提升到 main。
8. BUG 编号开工时先看远端最大号(写本任务书时 staging 最大 472streaming 分支占 473478**本轮从 479 起,仍需现场确认**)。
让步顺序:功能与测试不回归 > 可验证的修复 > 视觉一致 > 代码整洁。
---
## 任务 0(P0)· 六段空白与死角
### 0.1 入口有反馈,但不转圈
`starter-home.tsx``rectificationLoading` 时卡片 `aria-busy="true"``data-opening="true"`footer 的 action 文案换成「正在打开…」(静态文案,**不加 spinner**,遵守 unified-loading 裁决),卡片 `cursor: progress`。文案在 `rectificationCardLabel` 的派生处加 loading 分支。侧栏校正会话行在 `rectificationLoading && 目标是该行` 时同样只加 `aria-busy` 与静态「打开中」尾注,不转圈。
### 0.2 一次揭幕:open + 记录 + 快照并行拉完再切面板,面板挂一次不重挂
- `use-rectification-surface.ts` `openRectificationCase``/cases/open` 返回后**不立刻**切会话;改为 `Promise.allSettled([refreshRectificationCase(caseId, sessionId), fetch 案例快照])` 并行拉 turns 与快照(上限 4 秒,与 unified-loading 同一常量),全部落地后再一次性 `setRectificationTurns / setRectificationSnapshot / setRectificationSessionId / setActiveSessionId`。超时或失败:turns 用空数组、快照用 null,仍然切换(面板会走 0.6 空态或 0.4 的重试路径),并 composer notice「校正记录没有完全加载,可以继续」。
- `use-session-management.ts` `selectSession`:对校正会话**不再先 `setActiveSessionId`**,改为只调 `openRectificationSession(id)`,由上一条在数据齐了以后切换;期间旧画面保持不动(这就是"先闪普通对话"的消除)。URL 写入时机随之后移到切换那一刻。
- `page.tsx`:面板 key 去掉 `-ready/-loading` 后缀,只保留 `${sessionId}-${caseId}`props 增加 `initialSnapshot`。面板内 `useState(() => messagesFromTurns(initialTurns))``useState(() => initialSnapshot)` 初始化,`caseSnapshotLoaded` 初值 = `initialSnapshot !== null`;挂载后**不再**自己拉一次快照(0.4 的重试路径除外)。
- turns 的后续到达(`onCompleted``refreshRectificationCase`)改为 **prop 更新**:面板内 `useEffect([initialTurns])`——本地 `messages` 为空且 `initialTurns.length > 0` 时用 `messagesFromTurns` 填充;本地已有消息时忽略,不覆盖、不重挂。
- 卸载时 `runAbort.current?.abort()`cleanup effect)。
### 0.3 问题槽只有生成中态,且始终在对话流内
- 快照随揭幕一起到位后,问题槽没有"等待快照"这一态;仅当 0.4 的重试在跑时显示 live 行。
- **承接 polish D.2**:问题槽(live 选择卡 / spoken prompt / 状态行)渲染为 transcript 的**最后一条内容**——放在候选卡之后、`rectification-saved` 之前,用 `.message-entry` 的同一缩进与间距(`--assistant-content-inset`),不得悬在卡片外。
### 0.4 两条"等待服务端更新"文案改为有动作的状态
`showMissingQuestion` / `showUnavailableQuestion` 命中时:
1. 先自动重拉快照:若 polish 轮落地了 `question.ready` 公开事件,则收到即拉;否则在 `run.completed` 后立即拉一次,再最多 2 次、间隔 2s(`useVisibilityAwarePoll` 已有,复用)。期间问题槽显示 live 行「正在准备下一个问题…」——这是生成中等待,符合裁决。
2. 3 次后仍命中:显示「没有拿到下一个问题。」+ 一个 44px 次级按钮「重新加载」(调 `loadCaseSnapshot`)。
3. 两条旧文案从源码删除。
### 0.5 选择题点击后不留缝、不闪卡
`submitStructuredChoice``willContinue` 分支:**不删 thinking 行、不经 effect 中转**。把 continuation 收进同一个 async 流程:`fetch` 成功 → `await loadCaseSnapshot()` → 直接 `await send("read_only", "")`,并让 `send` 接受一个可选参数 `reuseAssistantRenderKey`,用已存在的那条 thinking 行(同一个 renderKey)承接后续事件,label 从「正在记录本次选择…」自然过渡到「正在处理…」/tool 文案。`busy` 全程为 true`setPending(false)` 只在整条链的最后)。`choiceContinuationPending` 这条 ref + 对应 effect 删除。
`acceptCandidate` 同样:点击即在 transcript 末尾追加 thinking 行「正在采用 HH:MM…」,POST → 快照 → `send("read_only")` 复用该行。
### 0.6 空 turns 的已有会话给出起点
面板挂载且 turns 已加载为空、`!shouldStartOpening``!readonly``.message-list` 显示空态:「这段校正还没有开始。」+ 主按钮「开始提问」(调 `send("opening", "")`)。服务端幂等由 :190 锁定,客户端只需 `openingStarted` 守卫。
### 0.7 停止后的文案
核对 `catch` 的 aborted 分支:已有内容时行内保留,composer notice 为「已停止,已生成的内容保留;本次不会扣点。」;无内容时移除该行、不报错。若现状已如此,只补一条源码锁。
### 验收(任务 0
- 契约测试(源码锁 + 纯函数):`page.tsx``"ready" : "loading"``use-rectification-surface.ts``allSettled` 与 4 秒常量;`selectSession` 对校正会话不直接 `setActiveSessionId`;面板源码含 `initialSnapshot`;揭幕后 `rectification-agentic-chat.tsx` / `starter-home.tsx` 无非生成中的 `InlineSpinner``rectification-agentic-chat.tsx``choiceContinuationPending`、无「等待服务端更新」;存在「正在打开…」「正在准备下一个问题」「这段校正还没有开始」;`send` 签名含 `reuseAssistantRenderKey`;卸载 cleanup 调 `abort`
- 纯函数:新增 `rectification-surface-state.ts`(把"恢复中 / 空态 / 问题槽四态"的判定抽成纯函数)并测全部分支。
- 手工清单追加到 `docs/testing/staging-manual-walkthrough-20260901.md`:① 首页点卡片看到「正在打开…」,随后一次性出现完整面板(消息 + 问题 + 盘面);② 侧栏切校正会话:旧画面保持到数据齐、不闪普通对话、不出现空白;③ 新建校正第一轮结束不闪、滚动不归零;④ 连点两道选择题中间无空帧无闪卡;⑤ 采用候选看到 live 行;⑥ 一个开场失败的旧会话进来有「开始提问」。
### 建档
BUG-479(校正会话面挂载/重挂造成三段空白)、BUG-480(选择题与采用候选之间的缝与闪卡)、BUG-481(空 turns 会话无起点)。
---
## 任务 1P1)· 交互摩擦
### 1.1 选择题卡有确认感
`rectification-choice-card.tsx` + CSS:选中项显示 `Check` 图标与「已选择」;`pending` 时卡片顶部一行 `InlineSpinner size={12}` + 「正在记录…」(同 timeline live 行的排版,不另造);未选项在 pending 时降到 `.48` 透明度(已有)。所有选项按钮 `min-height: 44px`(核对 `touch-target-contract`)。
### 1.2 用户选择回显为用户气泡
选择成功后,用服务端返回的 `userMessage``applied.userDisplay`)在 transcript 追加一条 **用户气泡**`role: "user"`),紧跟在答过的卡片之后、thinking 行之前。持久化侧:`isStructuredChoiceUserText` 过滤要改成**保留**这类 turn 并原样显示(否则刷新后回显消失)。`choiceAttachment` 贴卡逻辑保留(卡本身仍显示所选项)。`tests/rectification-answer-choice.test.ts` 若锁了过滤行为,按红线 4。
### 1.3 盘面首态不空
- `rectification-board.tsx``result` 为空时,header 时钟位显示 profile 的填报时间(面板 props 增加 `declaredTime: string | null`,由 `page.tsx` 从 profile 传入),正文改为两行:「填报出生时间 HH:MM」「回答几个问题后,这里会显示宫位随时间的变化。」;`rectificationBoardPeekCopy` 同步为「当前盘面 · 填报 HH:MM」。
- 若快照 API 已提供填报时间对应的宫位表(先 grep `natal`/`declared` 字段确认),则直接渲染那张表作为首态;**没有就不要造数据**,只做文案。
- CSS`result` 为空时 `.rectification-workspace` 的板列收为 `minmax(16rem, 18rem)`(加 `is-board-empty` 修饰类),有结果后恢复。
### 1.4 开场 live 行文案
`send("opening")` 的初始 live 行 label 用「正在读取你的出生资料,准备第一个问题…」;`send("message")` 用「正在处理…」;`read_only` 沿用上一步传入的 label。通过 `send` 的 action 分支决定,不要在渲染层判断。
### 1.5 402 跳转前先给提示
`window.location.assign(membershipHref("rectification"))` 前先 `setError("校正点数不足,正在前往兑换…")`,并延迟 600ms 再跳,避免页面无预警消失。
### 验收(任务 1
- 契约:choice card 源码含 `Check``isStructuredChoiceUserText` 不再用于过滤渲染;board 源码含 `declaredTime``send` 源码含开场文案。
- 手工:一轮完整校正(开场 → 3 道选择题 → 候选 → 采用)录屏,浅色一份。
### 建档
BUG-482(选择不回显、无确认感)、BUG-483(盘面首态空)。
---
## 任务 2P2)· DESIGN.md
§5 新增「Rectification surface」条目:
- **States**`opening`(首轮引导流中)、`resuming`(恢复记录/进度)、`empty`(无 turns 有起点)、`waiting-question`(准备下一题,含自动重试与手动重载)、`choice-live``choice-pending``candidates``accepted``confirmed``readonly``failed`。每态写明问题槽、transcript 末尾 live 行、composer 三者各显示什么。
- **Rule**:面板一个会话只挂载一次;turns 与快照都是 prop/state 更新,不是 remount。任何"等待"都必须是 timeline live 行或问题槽 live 行,禁止裸文案等待、禁止「等待服务端更新」类措辞。
- **Board**:首态显示填报时间;空态收窄;有结果后展开。
- §9 表不新增行;写一句"校正面所有等待复用行内等待"。
---
## 执行顺序
0.2 先做(它改变挂载模型,其余都建立在"不重挂"上)→ 0.1/0.3/0.4/0.6/0.7 → 0.5 → 任务 1 → 任务 2。每个任务单独 commit。
## PROGRESS 要求
`PROGRESS-rectification-ux-20260902.md`:每任务改动文件、被触碰断言(原值/新值/理由)、六段空白各自消除的证据(契约名或纯函数用例名)、tsc/build/测试数字与基线比对、BUG 编号、未做与原因。