diff --git a/TASK-rectification-ux-20260902.md b/TASK-rectification-ux-20260902.md index e2f115cf..5532d63f 100644 --- a/TASK-rectification-ux-20260902.md +++ b/TASK-rectification-ux-20260902.md @@ -1,6 +1,13 @@ # 任务书 · 生时校正会话面:消除空白假死与交互摩擦(2026-09-02) -基线:**`codex/streaming-ux-20260901`(HEAD `c846c44a`)合入后的 `origin/staging`**。本轮改的文件与那条分支高度重叠(`rectification-agentic-chat.tsx`、`page.tsx`、`globals.css`、`rectification-agentic-entry.test.ts`),**必须在它合入之后开工**;若开工时尚未合入,则基于该分支开 `codex/rectification-ux-20260902`,并在 PROGRESS 里写明。 +基线:**`codex/streaming-ux-20260901`(HEAD `c846c44a`)合入后的 `origin/staging`**。本轮改的文件与那条分支高度重叠(`rectification-agentic-chat.tsx`、`page.tsx`、`globals.css`、`rectification-agentic-entry.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。 | 用户反馈原话:"动画加载的过程中还有一段时间是空白状态,也没有加载也没有状态,导致用户以为页面卡了;交互也不是很友好。"下面每一条空白都对着代码找到了成因。**先读完「硬红线」再动手。** @@ -71,25 +78,27 @@ ## 任务 0(P0)· 六段空白与死角 -### 0.1 入口卡片有反馈 +### 0.1 入口有反馈,但不转圈 -`starter-home.tsx`:`rectificationLoading` 时卡片 `aria-busy="true"`,footer 的 action 文案换成「正在打开…」并在前面放 `InlineSpinner size={14}`;卡片整体 `cursor: progress`。文案在 `rectificationCardLabel` 的派生处加一个 loading 分支,不要在组件里硬编码两份。 +`starter-home.tsx`:`rectificationLoading` 时卡片 `aria-busy="true"`、`data-opening="true"`,footer 的 action 文案换成「正在打开…」(静态文案,**不加 spinner**,遵守 unified-loading 裁决),卡片 `cursor: progress`。文案在 `rectificationCardLabel` 的派生处加 loading 分支。侧栏校正会话行在 `rectificationLoading && 目标是该行` 时同样只加 `aria-busy` 与静态「打开中」尾注,不转圈。 -### 0.2 校正会话面板挂一次、不重挂、有恢复态 +### 0.2 一次揭幕:open + 记录 + 快照并行拉完再切面板,面板挂一次不重挂 -- `page.tsx`:`rectificationSurfaceOpen` 改为 `activeRectificationSession`(只要活动会话是校正类型就挂校正面板),面板 props 增加 `caseId: string | null`。`caseId` 为 null(open 尚未返回)或 turns 尚未加载时,面板内部渲染**恢复态**:`.message-list` 里一条 queued 行「正在恢复校正记录…」(用 `ConsultationRunTimeline` 的 `QUEUED_TIMELINE_ROW` 形态,或直接复用 `session-messages-loading` 的 spinner + sr-only 文案),composer 禁用、placeholder「正在恢复…」。 -- **key 去掉 `-ready/-loading` 后缀**,只保留 `${sessionId}-${caseId}`。turns 的到达改为 **prop 更新**:面板内 `useEffect([initialTurns])`——当本地 `messages` 为空且 `initialTurns.length > 0` 时用 `messagesFromTurns` 填充;本地已有消息(正在流或已流过)时**忽略**这次 turns,不覆盖。`refreshRectificationCase` 仍在 `onCompleted` 后调用,但不再引起重挂。 -- `use-session-management.ts` 的 `selectSession` 顺序不变;因为面板现在立即挂载,空白 2 的"先闪普通对话"自然消失。 -- 卸载时 `runAbort.current?.abort()`(cleanup effect),防止残余流写到已卸载实例。 +- `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.3 问题槽只有生成中态,且始终在对话流内 -`caseSnapshotLoaded === false && !busy && !readonly` → 问题槽渲染 live 行「正在恢复校正进度…」。快照回来后按既有逻辑切换。 +- 快照随揭幕一起到位后,问题槽没有"等待快照"这一态;仅当 0.4 的重试在跑时显示 live 行。 +- **承接 polish D.2**:问题槽(live 选择卡 / spoken prompt / 状态行)渲染为 transcript 的**最后一条内容**——放在候选卡之后、`rectification-saved` 之前,用 `.message-entry` 的同一缩进与间距(`--assistant-content-inset`),不得悬在卡片外。 ### 0.4 两条"等待服务端更新"文案改为有动作的状态 `showMissingQuestion` / `showUnavailableQuestion` 命中时: -1. 先自动重拉快照:最多 3 次、间隔 2s(`useVisibilityAwarePoll` 已有,复用),期间问题槽显示 live 行「正在准备下一个问题…」。 +1. 先自动重拉快照:若 polish 轮落地了 `question.ready` 公开事件,则收到即拉;否则在 `run.completed` 后立即拉一次,再最多 2 次、间隔 2s(`useVisibilityAwarePoll` 已有,复用)。期间问题槽显示 live 行「正在准备下一个问题…」——这是生成中等待,符合裁决。 2. 3 次后仍命中:显示「没有拿到下一个问题。」+ 一个 44px 次级按钮「重新加载」(调 `loadCaseSnapshot`)。 3. 两条旧文案从源码删除。 @@ -108,9 +117,9 @@ ### 验收(任务 0) -- 契约测试(源码锁 + 纯函数):`page.tsx` 无 `"ready" : "loading"`;`rectification-agentic-chat.tsx` 无 `choiceContinuationPending`、无「等待服务端更新」;存在「正在恢复校正记录」「正在恢复校正进度」「正在准备下一个问题」「这段校正还没有开始」;`send` 签名含 `reuseAssistantRenderKey`;卸载 cleanup 调 `abort`。 +- 契约测试(源码锁 + 纯函数):`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 行;⑥ 一个开场失败的旧会话进来有「开始提问」。 +- 手工清单追加到 `docs/testing/staging-manual-walkthrough-20260901.md`:① 首页点卡片看到「正在打开…」,随后一次性出现完整面板(消息 + 问题 + 盘面);② 侧栏切校正会话:旧画面保持到数据齐、不闪普通对话、不出现空白;③ 新建校正第一轮结束不闪、滚动不归零;④ 连点两道选择题中间无空帧无闪卡;⑤ 采用候选看到 live 行;⑥ 一个开场失败的旧会话进来有「开始提问」。 ### 建档