12 KiB
12 KiB
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. 事故实证(staging,2026-09-05/06,产品实测)
| # | 用户看到 | 期望(对标主流 AI 对话产品) |
|---|---|---|
| 1 | 发出消息后输入框整个变灰,不能打字;回答完毕后必须再点一次输入框才能继续输入 | 生成中随时能打字,焦点不丢;只有发送键换成停止键 |
| 2 | 生成中按回车什么都不发生(消息被丢弃) | 生成中发出的消息排队,当前回答一结束自动发出,且可撤回 |
| 3 | 生时校正里生成中点停止:对话区出现一条红色告警条,气泡按失败样式处理,看起来像"出错"而不是"我停下了" | 停止是用户动作,呈现为中性状态:已生成的内容保留、一句灰字说明已停止、不扣点 |
| 4 | 生时校正里回答选择题后的那段等待中,停止键在但点了没反应 | 停止键出现就必须能停 |
2. 根因(已在代码里核实)
#1、#2 是同一处:两个对话面都在生成期间把 <textarea> 整个禁用。
chat-composer.tsxL83:disabled={inputDisabled}直接落到<Textarea>。- 主咨询
page.tsxL1998:inputDisabled = isLoading || sessionMessagesLoading || cancellationPending || rectificationSurfaceOpen || (onboarding 条件);L2000submitBlocked同一组条件加pendingSessionId。 - 生时校正
rectification-agentic-chat.tsxL1430canSend = !busy && !readonly && !regeneratingMessageKey,L1704/1706 把!canSend同时喂给inputDisabled与submitBlocked。 - 浏览器对被
disabled的元素会立即移除键盘焦点,解禁后不会自动归还;两处都没有在结算后重新聚焦(主咨询只在chooseSuggestedQuestionL1390 和 onboarding L1313 聚焦)。所以每答一轮都要点一次。 - 回车:主咨询
handleComposerKeyDown→requestSubmit()→submit()→send(),而use-consultation-run.tsL474if (!resuming && (pendingSessionId || cancellationInFlight.current || pendingConsultation.current)) return false;静默丢弃;生时校正send()L732if (!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.tsxL1012–1034:捕获AbortError后把气泡写成failed: true,并setError(RECTIFICATION_STOPPED_NOTICE)。error只有一个渲染出口 L1659<p className="error-message" role="alert">,globals.cssL1503 是--color-danger左边框 + 红字。"已停止,已生成的内容保留;本次不会扣点。"这句话本身是对的,但穿着报错的衣服。- 若停止时还没有正文(还在工具阶段):
raw为空,不给任何提示;气泡failed且空文本时 L1601 条件不渲染行 → 那条思考行直接消失,用户不知道发生了什么。 - 服务端这部分是对的:
agent/route.tsL277/377/449/529/739 把request.signal一路传下去,agent-run.tsL553–559 联动中止模型流,route.tsL699releaseUsage(..., "rectification_cancelled")退点。本单不动服务端。
#4:生时校正的选择题路径 L1086–1135 与采用路径 L1212–1290 各自 fetch("/api/rectification/agent") 时没有把 AbortController 挂到 runAbort(只有 send() L795–796 挂了),而 busy 为真时停止键照样显示(L1707 stopVisible={busy})。所以在这段 fetch 里点停止,stopRun L564 runAbort.current?.abort() 是空操作;要等它接着跑 send("read_only", …) 续写时才停得下来。
3. 决策记录
- 产品已拍板做 1+2(2026-09-06):生成中输入框永不禁用;生成中发送 = 排队,当前回答结束后自动发出。不做"打断并合并推理"(那需要重定义取消计费与半截回答落库,另立项)。
- 排队的语义:
- 排队只保存一条;再按回车把新内容追加到同一条(换行拼接),不做多条队列。
- 排队条以卡片形式贴在输入框上方,文案见 §5.1,带「撤回」——撤回把文字放回输入框并聚焦。
- 当前回答正常结算后自动发出;用户停止、失败、断线恢复时不自动发,文字放回输入框,由用户决定。
- 生时校正面:排队文字在下一轮到来时走现有 typed 路径(等同用户手打回答,
markQuestionAnswered(..., "typed")),不改服务端。若下一轮结算后处于readonly/已采用等不可发送状态,同样放回输入框。
- 停止是中性动作,两个对话面统一遵守
chat-reply-announcement.ts的分级:停止走中性提示,不得走role="alert"的红色告警;气泡不得标成failed。 inputDisabled只保留结构性理由:readonly、主页上校正面打开时(rectificationSurfaceOpen)、会话消息加载中(sessionMessagesLoading)、onboarding 未完成。isLoading/busy/cancellationPending/regeneratingMessageKey一律不再禁用输入框,只影响发送键与排队。- 停止后不自动把焦点拉回输入框(用户可能正在看回答);输入框既然不再禁用,焦点本来就不会丢。
- 文案先对照
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. 开工前置命令
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%)。