docs(tasks): composer live input, queued send, neutral stop brief (BUG-551/552)

This commit is contained in:
Jesse_Chen
2026-09-06 03:14:29 +00:00
parent b8ea311235
commit 985c32583d
2 changed files with 104 additions and 0 deletions
+1
View File
@@ -55,6 +55,7 @@
| — | `PROGRESS-rectification-family-collect-continue-20260905.md` | 家人答「没有」后直接给结果,未继续收能区分分钟的经历 | 已由 cd704a92 修正并验收通过 | `codex/rectification-quality-probe-dedupe-20260904`BUG-546 |
| `TASK-rectification-family-collect-continue-fix-20260905.md` | `PROGRESS-rectification-family-collect-continue-20260905.md` | `998406ce` 修复单:家人之后的 dated 补采集无条件抢在现成区分卡之前(choice-card A/B 卡用例回归),另两条顺序断言缺三栏说明 | 已验收通过(浏览器走查欠) | `cd704a92`BUG-547 |
| `TASK-rectification-probe-answer-covers-domain-20260905.md` | `PROGRESS-rectification-probe-answer-covers-domain-20260905.md` | 区分卡答 A/B 后紧接着又问同领域"哪年…"采集题:点选答案只进推断层不写账本,计划层领域覆盖只看账本;改为 yes/weak_yes 视为该领域采集已覆盖(不写账本、不动门) | 待验收 | `988d97ae`BUG-549 |
| `TASK-composer-live-input-and-stop-20260906.md` | `PROGRESS-composer-live-input-and-stop-20260906.md` | 生成中输入框整个禁用致焦点丢失、回车丢消息(改为永不禁用 + 排队发送);生时校正停止呈现为红色告警且选择题等待中停止无效(改为中性停止态、所有 fetch 挂 abort | 待执行 | `codex/composer-live-input-and-stop-20260906`BUG-551、552 |
| `TASK-rectification-collect-vs-offer-consistency-20260905.md` | `PROGRESS-rectification-collect-vs-offer-consistency-20260905.md` | 带年份采集没问完就出采用卡 + 报告,同一轮又被搬家采集题把卡挤掉:决策层判 `adopt_representative` 而计划层仍有 dated 采集(BUG-546 只修了一半);改为剩余采集未完保持 `collect_evidence`,出牌轮才出卡写报告 | 已验收通过 `ca4e2408`2026-09-05staging 部署仍停在 `afd14948``deploy-staging``c295b853` 起连续失败,先解决 `bab07187` 的待迁移) | `codex/rectification-collect-vs-offer-consistency-20260905`BUG-550 |
| `TASK-api-not-configured-mislabel-20260904.md` | — | 16 处路由把数据库瞬断(部署切换窗口)兜底翻译成 503「服务尚未配置」;改为仅配置错误用该文案,其余 `service_unavailable`,收敛为共享 helper | 待执行 | `codex/api-not-configured-mislabel-20260904`BUG-542 起) |
| `TASK-rectification-ux-20260902.md` | `PROGRESS-rectification-ux-20260903.md` | 会话面空白假死与交互摩擦 | 已验收 | `d159f08e`(09-03 在新基线重做后合入,BUG-505509 |
@@ -0,0 +1,103 @@
# TASK · 生成中输入框不再禁用、回车排队发送、停止不再报错(2026-09-06)
- 基线:`origin/staging` @ `b8ea3112`
- 分支:`codex/composer-live-input-and-stop-20260906`
- 执行方:coding agent;验收:Claude
- 涉及文件:`frontend/src/components/chat-composer.tsx``frontend/src/app/page.tsx`(只改传参与调用,**不得增行**;新逻辑进 `frontend/src/hooks/` / `frontend/src/lib/`)、`frontend/src/hooks/use-consultation-run.ts``frontend/src/components/rectification-agentic-chat.tsx``frontend/src/lib/rectification-surface-state.ts``frontend/DESIGN.md``frontend/docs/VOICE.md`
- BUG 编号起点:**BUG-551**(开工时 `grep -o "^## BUG-5[0-9][0-9]" docs/BUG_HISTORY.md | tail -1` 复核;BUG-542 仍为 `TASK-api-not-configured-mislabel-20260904.md` 预留)
- 串行:本单改 `page.tsx``rectification-agentic-chat.tsx`;同日若有别的单改这两个文件,本单先做。
## 1. 事故实证(staging2026-09-0506,产品实测)
| # | 用户看到 | 期望(对标主流 AI 对话产品) |
| --- | --- | --- |
| 1 | 发出消息后输入框整个变灰,不能打字;回答完毕后必须再点一次输入框才能继续输入 | 生成中随时能打字,焦点不丢;只有发送键换成停止键 |
| 2 | 生成中按回车什么都不发生(消息被丢弃) | 生成中发出的消息排队,当前回答一结束自动发出,且可撤回 |
| 3 | 生时校正里生成中点停止:对话区出现一条红色告警条,气泡按失败样式处理,看起来像"出错"而不是"我停下了" | 停止是用户动作,呈现为中性状态:已生成的内容保留、一句灰字说明已停止、不扣点 |
| 4 | 生时校正里回答选择题后的那段等待中,停止键在但点了没反应 | 停止键出现就必须能停 |
## 2. 根因(已在代码里核实)
**#1#2 是同一处**:两个对话面都在生成期间把 `<textarea>` 整个禁用。
- `chat-composer.tsx` L83`disabled={inputDisabled}` 直接落到 `<Textarea>`
- 主咨询 `page.tsx` L1998`inputDisabled = isLoading || sessionMessagesLoading || cancellationPending || rectificationSurfaceOpen || (onboarding 条件)`L2000 `submitBlocked` 同一组条件加 `pendingSessionId`
- 生时校正 `rectification-agentic-chat.tsx` L1430 `canSend = !busy && !readonly && !regeneratingMessageKey`L1704/1706 把 `!canSend` 同时喂给 `inputDisabled``submitBlocked`
- 浏览器对被 `disabled` 的元素会立即移除键盘焦点,解禁后不会自动归还;两处都没有在结算后重新聚焦(主咨询只在 `chooseSuggestedQuestion` L1390 和 onboarding L1313 聚焦)。所以每答一轮都要点一次。
- 回车:主咨询 `handleComposerKeyDown``requestSubmit()``submit()``send()`,而 `use-consultation-run.ts` L474 `if (!resuming && (pendingSessionId || cancellationInFlight.current || pendingConsultation.current)) return false;` 静默丢弃;生时校正 `send()` L732 `if (!continuation && busy) return;` 同样丢弃。没有任何排队机制(`page.tsx` 里搜不到 queue/待发送)。
- 服务端没有"一个会话只能一条"的锁:`/api/consult` 的 409 只有模型变化、计划、出生资料模式;`/api/rectification/agent` 的 409 是 Case 状态。禁用输入框是纯前端决定。
**#3 是呈现层错位**:主咨询有明确的结果分级 `chat-reply-announcement.ts`——`stopped → chat_notice_toast`(中性 toast)、`failed → conversation_alert`(红色告警)。生时校正没有遵守:
- `rectification-agentic-chat.tsx` L10121034:捕获 `AbortError` 后把气泡写成 `failed: true`,并 `setError(RECTIFICATION_STOPPED_NOTICE)`
- `error` 只有一个渲染出口 L1659 `<p className="error-message" role="alert">``globals.css` L1503 是 `--color-danger` 左边框 + 红字。"已停止,已生成的内容保留;本次不会扣点。"这句话本身是对的,但穿着报错的衣服。
- 若停止时还没有正文(还在工具阶段):`raw` 为空,不给任何提示;气泡 `failed` 且空文本时 L1601 条件不渲染行 → 那条思考行直接消失,用户不知道发生了什么。
- 服务端这部分是对的:`agent/route.ts` L277/377/449/529/739 把 `request.signal` 一路传下去,`agent-run.ts` L553559 联动中止模型流,`route.ts` L699 `releaseUsage(..., "rectification_cancelled")` 退点。本单不动服务端。
**#4**:生时校正的选择题路径 L1086–1135 与采用路径 L12121290 各自 `fetch("/api/rectification/agent")` 时**没有**把 `AbortController` 挂到 `runAbort`(只有 `send()` L795796 挂了),而 `busy` 为真时停止键照样显示(L1707 `stopVisible={busy}`)。所以在这段 fetch 里点停止,`stopRun` L564 `runAbort.current?.abort()` 是空操作;要等它接着跑 `send("read_only", …)` 续写时才停得下来。
## 3. 决策记录
1. **产品已拍板做 1+2**(2026-09-06):生成中输入框永不禁用;生成中发送 = 排队,当前回答结束后自动发出。不做"打断并合并推理"(那需要重定义取消计费与半截回答落库,另立项)。
2. **排队的语义**
- 排队只保存一条;再按回车把新内容追加到同一条(换行拼接),不做多条队列。
- 排队条以卡片形式贴在输入框上方,文案见 §5.1,带「撤回」——撤回把文字放回输入框并聚焦。
- 当前回答**正常结算**后自动发出;**用户停止、失败、断线恢复**时不自动发,文字放回输入框,由用户决定。
- 生时校正面:排队文字在下一轮到来时走现有 typed 路径(等同用户手打回答,`markQuestionAnswered(..., "typed")`),不改服务端。若下一轮结算后处于 `readonly`/已采用等不可发送状态,同样放回输入框。
3. **停止是中性动作**,两个对话面统一遵守 `chat-reply-announcement.ts` 的分级:停止走中性提示,不得走 `role="alert"` 的红色告警;气泡不得标成 `failed`
4. `inputDisabled` 只保留**结构性**理由:`readonly`、主页上校正面打开时(`rectificationSurfaceOpen`)、会话消息加载中(`sessionMessagesLoading`)、onboarding 未完成。`isLoading` / `busy` / `cancellationPending` / `regeneratingMessageKey` 一律不再禁用输入框,只影响发送键与排队。
5. 停止后**不**自动把焦点拉回输入框(用户可能正在看回答);输入框既然不再禁用,焦点本来就不会丢。
6. 文案先对照 `frontend/docs/VOICE.md`;新增文案与视觉同提交更新 `DESIGN.md`「Input and composer」与「Message」。
## 4. 硬红线
- `page.tsx` 不得增行(当前 2041 行);排队逻辑放 `frontend/src/hooks/use-queued-message.ts`(参数式 hook 模式,内部 0 个 React hook 或按仓库既定模式)或 `frontend/src/lib/queued-draft.ts`,两个对话面共用。
- 不得再手写第二个输入框;排队条是 `ChatComposer` 的一个可选 slot/prop,不是新组件里的新 textarea。
- 不改服务端计费、取消(`/api/consult/cancel`)、`releaseUsage` 逻辑;不改 `agent-run.ts`
- 主咨询现有停止链路(退点确认、409 `request_completed` 时恢复完整回答、断线恢复轮询)原样保留,只改呈现与输入框禁用条件。
- `composer-ime-contract``composer-isolation-contract``chat-notice-and-scroll-contract``rectification-surface-contract``rectification-agentic-entry` 既有用例原样通过;改任何既有断言写「原值 / 新值 / 原因」三栏。
- `next build``/``○ Static`;首屏 gzip ±2%。
- 排队条、停止提示不得出现 spinner/骨架。
## 5. 任务分解
### 5.1 输入框不再禁用 + 排队发送(BUG-551)
- `ChatComposer`:新增可选 `queued?: { text: string; onRecall: () => void }`;有值时在 textarea 上方渲染一条排队卡片(文案:「已排队,回答结束后发出」+ 文字预览 + 「撤回」按钮)。`inputDisabled` 仍支持,但两处调用方只传 §3.4 的结构性条件。
- 主咨询:`isLoading``cancellationPending` 期间回车与点发送 → 进入排队(不再被 `send()` 静默丢弃);`replyOutcome.phase === "completed"` 后自动 `send(queued)``stopped``failed``recovering` 时把排队文字放回草稿(`setDraft`)并清空排队。发送键在有排队时禁用或改为「已排队」态由执行方在 DESIGN.md 记录。
- 生时校正:`busy``regeneratingMessageKey` 期间同上;`send` 结算成功(`succeeded`)后自动以 `message` 发出排队文字;`AbortError`/失败/`readonly` 时放回草稿。
- 验收:
- `tests/chat-composer-queue.test.ts`(新):生成中 textarea 无 `disabled`;回车把文字放进排队而非丢弃;再次回车追加到同一条;撤回放回输入框;结算成功自动发出、停止不发出。
- `composer-ime-contract` / `composer-isolation-contract` 原样通过。
- `rectification-surface-contract``busy``inputDisabled === false`
### 5.2 停止 = 中性状态(BUG-552
- 生时校正 `AbortError` 分支:气泡改为 `state: "settled"`、不标 `failed`,新增 `stopped: true``ChatMessageRow``stopped` 时于正文下渲染一行灰字(`RECTIFICATION_STOPPED_NOTICE` 或更短的「已停止,本次不扣点」),不再 `setError`。停止时无正文:保留气泡,只显示这行灰字,不让思考行凭空消失。
- `error-message` 只留给真正的失败;`rectification-surface-state.ts``RECTIFICATION_STOPPED_NOTICE` 若改字,VOICE.md 同步。
- 选择题 L1086 与采用 L1212 两条 fetch 挂上 `AbortController` 并写入 `runAbort`,被中止时与 `send()` 同样处理(气泡 `stopped`,不报错);服务端已按 `request.signal` 退点,前端不用额外调用。
- 主咨询:核对 `stopResponse` 各分支只用 toast(已如此);若产品复现的"报错"出现在主咨询,执行方在进度记录贴出脱敏后的提示原文并对照 §2 补根因,不得猜。
- 验收:
- `rectification-agentic-entry` 新增:流中 abort → 最后一条助手消息 `stopped === true``failed !== true`、页面无 `role="alert"`;无正文时 abort → 气泡仍在且带停止说明。
- 选择题 fetch 中 abort → 请求被中止、`busy` 归位、无告警。
- `chat-notice-and-scroll-contract``stopped` 仍映射 `chat_notice_toast`
### 5.3 记录
- `docs/BUG_HISTORY.md`:BUG-551(输入框禁用致焦点丢失、回车丢消息;关联 BUG-216、BUG-329)、BUG-552(停止穿报错衣服、选择题期间停止无效;关联 BUG-329,说明它"绑定 fetch abort"的防复发为何没覆盖选择题与采用路径)。
- `CHANGELOG.md` 一条;`frontend/DESIGN.md`「Input and composer」加排队条、「Message」加停止态;`frontend/docs/VOICE.md` 加排队与停止文案。
- `docs/tasks/PROGRESS-composer-live-input-and-stop-20260906.md``docs/testing/composer-live-input-and-stop-20260906.md`(真实环境:生成中连续打字不丢焦点;回车排队并在回答后自动发出;撤回;两个面停止后无红条、内容保留、点数未扣;选择题等待中点停止立即停)。
## 6. 让步顺序
5.1 与 5.2 可拆成两个提交但同一分支交付;5.3 不可省。若排队在生时校正面与 `readonly`/采用态冲突难以收敛,可先只做主咨询排队、校正面只做"不禁用 + 回车放回草稿并提示「回答结束后再发」",须在进度记录写明。
## 7. 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/composer-live-input-and-stop-20260906 .worktrees/composer-live-input-and-stop-20260906 origin/staging
cd .worktrees/composer-live-input-and-stop-20260906
ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules
cd frontend && npx tsx --test tests/composer-*.test.ts tests/chat-notice-and-scroll-contract.test.ts tests/rectification-surface-contract.test.ts tests/rectification-agentic-entry.test.ts tests/agent-voice-copy-contract.test.ts 2>&1 | grep -E "^# (tests|pass|fail)|^not ok"
wc -l src/app/page.tsx # 2041,收尾不得超过
```
收尾跑同一条命令 fail=0,再 `tsc --noEmit``npm run lint``npm run build``/` Static、gzip ±2%)。