Files
Jyotisha/docs/tasks/PROGRESS-chat-streaming-ux-20260901.md
T
Jesse_Chen 8db71aaf81 docs: product-level README, AGENTS.md split into code/reading parts, add CLAUDE.md, move task briefs to docs/tasks
- README.md is now the product/repo front door (architecture, repo map,
  local dev, test tiers, delivery flow, doc map). Engine positioning,
  VedAstro/Codex setup and the oracle/benchmark command reference move
  verbatim to docs/engine/README.md, docs/engine/vedastro-gateway.md and
  docs/benchmark/README.md. Capability badges realigned with the registry
  (91/78/8/0); tests/test_readme_badges.py was red on staging.
- AGENTS.md: Part A (environment truth, delivery, worktrees, record
  placement, bug workflow, growth freeze, frontend red lines, privacy,
  pre-work check, test tiers) and Part B (reading-rigor constraints).
  GitHub issue-tracker/triage boilerplate removed: GitHub is a read-only
  mirror. All strings locked by tests/ are preserved.
- CLAUDE.md added: roles, three working modes, task-brief sections,
  acceptance criteria, session discipline; imports AGENTS.md.
- 50 tracked TASK-*/PROGRESS-* files and 3 never-committed briefs move to
  docs/tasks/ with an index; REPO_LAYOUT.md merged into README.

Docs-only change (no gated path touched).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
2026-09-03 06:56:06 +00:00

