Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
12 KiB
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/写 URL;hook 在 /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)
- hydration 是一次
GET /api/rectification/cases/:id套 4 秒Promise.race,不是Promise.allSettled([turns, snapshot])(同一端点,两次请求会重复);上限常量直接等于BOOTSTRAP_PREPARE_TIMEOUT_MS,只有一个。 - turns 后到的填充与重试计数复位不用
useEffect(react-hooks/set-state-in-effect):前者渲染期按上一 prop 调整,后者在applyCaseSnapshot(有问题到达)与setPending(true)里复位。 - 首页卡片不带
aria-busy:tests/home-bootstrap-reveal.test.ts锁死starter-home.tsx不得出现aria-busy={。卡片只用data-opening+ 静态文案 +cursor: progress;侧栏行仍带aria-busy。 - 缺口的 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 :136–137 |
/当前没有可回答的问题,正在等待服务端更新/、/题目加载失败,请刷新/ |
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 error,73 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 不提供;四条书面偏差。