perf(web-shell): optimize streaming transcript rendering - #9672
Conversation
🖼️ web-shell visual previewRendered against a mock daemon (no real backend): the PR base vs this PR head Screenshots · before / afterFull-resolution recordings (.webm) are attached to the workflow run. — Qwen Code · web-shell visuals |
|
Thanks for the PR — this is a well-structured performance change.
Moving on to code review. 🔍 中文说明感谢贡献!这是一个结构清晰的高性能优化 PR。
进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewMy independent proposal for "streaming ticks project the whole transcript" was: propagate a change descriptor from the SDK store distinguishing pure tail appends from structural changes, skip the full projection in the message hook when an append is proven, extend MessageList's tail cache to thinking tails, memoize the collapsed thinking header, and skip virtual-scroll geometry work when measured size is unchanged. This PR matches that proposal point for point, and goes further (reducer side-index sharing, reload scheduling rework). The conservative-fallback posture is the right one, and I verified the load-bearing invariants against the base code rather than trusting the comments:
No correctness blockers found. Two non-blocking observations: (1) the full-projection fallback now disables prefix identity reuse whenever background-agent resolutions are present ( sequenceDiagram
participant P1 as Daemon event stream
participant P2 as SDK transcript store
participant P3 as RAF snapshot hook
participant P4 as useMessages
participant P5 as MessageList
P1->>P2: text delta events
P2->>P2: validate pure tail append, else advance barrier
P2-->>P3: blocks plus change summary
P3-->>P4: one coalesced snapshot per frame
P4->>P4: barrier unchanged, append tail text only
P4-->>P5: messages with stable identities
P5-->>P5: update tail row, skip geometry work
Files changed (26)
Testing evidenceUnattended CI run — per gate policy I never build or execute PR code here; the evidence below is the PR's own CI on the reviewed commit, read through the API at the time of this pass (the table is updated in place once CI settles). No failures at this point; the main unit suite is still running and the macOS/Windows unit legs plus CLI integration tests are gated behind it. Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 Not verified: the actual performance win. Unit tests pin the fast-path logic (projection skipped, identities preserved, fallbacks fire) but no CI job measures frame gaps, and the author tested on macOS only (their claim, not independently re-run). The perf claim is the central claim of this PR and it currently rests on code reading plus the author's word. Sandboxed verification would settle this: 中文说明代码审查我的独立方案是:由 SDK store 下发"纯尾部追加 vs 结构变更"的变更描述符,消息 hook 在可证明为追加时跳过全量投影,MessageList 尾部缓存扩展到 thinking,折叠的 thinking 头部做 memo,虚拟滚动在测量尺寸不变时跳过几何计算。本 PR 与这一方案逐点对齐,并且做得更多(reducer 侧索引共享、重载调度重构)。保守回退的姿态是正确的,且关键不变量我都对照 base 代码验证过,而不是只看注释:
未发现正确性阻塞项。两个非阻塞观察:(1) 存在后台 agent resolution 时,全量回退路径放弃了 prefix 身份复用——正确性优先的取舍,路径少见,可接受;(2) Stage 1 已点出的打包行为变化(120s→15s 空闲重载、分页 100→200)仍需评审者知悉。 (流程图与文件清单见英文部分。) 测试证据无人值守 CI 运行——按门禁策略,此处从不构建或执行 PR 代码;以上证据是通过 API 读取的该 commit 自身 CI 结果(表格会在 CI 结束后原地更新)。目前无失败:主单测套件仍在运行,macOS/Windows 单测与 CLI 集成测试在其之后门禁执行。 未验证:实际性能收益本身。单测钉住了快速路径逻辑,但没有 CI 任务测量帧间隔,且作者仅在 macOS 上测试(作者自述,非独立复核)。性能收益是本 PR 的核心主张,目前只由代码阅读与作者陈述支撑。 沙箱验证可以补齐这一点: — Qwen Code · qwen3.8-max Reviewed at |
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
|
Confidence: 3/5 — clean review with no correctness findings, but the cross-package core-infrastructure escalation needs a maintainer's sign-off, and the performance win itself is still unmeasured. Stepping back: this is genuinely good work. The problem is real (I verified on Two things keep this at a defer rather than an approval:
Also factual at review time: the main unit suite ( ⏸️ Deferring to @wenshao — cross-package core-infrastructure change (SDK transcript store + Web Shell, ~920 production lines) with a clean but static review, plus an unmeasured performance claim. Needs a human call on: (1) sign-off on the scope, (2) whether to require a 中文说明置信度:3/5 —— 代码审查干净、无正确性问题,但跨包核心基础设施升级需要维护者签核,且性能收益本身仍未被测量。 整体来看:这是一份高质量的改动。问题真实存在(我在 两点让它停在 defer 而不是 approve:
另外,审查时主单测套件( ⏸️ 转交 @wenshao —— 跨包核心基础设施改动(SDK transcript store + Web Shell,约 920 行生产代码),审查干净但为静态审查,且性能收益未被测量。需要人工决定:(1) 规模签核;(2) 合并前是否要求 — Qwen Code · qwen3.8-max Reviewed at |
ytahdn
left a comment
There was a problem hiding this comment.
Review — perf(web-shell): streaming transcript rendering
Verdict: 💬 Comment — no blocking issues found. Full static review against head 20bb232 (all 26 files: SDK reducer/store, web-shell hooks/components, tests, docs). The design is conservative in the right way: every fast path carries a proof obligation, and anything unproven falls back to the complete path. CI was still triaging at review time — happy to approve once it is green.
🎉 What stands out
- The barrier protocol is airtight.
tailAppendBarrierRevisionis held only whenstreamingTailAppendBlockIdproves a pure top-level tail append (store.ts:195-258), and every blocks-changing entry point handles it correctly:dispatch→nextBlockChangeSummary,appendLocalUserMessage→invalidateTailAppend(correctly guarded bystate.blocks !== previousBlocks, so no-op local appends don't churn the barrier),reset→ always invalidate,clearAwaitingResync/clearFollowupSuggestion→ never touch blocks. The validator chain is exemplary: same event types, noparentToolCallId, equal block counts,blockIndexByIdidentity, tail === active block before and after,streaming === true, reference equality onmeta/usage/branchRecordId/clientReceivedAt/promptId/sourceRecordIds, and exact text-length additivity — that last check also catches bounded-truncation merges. - Index sharing is trim-safe.
shareSideIndexesonly admits top-level text-delta batches undermaxBlocks; top-level events never write the shared by-parent indexes (parentToolCallId === undefined), and every merge-path write inappendTextDelta(meta merge,promptIdbackfill,branchRecordId) is barrier-checked.trimTranscriptStatere-clones the five map indexes it mutates before touching them whensideIndexesShared(transcript.ts:1712-1726), withtoolProgresscorrectly absent from the re-clone list (trim/prune helpers never touch it); both early returns precede any mutation. - The reload rework removes a latent oscillation. Making the reload baseline lastEventId-only is strictly stronger than the old (lastEventId, blockCount) comparison and kills the trim→reload→trim loop the old guard could admit. The in-timer guard re-check,
transcriptReloadPaused(wired tostreamingState !== 'idle'in App, defaulting toisRespondingin ChatPane), and cancellation on turn start are all in the right places. - Test quality. The Proxy-based indexed-read counter (asserting fewer than 10 reads against 100 history blocks), the
adapterSpies.projectoracle, the cross-store source-rejection case, the translator-call-count memo checks, and the no-opclearTimeouttrick that pins the in-timer re-check — this is exactly how a perf fast path should be tested.
💡 Suggestions (non-blocking)
1. Full-path prefix reuse is disabled once resolutions exist — intra-session identity churn
In useMessagesFromBlocks, the fallback gate previous?.resolutions === undefined ? previous : undefined means that once a session has any background-agent resolutions, every barrier-advancing (structural) render re-projects from scratch and all earlier message identities churn — even though reconciliation only ever touches tool-group messages. (Cross-session churn is not the concern; differing blocks already defeat reuse there.) The previous code committed the pre-reconciliation array and let reconciliation re-run on top. One option: keep the reconciled commit for the streaming-tail fast path, but feed reuseUnchangedProjectedPrefix a separate pre-reconciliation commit; reconcileBackgroundAgentResolutions is cheap and short-circuits when no tool matches. Not blocking — the cost only shows up in sessions with background agents, and only on structural renders.
2. Regression test for "shared delta dispatch × concurrent byte-budget trim"
The trim path re-clones indexes before mutation, and a delta dispatch that crosses the byte budget within the same reduce exercises both mechanisms at once (block counts change → the barrier must advance). The store-level suite covers sharing and trim separately; adding one case where a pure top-level delta batch triggers byte-budget trim in the same dispatch — asserting the barrier advanced and that the pre-dispatch state's indexes were not mutated through the shared reference — would pin the interaction.
3. Freeze the summary objects
getBlockChangeSummary() hands the object to arbitrary consumers. Applying Object.freeze to the literals produced by nextBlockChangeSummary / invalidateTailAppend (and the initial summary) would make the read-only contract enforceable rather than honor-system, protecting the source / revision / tailAppendBarrierRevision protocol from accidental mutation. Cheap insurance.
Note — Confidence: low, awareness only
Baked terminal status in the committed prefix. The commit now stores reconciled messages, so a grace-fail { status: 'failed' } (useMessages.ts:684/722) can be baked into the reused prefix, and the override guard (isActiveToolStatus, useMessages.ts:332-338) means a later resolution cannot overwrite a terminal status. I traced the state machine and believe this is unreachable today: after a grace-fail the tool drops from pendingBackgroundAgentKey (derived from reconciled messages, active statuses only) so no later round probes it; a single round cannot settle the same call both grace-failed and resolved; healing arrives through the transcript's own tool.update (barrier advance → full re-projection, where the same guard protects terminal statuses); and a session switch clears the baked state. Flagging only because the property is new relative to the old pre-reconciliation commit — worth keeping in mind if the grace mechanism ever gains a re-probe path.
中文
结论:💬 评论 — 未发现阻塞问题。 已对 head 20bb232 做全量静态审查(26 个文件:SDK reducer/store、web-shell hooks/组件、测试、文档)。设计足够保守:每条快速路径都带证明义务,无法证明时一律回退完整路径。审查时 CI 仍在分诊——变绿后乐意批准。
🎉 亮点
- barrier 协议严丝合缝。 只有
streamingTailAppendBlockId证明是纯顶层尾部追加时tailAppendBarrierRevision才保持不变(store.ts:195-258),且所有会改变 blocks 的入口都正确处理:dispatch→nextBlockChangeSummary、appendLocalUserMessage→invalidateTailAppend(由state.blocks !== previousBlocks正确守卫,空操作的本地追加不会扰动 barrier)、reset→ 总是失效、clearAwaitingResync/clearFollowupSuggestion→ 从不触碰 blocks。校验链堪称典范:事件类型一致、无parentToolCallId、block 数相等、blockIndexById同一性、前后尾部均为 active block、streaming === true、meta/usage/branchRecordId/clientReceivedAt/promptId/sourceRecordIds引用相等、文本长度精确可加——最后一条连有界截断合并也能拦住。 - 索引共享对 trim 安全。
shareSideIndexes只接受maxBlocks以内的顶层文本 delta 批次;顶层事件不会写共享的 by-parent 索引(parentToolCallId === undefined),appendTextDelta合并路径上的每次写入(meta 合并、promptId回填、branchRecordId)都被 barrier 校验覆盖。trimTranscriptState在sideIndexesShared时会先重新克隆将要修改的 5 个 map 索引再动手(transcript.ts:1712-1726),toolProgress正确地不在重克隆列表里(trim/prune 辅助函数从不碰它);两个 early return 都在任何修改之前。 - reload 重构消除了潜在的振荡。 把 reload 基线改为仅 lastEventId 严格强于旧的 (lastEventId, blockCount) 比较,消除了旧守卫可能放行的 trim→reload→trim 循环。定时器触发时的守卫复查、
transcriptReloadPaused(App 中接streamingState !== 'idle',ChatPane 默认isResponding)、turn 开始时取消重载,都落在正确的位置。 - 测试质量。 基于 Proxy 的索引读计数(100 个历史 block 断言少于 10 次索引读)、
adapterSpies.project预言机、跨 store source 拒绝用例、翻译器调用计数的 memo 校验、以及钉住定时器内复查的 no-opclearTimeout技巧——性能快速路径就该这样测。
💡 建议(不阻塞)
1. resolutions 存在后完整路径的前缀复用被禁用——会话内身份抖动
useMessagesFromBlocks 中的回退门 previous?.resolutions === undefined ? previous : undefined 意味着:一旦会话中出现任何 background-agent resolutions,每次 barrier 推进(结构性)渲染都会从头重新投影,前面所有 message 的身份都会抖动——尽管 reconciliation 只会影响 tool-group 消息。(跨会话抖动不是问题,blocks 不同本身就会阻止复用。)旧代码提交的是 reconciliation 之前的数组,再在其上重跑 reconciliation。一个可选方案:流式尾部快速路径继续用 reconciled 提交,但给 reuseUnchangedProjectedPrefix 单独喂一份 pre-reconciliation 提交;reconcileBackgroundAgentResolutions 很廉价,且无匹配时会短路。不阻塞——代价只在有 background agent 的会话、且仅在结构性渲染时出现。
2. "共享 delta dispatch × 同批字节预算 trim" 的回归测试
trim 路径在修改前重克隆索引,而同一 reduce 内越过字节预算的 delta dispatch 会同时触发两个机制(block 数变化 → barrier 必须推进)。store 层测试分别覆盖了共享与 trim;建议补一个用例:纯顶层 delta 批次在同一 dispatch 内触发字节预算 trim——断言 barrier 已推进、且 dispatch 前状态的索引没有经由共享引用被修改——把这个交互钉死。
3. 冻结 summary 对象
getBlockChangeSummary() 会把对象交给任意消费者。对 nextBlockChangeSummary / invalidateTailAppend 产出的字面量(以及初始 summary)应用 Object.freeze,可以让只读契约可强制而非靠自觉,保护 source / revision / tailAppendBarrierRevision 协议免遭意外修改。成本很低的保险。
备注 — Confidence: low,仅供知悉
提交前缀中固化的终态状态。 现在提交的是 reconciled messages,因此 grace-fail 的 { status: 'failed' }(useMessages.ts:684/722)可能被固化进复用的前缀,而覆盖守卫(isActiveToolStatus,useMessages.ts:332-338)意味着后续 resolution 无法覆盖终态状态。我追踪了状态机,认为目前不可达:grace-fail 之后该工具会从 pendingBackgroundAgentKey(由 reconciled messages 派生,仅含 active 状态)中消失,后续轮次不会再探测它;单轮不可能把同一个 call 既判 grace-fail 又判成功;自愈经由 transcript 自身的 tool.update 到来(barrier 推进 → 完整重投影,同样的守卫在那里保护终态);切换会话会清空固化状态。之所以提一句,是因为相对旧的 pre-reconciliation 提交这是个新属性——如果 grace 机制未来增加重探测路径,值得留意。
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
Not explored to full depth (tool budget reached): chunk 4: executing AssistantMessage.thinking-memo.test.tsx — the shared review worktree has no node_modules and installing would collide with concurrent review agents, s…; "agent 8b": verify the exact post-reload retention count (the daemon-side replay window behind the claimed 500-block retention) — not locatable from the client side; oscill….
中文说明
已审查。 建议见行内评论。
未探索到全部深度(达到工具调用预算):chunk 4:executing AssistantMessage.thinking-memo.test.tsx — the shared review worktree has no node_modules and installing would collide with concurrent review agents, s…;"agent 8b":verify the exact post-reload retention count (the daemon-side replay window behind the claimed 500-block retention) — not locatable from the client side; oscill…。
— qwen3.8-max via Qwen Code /review (v0.21.15)
…ell-thinking-stream # Conflicts: # packages/web-shell/client/components/ChatPane.tsx
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No blocking issues. LGTM! ✅
Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:
packages/web-shell/client/components/messages/ToolGroup.tsx:1543 — [review] ThoughtLineHeader memo boundary has no testpackages/web-shell/client/hooks/useMessages.test.ts:280 — [probe] Same-source non-advancing revision rejection branch has no paired testpackages/web-shell/client/hooks/useMessages.ts:400 — [probe] Empty resolutions map disables prefix reuse for the rest of the sessionpackages/web-shell/client/hooks/useMessages.test.ts:305 — [probe] tailBlockId-mismatch rejection branch has no paired testpackages/sdk-typescript/test/unit/transcriptStoreChanges.test.ts:251 — [probe] Four of five trim re-clone statements have no coverage of the mutations they protectpackages/web-shell/client/App.tsx:12444 — [probe] transcriptReloadPaused wiring has no idle-state assertionpackages/web-shell/client/hooks/useMessages.ts:408 — [probe] reconciledMessages reused-tail skip is untestable with the PR's assertionspackages/sdk-typescript/src/daemon/ui/store.ts:208 — [probe] Producer-side trim-crossing invalidation guards have no paired store-level test
中文说明
无阻断问题。LGTM!✅
收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 8 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.21.15)
chiga0
left a comment
There was a problem hiding this comment.
Scope: SDK store + transcript fast-path fast-path contracts, idle-reload logic.
CI: ubuntu-latest Test ✅ · E2E Smoke ✅ · Visual regression ✅ · TypeScript ✅. Test (windows-latest), Test (macos-latest), Integration Tests (CLI, No Sandbox) are SKIPPED — platform behaviour unverified for path-separator and case-folding code; no impact on the files changed here.
F1 — toolProgress not unshared in trimTranscriptState when sideIndexesShared = true (inline below)
When sideIndexesShared = true, cloneTranscriptState intentionally leaves toolProgress pointing at the previous snapshot's object (the sharing is correct, because pure text-delta events never touch toolProgress). The trimTranscriptState path gains the same flag and correctly replaces the five other shared side-indexes before mutating them. But it omits toolProgress.
trimTranscriptState calls pruneTrimmedToolIndexes(state), which does:
for (const toolCallId of Object.keys(state.toolProgress)) {
if (!liveToolCallIds.has(toolCallId)) {
delete state.toolProgress[toolCallId]; // in-place mutation
}
}In a production build (FREEZE_TRANSCRIPT_COLLECTIONS = false), this delete silently mutates the previous snapshot's shared toolProgress object. Any component holding a reference to the old snapshot sees entries disappear mid-render.
The scenario that exercises this: streaming pure top-level text deltas (block count ≤ maxBlocks) while one or more tool calls are still live in toolProgress, with retainedBytes > maxRetainedBytes triggering a byte-budget trim. Not exotic — a long assistant response following tool use can hit this.
The fix is to add toolProgress to the unsharing block, parallel to the other five side indexes:
if (sideIndexesShared) {
state.toolBlockByCallId = createIndex(state.toolBlockByCallId);
// … existing five …
state.toolProgress = createIndex(
Object.fromEntries(Object.entries(state.toolProgress).map(([k, v]) => [k, { ...v }])),
);
}(Cross-check: the existing review noted this path is untested — R1-5 — but filed it as a coverage suggestion. The mechanism above is why the test gap is load-bearing.)
No other blockers. The streamingTailAppendBlockId guard is conservative and the consumer correctly falls back to full comparison for anything it cannot prove. Freeze interactions with shared indexes are safe — freezing an already-frozen object is a no-op. The idle-reload reduction from 120 s → 15 s is guarded at both schedule time and timer-fire, and the transcriptReloadPaused / streamingState !== 'idle' wiring in App.tsx is correct.
Reviewed with AI assistance.
| state.trimmedToolNotificationByCallId, | ||
| ); | ||
| state.permissionBlockByRequestId = createIndex( | ||
| state.permissionBlockByRequestId, |
There was a problem hiding this comment.
trimTranscriptState with sideIndexesShared = true replaces toolBlockByCallId, activeAssistantBlockByParent, activeThoughtBlockByParent, trimmedToolNotificationByCallId, and permissionBlockByRequestId before mutating them — but leaves toolProgress pointing at the previous snapshot's shared object.
pruneTrimmedToolIndexes(state) then does:
for (const toolCallId of Object.keys(state.toolProgress)) {
if (!liveToolCallIds.has(toolCallId)) {
delete state.toolProgress[toolCallId];
}
}In a production build (FREEZE_TRANSCRIPT_COLLECTIONS = false), this silently corrupts the previous snapshot — any component still holding a reference to the old snapshot's toolProgress sees entries deleted mid-render.
Suggested fix: add toolProgress to the unsharing block here:
if (sideIndexesShared) {
state.toolBlockByCallId = createIndex(state.toolBlockByCallId);
// … existing five …
state.toolProgress = createIndex(
Object.fromEntries(Object.entries(state.toolProgress).map(([k, v]) => [k, { ...v }])),
);
}There was a problem hiding this comment.
感谢指出。这里核对后确认不需要修改:评论中引用的 toolProgress 删除循环实际位于 rebuildTranscriptIndexes,不是 trimTranscriptState 调用的 pruneTrimmedToolIndexes。
pruneTrimmedToolIndexes 只会修改 toolBlockByCallId 和 trimmedToolNotificationByCallId;这两个索引已经在 sideIndexesShared 分支中解除共享。纯顶层 text delta + byte-budget trim 路径不会读写 toolProgress,所以继续共享该对象不会改变旧快照。
rebuildTranscriptIndexes 仅由 session rewind 的 truncateTranscriptBeforeBlock 路径调用,而 rewind 事件不会满足 shareSideIndexes 条件,因此其删除 toolProgress 前拿到的是深拷贝。按建议在 trim 中额外深拷贝 toolProgress 不会修复实际问题,只会增加裁剪路径的开销。
chiga0
left a comment
There was a problem hiding this comment.
F1 retracted — author's correction is right: does not touch ; the deletion loop I referenced is in , which is only reached via the rewind path that never satisfies . No outstanding blockers. CI green (ubuntu). Approving.
Reviewed with AI assistance.
|
Released in v0.22.0. |
What this PR does
This PR reduces Web Shell main-thread work during long assistant and thinking streams. Pure top-level tail text appends now carry a validated transcript change summary, allowing the projected tail to update without converting the complete retained transcript. Earlier message and display-item identities are preserved when only the streaming tail changes, safe reducer indexes are reused for top-level text deltas, and virtual scrolling avoids geometry work when streamed content does not change measured layout.
Oversized idle transcripts are reloaded after a 15-second quiet period at the bottom, retaining the recent 500-block window without trim/load oscillation. Reload scheduling uses the real session activity rather than visual loading state, rechecks its guards when the timer fires, and cancels pending or in-flight reloads when a turn starts.
Why it's needed
Collapsed thinking content was hidden, but every visible streaming tick still projected the complete transcript. With long histories, that repeated work competed with browser painting and made the Thinking animation, typing, and other UI interactions less smooth. The previous retention window also kept avoidable history in memory well after a turn completed.
Reviewer Test Plan
How to verify
Evidence (Before & After)
N/A — the visual design and animation are unchanged. This is an internal projection, rendering, and transcript-retention optimization, with a deterministic browser performance scenario included for comparison.
Tested on
Environment (optional)
Local Node.js workspace. Focused SDK and Web Shell Vitest suites, TypeScript checks, production builds, ESLint, and Prettier passed.
Risk & Scope
Linked Issues
N/A
中文说明
本 PR 做了什么
本 PR 减少 Web Shell 在长时间 assistant 与 thinking 流式输出期间的主线程工作。纯顶层尾部文本追加现在会携带经过校验的 transcript 变更摘要,因此可以只更新投影后的尾部而无需转换完整保留 transcript;当只有流式尾部变化时会保留之前 message 和 display item 的对象身份;安全的顶层文本 delta 会复用 reducer 索引;流式内容未改变测量布局时,虚拟滚动不会执行多余的几何计算。
超大 transcript 在页面置底且空闲 15 秒后会重新加载,保留最近 500 个 block,并避免 trim/load 循环。重载调度使用真实 session 活动状态而不是视觉 loading 状态;定时器触发时会重新检查条件;turn 开始后会取消待执行或在途重载。
为什么需要
即使 thinking 内容处于折叠状态,每个可见流式 tick 仍会投影完整 transcript。历史较长时,这些重复工作会与浏览器绘制竞争,导致 Thinking 动画、输入和其他 UI 操作不够流畅。之前的保留窗口也会在 turn 完成较长时间后继续保留不必要的历史内容。
Reviewer 测试计划
如何验证
前后证据
N/A——视觉设计和动画方式没有变化。这是内部投影、渲染和 transcript 保留策略优化,并包含可用于对照的确定性浏览器性能场景。
测试平台
环境(可选)
本地 Node.js workspace。聚焦的 SDK 与 Web Shell Vitest、TypeScript 检查、生产构建、ESLint 和 Prettier 均通过。
风险与范围
关联 Issue
N/A