125 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PROGRESS · Agent 聊天流式体验与双会话面统一(2026-09-01)
工作树:`.worktrees/streaming-ux-20260901`
分支:`codex/streaming-ux-20260901`
基线:任务 02 时为 `origin/staging` @ `fbb80fa3`2026-09-02 远端前进到 `058e5db9``bf6989ec` 第三批拆页、`124d3990` 引擎卫生、`058e5db9` Home lint 修复)后本分支 **rebase 到其上**,任务 3 在该基线上做。
**BUG 编号顺延**staging 的 lint 修复占用了 BUG-470,本分支四条整体后移(470→471、471→472、472→473、473→474),`docs/BUG_HISTORY.md`、本文件、四个测试文件里的注释与四个 commit message 一并改(用 `git filter-branch --tree-filter/--msg-filter` 在原提交上逐个改,再 rebase,只在 `BUG_HISTORY.md` 冲突一次)。任务书 `TASK-chat-streaming-ux-20260901.md` 未动。
未改 `.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 | 完成(第三批合入后解除阻塞) | `b72c1efc` | BUG-477、BUG-478 |
| 4 · DESIGN.md | 与 0/1/2 对应的条目随各任务改;与 3 对应的四条随任务 3 同 commit | — | — |
**第二次 rebase 与编号再顺延(2026-09-02**:远端又合入 `1a3e14e7`(问题题干显示/占用后解锁采用),占了 BUG-471/472。本分支再次用 `filter-branch` 在原提交上改号后 rebase 到 `1a3e14e7`471→473、472→474、473→475、474→476、475→477、476→478`BUG_HISTORY`、本文件、测试注释、commit message 一并改。冲突两处:`docs/BUG_HISTORY.md`(取 staging 全文 + 我方条目续在后面);`rectification-agentic-chat.tsx`(两边都保留——staging 的 `collectSpokenPromptId` 与题干 `<p>` 原样,staging 加在 `<Textarea>` 上的 `aria-describedby` 改为经 `ChatComposer` 新增的 `describedBy` prop 传入,composer 把它与字数计数 id 合并成一个 `aria-describedby`)。staging 新测试 `rectification-spoken-collect``aria-describedby={collectSpokenPrompt`(内联 Textarea),按红线 3 改锁 `describedBy={collectSpokenPrompt …}` + composer 的合并表达式,注释原值。`character-remaining-contract``aria-describedby={showRemaining ? remainingId : undefined}` 锁随之改为合并表达式。
## 任务 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.16DESIGN.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 · 校正复用 ChatComposer、统一滚动跟随与「跳到最新」
开工时 `git log origin/staging -- frontend/src/app/page.tsx` 最新为 `551d6317`,第三批未合入,先按红线 7 登记 `BLOCKED.md`;远端合入 `bf6989ec`/`058e5db9` 后 rebase,阻塞解除,`BLOCKED.md` 该条改为已完成。
改动:`src/hooks/use-conversation-scroll-anchor.ts``src/components/jump-to-latest-button.tsx`(新)、`src/components/chat-composer.tsx``src/app/page.tsx``src/components/rectification-agentic-chat.tsx``src/app/globals.css`;删除 `src/lib/rectification-sticky-scroll.ts`
- **ChatComposer**:新增可选 `value`(受控)与 `remainingId``const storeDraft = useComposerDraft(); const draft = value ?? storeDraft;`——store 订阅仍在组件内,主会话行为不变;校正面传 `value={draft}``grep composer-draft` 在校正文件里为 0(契约锁死)。校正面 `maxLength=500``RECTIFICATION_COMPOSER_MAX_LENGTH`,与主对话同上限),footer 放 `CharacterRemaining`id 用 `useId`);三段 placeholder、Enter/Shift+Enter/`isComposing`、「停止回答」文案与 `stopRun` 原样。
- **滚动跟随只留一份**`useConversationScrollAnchor` 增加 follow`ResizeObserver` 观察滚动容器的子元素(`MutationObserver childList` 跟踪子元素增删并重新观察),anchored 时下一帧 `scrollTop = scrollHeight``resetKey` 变化时先落底一次;`anchored``anchoredRef` 在 effect 里同步(不在 render 写 ref,过 `react-hooks` 规则)。`page.tsx` 那个按 `activeStreamingText` 触发、结算时再补 smooth 的 effect 删除;hook 调用签名 `(conversation, !rectificationSurfaceOpen && !starterHomeVisible, activeSessionId)` 不变,starter home 不滚动的性质由 `active=false` 承担。校正面 `useConversationScrollAnchor(conversation, true, caseId)`,删除 `followTailRef` / `scrollFrameRef` / `updateFollowState` / `scrollToLatest` / `followLatestContent` / `showJumpToLatest` / `choiceCardsOpen` 与两个 `useLayoutEffect``rectification-sticky-scroll.ts` 删除。选择卡 360px 带宽的特例随之退出:卡片本来就在 transcript 里,看着它的人离底 ≤96px,本就 anchored。
- **JumpToLatestButton**`.jump-to-latest`absolute、`bottom: 100%``pointer-events: none`+ `.jump-to-latest__button`44px、`--shadow-elevated`、focus ring、hover 暖卡片色),两面共用,文案统一「跳到最新」带 `ArrowDown``page.tsx` 内联 Tailwind 版(`shadow-md`)与 `.rectification-jump-latest` 三条规则删除。
- **宽度**`.rectification-workspace__chat .message-list { max-width: 720px }` 删除,两面同 `.message-list` 的 900px`--rectification-jump-clearance` 底部留白保留。
- 手工项(两面各做一次「流式中上滚→停跟随→出现按钮→点回底并恢复跟随」)**未做**,无登录态无 Chrome;行为由 `chat-notice-and-scroll-contract``rectification-agentic-entry``rectification-answer-choice` 的 hook/组件锁与 `nextAnchorState` 纯函数断言代替。
## 任务 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 阅读宽度写明两面同 900px;§5 Input and composer 加「One composer」条;§5 新增「Jump to latest」条目;§7 写明 `--shadow-soft` 的实际归属(叙事卡、会话内 composer 字段、starter 主题卡 focus、auth 模式 tab)与「浮动控件一律 `--shadow-elevated`、不得用工具类阴影」。
## 被触碰的既有断言(红线 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 160ms0.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 一次 setMessagesBUG-473),后者锁的是校正专属红字失败行(BUG-476)。**注**:这个文件在任务 0 的聚焦跑里漏跑了,任务 2 跑全量前才发现,改在 `cc17d971` |
**任务 3 触碰的断言**(同样每处有原值与错因注释):
| 文件 | 原断言 | 现在 | 理由 |
| --- | --- | --- | --- |
| `tests/character-remaining-contract.test.ts` | `aria-describedby={showRemaining ? composerRemainingId : undefined}` | `remainingId = composerRemainingId` 默认 + `aria-describedby={showRemaining ? remainingId : undefined}` | composer 现为两面共用,校正传自己的计数 id(BUG-477) |
| `tests/composer-isolation-contract.test.ts` | `const draft = useComposerDraft()` | `const storeDraft = useComposerDraft(); const draft = value ?? storeDraft;` + 校正传 `value={draft}` 且不 import `composer-draft` | 受控 `value` 是任务书要求,隔离性质加强而非减弱(BUG-477) |
| `tests/chat-navigation-a11y-contract.test.ts` | 内联按钮的 `min-h-11` / `focus-visible:ring-3` / `pointer-events-auto` / `<ArrowDown … className="size-4"` | 页面锁 `<JumpToLatestButton onClick={conversationAnchor.anchorToLatest} />`,语义改锁组件源码 + `.jump-to-latest__button` 的 44px / focus-visible / pointer-events | 锁的是内联 Tailwind 按钮本身(BUG-478 |
| `tests/chat-notice-and-scroll-contract.test.ts` | 三处:page effect 内 `anchored` 守卫先于 `container.scrollTo`jump control 的 Tailwind 类;`absolute inset-x-0 bottom-full` | follow 在 hook 内、`anchoredRef` 守卫先于 `scrollTop` 赋值、`page.tsx``scrollTo``ResizeObserver`/`MutationObserver` 存在;按钮改锁组件与 CSS;定位改锁 `.jump-to-latest { position: absolute; bottom: 100% }` | 锁的是每 token 一次 `scrollTo` 的 effect 与内联按钮(BUG-478 |
| `tests/chat-stream-layout.test.ts` | composerWrap 含 `pointer-events-none absolute inset-x-0 bottom-full` | 含 `<JumpToLatestButton` + CSS 锁 | 同上 |
| `tests/starter-questions.test.ts` | page effect 以 `if (starterHomeVisible) return` 开头并调 `scrollTo` | hook 调用第二参含 `!starterHomeVisible`follow effect `if (!active \|\| !element) return` 先于赋值,page 无 `scrollTo` | 性质(starter home 不自动滚)由 `active` 承担(BUG-478)。这条在任务 3 第一次全量跑才暴露,改在最后的 docs commit |
| `tests/rectification-agentic-entry.test.ts` | 整段校正专属跟随锁(`followTailRef``isNearBottom``shouldShowJumpToLatest``choiceCardsOpen``回到最新``.rectification-jump-latest`);`className="composer-stop"``aria-label="停止回答"``<form className="composer"` | 锁 `useConversationScrollAnchor(conversation, true, caseId)``<JumpToLatestButton …>`、这些符号不再出现、`.jump-to-latest` 居中;`stopLabel="停止回答"``onStop={stopRun}``composer-stop``chat-composer.tsx``<ChatComposer` + `RECTIFICATION_COMPOSER_MAX_LENGTH = 500` + `<CharacterRemaining` | 锁的是第二套滚动与第二个输入框(BUG-477/476 |
| `tests/rectification-answer-choice.test.ts` | import `rectification-sticky-scroll` 并断言 `isNearBottom`/`shouldFollowLatest`/`shouldShowJumpToLatest`(含 360px 选择卡带宽);`回到最新``followTailRef.current` | import hook 的 `conversationAnchorThreshold`/`nextAnchorState`,断言 96px 与同一组距离判定;锁 `<JumpToLatestButton …>``conversationAnchor.anchored` | 模块已删除(BUG-478 |
新增测试:`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 各一次;rebase 后一次;任务 3 后一次,均无诊断)。
`./node_modules/.bin/eslint` 触及文件 → **0 error**。warning 只落在 `page.tsx`43 条,与 `origin/staging` 同文件同数)与 `use-consultation-run.ts`9 条既有 `no-unused-vars`,本轮只加一行 import);本轮新建的六个文件 0 warning。
`./node_modules/.bin/tsx --test tests/*.test.ts`(基线在干净的 `origin/staging` @ `058e5db9` 独立 worktree 里 `npm ci` 后跑):
| | 旧基线 `fbb80fa3` | 基线 `058e5db9` | 任务 3 后(`058e5db9` 上) | 最终(rebase 到 `1a3e14e7` 后,HEAD 树) |
| --- | ---: | ---: | ---: | ---: |
| tests | 2460 | 2461 | 2482 | 2484 |
| pass | 2426 | 2427 | 2448 | 2450 |
| fail | 24 | 24 | 24 | 24 |
| skipped | 10 | 10 | 10 | 10 |
`1a3e14e7` 本身只改了校正类测试文件(+2 用例),没有另跑一份干净基线;最终列的 24 条失败清单与 `058e5db9` 基线逐条比对差集为空,仍是同一组环境缺口。
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 等)。10 条 skipped 同基线。无产品逻辑新增失败。任务 3 第一次全量跑曾多出 1 条 `starter-questions` 的源码锁失败,按红线 3 改锁 hook 后(见上表)复跑归零。
rebase 后、任务 3 前的聚焦 14 套件 117/117;任务 3 后的聚焦 17 套件 177/177。
`./node_modules/.bin/next build`**0**`✓ Compiled successfully in 22.6s``Finished TypeScript in 28.4s``Generating static pages (62/62)``/` 仍是 `○ (Static)`。5 条既有 `skill-package-registry.ts` filesystem-tracing warning,未失败。
## 未做与原因
`BLOCKED.md` 顶部:任务 3 已解除并完成;仍未做的是浏览器级手工验收(Performance 录制、结算录屏、两面并排截图、流式期间折叠、两面滚动跟随实机)——无登录态无 Chrome;`latestEntryMounts` 探针无 DOM 测试库不可驱动。