DESIGN.md gains the streaming-states table, the thinking-collapse and label-swap motion rows, the 17px body correction and the single live marker rule; task 3 is registered in BLOCKED.md until the home split batch three lands; PROGRESS lists every touched assertion with its former value. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
12 KiB
PROGRESS · Agent 聊天流式体验与双会话面统一(2026-09-01)
工作树:.worktrees/streaming-ux-20260901
分支:codex/streaming-ux-20260901
基线:origin/staging @ fbb80fa3(任务书写 80e77361,开工时远端已多一条任务书本身的 docs commit)。
未改 .gitea/workflows/**。未合并 staging,未碰 main。未手写 useCallback / useMemo(rectification-agentic-chat.tsx 既有的 useCallback 原样保留)。未动服务端事件语义;校正 thinking.delta 在 stream-mapping.ts 公开边界的丢弃原样保留。page.tsx 一行未动。
| 任务 | 状态 | commit | BUG |
|---|---|---|---|
| 0 · 流式按帧合并 + 匀速吐字 + Markdown 前缀记忆 | 完成 | dd4e3b37 |
BUG-473 |
| 1 · 结算不闪、时间线开合过渡、流式可折叠、queued 行 | 完成 | 69d19e0f |
BUG-474、BUG-475 |
| 2 · 校正接入同一套时间线、删三条死路径、移除 thinking-orbs | 完成 | cc17d971 |
BUG-476 |
| 3 · 校正复用 ChatComposer / 统一滚动跟随 / JumpToLatest / 900px | 未做,见 BLOCKED.md |
— | BUG-476/475 未占号 |
| 4 · DESIGN.md | 与 0/1/2 对应的条目已改;与 3 对应的条目随 3 延后 | 本文件同 commit | — |
任务 0 · 流式渲染
改动:src/lib/stream-frame-buffer.ts(新)、src/lib/chat-markdown-split.ts(新)、src/components/chat-message-content.tsx、src/components/chat-message-row.tsx(streaming prop)、src/hooks/use-consultation-run.ts、src/components/rectification-agentic-chat.tsx。
- 两个会话面的事件回调都不再直接
setState:事件写入累加器,requestAnimationFrame每帧最多一次 flush;run.completed/run.failed/ abort / 读流结束时settle()同步冲干净;finally里dispose();校正attempt.reset走reset()。 - 释放节奏:
max(2, ceil(积压 ÷ 12))字/帧,积压按「新文本到达时的未释放量」记,所以 3,000 字突发恰好 12 帧追平(测试锁死),稳态约落后网络 12 帧(≈200ms);页面隐藏走 250mssetTimeout并一次放完。 - 重活只做一次:
parseAgentReply/applyThinkingSectionProgress只在 flush 里对已释放文本跑。 - Markdown:流式时在最后一个空行切「稳定前缀 / 尾块」,不切进代码围栏、同一列表的项之间、表格行之间;前缀交给
memo的StableMarkdownPrefix,只有尾块每帧 parse;切分对象是splitSpokenAnswerAndTechniqueAudit(text).spoken,技法审计表留在审计折叠里。结算后仍整篇一次 parse,DOM 结构与流式时一致(react-markdown 不加包裹元素)。 - 没有另写"薄 hook":
use-consultation-run按前两轮拆页公证是"内部 0 个 React hook",缓冲器按每次 send 创建/销毁的工厂用法更贴合,两处调用都是这个形态。
渲染次数探针(tests/home-streaming-render-split.test.ts 新增用例,假 rAF 驱动):200 个单字 token、每帧到 4 个、共 50 帧 → streamingRowRenders = 50(改前的架构每 token 一次 = 200)。tests/stream-frame-buffer.test.ts 同一场景 flush 50 次,且每次释放 ≥2 字、结算后与网络全文一致。
Performance 面板录制(3k 字回答无 >50ms 长任务)未做:无登录态、无 Chrome,见 BLOCKED.md。
任务 1 · 结算与时间线
改动:src/components/chat-transcript.tsx(重写)、src/lib/chat-message-view.ts(latestAssistantView)、src/lib/home-streaming-render-probe.ts(latestEntryMounts)、src/components/consultation-run-timeline.tsx(重写)、src/components/chat-message-row.tsx(GSAP 0.16)、src/app/globals.css、scripts/home-streaming-render-benchmark.mts(改用 LatestAssistantEntry)。
ChatTranscript=SettledMessageList excludeLatestAssistant+ 一个LatestAssistantEntry key={renderKey}。同一个MessageEntry渲染历史行、流式行、刚结算行,动作条与追问只在 settled 出现。renderKey在流式(message-N,N=历史条数)与结算(末条下标 N)下相同,纯函数latestAssistantView有测试锁。.message的 CSSmessage-enter关键帧删除,只剩 GSAP 一份,时长 0.18 → 0.16(DESIGN.md §6 = 160ms)。- 时间线:去掉
key={live…};外层<section>+button[aria-expanded][aria-controls];open = userOpen ?? live;折叠是grid-template-rows 0fr↔1fr180ms,折叠后内容inert;summary 文案换行 120ms 淡入;live 行与 summary 文案共用既有agent-activity-status__textshimmer;rows为空且 live 时渲染QUEUED_TIMELINE_ROW「正在处理…」。reduced-motion 下三处过渡为 0(全局*规则之外再显式写了一次)。 latestEntryMounts探针只能在浏览器里手动读(无 DOM 测试库,renderToString不跑 effect),见BLOCKED.md。
任务 2 · 双会话面统一
改动:src/lib/rectification-timeline-adapter.ts(新)、src/components/rectification-agentic-chat.tsx、src/components/agent-activity-status.tsx(重写为兜底)、src/components/chat-message-row.tsx、src/app/globals.css、package.json / package-lock.json(npm uninstall thinking-orbs);删除 consultation-thinking-report.tsx、thinking-step-tree.tsx、completed-activity-receipt.tsx。
- 适配:tool trace →
calculate行(started=live / completed=done,标签沿用RECTIFICATION_TOOL_*_LABELS);failedTool的行标签加「未完成」且不挂 chips;回执methods经PUBLIC_RECTIFICATION_METHOD_LABELS去重 ≤8 作为最后一条完成 calculate 行的sources;没有 live 行且activity.label以「正在」开头时补一条 live 行(answer-composition→write,其余 →calculate);think 类 trace 项映射为 think 行(现网不会出现,服务端丢弃)。 - 校正结算后与普通会话一样收成「已完成 N 步」一行,方法 chips 在行内展开;第二个回执折叠块、消息上方红字失败行、重新生成时的假「正在组织回答…」全部删除;失败走既有
error通知;重新生成显示 queued 行。 chat-message-row.tsx只剩 timeline 一条主路径 +AgentActivityStatus兜底(无 trace 分支、无 orb);live 标记全部InlineSpinner;.conversation.is-rectification .agent-thinking-step/.agent-thinking-marker14px 覆写删除。- 校正的「已用时 N 秒」计时(
ActivityElapsed)随 trace 面板一起退出——时间线本来就没有这个元素,两面一致优先。
任务 3 · 未做
git log origin/staging -- frontend/src/app/page.tsx 最新 551d6317(第二批),第三批未合入。按红线 7 登记 BLOCKED.md,未占 BUG-476/475。
任务 4 · DESIGN.md
已改:§3 思考正文 13px 写成显式例外;§5 Message 正文 16px → 17px(≤767px 时 16px,与 globals.css 一致)、Motion 改为"只有 GSAP 一份 160ms、结算不重放"、新增「Streaming states」七态表(含 failed 不用消息内红字、pacing 公式、reduced-motion 行为、校正只差不显示思考正文);§6 表新增 Thinking collapse 180ms / Timeline label swap 120ms / Text reveal;§9 等待表把时间线 live 步写成两面共用,注明 live 标记只有 InlineSpinner、orb 已移除。
随任务 3 延后:§4 阅读宽度(720 vs 900)、§5 composer 共用一句、§5 Jump to latest 条目、§7 --shadow-soft。
被触碰的既有断言(红线 3,每处源码上方均有原值与错因注释)
| 文件 | 原断言 | 现在 | 理由 |
|---|---|---|---|
tests/home-streaming-render-split.test.ts |
split.streamingRowRenders === tokens.length |
≤ tokens.length && ≥ 1,另加按帧驱动的新用例 |
原值是"每 token 一次提交"的现状快照(BUG-473) |
tests/chat-bundle-splitting-contract.test.ts |
duration: 0\.18 |
0\.16 |
DESIGN.md §6 160ms,0.18 是 GSAP 副本漂移(BUG-474) |
tests/session-conversation-layout.test.ts |
duration:\s*0\.18 |
0\.16 |
同上 |
tests/class-name-definition-contract.test.ts |
knownUnstyled 含 consultation-run-timeline__summary、consultation-step-tree |
两条移除 | 前者现在有样式(按钮),后者类名已随组件删除;该测试本身要求列表无过期项 |
tests/chat-bundle-splitting-contract.test.ts |
"the thinking orb loads on demand…"(OrbState 导入、dynamic(...)、20px 占位、prefetchOnIdle(importThinkingOrb)) |
改为"live 标记是共享 InlineSpinner,无 orb chunk" | orb 是第二种 live 标记且被 14px 裁切(BUG-476) |
tests/chat-stream-layout.test.ts |
reportSource/treeSource 读取;<ThinkingOrb … size={20};ConsultationThinkingReport、hasTrace、activityTrace={message.activityTrace}、activityTrace ?? []、function TraceStep、.agent-thinking-step.is-think:has(...)、report/tree 内容锁、consultation-step-tree__* CSS 锁、.consultation-thinking-report .message-thinking |
改为锁"只剩一条路径"(doesNotMatch 这些符号)+ InlineSpinner size={12} |
锁的正是被删除的第二/第三条思考渲染路径(BUG-476) |
tests/rectification-agentic-entry.test.ts |
读取 completed-activity-receipt.tsx 并锁其 <details>/不 open/「本轮完成 · N 个步骤」;activityStatus /activityTrace/ |
改为锁 chat /rectificationTimelineRows/、chat /completedReceipt/、activityStatus 不含 activityTrace |
锁的是校正专属第二折叠块与 trace 面板(BUG-476) |
tests/rectification-activity-receipt.test.ts |
deltaBranch 内 state: raw.trim() ? "streaming" : "thinking"、text: raw,;message.state === "settled" && message.failed |
状态派生改锁 frame flush 段(state: text.trim() ? …、text,、frames.setAnswer(raw));失败行改锁 && !message.failed 且无 rectification-activity-failure |
前者锁的是每 chunk 一次 setMessages(BUG-473),后者锁的是校正专属红字失败行(BUG-476)。注:这个文件在任务 0 的聚焦跑里漏跑了,任务 2 跑全量前才发现,改在 cc17d971 |
新增测试:tests/stream-frame-buffer.test.ts(7)、tests/chat-markdown-split.test.ts(4)、tests/chat-stream-settle-contract.test.ts(4)、tests/rectification-timeline-adapter.test.ts(6)、home-streaming-render-split +1。
命令输出
./node_modules/.bin/tsc --noEmit → 0(任务 0/1/2 各跑一次,均无诊断)。
./node_modules/.bin/eslint 触及文件 → 0 error。use-consultation-run.ts 有 9 条既有 no-unused-vars warning(未使用的导入/startNewChat),本轮只加了一行 import,与 origin/staging 一致;stream-frame-buffer.ts 一条 dirty 未用已在本 commit 清掉。
./node_modules/.bin/tsx --test tests/*.test.ts:
基线(开工时 fbb80fa3) |
现在 | |
|---|---|---|
| tests | 2460 | 2482 |
| pass | 2426 | 2448 |
| fail | 24 | 24 |
| skipped | 10 | 10 |
24 条失败逐条比对完全同一集合(comm 差集为空):全部是本机无 Docker / PostgreSQL / 部署夹具的数据库、Better Auth、staging workflow YAML、live sync 类(admin code functions、Owner recovery、local PostgreSQL、redeem security、personal report job/sections RLS、migration runner、v9 migration 等),与上一轮 PROGRESS 记录的环境缺口同类。10 条 skipped 同基线。无产品逻辑新增失败。
./node_modules/.bin/next build → 0。✓ Compiled successfully in 21.7s、Finished TypeScript in 58s、Generating static pages (62/62),/ 仍是 ○ (Static)。5 条既有 skill-package-registry.ts filesystem-tracing warning,未失败。
未做与原因
见 BLOCKED.md 顶部三条:任务 3 等第三批拆页;浏览器级手工验收(Performance 录制、结算录屏、两面并排截图、流式期间折叠)无登录态无 Chrome;latestEntryMounts 探针无 DOM 测试库不可驱动。