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
96 lines
12 KiB
Markdown
96 lines
12 KiB
Markdown
# 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);页面隐藏走 250ms `setTimeout` 并一次放完。
|
||
- 重活只做一次:`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` 的 CSS `message-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↔1fr` 180ms,折叠后内容 `inert`;summary 文案换行 120ms 淡入;live 行与 summary 文案共用既有 `agent-activity-status__text` shimmer;`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-marker` 14px 覆写删除。
|
||
- 校正的「已用时 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 测试库不可驱动。
|