Files
Jyotisha/PROGRESS-rectification-ux-20260902.md
T

11 KiB
Raw Blame History

PROGRESS · 生时校正会话面:消除空白假死与交互摩擦(2026-09-02)

工作树:.worktrees/rectification-ux-20260902 分支:codex/rectification-ux-20260902 基线:origin/staging @ 4dc0c8c7codex/streaming-ux-20260901 的 merge commit;任务书前置条件满足)。 执行时 TASK-rectification-walkthrough-polish-20260902.mdTASK-unified-loading-20260902.md 均未落地(远端无对应分支):B.2/D.2 按任务书由本轮承担,没有 question.ready 事件,0.4 走"完成后立即拉一次 + 定时重试"的分支。

未改服务端与 SQL。未改 .gitea/workflows/**。未合并 staging,未碰 main。未手写 useCallback / useMemorectification-agentic-chat.tsx 既有的原样保留,新增的 refetchQuestion / reloadQuestion / startOpeningManually 是普通函数)。BUG 编号开工时确认远端最大号为 478,本轮 479–483。

任务 状态 commit BUG
0.1 入口有反馈不转圈 完成 91b8b33a BUG-479
0.2 一次揭幕、面板不重挂 完成 91b8b33a BUG-479
0.3 问题槽只有生成中态、在对话流内 完成 91b8b33a BUG-479
0.4 「等待服务端更新」→ live 行 + 重试 + 重新加载 完成 91b8b33a BUG-479
0.5 选择题/采用不留缝不闪卡 完成 91b8b33a BUG-480
0.6 空 turns 会话的起点 完成 91b8b33a BUG-481
0.7 停止后的文案 完成(现状不足,已补) 91b8b33a
1.1 选择卡确认感 完成 ef11f6e3 BUG-482
1.2 用户选择回显 完成(在 91b8b33a,与 send 重写同一处) 91b8b33a BUG-482
1.3 盘面首态 完成(文案 + 收窄;宫位表 API 不提供,未造数据) ef11f6e3 BUG-483
1.4 开场 live 文案 完成(在 91b8b33a 91b8b33a
1.5 402 跳转前提示 完成(在 91b8b33a 91b8b33a
2 DESIGN.md 完成 本 docs commit

1.2 / 1.4 / 1.5 的改动都落在 sendsubmitStructuredChoice 的重写里,无法从任务 0 的 commit 里干净拆出,所以随 91b8b33a 提交。

六段空白与死角:各自的消除证据

空白 消除方式 证据(契约 / 纯函数用例)
1 首页卡片无反馈 rectificationCardLabelrectificationLoading 时为「正在打开…」;卡片 aria-busy + data-opening + cursor: progress;侧栏目标行「打开中」 rectification-surface-contract → "entry feedback is static…"
2 侧栏进入:普通对话 → 空白 → 重挂 selectSession 对未打开的校正会话不再先 setActiveSessionId/写 URLhook 在 /cases/openhydrateRectificationCase(一次 Case 读取取 turns + 快照,4 秒上限)再一次性切换;面板 key 去掉 -ready/-loading 同上 → "…revealed once…"rectification-surface-state → "hydration reads turns and snapshot from one Case read" / "…resolves incomplete…"
3 第一轮结束整面重挂 key 只剩 ${sessionId}-${caseId};后到的 turns 经 prop 更新只填空 transcript(渲染中按上一 prop 调整) 同上(doesNotMatch(page, /"ready" : "loading"/)seededTurns 锁)
4 快照回来前问题槽空 快照随揭幕到位(initialSnapshot 初始化 statecaseSnapshotLoaded 初值 initialSnapshot !== null);面板不再自拉 同上(useState(initialSnapshot !== null)、无挂载 fetch
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-staterectificationAdoptingLabel
死角 空 turns 会话 rectificationConversationStateempty → 「这段校正还没有开始。」+「开始提问」(send("opening")openingStarted 守卫) rectification-surface-state → "conversation state…";契约 → "an empty Case…"
文案 「等待服务端更新」 rectificationQuestionSlotState 四态:preparing(时间线 live 行「正在准备下一个问题…」+ 2s 定时重拉 ≤2 次)→ unavailable(「没有拿到下一个问题。」+ 44px「重新加载」) rectification-surface-state → "question slot: a gap is preparing…";契约 → "…never tells the reader to wait for the server"

改动文件

  • 新建:frontend/src/lib/rectification-surface-state.ts(常量、hydration、turn 解析、两个纯状态函数、板空态文案、填报时间派生)、frontend/tests/rectification-surface-state.test.ts9 用例)、frontend/tests/rectification-surface-contract.test.ts8 用例)。
  • 改动:use-rectification-surface.tsuse-session-management.tspage.tsxstate 2 处、hook 参数、卡片文案、面板 key/props、侧栏 prop、import)、app-sidebar.tsxsidebar-session-row.tsxstarter-home.tsxconversational-birth-time-rectification.tsxprops)、rectification-agentic-chat.tsxconsultation-run-timeline.tsx(导出 ConsultationTimelineLiveRow,同一行形态)、rectification-choice-card.tsxrectification-board.tsxrectification-board-model.tsrectificationBoardPeekCopy 第二参可选,旧调用不变)、globals.cssDESIGN.mddocs/BUG_HISTORY.mddocs/testing/staging-manual-walkthrough-20260901.md(第 8 节)、BLOCKED.md

1.3 核实

frontend/src/app/api/rectification/cases/[caseId]/route.tsdossierResponse 只带 accepted_time / confirmed_time / candidate_rangegrep -n 'natal\|declared\|birth_time\|profile' 无命中 → 快照 API 不提供填报时间的宫位表。按任务书只做文案与收窄,declaredTimepage.tsx 从 profilereportedTime || time,须为 H:MM)派生。

与任务书的书面偏差(均登记 BLOCKED.md)

  1. hydration 是一次 GET /api/rectification/cases/:id(turns 与快照同一端点)套 4 秒 Promise.race,不是 Promise.allSettled([turns, snapshot]);语义不变。契约锁改锁 hydrateRectificationCase + RECTIFICATION_OPEN_HYDRATE_TIMEOUT_MS。unified-loading 落地时应把它的 4 秒常量与 RECTIFICATION_OPEN_HYDRATE_TIMEOUT_MS 合一。
  2. turns 后到的填充与重试计数复位不用 useEffectnpm run lintreact-hooks/set-state-in-effect 拦):前者是渲染中按上一 prop 调整 state,后者在 applyCaseSnapshot(有问题到达)与 setPending(true) 里复位。
  3. 选择卡 pending 行用 InlineSpinner + shimmer 文案而不是嵌一个 ConsultationTimelineLiveRow:卡片内是 <p role="status">,与时间线 live 行同一形态(同 spinner、同 agent-activity-status__text)。

被触碰的既有断言(红线 4,每处源码上方均注明原值与错因)

文件 原断言 现在 理由
tests/rectification-agentic-entry.test.ts :85、:194 key={…-${rectificationTurns.length > 0 ? "ready" : "loading"}} key 只含 session/CasedoesNotMatch /"ready" : "loading"/initialSnapshot={rectificationSnapshot} 锁的正是重挂缺陷(BUG-479
同上 :93 page 含 turn 解析的 methods: Array.isArray(…) 改读 rectification-surface-state.tspageparsePersistedRectificationTurns(payload?.turns) 解析器搬家,形状不变(BUG-479
同上 :221:224 await loadCaseSnapshot(); … choiceContinuationPending.current = true;、effect 里 void send("read_only", "")doesNotMatch 同链 await send await loadCaseSnapshot(); … await send("read_only", "", { reuseAssistantRenderKey…, label: adoptingLabel })doesNotMatch /choiceContinuationPending/ 锁的是 effect 中转本身(BUG-480
同上 :504 workspace className 以 is-board-open 结尾 追加 ${candidateResult ? "" : " is-board-empty"} 空板收窄(BUG-483
同上 :557 match(chat, /isStructuredChoiceUserText/) doesNotMatch + userVisibleChoiceLine(answeredCard, optionId) 过滤掉的正是用户回显(BUG-482
tests/rectification-answer-choice.test.ts :1123 send\("read_only", ""\) await send("read_only", "", { reuseAssistantRenderKey…, label: recordingLabel }) 续接参数(BUG-480
tests/rectification-spoken-collect.test.ts :134:138 showMissingQuestion = Boolean(caseSnapshotLoaded…currentQuestion === null + 「当前没有可回答的问题,正在等待服务端更新」 rectificationQuestionSlotState({… snapshotLoaded: caseSnapshotLoaded, … resumableCase,doesNotMatch /等待服务端更新/ 锁的正是开发者文案(BUG-479

未改任何其它既有断言;rectification-answer-choice :583 的 isStructuredChoiceUserText 纯函数用例原样保留(函数仍存在,只是不再用于过滤渲染)。

命令输出

./node_modules/.bin/tsc --noEmit0(任务 0、任务 1 后各一次)。

./node_modules/.bin/eslint 触及 13 个文件 → 0 error42 warning 全在 page.tsx(与 origin/staging 同文件既有 react-hooks warning;本轮在 page.tsx 只加了 2 个 state、1 个派生值、4 行 props,未新增 warning)。开发中一度因 effect 内 setState 报 2 个 error,已改(见偏差 2)。

Python quick gate 4 个文件(test_daily_and_rectification_entrypoints / test_session_management_entrypoints / test_birth_time_journey_contract / test_supabase_user_data_contract)→ 24 passed

./node_modules/.bin/tsx --test tests/*.test.ts(基线在同一 worktree 的 4dc0c8c7 干净状态上跑):

基线 4dc0c8c7 现在
tests 2486 2503
pass 2452 2469
fail 24 24
skipped 10 10

失败清单与基线逐条比对:comm -3 差集为空,同一集合。24 条失败全是本机无 Docker / PostgreSQL / 部署夹具的数据库、Better Auth、staging workflow、live sync 类既有缺口(与前两轮 PROGRESS 同一组)。

聚焦 26 套件(本轮 2 个新文件 + 校正/会话/composer/滚动/样式契约)→ 248 / 248。

./node_modules/.bin/next build0✓ Compiled successfully in 49sFinished TypeScript in 49sGenerating static pages (62/62)/ 仍是 ○ (Static)。5 条既有 skill-package-registry.ts filesystem-tracing warning,未失败。

未做与原因

BLOCKED.md 顶部:浏览器级手工验收(六条手工项 + 整轮录屏)无登录态无 Chrome,已写进 docs/testing/staging-manual-walkthrough-20260901.md 第 8 节;1.3 的宫位表 API 不提供;两条书面偏差。