diff --git a/docs/design/tui-optimization/00-overview.md b/docs/design/tui-optimization/00-overview.md index cf772f0ff..97c3585d1 100644 --- a/docs/design/tui-optimization/00-overview.md +++ b/docs/design/tui-optimization/00-overview.md @@ -105,6 +105,8 @@ Entry (gemini.tsx) **执行顺序**:观测基线 -> issue-backed P0 问题治理(动态闪烁、`refreshStatic()`、大输出预裁剪、窄屏回归 harness) -> 启动/MCP 渐进可用 -> 渲染缓存与扩展。MCP 与渲染可并行推进,但必须共享同一套指标口径。 +**PR 编排原则**:从这一版开始,闪屏优化不再以 `#3013` 这类“大杂烩 PR”作为实施基线,而是按用户 issue 归纳出的故障类来组织 PR。`#3013` 只保留为参考样本,用来印证哪些 patch 确实有收益、哪些边界条件需要补齐;真正的实施顺序和 PR 粒度以 [10-issue-oriented-flicker-plan.md](./10-issue-oriented-flicker-plan.md) 为准。 + **实施约束**:从这一版开始,所有落地工作默认都应同时参考 [06-implementation-rollout-checklist.md](./06-implementation-rollout-checklist.md)。如果某项优化没有满足对应的验收清单、灰度顺序和回滚条件,就不应直接进入默认开启阶段。 ## 4. 分阶段实施计划 @@ -193,5 +195,5 @@ Entry (gemini.tsx) | [06-implementation-rollout-checklist.md](./06-implementation-rollout-checklist.md) | 实施门禁、验收、灰度与回滚清单 | | [07-issue-backed-failure-taxonomy.md](./07-issue-backed-failure-taxonomy.md) | 基于 issue 与当前源码的故障分类和修复路线 | | [08-execution-plan-and-test-matrix.md](./08-execution-plan-and-test-matrix.md) | 按文件落点、阶段拆解和测试矩阵整理的执行稿 | -| [09-pr-3013-gap-analysis.md](./09-pr-3013-gap-analysis.md) | 专门分析 `#3013` 已修复范围、剩余缺口与后续拆分建议 | -| [10-pr-3013-split-plan.md](./10-pr-3013-split-plan.md) | 将 `#3013` 拆成若干小 PR 的执行路线、验证边界与推荐顺序 | +| [09-pr-3013-gap-analysis.md](./09-pr-3013-gap-analysis.md) | 专门分析 `#3013` 已修复范围、剩余缺口与可复用 patch;仅作为参考样本 | +| [10-issue-oriented-flicker-plan.md](./10-issue-oriented-flicker-plan.md) | 按用户 issue 类别组织的闪屏实施路线、PR 粒度、验证场景与推荐顺序 | diff --git a/docs/design/tui-optimization/02-screen-flickering.md b/docs/design/tui-optimization/02-screen-flickering.md index ea21bb8e3..4ff7e4f3d 100644 --- a/docs/design/tui-optimization/02-screen-flickering.md +++ b/docs/design/tui-optimization/02-screen-flickering.md @@ -528,7 +528,7 @@ Claude Code 的自研 Ink 内核提供了五层防闪烁保护: ## 4. 实施优先级与里程碑 -如果目标是把 `#3013` 拆成更小、可验证的 PR 再逐步合入,建议不要直接按原 PR 的 patch 混合推进,而是遵循 [10-pr-3013-split-plan.md](./10-pr-3013-split-plan.md) 中的拆分路线。下面这张里程碑表给的是**技术演进顺序**,`10` 给的是**真正开 PR 时的切分粒度**。 +如果目标是彻底解决闪屏,而不是继续维护一条“大而全”的 patch 集合,那么实施顺序就不应再建立在 `#3013` 的 diff 结构上。`#3013` 只保留为参考样本:它证明了 pre-slicing、stable height、stream throttle 这些方向有收益,但真正开 PR 时应按用户 issue 归纳出的故障类来组织,详见 [10-issue-oriented-flicker-plan.md](./10-issue-oriented-flicker-plan.md)。下面这张里程碑表给的是**技术演进顺序**,`10` 给的是**真正开 PR 时的切分粒度**。 | 优先级 | 方案 | 周次 | 风险 | 预期收益 | | ------ | --------------------------- | ----- | ---- | ------------------------- | @@ -542,30 +542,35 @@ Claude Code 的自研 Ink 内核提供了五层防闪烁保护: | P2 | 双缓冲 + diff patch | 11-13 | 高 | stdout 字节/帧 -80% | | P2 | DECSTBM 滚动区域 | 13+ | 高 | 滚动性能接近原生 | -### 4.1 针对 `#3013` 的 PR 拆分建议 +### 4.1 按用户 Issue 组织的 PR 编排 -`#3013` 当前混合了: +从用户角度看,当前“闪屏”其实至少包含 5 类不同故障。真正落地时,PR 也应按这 5 类问题来组织,而不是继续沿用某个大 PR 的 patch 边界。 -- pre-render slicing -- stable height -- assistant stream throttle -- synchronized output -- resize 微抖动 guard -- tool/subagent content budget +| PR | 解决的问题类 | 代表 issue | 主要范围 | 代表性验证场景 | +| --- | --- | --- | --- | --- | +| PR-Prep | 观测与回归基线 | 全部 | 输出 counters、回归 harness、固定复现场景 | 长回答、resize、窄屏、tool expand | +| PR-A1 | 普通流式闪烁 / 滚动条抖动 | `#1184` `#1491` `#3007` `#3144` | `useGeminiStream.ts` 的 content/thought throttle 与强制 flush 语义 | 长 assistant 回答、thought + content、中途 cancel | +| PR-A2 | 终端帧撕裂 / 残余闪烁 | `#2903` `#3144` | synchronized output、frame write 合并、runtime probe | WezTerm、kitty、JetBrains 终端、tmux/SSH | +| PR-B1 | `refreshStatic()` 型整屏闪烁 | `#938` `#1861` `#2924` `#2748` | `refreshStatic()` 语义拆分、resize/view-switch 触发源收紧 | `/settings`、compact merge、view switch、resize | +| PR-C1 | 窄屏重复输出 / 无限滚动 | `#2912` `#2972` `#1591` | shell serializer、live viewport 与 transcript 分离 | 40 列窄终端、tmux 多 pane、`git commit` | +| PR-D1 | 大输出布局抖动 / 工具结果不可读 | `#2748` `#1479` `#2818` | plain text / ANSI pre-slicing | `npm install`、`git log`、5000 行工具输出 | +| PR-D2 | 通用 tool budgeting 与摘要/详情分离 | `#2818` `#1008` `#355` | scheduler budgeting、summary/detail 语义 | `grep`、`glob`、`read_file`、MCP 大结果 | +| PR-E1 | 工具 / 子 agent 展开闪烁 | `#1491` `#1861` `#2424` `#2924` | stable height、bounded detail panel、展开节流 | `ctrl+e`、`ctrl+f`、subagent 执行中展开 | -这类 patch 如果继续放在一条 PR 里,会让 review 很难回答“到底是哪一部分带来了收益”。因此,闪屏相关落地建议以以下顺序拆分: +**推荐顺序**: -| PR | 目标 | 是否直接来自 `#3013` | 说明 | -| --- | --- | --- | --- | -| PR-0 | 观测基线 | 否 | 先补 counters,给后面每条 PR 建立统一验证口径 | -| PR-1 | 大 plain-text 工具输出 pre-slicing | 是 | 最容易视频和基准验证 | -| PR-2 | assistant pending render throttle | 是 | 聚焦普通流式输出抖动 | -| PR-3 | tool/subagent stable height 与 content budget | 是 | 聚焦 `ctrl+e` / `ctrl+f` 展开闪烁 | -| PR-5 | `refreshStatic()` 语义拆分 | 否 | 是所有闪屏修复的补漏地基 | -| PR-4 | synchronized output 灰度接入 | 是 | 放在更后面,避免和现有 stdout 优化层打架 | -| PR-6 | 窄屏 / interactive shell 专项 | 否 | 完全独立推进,不拖慢前面主 UI patch | +1. `PR-Prep` +2. `PR-A1` +3. `PR-B1` +4. `PR-D1` +5. `PR-E1` +6. `PR-C1` +7. `PR-A2` +8. `PR-D2` -完整的文件落点、非目标和测试矩阵见 [10-pr-3013-split-plan.md](./10-pr-3013-split-plan.md)。 +这样排序的原因是:先处理主路径上的高频闪烁和整屏清除,再处理大输出与详情展开,随后再进入最复杂的窄屏 shell 场景;synchronized output 放到更后面,是为了避免在应用层问题尚未收敛前,就把终端协议层的风险一起引进来。 + +完整的文件落点、非目标、借鉴来源和验收矩阵见 [10-issue-oriented-flicker-plan.md](./10-issue-oriented-flicker-plan.md)。 ## 5. 验证方案 diff --git a/docs/design/tui-optimization/06-implementation-rollout-checklist.md b/docs/design/tui-optimization/06-implementation-rollout-checklist.md index 5050b95a6..d0caa6178 100644 --- a/docs/design/tui-optimization/06-implementation-rollout-checklist.md +++ b/docs/design/tui-optimization/06-implementation-rollout-checklist.md @@ -3,7 +3,7 @@ > 本文档给 `00-05` 各设计/调研文档补齐实施门槛、验收标准、灰度顺序和回滚条件。目标不是重复方案细节,而是把“什么时候能开始做、做到什么算完成、什么情况下必须停下来”写清楚。 如果需要把设计进一步落成开发排期,请与 [08-execution-plan-and-test-matrix.md](./08-execution-plan-and-test-matrix.md) 配套阅读:本清单负责“能不能上线”,`08` 负责“先改哪里、先测什么、先拆哪几条 PR”。 -如果要把 `#3013` 拆成多个小 PR,请再配合 [10-pr-3013-split-plan.md](./10-pr-3013-split-plan.md) 使用:`10` 负责“这一大条 PR 应该拆成哪几条更好 review、每条故意不带什么”。 +如果要把闪屏治理真正拆成可 review、可复现、可关闭 issue 的一组小 PR,请再配合 [10-issue-oriented-flicker-plan.md](./10-issue-oriented-flicker-plan.md) 使用:`10` 负责“按用户问题类拆 PR、每条 PR 故意不带什么、应该先验证哪些场景”。 ## 1. 使用方式 @@ -34,13 +34,16 @@ ## 2A. PR 拆分门禁 -如果某条闪屏修复 PR 是从 `#3013` 或其后续补漏中拆出来的,进入 review 前还应满足: +如果某条闪屏修复 PR 是按用户 issue 类别组织的,进入 review 前还应满足: -- [ ] PR 只覆盖一种主闪屏机制 +- [ ] PR 标题和描述明确写出目标问题类,并链接对应 issue +- [ ] PR 只覆盖一种主故障簇,不同时声称“顺手解决”其他类型闪屏 - [ ] PR 描述明确写出非目标(哪些问题故意留给下一条) +- [ ] PR 附带至少一个稳定复现脚本或固定操作步骤 - [ ] PR 中不包含临时 diagnostics / `stderr` 调试输出 - [ ] PR 有独立的验证场景,而不是复用“大而全”视频证明 - [ ] PR 没有同时引入两层以上 stdout monkeypatch / render hook 变化 +- [ ] 若借鉴 `#3013`、Gemini CLI、Claude Code 的 patch 或思路,PR 描述明确写出“借鉴了什么、故意没带什么” ## 3. 启动与 MCP 验收清单 diff --git a/docs/design/tui-optimization/07-issue-backed-failure-taxonomy.md b/docs/design/tui-optimization/07-issue-backed-failure-taxonomy.md index d647e99fb..481003694 100644 --- a/docs/design/tui-optimization/07-issue-backed-failure-taxonomy.md +++ b/docs/design/tui-optimization/07-issue-backed-failure-taxonomy.md @@ -236,9 +236,9 @@ - Gemini CLI 已经在 `ToolResultDisplay` 中为普通模式使用 `SlicingMaxSizedBox`,先做**字符/行切片,再交给 `MaxSizedBox`** - Claude Code 则进一步把长会话放进 `ScrollBox` / `useVirtualScroll()` 体系 -### 6.4 当前维护者方向信号 +### 6.4 当前维护者方向信号(仅作为参考样本) -`qwen-code#2748` 的维护者评论指向 PR `#3013`。截至 **2026-04-22**: +`qwen-code#2748` 的维护者评论指向 PR `#3013`。按 **2026-04-23** 重新核对后,PR 状态仍与 **2026-04-22** 的最近更新时间一致: - `#3013` 仍是 **OPEN** - reviewDecision 为 **CHANGES_REQUESTED** @@ -252,6 +252,8 @@ 1. 不能为了防闪烁直接移除 markdown 呈现 2. 预切片之后,hidden lines 统计和软换行仍需严谨处理 +从这一版开始,`#3013` 只用来做**方向印证**,不再作为实施拆分的主轴。真正的 PR 编排改为按用户问题类组织,见 [10-issue-oriented-flicker-plan.md](./10-issue-oriented-flicker-plan.md)。 + ### 6.5 可执行修复方案 **P0.1 预裁剪优先于视觉裁剪** @@ -347,13 +349,27 @@ - output diff / cursor-home - 更深的终端协议/scroll region 优化 +### 8.1 Issue 驱动的 PR 组织方式 + +为了保证每条 PR 都能单独复现、单独验证、单独关闭一类用户问题,推荐把分类 A-E 直接映射到实施 PR,而不是继续沿用 `#3013` 的 patch 边界: + +| 问题类 | 推荐 PR | 说明 | +| --- | --- | --- | +| A. 动态区重绘闪烁 | `PR-A1` `PR-A2` | 先降更新频率,再视终端家族灰度 synchronized output | +| B. `refreshStatic()` 整屏闪烁 | `PR-B1` | 单独拆语义,不和其他 patch 混入一条 PR | +| C. 窄屏重复输出 / 无限滚动 | `PR-C1` | 先建稳定回归,再改 shell serializer 语义 | +| D. 大输出不可读 / 长会话预算不足 | `PR-D1` `PR-D2` | 先做 pre-slicing,再补统一 budgeting 与 summary/detail | +| E. 工具 / 子 agent 详情展开闪烁 | `PR-E1` | 把 stable height 与 bounded detail panel 一起收口 | + +其中 `PR-Prep` 是所有闪屏修复的共用前置,不负责关闭具体 issue,但负责为后续每条 issue PR 提供统一的观测和回归口径。 + ## 9. 三轮无方向自审结论 ### Pass 1:事实核对 - 已把 `#1778` 的 one-line fix 降级为历史信号 - 已把 `refreshStatic()` 与 Ink `eraseLines` 明确拆开 -- 已把 `#3013` 标注为 2026-04-22 时仍未合入 +- 已把 `#3013` 标注为 2026-04-22 时仍未合入,且仅作为参考样本 ### Pass 2:边界条件核对 diff --git a/docs/design/tui-optimization/08-execution-plan-and-test-matrix.md b/docs/design/tui-optimization/08-execution-plan-and-test-matrix.md index ae3190d86..087b15638 100644 --- a/docs/design/tui-optimization/08-execution-plan-and-test-matrix.md +++ b/docs/design/tui-optimization/08-execution-plan-and-test-matrix.md @@ -1,6 +1,6 @@ # TUI 优化执行计划与测试矩阵 -> 本文档把 `00-09` 的设计与调研进一步压缩成“可以直接排期和拆任务”的执行稿。 +> 本文档把 `00-10` 的设计与调研进一步压缩成“可以直接排期和拆任务”的执行稿。 > 校准时间点:2026-04-22。若 issue / PR / 上游源码继续变化,需要重新核对后再执行。 ## 1. 目标 @@ -37,21 +37,21 @@ | S6 | 窄屏 / interactive shell 专项回归与修复 | `shellExecutionService.ts`, `terminalSerializer.ts`, CLI tests | 中高 | 3-5 天 | | S7 | bounded detail panel + stable height | tool/subagent 相关组件 | 中高 | 3-5 天 | -### 2A. 与 `#3013` 的对应关系 +### 2A. 与用户 Issue 类别的对应关系 -为了方便把 `#3013` 拆成若干小 PR,建议把这些 slice 和真正的 PR 切分对应起来: +从这一版开始,切片和 PR 的对应关系不再围绕 `#3013` 组织,而是围绕用户问题类组织: | Slice | 对应 PR | 说明 | | --- | --- | --- | -| S1 | PR-0 | 观测基线,不直接来自 `#3013`,但建议所有后续 PR 先依赖它 | -| S2 | PR-2 | assistant pending render throttle | -| S3 | PR-5 | `refreshStatic()` 语义拆分,是 `#3013` 外的关键补漏 | -| S4 | PR-1 | 大 plain-text 工具输出 pre-slicing | -| S5 | 后续独立 PR | 不在 `#3013` 当前 patch 中,建议后移 | -| S6 | PR-6 | 窄屏 / interactive shell 专项,不建议混入主 UI patch | -| S7 | PR-3 | tool/subagent stable height 与 content budget | +| S1 | `PR-Prep` | 闪屏观测与回归基线;所有后续 issue PR 的前置 | +| S2 | `PR-A1` | 动态流式闪烁的主路径修复:content/thought throttle | +| S3 | `PR-B1` | `refreshStatic()` 整屏闪烁 | +| S4 | `PR-D1` | 大输出 pre-render slicing | +| S5 | `PR-D2` | 通用 tool budgeting 与摘要/详情分离 | +| S6 | `PR-C1` | 窄屏 / interactive shell 重复输出与无限滚动 | +| S7 | `PR-E1` | tool/subagent 展开闪烁与 bounded detail panel | -而 `PR-4` synchronized output 灰度接入横跨 S1 之后的输出层验证与 rollout,不单独落在某一个 slice 上,需结合 [10-pr-3013-split-plan.md](./10-pr-3013-split-plan.md) 的约束执行。 +而 synchronized output 的灰度接入单独作为 `PR-A2`,属于“动态流式闪烁”这一大类的终端层补强,不建议和上面的任一 slice 混成一条 PR。完整说明见 [10-issue-oriented-flicker-plan.md](./10-issue-oriented-flicker-plan.md)。 ## 3. Slice S1:建立可观测性 @@ -298,14 +298,14 @@ 建议不要把这些 slice 混成一个超大 PR。更稳的顺序是: -1. PR-0:S1 观测 -2. PR-1:S4 大工具输出 pre-slicing -3. PR-2:S2 assistant pending render throttle -4. PR-3:S7 bounded detail panel / stable height -5. PR-5:S3 `refreshStatic()` 语义拆分 -6. PR-4:synchronized output 灰度接入 -7. PR-6:S6 窄屏专项 -8. 通用 tool budgeting 另开独立 PR +1. `PR-Prep`:S1 观测与回归基线 +2. `PR-A1`:S2 动态流式闪烁主路径修复 +3. `PR-B1`:S3 `refreshStatic()` 整屏闪烁修复 +4. `PR-D1`:S4 大输出 pre-slicing +5. `PR-E1`:S7 bounded detail panel / stable height +6. `PR-C1`:S6 窄屏专项 +7. `PR-A2`:synchronized output 灰度接入 +8. `PR-D2`:S5 通用 tool budgeting 这样做的好处是: diff --git a/docs/design/tui-optimization/09-pr-3013-gap-analysis.md b/docs/design/tui-optimization/09-pr-3013-gap-analysis.md index 1741aebbb..6fc6b7120 100644 --- a/docs/design/tui-optimization/09-pr-3013-gap-analysis.md +++ b/docs/design/tui-optimization/09-pr-3013-gap-analysis.md @@ -8,6 +8,8 @@ > 2. 它还没覆盖哪些根因 > 3. 如果继续做,最合理的拆分方案是什么 +**使用边界**:从当前文档版本开始,本文件只作为 `#3013` 的参考分析,不再作为闪屏实施排期的主轴。真正按用户问题类组织的实施方案见 [10-issue-oriented-flicker-plan.md](./10-issue-oriented-flicker-plan.md)。 + ## 1. 结论摘要 `#3013` 不是“无效 PR”,相反,它已经覆盖了**闪屏问题里最容易被用户看到的一块**: @@ -34,7 +36,7 @@ ## 2. 基于 PR 页面可确认的事实 -截至 **2026-04-22**,`#3013` 的状态是: +按 **2026-04-23** 重新核对后,`#3013` 的状态仍是: - `OPEN` - `CHANGES_REQUESTED` @@ -348,7 +350,7 @@ PR 虽然做了: 这条是 `#3013` 完全没碰到、但用户影响很重的一类问题。 -如果要看更细的“每条 PR 带哪些文件、故意不带哪些 patch、先后顺序怎么排”,请直接参考 [10-pr-3013-split-plan.md](./10-pr-3013-split-plan.md)。本文件保留拆分结论,`10` 负责执行方案。 +如果要看真正按用户 issue 类别组织的“每条 PR 带哪些文件、故意不带哪些 patch、先后顺序怎么排”,请直接参考 [10-issue-oriented-flicker-plan.md](./10-issue-oriented-flicker-plan.md)。本文件保留 `#3013` 的参考价值,`10` 负责执行方案。 ## 7. 推荐新增的分析结论 diff --git a/docs/design/tui-optimization/10-issue-oriented-flicker-plan.md b/docs/design/tui-optimization/10-issue-oriented-flicker-plan.md new file mode 100644 index 000000000..a1d10f2bb --- /dev/null +++ b/docs/design/tui-optimization/10-issue-oriented-flicker-plan.md @@ -0,0 +1,444 @@ +# 闪屏治理:Issue 驱动的 PR 编排 + +> 目标:把闪屏优化从“围绕某个大 PR 拆补丁”改成“围绕真实用户问题逐类关闭 issue”。 +> 校准时间点:2026-04-23 +> 使用方式:`09-pr-3013-gap-analysis.md` 只用于参考 `#3013` 里哪些 patch 被证明有价值;真正的实施排期、PR 粒度和验证矩阵以本文档为准。 + +## 1. 为什么要改成 Issue 驱动 + +`#3013` 已经证明三件事: + +1. pre-render slicing 能明显降低大工具输出的闪烁 +2. stable height 能缓解详情展开时的高度抖动 +3. assistant pending throttle 能降低普通流式输出的抖动 + +但它也同时说明了一个反面事实: + +- 一条 PR 一旦同时混入 pre-slicing、stable height、synchronized output、stream throttle、resize guard、content budget,review 很难回答“到底哪一类问题被解决了” + +因此,从这一版开始,闪屏治理统一遵循两个原则: + +1. **PR 单位是用户可感知的问题类** +2. **每条 PR 都要有固定复现步骤、明确非目标、独立验收信号** + +## 2. 证据边界 + +本文档只使用四类证据: + +1. **当前 qwen-code 源码** +2. **真实用户 issue** +3. **Gemini CLI / Claude Code 的可借鉴实现** +4. **`#3013` 中已被证明有价值的 patch** + +其中第 4 类只做交叉印证,不作为拆 PR 的主轴。 + +## 3. 问题类与 PR 总览 + +| PR | 问题类 | 代表 issue | 主要范围 | `#3013` 可借鉴内容 | 不带什么 | +| --- | --- | --- | --- | --- | --- | +| `PR-Prep` | 观测与回归基线 | 全部 | counters、回归 harness、固定复现场景 | 无需依赖 | 不改用户可见行为 | +| `PR-A1` | 动态流式闪烁 / 滚动条抖动 | `#1184` `#1491` `#3007` `#3144` | content/thought throttle、强制 flush 语义 | pending render throttle | 不带 sync output、不带 tool/detail 高度补丁 | +| `PR-A2` | 终端帧撕裂 / 残余闪烁 | `#2903` `#3144` | synchronized output、frame write 合并、runtime probe | synchronized output 原型 | 不带 `refreshStatic()` 改造、不带 narrow-shell 修复 | +| `PR-B1` | `refreshStatic()` 型整屏闪烁 | `#938` `#1861` `#2924` `#2748` | 语义拆分、resize/view switch/compact merge 触发源收紧 | `AppContainer` 中的 resize 微抖动 guard 仅作思路参考 | 不带 shell serializer、不带 sync output | +| `PR-C1` | 窄屏重复输出 / 无限滚动 | `#2912` `#2972` `#1591` `#1778` | shell serializer、live viewport vs transcript、interactive prompt 回归 | 无直接 patch,可参考 `#1778` 的历史分析但不能照抄结论 | 不带 tool budgeting、不带 detail panel | +| `PR-D1` | 大输出布局抖动 / 工具结果不可读 | `#2748` `#1479` `#2818` | plain text/ANSI pre-render slicing | `SlicingMaxSizedBox` | 不带统一 scheduler budgeting、不带 markdown 降级 | +| `PR-D2` | 通用 tool budgeting 与摘要/详情分离 | `#2818` `#1008` `#355` | scheduler budgeting、summary/detail 语义 | `MAX_TOOL_OUTPUT_LINES` 的“预算”思路 | 不带 synchronized output、不带 narrow-shell 修复 | +| `PR-E1` | 工具 / 子 agent 展开闪烁 | `#1491` `#1861` `#2424` `#2624` `#2924` | stable height、bounded detail panel、展开时钟解耦 | `useStableHeight`、content budget 思路 | 不带全局渲染模式改造、不带 shell serializer | + +## 4. 推荐顺序 + +推荐按下面的顺序推进,而不是按某个大 PR 的 diff 顺序推进: + +1. `PR-Prep` +2. `PR-A1` +3. `PR-B1` +4. `PR-D1` +5. `PR-E1` +6. `PR-C1` +7. `PR-A2` +8. `PR-D2` + +排序理由: + +- `PR-A1` 和 `PR-B1` 先解决主路径上最常见的“普通流式闪烁”和“整屏闪” +- `PR-D1` 先把大输出导致的 layout 风暴压下来,避免后续验证全被大工具结果噪声淹没 +- `PR-E1` 单独收 subagent/tool detail 展开问题,不和 `refreshStatic()` 或大输出混在一起 +- `PR-C1` 最复杂,等主屏普通链路收敛后再单独处理 +- `PR-A2` 放后面,因为终端协议层 rollout 必须建立在应用层已经够稳定的前提上 +- `PR-D2` 最后做,是因为它涉及 UI 语义和模型预算双重约束,产品面更广 + +## 5. `PR-Prep`:观测与回归基线 + +### 5.1 目标 + +建立所有闪屏修复共享的观测口径和复现场景。 + +### 5.2 范围 + +- `terminalRedrawOptimizer.ts` +- `startupProfiler.ts` +- CLI / interactive regression harness + +### 5.3 必备产物 + +- `stdout_write_count` +- `stdout_bytes` +- `clear_terminal_count` +- `erase_lines_optimized_count` +- `bsu_frame_count` +- `esu_frame_count` +- 固定复现场景脚本或手册: + - 长 assistant 回答 + - tool detail expand + - resize / view switch + - 40 列窄终端 + - interactive shell(`git commit`) + +### 5.4 Done 定义 + +- 不改变用户可见行为 +- 所有后续 PR 都能复用同一组统计项 +- 至少每个问题类有一个稳定回归入口 + +## 6. `PR-A1`:动态流式闪烁 / 滚动条抖动 + +### 6.1 关闭的问题类 + +主路径上的普通流式闪烁:回答过程中持续抖动、滚动条轻微抽动、thought/content 高频更新导致的密集重绘。 + +### 6.2 主要范围 + +- `packages/cli/src/ui/hooks/useGeminiStream.ts` +- 如需抽 hook:`useRenderThrottledStateAndRef.ts` + +### 6.3 应包含的改动 + +- content stream buffer + timer flush +- thought stream 共用同一 flush 模型 +- 在以下场景强制 flush: + - stream end + - cancel + - tool call start + - confirm dialog render 前 + +### 6.4 明确不带 + +- synchronized output +- `refreshStatic()` 语义拆分 +- tool/subagent stable height +- tool output pre-slicing + +### 6.5 借鉴来源 + +- `#3013` 中 `pendingHistoryItem` 的 render throttle 思路 +- Gemini CLI 中“缩小动态区、降低更新频率”的中层治理思路 + +### 6.6 固定验证场景 + +1. 长 assistant 回答(至少 500 token) +2. thought + content 混合流 +3. 中途取消 +4. split point 密集命中 + +### 6.7 Done 定义 + +- `stdout.write` 频率显著下降 +- 结束和取消不丢尾部内容 +- 不引入 split/promote 双重渲染 + +## 7. `PR-A2`:终端帧撕裂 / 残余闪烁 + +### 7.1 关闭的问题类 + +在应用层节流之后仍然存在的可见帧撕裂,尤其是 JetBrains 终端、tmux/SSH、长回答滚动阶段的“画面还在抖”问题。 + +### 7.2 主要范围 + +- synchronized output wrapper +- output frame coalescing +- runtime probe / allowlist + +### 7.3 应包含的改动 + +- allowlist + runtime probe +- 必要时对单帧多次 `stdout.write()` 做 frame 内合并 +- `bsu_frame_count === esu_frame_count` 守护 +- screen reader / unknown terminal fallback + +### 7.4 明确不带 + +- `refreshStatic()` 改造 +- narrow-shell 修复 +- tool budgeting + +### 7.5 借鉴来源 + +- `#3013` 的 synchronized output 原型 +- Claude Code 的 runtime gating 经验 + +### 7.6 固定验证场景 + +1. WezTerm +2. kitty +3. JetBrains 终端 +4. tmux / SSH +5. Terminal.app 或未命中 allowlist 的终端 + +### 7.7 Done 定义 + +- 已支持终端的可见帧撕裂下降 +- 未纳入 allowlist 的终端不出现退化 +- Buffer / callback 语义不变 + +## 8. `PR-B1`:`refreshStatic()` 型整屏闪烁 + +### 8.1 关闭的问题类 + +`/settings` 上下切换、compact merge、active view 切换、resize 等场景的一整屏 clear + redraw。 + +### 8.2 主要范围 + +- `AppContainer.tsx` +- `MainContent.tsx` +- `DefaultAppLayout.tsx` +- 相关触发源 + +### 8.3 应包含的改动 + +- `remountStaticHistory()` +- `clearTerminalAndRemount()` +- compact merge / active view / resize 触发源改道 +- `clear_terminal_count` 与 `history_remount_count` 分开统计 + +### 8.4 明确不带 + +- shell serializer 改造 +- synchronized output +- stable height + +### 8.5 借鉴来源 + +- `#3013` 里对 width micro-oscillation 的 guard,只能作为“不要过度清屏”的参考 +- Gemini CLI / Claude Code 都说明“减少全屏 reset 次数”比“试图让所有 reset 不可见”更重要 + +### 8.6 固定验证场景 + +1. `/settings` 上下切换 +2. compact mode merge +3. active view switch +4. 终端宽高 resize +5. `/clear` + +### 8.7 Done 定义 + +- 非致命布局变化不再默认清屏 +- `/clear` 语义保留 +- `clear_terminal_count` 明显下降 + +## 9. `PR-C1`:窄屏重复输出 / 无限滚动 + +### 9.1 关闭的问题类 + +窄终端、多 pane tmux、interactive shell 场景下的重复打印、顶部/底部来回跳、无限滚动。 + +### 9.2 主要范围 + +- `packages/core/src/services/shellExecutionService.ts` +- `packages/core/src/utils/terminalSerializer.ts` +- interactive/integration tests + +### 9.3 应包含的改动 + +- 固定窄屏回归场景 +- 审查 `serializeTerminalToObject(headlessTerminal)` 的触发频率 +- 分离: + - live viewport + - transcript archival +- 收紧 `onScroll()` 与 transcript 更新的耦合 + +### 9.4 明确不带 + +- tool budgeting +- detail panel +- synchronized output + +### 9.5 借鉴来源 + +- `#1778` 的历史分析只能作为排查线索,不能直接照抄结论 +- Gemini CLI / Claude Code 的滚动容器设计说明“实时 viewport”不应直接回灌主 transcript + +### 9.6 固定验证场景 + +1. 40 列以下窄终端 +2. tmux 多 pane +3. 宽度缩小后继续输出 +4. `git commit` +5. interactive shell prompt / pager + +### 9.7 Done 定义 + +- 不再重复刷旧 viewport +- 顶/底往返滚动停止 +- 文档与实现中不再把 `#1778` 的 one-line fix 当作现状根因 + +## 10. `PR-D1`:大输出布局抖动 / 工具结果不可读 + +### 10.1 关闭的问题类 + +长工具输出导致的 Ink 全量 layout、输出一多就闪、工具结果读不全。 + +### 10.2 主要范围 + +- `ToolMessage.tsx` +- `AnsiOutput.tsx` +- shared slicing component + +### 10.3 应包含的改动 + +- plain text pre-render slicing +- ANSI logical line slicing +- `MaxSizedBox` 退回到 safety net 角色 +- 明确 hidden lines 统计规则 + +### 10.4 明确不带 + +- scheduler 层统一 budgeting +- markdown path 粗暴降级 +- synchronized output + +### 10.5 借鉴来源 + +- `#3013` 的 `SlicingMaxSizedBox` +- Gemini CLI 的 plain-text slicing 路线 + +### 10.6 固定验证场景 + +1. `npm install` +2. `git log --oneline` +3. 5000 行纯文本 +4. ANSI 彩色输出 + +### 10.7 Done 定义 + +- 大工具输出不再每次 layout 全量内容 +- 短输出行为不变 +- hidden lines 统计可解释 + +## 11. `PR-D2`:通用 Tool Budgeting 与摘要/详情分离 + +### 11.1 关闭的问题类 + +只有少数工具有 budget,导致模型上下文和 UI 都被大结果拖垮;同时主 transcript 和完整详情没有明确边界。 + +### 11.2 主要范围 + +- `coreToolScheduler.ts` +- truncation / preview utils +- tool summary/detail UI 语义 + +### 11.3 应包含的改动 + +- scheduler 层统一 string budget +- 区分: + - 模型可见预算 + - 用户界面可见预算 +- main transcript 显示 summary +- detail 容器显示 full output 引用或完整结果 + +### 11.4 明确不带 + +- synchronized output +- shell serializer +- 全量虚拟滚动 + +### 11.5 借鉴来源 + +- `#3013` 中 `MAX_TOOL_OUTPUT_LINES` 的预算意识 +- 现有 shell/MCP 截断路径 + +### 11.6 固定验证场景 + +1. `grep` +2. `glob` +3. `read_file` +4. `edit` +5. MCP / declarative tool 大输出 + +### 11.7 Done 定义 + +- 通用工具结果都受统一 budget 保护 +- 主 transcript 更短、更稳定 +- full output 仍可访问 + +## 12. `PR-E1`:工具 / 子 agent 展开闪烁 + +### 12.1 关闭的问题类 + +`ctrl+e` / `ctrl+f` 展开详情时高度暴涨、布局抖动、整块动态区跟着闪。 + +### 12.2 主要范围 + +- `AgentExecutionDisplay.tsx` +- `ToolMessage.tsx` +- `ToolGroupMessage.tsx` +- `useStableHeight.ts` 或等价 hook + +### 12.3 应包含的改动 + +- stable height 吸收层 +- bounded detail panel +- assistant / tool progress / subagent progress 的刷新节奏解耦 +- 保留 force expand、pending confirmation、focus lock + +### 12.4 明确不带 + +- `refreshStatic()` 清屏语义改造 +- shell serializer +- global render mode overhaul + +### 12.5 借鉴来源 + +- `#3013` 的 `useStableHeight` +- Claude Code / Gemini CLI 的滚动详情容器思路 + +### 12.6 固定验证场景 + +1. `ctrl+e` +2. `ctrl+f` +3. subagent 执行中展开 +4. confirmation 出现时展开 / 收起 + +### 12.7 Done 定义 + +- 展开详情不再造成明显闪屏 +- 键盘交互和 focus 语义不退化 +- 不把主流式区域整体撑爆 + +## 13. 统一 PR 模板 + +每条 issue PR 都建议强制使用同一模板: + +1. **Problem Class** + - 本 PR 关闭哪一类用户问题 +2. **Linked Issues** + - 只链接与这一类问题直接相关的 issue +3. **Scope** + - 明确文件和模块 +4. **Non-goals** + - 明确故意不带什么 +5. **Reference** + - 若借鉴 `#3013` / Gemini CLI / Claude Code,写清楚借鉴点 +6. **Validation** + - 固定复现场景 +7. **Rollback** + - 开关或失败信号 + +## 14. 最终建议 + +如果要马上开始实施,我建议第一轮就按下面三条开工: + +1. `PR-Prep` +2. `PR-A1` +3. `PR-B1` + +这三条做完之后,再推进 `PR-D1` 和 `PR-E1`,闪屏问题的主路径就会先被压住;此时再看 `PR-C1` 和 `PR-A2`,验证噪声也会小很多。 diff --git a/docs/design/tui-optimization/10-pr-3013-split-plan.md b/docs/design/tui-optimization/10-pr-3013-split-plan.md deleted file mode 100644 index e8532b32b..000000000 --- a/docs/design/tui-optimization/10-pr-3013-split-plan.md +++ /dev/null @@ -1,266 +0,0 @@ -# PR #3013 拆分计划 - -> 目标:把 [`QwenLM/qwen-code#3013`](https://github.com/QwenLM/qwen-code/pull/3013) 拆成若干更小、更容易验证、边界更清晰的 PR。 -> 校准时间点:2026-04-22 -> 本文档是行动计划,不重复 `09-pr-3013-gap-analysis.md` 的全部论证;若要看“为什么要拆、哪些问题仍未覆盖”,先读 [09-pr-3013-gap-analysis.md](./09-pr-3013-gap-analysis.md)。 - -## 1. 拆分原则 - -拆分时遵守四条硬规则: - -1. **每个 PR 只解决一种主闪屏机制** - - 不把“大工具输出 pre-slicing”和“synchronized output”混进一条 PR -2. **每个 PR 都有单独可复现的验证场景** - - reviewer 不需要同时验证 5 种变化 -3. **每个 PR 都有清晰非目标** - - 明确哪些问题故意留给下一条 PR -4. **禁止带入临时诊断代码** - - `stderr` diagnostics、一次性 debug 输出、实验性钩子不进正式 PR - -## 2. 拆分总览 - -| PR | 类型 | 主要目标 | 关联 issue | 主要文件 | 非目标 | -| --- | --- | --- | --- | --- | --- | -| PR-0 | 前置 | 建立闪屏观测基线 | 全部 | `terminalRedrawOptimizer.ts`, profiler | 不改行为 | -| PR-1 | 从 `#3013` 拆出 | 大 plain-text 工具输出 pre-render slicing | `#2748` `#2818` | `ToolMessage.tsx`, `SlicingMaxSizedBox.tsx` | 不碰 markdown / sync output | -| PR-2 | 从 `#3013` 拆出 | assistant pending render throttle | `#1184` `#1491` `#2748` | `useGeminiStream.ts`, `useRenderThrottledStateAndRef.ts` | 不碰 tool/subagent 高度 | -| PR-3 | 从 `#3013` 拆出 | tool/subagent stable height 与 content budget | `#1491` `#1861` `#2924` | `useStableHeight.ts`, `ToolGroupMessage.tsx`, `AgentExecutionDisplay.tsx`, `AppContainer.tsx` | 不碰 sync output / narrow-shell | -| PR-4 | 从 `#3013` 拆出 | synchronized output 灰度接入 | `#3007` `#3144` `#2903` | `synchronizedOutput.ts`, `gemini.tsx` | 不碰 `refreshStatic()` 语义 | -| PR-5 | `#3013` 外补漏 | `refreshStatic()` 语义拆分 | `#938` `#1861` `#2924` | `AppContainer.tsx`, `MainContent.tsx`, `DefaultAppLayout.tsx` | 不碰 shell serializer | -| PR-6 | `#3013` 外补漏 | 窄屏 / interactive shell 重复输出专项 | `#2912` `#2972` `#1591` | `shellExecutionService.ts`, `terminalSerializer.ts` | 不碰 tool budgeting | - -## 3. 推荐顺序 - -建议顺序不是按代码依赖排,而是按**验证难度和收益比**排: - -1. PR-0 观测基线 -2. PR-1 大 plain-text 工具输出 pre-slicing -3. PR-2 assistant pending render throttle -4. PR-3 tool/subagent stable height 与 content budget -5. PR-5 `refreshStatic()` 语义拆分 -6. PR-4 synchronized output 灰度接入 -7. PR-6 窄屏 / interactive shell 专项 - -这个顺序的好处是: - -- 前 4 条都是主 UI 层 patch,容易快速验证 -- `refreshStatic()` 语义拆分提前,能防止后续 patch 被整屏 clear 抵消 -- synchronized output 放在更后面,避免两层 monkeypatch 还没观测就上线 -- 窄屏 / interactive shell 最后独立推进,不把最复杂的 bug 混进常规 UI PR - -## 4. PR-0:观测基线 - -### 4.1 为什么先做 - -`#3013` 里已经混入了 synchronized output、render throttle、stable height 等高风险逻辑;如果没有统一指标,后面的 review 只能靠“视频更稳了”这种主观判断。 - -### 4.2 范围 - -- `stdout_write_count` -- `stdout_bytes` -- `clear_terminal_count` -- `erase_lines_optimized_count` -- `bsu_frame_count` -- `esu_frame_count` -- `flicker_frame_count` - -### 4.3 验证 - -- 现有行为不变 -- `git diff --check` / 单测通过 -- 指标可在固定场景下重复采样 - -## 5. PR-1:大 plain-text 工具输出 pre-render slicing - -### 5.1 保留内容 - -- 新增 `SlicingMaxSizedBox` -- `ToolMessage.tsx` plain text path 切到 pre-slicing -- 必要的 `ToolMessage` / `SlicingMaxSizedBox` / `MaxSizedBox` 测试 - -### 5.2 明确不带内容 - -- 不带 `useStableHeight` -- 不带 synchronized output -- 不带 `useRenderThrottledStateAndRef` -- 不带 `stderr` diagnostics - -### 5.3 关键 review 点 - -- hidden lines 统计不能双重计算 -- `maxHeight` 与 `maxLines` 的职责要写清楚 -- plain text path 优化后,短输出行为不能变 - -### 5.4 验证场景 - -- `npm install` -- `git log --oneline` -- 5000 行纯文本 -- compact mode - -## 6. PR-2:assistant pending render throttle - -### 6.1 保留内容 - -- `useRenderThrottledStateAndRef.ts` -- `useGeminiStream.ts` 的 pending item throttle / flush - -### 6.2 明确不带内容 - -- 不带 tool output pre-slicing -- 不带 stable height -- 不带 synchronized output - -### 6.3 关键 review 点 - -- split/promote 到 `` 时不能出现临时重复渲染 -- flush 时机要覆盖: - - stream end - - cancel - - tool call start - - confirm dialog 前 - -### 6.4 验证场景 - -- 长 assistant 回答 -- thought + content 混合 -- 中途取消 -- split point 密集命中 - -## 7. PR-3:tool/subagent stable height 与 content budget - -### 7.1 保留内容 - -- `useStableHeight.ts` -- `ToolGroupMessage.tsx` -- `AgentExecutionDisplay.tsx` -- `AppContainer.tsx` 中 raw/stable height 分离 - -### 7.2 明确不带内容 - -- 不带 synchronized output -- 不带 pre-slicing -- 不带 `refreshStatic()` 语义拆分 - -### 7.3 关键 review 点 - -- `useStableHeight` 是战术补丁,不应被包装成全局渲染模式 -- raw height 必须继续传给 shell PTY -- `ctrl+e` / `ctrl+f`、confirmation、focus lock 不能退化 - -### 7.4 验证场景 - -- subagent 展开 / 收起 -- 工具数从 2 -> 3 -> 1 波动 -- 小终端高度 -- shell 正在执行时的 layout 稳定性 - -## 8. PR-4:synchronized output 灰度接入 - -### 8.1 保留内容 - -- `synchronizedOutput.ts` -- `gemini.tsx` 安装逻辑 -- 与 output counters 配套的 feature flag / rollback - -### 8.2 明确不带内容 - -- 不带 pre-slicing -- 不带 stable height -- 不带 `refreshStatic()` 语义改造 - -### 8.3 关键 review 点 - -- 不能和现有 `terminalRedrawOptimizer.ts` 形成不可控的双层 monkeypatch -- 必须有 allowlist / probe / fallback -- screen reader、tmux、SSH、Buffer/callback 语义必须单独验证 - -### 8.4 验证场景 - -- WezTerm / kitty / iTerm2 -- tmux / SSH -- Terminal.app / 未命中的终端 -- screen reader - -## 9. PR-5:`refreshStatic()` 语义拆分 - -### 9.1 这条为什么单独开 - -这不是 `#3013` 现有 patch 的一部分,但它是最关键的补漏项之一。没有它,前面任何 patch 都可能被 `clearTerminal` 一把抹掉。 - -### 9.2 范围 - -- `remountStaticHistory()` -- `clearTerminalAndRemount()` -- compact merge、view switch、resize 的触发源重定向 - -### 9.3 关键 review 点 - -- `/clear` 语义必须保留 -- 高度变化默认不清屏 -- 宽度变化只在必须重排历史时才升级为 clear - -### 9.4 验证场景 - -- compact mode merge -- active view switch -- terminal resize -- `/clear` - -## 10. PR-6:窄屏 / interactive shell 重复输出专项 - -### 10.1 这条为什么最后做 - -它最复杂,而且和 `#3013` 的主收益几乎不是同一类 bug。应该单独推进、单独验证,不要拖慢前面更容易落地的 patch。 - -### 10.2 范围 - -- `shellExecutionService.ts` -- `terminalSerializer.ts` -- live viewport 与 transcript archival 的职责分离 - -### 10.3 关键 review 点 - -- 不要把 `#1778` 的历史 one-line fix 直接当成当前结论 -- 先把窄屏场景做成稳定回归,再改实现 -- 避免“继续往主 transcript 回灌完整 viewport” - -### 10.4 验证场景 - -- 40 列以下窄终端 -- tmux 多 pane -- `git commit` -- interactive shell prompt - -## 11. 每条 PR 的统一模板 - -为防止后续又长成一个“大杂烩 PR”,建议每条 PR 都遵守同一模板: - -1. **Problem** - - 只描述这一类闪屏机制 -2. **Scope** - - 明确涉及文件 -3. **Non-goals** - - 明确不包含哪些 patch -4. **Validation** - - 至少 2-4 个可复现场景 -5. **Rollback** - - 开关、回退策略或失败信号 - -## 12. 最终建议 - -如果现在就要开始真正拆分 `#3013`,我建议从这三条先动: - -1. **PR-1 大 plain-text 工具输出 pre-slicing** - - 收益最直接 - - 用户体感最明显 - - 最容易用视频和基准验证 -2. **PR-2 assistant pending render throttle** - - 影响面小 - - 易于单测 - - 能立刻降低普通流式输出抖动 -3. **PR-5 `refreshStatic()` 语义拆分** - - 不是 `#3013` 现成 patch - - 但它是所有后续闪屏修复的地基 - -如果把这三条先做好,再继续推进 PR-3 / PR-4 / PR-6,整个闪屏治理路线会清晰很多,也更适合 reviewer 验证。