docs(chat): add streaming-ux and dual-surface unification brief
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
# 任务书 · Agent 聊天流式体验与双会话面统一(2026-09-01)
|
||||
|
||||
基线:`origin/staging` @ `80e77361`。
|
||||
|
||||
对标物是 Claude.ai 的对话面:吐字匀速、思考块有开合过渡、步骤行结算后收成一行、回答结束不闪、滚动跟随不抢手、校正会话和普通会话看起来是同一个产品。本轮四条任务全部来自对着 `origin/staging` 代码逐行审计,每条附了实证位置。**先读完「硬红线」再动手。**
|
||||
|
||||
---
|
||||
|
||||
## 事故实证(为什么现在不流畅)
|
||||
|
||||
下面所有行号基于 `80e77361`,**按选择器 / 符号定位**,不要信行号。
|
||||
|
||||
### A. 每个流式事件都同步触发一次整条消息的全量重渲染
|
||||
|
||||
- `frontend/src/hooks/use-consultation-run.ts` `createNdjsonParser` 回调(约 :770–:830):`answer.delta` / `thinking.delta` / `thinking.section` / `activity` 每一条事件都各自 `setStreamingReply(...)`。服务端 `stream-agent-response.ts` `outputText`(:340–:362)是**每个模型 text-delta 立即 send 一条 `answer.delta`**,没有任何合并。`reader.read()` 每次 resolve 都是独立 microtask,React 19 不会把它们合并成一帧。
|
||||
- 每次 `setStreamingReply` 都要跑 `parseAgentReply(answer)` + `applyThinkingSectionProgress(...)`(对全文),再渲染 `StreamingMessageEntry` → `ChatMessageContent` → `splitSpokenAnswerAndTechniqueAudit(text)` → `promoteDefinitionLists(text)` → `react-markdown` **对整段部分回答重新 parse**。回答长到 3–4k 字时是 O(n²),肉眼可见的卡顿就是它。
|
||||
- `frontend/src/components/rectification-agentic-chat.tsx` `send`(:559–:700)同样:每个事件一次 `setMessages(current => current.map(...))`,整份消息数组重建。
|
||||
- `tests/home-streaming-render-split.test.ts` 锁的是"settled 列表只渲染一次",**没有锁"每 token 一次 streaming 行渲染"是不是合理**(它断言 `streamingRowRenders === tokens.length`,这条是现状的写照,不是目标)。
|
||||
|
||||
### B. 回答结束那一刻会闪一下
|
||||
|
||||
- `chat-transcript.tsx`:流式中最后一条走 `StreamingMessageEntry`,结算后走 `SettledMessageEntry`。两者是**不同组件、不同树位置**,`renderKey` 相同也没用——React 会卸载前者、挂载后者,`ChatMessageRow` 的 `useEntryEffect` 在新挂载的 `<article>` 上**重放 GSAP 入场动画**(autoAlpha 0→1、y 12→0),用户正在读的回答整体闪一次。`tests/chat-stream-layout.test.ts` 第一条"keeps the assistant render identity stable"想锁的正是这件事,但只锁了 `renderKey` 字符串,没锁组件身份。
|
||||
- `consultation-run-timeline.tsx`:`<details key={live ? "live" : "settled"} open={live ? true : undefined}>`,结算时**整个 timeline 重挂**,从展开直接跳到折叠,summary 文案从「正在分析」瞬间变「已完成 N 步」,下方回答向上跳一段,没有任何过渡。
|
||||
- 入场动画写了两份:`globals.css` `.message { animation: message-enter 160ms }` 和 `chat-message-row.tsx` GSAP `duration: 0.18`。同一元素两个 fade 叠加,且时长不一致(160 vs 180)。
|
||||
|
||||
### C. 流式期间 timeline 是受控 `open={true}`
|
||||
|
||||
每来一个 delta 重渲染一次,用户点 summary 折叠后下一个 token 又被撑开。流式期间用户无法折叠思考块。
|
||||
|
||||
### D. 滚动跟随写了两份,行为不同
|
||||
|
||||
- 普通会话:`page.tsx` 约 :1229–:1236 一个 effect,依赖 `activeStreamingText`,**每个 token 触发一次 `scrollTo`**(loading 时 `auto`),与 A 的重渲染同帧叠加。结算瞬间 `isLoading` 翻 false,同一 effect 再跑一次 `smooth` 滚动——用户看到"先跳到底、再平滑滚一下"。
|
||||
- 校正会话:`rectification-agentic-chat.tsx` `followLatestContent`(:381–:392)用 rAF 合并后直接赋 `scrollTop`,另有 `rectification-sticky-scroll.ts` 一套阈值。
|
||||
- 「跳到最新」按钮也是两份:`page.tsx` :2375 用 Tailwind 内联类(`shadow-md`、`bg-canvas`,绕开 §7 只允许 `--shadow-elevated` 的规则),文案「跳到最新」带图标;校正面 `.rectification-jump-latest` 用 `--shadow-soft`,文案「回到最新」无图标。
|
||||
|
||||
### E. 校正会话和普通会话是两套 UI
|
||||
|
||||
| 维度 | 普通会话 | 校正会话 |
|
||||
| --- | --- | --- |
|
||||
| 消息模型 | `ChatMessage` → `settledChatMessageViews` 永远给 assistant 挂 `timeline` | 自有 `RenderMessage` + `activityTrace`,无 `timeline` |
|
||||
| 活动面板 | `ConsultationRunTimeline`(`<details>`,结算收成「已完成 N 步」一行) | `AgentActivityStatus` trace 路径(`<ol>` 永远全展开,**结算后不折叠**)+ 第二个 `<details>` `rectification-activity-receipt` |
|
||||
| live 标记 | `InlineSpinner` 12px | `ThinkingOrb` canvas 20px,但 `.conversation.is-rectification .agent-thinking-marker` 又把格子压到 **14px**,canvas 溢出裁切 |
|
||||
| 活动文案 | 无 shimmer | `agent-activity-status__text` shimmer |
|
||||
| 输入框 | `ChatComposer`(maxLength 500 + 接近上限计数,BUG-447) | 裸 `<Textarea>`,**没有 maxLength、没有计数** |
|
||||
| 阅读宽度 | `.message-list` 900px | 720px |
|
||||
| 失败态 | composer notice | 消息上方红色 `rectification-activity-failure` 一行 |
|
||||
| 重新生成 | 走真实管线 | 立刻把消息改成假的「正在组织回答…」`answer-composition` 态 |
|
||||
|
||||
`chat-message-row.tsx` 里为此并存**三条**思考渲染路径:`ConsultationRunTimeline`(timeline 有值)、`ConsultationThinkingReport`+`ThinkingStepTree`(sections 有值且 timeline 无值)、`AgentActivityStatus`(其余)。普通会话 `timeline` 永远有值,所以后两条对普通会话是死路径,只有校正在用第三条;第二条目前**没有任何调用方能走到**(grep `ConsultationThinkingReport` 只有 `chat-message-row.tsx` 一处引用,且被 `consultTimeline === undefined && thinkingSections.length > 0` 门住,校正的 `RenderMessage` 不带 sections)。
|
||||
|
||||
一条**不是缺陷**、本轮不许改的差异:校正的 `thinking.delta` 在 `rectification-agentic/v9/stream-mapping.ts` 公开边界(`if (event.type === "thinking.delta") return null;`)被有意丢弃,校正会话本来就不展示思考正文。统一的是壳,不是内容。
|
||||
|
||||
### F. DESIGN.md 与实现不一致(见任务 4)
|
||||
|
||||
---
|
||||
|
||||
## 硬红线
|
||||
|
||||
1. **不得改服务端事件语义。** `consultation-run-timeline.ts` reducer 有 `tests/consultation-run-timeline.test.ts` 锁着;校正 `thinking.delta` 的丢弃是隐私边界,不许打开。本轮只动客户端渲染、状态合并与样式。
|
||||
2. **不得手写 `useCallback` / `useMemo`**(React Compiler)。`rectification-agentic-chat.tsx` 里既有的 `useCallback` 不要顺手删,也不要新增。
|
||||
3. **不得修改既有测试断言**,除非该断言锁住的正是本轮要修的缺陷;那种情况在断言上方注释「原值是什么、为什么它是错的」,PROGRESS 单列。已知会碰到的:`tests/chat-stream-layout.test.ts` 的源码正则锁(`LoaderCircle`、`showActivity = message.state !== "settled"`、`stackedThinkingAndAnswer` 等)和 `tests/home-streaming-render-split.test.ts` 的 `streamingRowRenders === tokens.length`。
|
||||
4. 推 staging 前 `./node_modules/.bin/tsc --noEmit` 通过。**不要用 `npx tsc`**。
|
||||
5. 测试数不得低于基线,`fail=0`、`skipped=0`(有 Docker 的环境);本机无 Docker 的既有缺口逐条比对,不得新增。
|
||||
6. 浅色、深色两套都验;`prefers-reduced-motion: reduce` 下所有新增过渡必须收成一帧。
|
||||
7. **`page.tsx` 冲突**:`TASK-home-split-batch3-20260901.md` 正在拆 `page.tsx`。本轮任务 0、1、2 不碰 `page.tsx`;任务 3 要动 `page.tsx` 两处(滚动 effect、跳到最新按钮),**必须等第三批合入后再做**。开工前 `git log origin/staging -- frontend/src/app/page.tsx` 确认,有未验收改动就把任务 3 登记 `BLOCKED.md`,先交 0–2。
|
||||
8. 不得改 `.gitea/workflows/**`。不得在有未提交改动的工作树上切分支。不得自行把 staging 提升到 main。
|
||||
|
||||
让步顺序:功能与测试不回归 > 可验证的修复 > 视觉一致 > 代码整洁。
|
||||
|
||||
## 开工前置
|
||||
|
||||
```bash
|
||||
git fetch origin --prune
|
||||
git worktree add -b codex/streaming-ux-20260901 \
|
||||
../.worktrees/streaming-ux-20260901 origin/staging
|
||||
```
|
||||
|
||||
读 `frontend/AGENTS.md`(Next 16.3 与训练数据不同,先看 `node_modules/next/dist/docs/`),`pre_work_error_ledger.md`,跑 `scripts/pre_work_check.py`。改前在 `docs/BUG_HISTORY.md` 检索同类。BUG 编号先看远端最大号(当前是 469),本轮从 470 起。
|
||||
|
||||
---
|
||||
|
||||
## 任务 0(P0)· 流式渲染按帧合并 + 匀速吐字
|
||||
|
||||
### 做法
|
||||
|
||||
新建 `frontend/src/lib/stream-frame-buffer.ts`(纯函数 + 一个薄 hook),两个会话面共用:
|
||||
|
||||
1. **帧合并**:所有流式事件先落到一个 mutable 累加器(answer 文本、thinking 文本、timeline state、activity),用 `requestAnimationFrame` 合并成**每帧最多一次** `setState`。页面不可见(`document.hidden`)时退化为 250ms `setTimeout`。`run.completed` / `run.failed` / abort 时**同步冲干净**,不能让最后几个字延迟到下一帧之后才出现。
|
||||
2. **匀速释放**:answer 文本不直接把网络到达的整块吐出,而是按帧释放:每帧释放的字符数 = `max(2, ceil(pendingChars / 12))`(约 200ms 内追平积压,网络快时自动加速,慢时不会"一顿一顿")。thinking 文本同样处理。结算时把剩余全部冲出。这是 Claude.ai 那种"字是流出来的"的手感来源,不是 CSS 能做出来的。
|
||||
3. **重活只做一次**:`parseAgentReply` / `applyThinkingSectionProgress` 只在帧 flush 时对当前释放到的文本跑一次,不再每事件一次。
|
||||
4. **Markdown 前缀记忆**:`chat-message-content.tsx` 把文本按**最后一个 `\n\n`** 切成「已稳定前缀」和「尾块」:前缀交给一个用前缀内容做 key 的 memo 子组件(前缀不变就不重新 parse),只有尾块每帧重 parse。切分对象是 `splitSpokenAnswerAndTechniqueAudit(text).spoken`,不是原始 `text`——技法审计表(`AUDIT_HEADER` 起)必须留在审计折叠里,不能被当成前缀 parse 进正文;`parseAgentReply` 处理的 `<!--AYANAM_*` 标记在服务端 `createVisibleTextTransformer` 已剥离,客户端不需要再防。
|
||||
|
||||
两个会话面都接入:`use-consultation-run.ts` 的 NDJSON 回调和 `rectification-agentic-chat.tsx` 的 `send` 循环。校正面 `attempt.reset` 时要把缓冲器清零。
|
||||
|
||||
### 验收
|
||||
|
||||
- `tests/home-streaming-render-split.test.ts`:把「`streamingRowRenders === tokens.length`」改成「`≤ ceil(tokens.length / 每帧合并数)` 且 `≥ 1`」,注释写明原断言是现状快照。用假 rAF(`node:test` mock timers)喂 200 个 token,断言 streaming 行渲染次数 ≤ 20。
|
||||
- 新增 `tests/stream-frame-buffer.test.ts`:匀速释放的字符数公式、结算冲干净、`document.hidden` 退化、reset 清零。
|
||||
- 新增契约:`chat-message-content.tsx` 存在前缀/尾块切分,且前缀不变时 `renderChatMarkdown` 调用次数不增(用注入的 renderer 计数)。
|
||||
- 手工:Chrome Performance 录一次 3k 字回答,流式期间**无 >50ms 长任务**;录屏对比改前改后(浅色即可)。
|
||||
|
||||
### 建档
|
||||
|
||||
BUG-470(流式重渲染 O(n²) 导致卡顿)。防复发:任何流式状态必须经 `stream-frame-buffer`,不得在事件回调里直接 `setState`。
|
||||
|
||||
---
|
||||
|
||||
## 任务 1(P0)· 结算不闪、思考块有开合、流式期间可折叠
|
||||
|
||||
### 做法
|
||||
|
||||
1. **同一条 assistant 行只有一个组件身份。** `chat-transcript.tsx`:最后一条 assistant 视图(不论 streaming 还是刚 settled)都由同一个 `LatestAssistantEntry` 渲染,`ChatTranscript` 用「`messages` 最后一条是否 assistant」决定它拿 settled 数据还是 streaming 数据;`SettledMessageList` 只渲染倒数第二条及之前。这样 streaming→settled 不卸载、不重放入场。渲染次数探针(`home-streaming-render-probe.ts`)加一个 `latestEntryMounts` 计数,契约断言一次会话内只 mount 一次。
|
||||
2. **入场动画二选一。** 保留 GSAP(它已经做了 reduced-motion 分支),删掉 `.message` 上的 CSS `message-enter`;GSAP 时长改回 **0.16s** 与 DESIGN.md §6 一致。`useEntryEffect` 依赖数组保持 `[message.role]` 不变(它就是只在 mount 跑)。
|
||||
3. **timeline 结算过渡。** `consultation-run-timeline.tsx` 去掉 `key={live ? "live" : "settled"}`。live 期间 `<details>` **非受控**:`defaultOpen`(首次挂载 open),用户折叠后不再被撑开。结算时若用户没手动动过,则程序折叠——折叠用 **180ms** 高度过渡:`.consultation-run-timeline__list` 包一层 `display: grid; grid-template-rows: 1fr → 0fr; transition: grid-template-rows 180ms var(--ease-out)`,内层 `min-height: 0; overflow: hidden`(不要用 `interpolate-size`,Safari 未落地)。summary 文案「正在分析」→「已完成 N 步」在同一元素上切换,加 120ms 淡入即可。`prefers-reduced-motion` 下过渡为 0。
|
||||
4. **live 行文案 shimmer 统一。** `agent-activity-status__text` 的 shimmer 提到 `consultation-run-timeline__label` 的 live 行也用(同一份 keyframe,不要复制第二份)。
|
||||
|
||||
### 验收
|
||||
|
||||
- 契约:`chat-transcript.tsx` 源码不再同时存在 `StreamingMessageEntry` 与 `SettledMessageEntry` 两个渲染最后一条的分支;`consultation-run-timeline.tsx` 无 `key={live`。
|
||||
- `globals.css` 中 `message-enter` keyframe 不再被 `.message` 引用(`tests/chat-stream-layout.test.ts` 若锁了它,按红线 3 处理)。
|
||||
- 手工:录屏,回答结束瞬间消息不闪、timeline 折叠有过渡、下方回答不跳;流式期间点 summary 能折叠且不被下一个 token 撑开。
|
||||
|
||||
### 建档
|
||||
|
||||
BUG-471(结算重挂重放入场动画)、BUG-472(流式期间 timeline 不可折叠)。
|
||||
|
||||
---
|
||||
|
||||
## 任务 2(P1)· 校正会话接入同一套活动 UI,收敛三条思考渲染路径
|
||||
|
||||
### 做法
|
||||
|
||||
1. **校正 trace → timeline 行。** 新建 `frontend/src/lib/rectification-timeline-adapter.ts`:把 `AgentActivityTraceItem[]` + `CompletedActivityReceiptView` 映射成 `ConsultationTimelineRow[]`——`activity` 项 → `kind: "calculate"`(label 沿用 `RECTIFICATION_TOOL_PROGRESS_LABELS` / `_DONE_LABELS`),回执里的方法(`PUBLIC_RECTIFICATION_METHOD_LABELS`)作为该 calculate 行的 `sources` chips,`failedTool` 用 `label` 带「未完成」后缀。`RenderMessage` 多一个 `timeline` 字段由适配器派生;`ChatMessageRow` 对校正消息也就自然走 `ConsultationRunTimeline`。
|
||||
2. **结算后校正也收成一行**。`CompletedActivityReceipt` 第二个 `<details>` 删除,内容并入上面的 sources;`rectification-activity-receipt` 相关 CSS 删除。`tests/rectification-activity-receipt.test.ts` 锁的是 reducer(`rectification-activity-receipt.ts`),不动 reducer 就不会红;若它锁了组件源码,按红线 3。
|
||||
3. **删死路径。** 确认无调用方后删除 `consultation-thinking-report.tsx`、`thinking-step-tree.tsx`,以及 `chat-message-row.tsx` 里的 `showReport` 分支。`AgentActivityStatus` 只保留「无 trace、无 timeline 的兜底」(onboarding 等地方若在用要先 grep),其 trace 分支删除。
|
||||
4. **live 标记统一为 `InlineSpinner`,移除 `thinking-orbs`。** 决策见文末。`agent-activity-status.tsx` 的 `ThinkingOrb` 改 `InlineSpinner size={12}`;`package.json` 去依赖;`.conversation.is-rectification .agent-thinking-step / .agent-thinking-marker` 两条 14px 覆写删除。
|
||||
5. **失败态与重新生成对齐。** 校正的「回答未完成」改为和普通会话一样走 composer notice(`rectification-agentic-chat.tsx` 已有 `error` state,直接复用,删除 `rectification-activity-failure` 那行)。重新生成时不再伪造 `answer-composition` 活动,改为 `state: "thinking"` + 空 timeline,由真实事件填充。
|
||||
|
||||
### 验收
|
||||
|
||||
- `tests/chat-bundle-splitting-contract.test.ts` 之类若锁了 `thinking-orbs` 动态导入,按红线 3。`grep -rn thinking-orbs frontend/src frontend/package.json` 为 0。
|
||||
- 新增 `tests/rectification-timeline-adapter.test.ts`:started/completed/failed 三种 trace 映射;方法 chips 去重且 ≤8;`attempt.reset` 后为空。
|
||||
- 手工(无登录态则用 `docs/testing/staging-manual-walkthrough-20260901.md` 的方式留给用户):一次校正对话,流式期间与结算后截图,和普通会话并排——标记、行高、折叠行为一致。
|
||||
|
||||
### 建档
|
||||
|
||||
BUG-473(校正会话活动 UI 与普通会话不一致,含 14px 裁切)。
|
||||
|
||||
---
|
||||
|
||||
## 任务 3(P1,依赖第三批拆页合入)· 校正面复用 ChatComposer 与统一滚动跟随
|
||||
|
||||
### 做法
|
||||
|
||||
1. `rectification-agentic-chat.tsx` 的裸 `<Textarea>` + 两个按钮换成 `ChatComposer`(`maxLength=500`,接近上限计数随之而来;`inputLabel` / `placeholder` 保持现有三段文案)。`ChatComposer` 目前从 `composer-draft` 外部 store 读草稿,校正面有自己的 `draft` state——给 `ChatComposer` 加一个可选 `value` prop(有值则受控,无值则读 store),不要让校正面写主会话的草稿 store。`tests/composer-isolation-contract.test.ts`、`composer-ime-contract` 会锁到,按红线 3。
|
||||
2. 滚动跟随只留 `useConversationScrollAnchor` 一份:校正面接入它(`active` = 面板打开,`resetKey` = caseId),删除 `rectification-sticky-scroll.ts`、`followTailRef` / `scrollFrameRef` / `updateFollowState` / `followLatestContent` / `showJumpToLatest`。跟随本身改成 hook 内部的 rAF 合并(把 `page.tsx` 那个按 `activeStreamingText` 触发的 effect 搬进 hook,改为「anchored 且内容高度变化时,下一帧 `scrollTop = scrollHeight`」,用 `ResizeObserver` 监听 `.message-list` 而不是依赖 token 文本)。结算时不再补一次 `smooth` 滚动。
|
||||
3. 「跳到最新」抽成 `frontend/src/components/jump-to-latest-button.tsx`,样式走 `globals.css` token(`--shadow-elevated`、`--color-canvas`),删除 `page.tsx` 内联 Tailwind 版本和 `.rectification-jump-latest`。文案统一「跳到最新」带 `ArrowDown`。
|
||||
4. 校正 `.message-list` 720px 覆写删除,与普通会话同 900px;候选卡片等自身 `max-width` 不受影响。
|
||||
|
||||
### 验收
|
||||
|
||||
- `tests/chat-notice-and-scroll-contract.test.ts`、`tests/birth-time-mobile-scroll-contract.test.ts` 保持绿或按红线 3 处理。
|
||||
- 契约:`rectification-agentic-chat.tsx` 引用 `ChatComposer` 与 `useConversationScrollAnchor`;仓库无 `rectification-sticky-scroll`;`page.tsx` 无 `shadow-md`。
|
||||
- 手工:两个会话面各做一次「流式中向上滚→停止跟随→出现按钮→点击回到底并恢复跟随」。
|
||||
|
||||
### 建档
|
||||
|
||||
BUG-474(校正输入框无字数上限)、BUG-475(滚动跟随双实现)。
|
||||
|
||||
---
|
||||
|
||||
## 任务 4(P2)· DESIGN.md 对齐
|
||||
|
||||
与代码同 PR 改,不单独提。逐条:
|
||||
|
||||
1. **§5 Message** 新增小节「Streaming states」:`queued → loading-method → calculating → thinking → composing → settled → failed` 七态;每态可见什么(timeline 行、live 标记、shimmer 文案、回答正文);态间过渡各用哪一档动效;`failed` 用 composer notice 而不是消息内红字。写明**校正会话共用同一套壳,仅不展示思考正文**(服务端边界)。
|
||||
2. **§5 Message → Typography**「assistant body 16px」与 `.message-markdown { font-size: 17px }` 不符,二选一改齐(建议文档改 17,代码已上线很久)。
|
||||
3. **§5 Message → Motion**「new messages enter with a short opacity/translate transition only」改为写明只有 GSAP 一份、160ms,且 streaming→settled 不重放。
|
||||
4. **§6 表**新增三行:`Thinking collapse 180ms`、`Timeline label swap 120ms`、`Text reveal — per-frame release, not a duration`。
|
||||
5. **§9 等待表**:「流式生成中」一行删掉 `agent-activity-shimmer` 之外的描述;`ThinkingOrb` 若按决策移除,就不用写;若保留,必须作为第四类进表并写清与 `InlineSpinner` 的分工。
|
||||
6. **§4「Chat reading width」**:写明两个会话面统一 900px(任务 3 完成后)或写明 720 的理由。
|
||||
7. **§5 Input and composer**:加一句「The rectification surface renders the same `ChatComposer`; there is no second composer」。
|
||||
8. **§5 新增「Jump to latest」**组件条目:结构、出现条件(离底 >96px 且非用户主动滚动到底)、样式 token、44px 目标。
|
||||
9. **§7**:写明 `--shadow-soft` 的用途(现在只有 `--shadow-elevated` 入文档,但 CSS 有两个 token 且都在用)。
|
||||
10. **§3**:思考正文长段落用 13px 违反「body copy never below 14px」,把 timeline 内 `thinkingText` 改 14px 或在文档写明例外。
|
||||
|
||||
---
|
||||
|
||||
## 决策记录
|
||||
|
||||
**移除 `thinking-orbs`,全部 live 标记用 `InlineSpinner` + shimmer 文案。** 理由:它只在校正面和一条死路径里用;每个 live 行一个 canvas + `MutationObserver` 主题探测 + 动态导入 20px 占位;DESIGN.md §9 从未把它列入等待词汇;并且它与 14px 容器的裁切就是本轮要修的缺陷之一。Claude.ai 的思考态也只是 shimmer 文字加一个小 spinner。若产品方想保留 orb,则改成反向决策:全部 live 标记用 orb,`InlineSpinner` 从 timeline 里退出——**只能二选一,不许并存**。执行前如无异议按移除做。
|
||||
|
||||
## 执行顺序
|
||||
|
||||
任务 0 → 任务 1 → 任务 2 可连续做,都不碰 `page.tsx`。任务 3 等 `TASK-home-split-batch3` 合入。任务 4 随各任务同 PR 更新对应小节,最后一条 PR 补齐剩余。
|
||||
|
||||
## PROGRESS 要求
|
||||
|
||||
`PROGRESS-chat-streaming-ux-20260901.md`:每任务列改动文件、被触碰的测试断言(原值/新值/理由)、渲染次数探针前后数字、Performance 长任务前后数字、BUG 编号、未做与原因。
|
||||
Reference in New Issue
Block a user