Files
Jyotisha/docs/tasks/PROGRESS-rectification-ux-20260903.md
T

94 lines
12 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.
# PROGRESS · 生时校正会话面:消除空白假死与交互摩擦(重做,2026-09-03)
工作树:`.worktrees/rectification-ux-20260903`
分支:`codex/rectification-ux-20260903`
基线:`origin/staging` @ `af411f59`(任务书重启说明所在),收尾时 rebase 到 `0cefaea6`(两次均为文档提交:任务书搬家、任务状态板;无代码变化)。
任务书:`docs/tasks/TASK-rectification-ux-20260902.md`,以其「2026-09-03 重启说明」为准。第一次执行的分支 `codex/rectification-ux-20260902` @ `fec000f7` 只作参考,未 cherry-pick。
未改服务端与 SQL。未改 `.gitea/workflows/**`。未合并 staging,未碰 main。未手写 `useCallback` / `useMemo``rectification-agentic-chat.tsx` 既有的原样保留;新增的 `refetchQuestion` / `reloadQuestion` / `startOpeningManually` 是普通函数)。BUG 编号:开工确认远端最大号 504,本轮 505–509。未复活问题槽、未复活 consult handoff。
| 任务 | 状态 | commit | BUG |
| --- | --- | --- | --- |
| 0.2 一次揭幕、面板不重挂、深链/popstate 并入 prepare 阶段 | 完成 | `53017d41` | BUG-505 |
| 0.1 入口有反馈不转圈 | 完成(同 commit) | `53017d41` | BUG-505 |
| 0.3 问题槽只有生成中态、在对话流内 | 前半由 staging `d9404976` 完成(题目内嵌消息,槽已删);后半"没有等待快照这一态"随 0.2 成立 | — | — |
| 0.4 「等待服务端更新」→ live 行 + 重试 + 重新加载 | 完成 | `360989ca` | BUG-505 |
| 0.6 空 turns 会话的起点 | 完成 | `360989ca` | BUG-507 |
| 0.7 停止后的文案 | 完成(现状是静默返回,已补通知) | `360989ca` | — |
| 1.4 开场 live 文案 | 完成 | `360989ca` | — |
| 1.5 402 跳转前提示 | 完成 | `360989ca` | — |
| 0.5 选择题/采用不留缝不闪卡 | 完成(接 `beginLiveRun` / `rememberLiveActivity` | `1e50007c` | BUG-506 |
| 1.1 选择卡确认感 | 补差额完成(选中态进按钮由 `35688015`、内嵌变体由 `d9404976` 已做;本轮补对勾「已选择」与卡顶「正在记录…」行) | `3c33e598` | BUG-508 |
| 1.2 用户选择回显 | **跳过**`d9404976` 已把题目内嵌进 assistant 消息,答过的卡以 `question.answer_option` 标出所选项,持久化 turn 的 `question` 也带 `answer_option`,刷新前后都能看到自己选了什么;再加一条用户气泡会重复 | — | — |
| 1.3 盘面首态 | 完成(文案 + 收窄 + peek;宫位表 API 不提供,未造数据) | `3c33e598` | BUG-509 |
| 2 DESIGN.md | 完成 | 本 docs commit | — |
## 六段空白与死角:各自的消除证据
| 空白 | 消除方式 | 证据 |
| --- | --- | --- |
| 1 首页卡片无反馈 | `rectificationCardLabel``rectificationLoading` 时为「正在打开…」;卡片 `data-opening` + `cursor: progress`;侧栏目标行「打开中」+ `aria-busy` | `rectification-surface-contract` → "entry feedback is static…" |
| 2 侧栏进入:普通对话 → 空白 → 重挂 | `selectSession` 对未打开的校正会话不先 `setActiveSessionId`/写 URLhook 在 `/cases/open``hydrateRectificationCase`(一次 Case 读取 turns + 快照,`BOOTSTRAP_PREPARE_TIMEOUT_MS` 上限)再一次性切换;面板 key 去掉 `-ready/-loading` | 同上 → "…revealed once…"`rectification-surface-state` → hydration 两条 |
| 2 深链 / 刷新 / popstate | page 的 resume effect 从"揭幕后"改为"prepare 阶段起"`bootstrapPrepareSettled` 新增 `rectificationApplicable/Settled`,校正会话 hydration 完成(或 open 报错 / 资料不全)才揭幕;popstate 走 `selectSession`,同样延后切换 | `rectification-surface-contract` → "…hydrated before the reveal, and popstate defers…"`home-bootstrap-reveal` 既有用例不变(新字段可选) |
| 3 第一轮结束整面重挂 | key 只剩 `${sessionId}-${caseId}`;后到的 turns 经渲染期按上一 prop 调整只填空 transcript;卸载 `abort()` | 同上(`doesNotMatch(page, /"ready" : "loading"/)``seededTurns` 锁) |
| 4 快照回来前问题区空 | 快照随揭幕到位(`initialSnapshot` 初始化 state`caseSnapshotLoaded` 初值 `initialSnapshot !== null`);面板不再挂载自拉;hydration 超时时走 0.4 的缺口重试 | 同上(`useState(initialSnapshot !== null)`、无挂载 fetch);`rectification-surface-state` → "a snapshot that never arrived is a gap…" |
| 5 选择题两段缝 + 闪卡 | `send(action, text, continuation)` 续接同一条 live 行并跳过 busy 守卫;`willContinue` 在同一 async 链里 `await send("read_only", …)``choiceContinuationPending` 与 effect 删除;`busy` 全程 true | 同上 → "…keeps one live row through the follow-up turn" |
| 6 采用只有按钮变灰 | 点击即追加「正在采用 HH:MM…」行,POST → 快照 → 复用该行续接 read_only;失败移除该行 | 同上;`rectification-surface-state``rectificationAdoptingLabel` |
| 死角 空 turns 会话 | `rectificationConversationState``empty` → 「这段校正还没有开始。」+「开始提问」(`send("opening")``openingStarted` 守卫) | `rectification-surface-state` → "conversation state…";契约 → "an empty Case…" |
| 文案 「等待服务端更新」 | `rectificationQuestionGapState``preparing`transcript 末尾一条时间线 live 行「正在准备下一个问题…」+ 2s 定时重拉 ≤2 次)→ `unavailable`(「没有拿到下一个问题。」+ 44px「重新加载」);`question_source: "unavailable"` 直接 `unavailable`;候选卡待采用时不算缺口 | `rectification-surface-state` → 缺口三条;契约 → "…never tells the reader to wait for the server" |
## 改动文件
- 新建:`frontend/src/lib/rectification-surface-state.ts`(常量、hydration、turn 解析含 `question`/`offer_result_id`/`tool_activities`、会话态与缺口态纯函数、板文案、填报时间派生)、`frontend/tests/rectification-surface-state.test.ts`10 用例)、`frontend/tests/rectification-surface-contract.test.ts`8 用例)。
- 改动:`use-rectification-surface.ts``use-session-management.ts``home-bootstrap.ts`(可选字段)、`page.tsx`2 个 state、hook 参数、卡片文案、resume effect 门、prepare 判定、面板 key/props、侧栏 prop)、`app-sidebar.tsx``sidebar-session-row.tsx``starter-home.tsx``conversational-birth-time-rectification.tsx`props 与 turn 类型)、`rectification-agentic-chat.tsx``consultation-run-timeline.tsx`(导出 `ConsultationTimelineLiveRow`)、`rectification-choice-card.tsx``rectification-board.tsx``rectification-board-model.ts``rectificationBoardPeekCopy` 第二参可选)、`globals.css``DESIGN.md``docs/BUG_HISTORY.md``docs/testing/staging-manual-walkthrough-20260901.md`(第 8 节)、`docs/tasks/README.md`(索引)、`BLOCKED.md`
## 与任务书的书面偏差(均登记 BLOCKED.md
1. hydration 是一次 `GET /api/rectification/cases/:id` 套 4 秒 `Promise.race`,不是 `Promise.allSettled([turns, snapshot])`(同一端点,两次请求会重复);上限常量直接等于 `BOOTSTRAP_PREPARE_TIMEOUT_MS`,只有一个。
2. turns 后到的填充与重试计数复位不用 `useEffect``react-hooks/set-state-in-effect`):前者渲染期按上一 prop 调整,后者在 `applyCaseSnapshot`(有问题到达)与 `setPending(true)` 里复位。
3. 首页卡片不带 `aria-busy``tests/home-bootstrap-reveal.test.ts` 锁死 `starter-home.tsx` 不得出现 `aria-busy={`。卡片只用 `data-opening` + 静态文案 + `cursor: progress`;侧栏行仍带 `aria-busy`
4. 缺口的 live 行放在 transcript 末尾(`.rectification-message-wrap.rectification-question-gap`),不是 staging 原来的 composer 上方状态行;`.rectification-composer-status` 样式删除。
## 1.3 核实
`frontend/src/app/api/rectification/cases/[caseId]/route.ts` 的 dossier 响应只带 `accepted_time` / `confirmed_time` / `candidate_range`,无 natal/declared 宫位表;按任务书只做文案与收窄,`declaredTime``page.tsx` 从 profile`reportedTime || time`,须为 `H:MM`)派生。
## 被触碰的既有断言(红线 4,每处源码上方均注明原值与错因)
| 文件 | 原断言 | 现在 | 理由 |
| --- | --- | --- | --- |
| `tests/rectification-agentic-entry.test.ts` :85、:194 附近 | `key={…-${rectificationTurns.length > 0 ? "ready" : "loading"}}` | key 只含 session/Case`doesNotMatch /"ready" : "loading"/` | 锁的正是重挂缺陷(BUG-505) |
| 同上 :87 | hook 内联 `methods: Array.isArray((turn.receipt as { methods?: unknown }).methods)` | `parsePersistedRectificationTurns(payload?.turns)` + lib 里 `methods: stringList(receipt.methods)` | 解析器搬到 lib 供 hydration 与刷新共用(BUG-505 |
| 同上 :233:236 | `await loadCaseSnapshot(); choiceContinuationPending.current = true;`、effect 内 `void send("read_only", "")` | `await loadCaseSnapshot(); … await send("read_only", "", { reuseAssistantRenderKey: assistantRenderKey, label: adoptingLabel })``doesNotMatch /choiceContinuationPending/`;原 `doesNotMatch` 裸调用锁保留 | 锁的是 effect 中转(BUG-506 |
| 同上 workspace class | 无 `is-board-empty` 的类模板 | 含 `${candidateResult ? "" : " is-board-empty"}` | 板收窄(BUG-509 |
| `tests/rectification-question-in-message.test.ts` :136137 | `/当前没有可回答的问题,正在等待服务端更新/``/题目加载失败,请刷新/` | `doesNotMatch` 两句 + 锁 `RECTIFICATION_QUESTION_PREPARING_LABEL` / `_UNAVAILABLE_COPY` | 锁的是裸等待文案(BUG-505) |
| `tests/rectification-spoken-collect.test.ts` "missing current_question…" | `showMissingQuestion = Boolean(…)` 与同两句文案 | `rectificationQuestionGapState({ … questionMissing: currentQuestion === null … resumableCase })``doesNotMatch` 两句 | 同上;仍锁 resumable 门 |
| `tests/rectification-answer-choice.test.ts` :1201 | `/send\("read_only", ""\)/` | `send("read_only", "", { reuseAssistantRenderKey: assistantRenderKey, label: recordingLabel })` | 后续轮带续接行(BUG-506 |
新增测试:`tests/rectification-surface-state.test.ts`10)、`tests/rectification-surface-contract.test.ts`8)。
## 命令输出
`./node_modules/.bin/tsc --noEmit`**0**(每个 commit 前各一次,rebase 后再一次)。
`npm run lint`**0 error73 warning**;触及文件里唯一的 warning 是 `rectification-agentic-chat.tsx` `submitStructuredChoice` 的既有 `exhaustive-deps`(staging 同在,依赖表原本就缺 `busy` / `actionIdForChoice`,本轮未动);新建的三个文件 0 warning。
`./node_modules/.bin/tsx --test tests/*.test.ts`(基线在干净 `af411f59` 上跑):
| | 基线 `af411f59` | 现在 |
| --- | ---: | ---: |
| tests | 2584 | 2602 |
| pass | 2550 | 2568 |
| fail | 24 | 24 |
| skipped | 10 | 10 |
24 条失败与基线逐条比对**完全同一集合**(`comm -3` 差集为空):全部是本机无 Docker / PostgreSQL / 部署夹具的既有缺口。10 条 skipped 同基线。rebase 到 `8db71aaf` / `0cefaea6` 只改了 md 文件,src/tests 逐字节相同,全量数字沿用;rebase 后复跑 tsc 与校正相关聚焦套件(见下)。
聚焦套件(rebase 后,15 个文件):`rectification-surface-state``rectification-surface-contract``rectification-agentic-entry``rectification-question-in-message``rectification-spoken-collect``rectification-answer-choice``rectification-board-model``rectification-varga-style-copy``home-bootstrap-reveal``chat-session-url``chat-notice-and-scroll-contract``starter-questions``class-name-definition-contract``touch-target-contract``dark-theme-contract`**173 / 173**
`./node_modules/.bin/next build`**0**`✓ Compiled successfully in 17.8s``Finished TypeScript in 41s``Generating static pages (62/62)``/` 仍是 `○ (Static)`
## 未做与原因
`BLOCKED.md` 顶部:浏览器级手工验收(六条手工项 + 深链/刷新 + 缺口重试 + 整轮录屏)无登录态无 Chrome,已写进 `docs/testing/staging-manual-walkthrough-20260901.md` 第 8 节;1.3 的宫位表 API 不提供;四条书面偏差。