Skip to content

fix(terminal): 锁定用户滚动位置防止 AI 输出时内容跳动#418

Merged
J3n5en merged 1 commit into
J3n5en:mainfrom
lwt-sadais:fix/terminal-scroll-position
Apr 11, 2026
Merged

fix(terminal): 锁定用户滚动位置防止 AI 输出时内容跳动#418
J3n5en merged 1 commit into
J3n5en:mainfrom
lwt-sadais:fix/terminal-scroll-position

Conversation

@lwt-sadais

Copy link
Copy Markdown
Contributor

Summary

  • 修复 AI 回答过程中,用户手动向上滚动查看历史内容时出现的文字跳动/重叠问题
  • terminal.write() 前后锁定用户距离底部的滚动偏移量,写入完成后用 scrollLines() 恢复 viewport 位置
  • 解决 TUI 控制序列(\x1b[2J\x1b[3J 等清屏操作)无视 isUserScrolling 状态导致的渲染错乱

Root Cause

Claude Code 等 agent CLI 使用 TUI 框架,会周期性地发送清屏/刷新序列来重绘界面。xterm 的 isUserScrolling 保护仅阻止普通换行时的自动滚动,但 eraseInDisplay(清屏)等控制序列会直接修改 viewport 内容,导致用户正在阅读的内容位置跳变。

Changes

  • src/renderer/hooks/useXterm.ts — 在 PTY 数据写入时检测用户是否已向上滚动,如果是则记录 offsetFromBottom 并在写入后恢复

Test plan

  • TypeScript 编译通过
  • 手动测试:AI 输出过程中向上滚动,验证正在阅读的文字不跳动
  • 手动测试:底部跟随输出,确认行为无变化
  • 手动测试:TUI 清屏场景下滚动位置稳定

当用户向上滚动查看历史内容时,AI 持续输出的 TUI 控制序列
(如清屏 \x1b[2J、清滚动缓冲区 \x1b[3J)会改变 viewport 位置,
导致正在阅读的文字跳动或重叠。

修复方案:在 terminal.write() 前后锁定用户距离底部的滚动偏移量,
写入完成后用 scrollLines() 恢复 viewport 位置。
@github-actions

github-actions Bot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

Claude finished @lwt-sadais's task in 3m 33s —— View job


🔍 代码审查报告

👋 欢迎查看审查结果! 本次审查已完成,以下是详细分析和建议。
💡 使用提示:点击 📁 文件链接可直接跳转到代码位置,建议按优先级处理问题。


📊 审查概览 · 一目了然

📋 审查项目 🔢 发现数量 📈 占比 🎯 处理状态
📁 审查文件 1 100% 已完成
🚨 严重问题 0 0%
⚠️ 中等问题 1 33% 🟡 本版本修复
轻微问题 2 67% 🔵 后续优化

🏆 代码质量评分

📊 综合评分: 78/100  🌟🌟🌟🌟⭐
📋 本次等级: 良好

📋 问题分布

🚨 严重: ░░░░░░░░░░  0%
⚠️ 中等: ███░░░░░░░ 33%
⚡ 轻微: ███████░░░ 67%
总体评分: ⭐⭐⭐⭐☆ (4/5)

🎯 关键发现

🏷️ 类别 🔍 发现数量 🎯 优先级 📈 趋势
🔒 安全性 0 个 ✅ 无风险 ✅ 良好
⚡ 性能 0 个 ✅ 无影响 ✅ 良好
🎨 代码质量 1 个 🟡 中 📊 可改进
🛠️ 健壮性 2 个 🔵 低 📊 可改进

💡 总体评价

本次 PR 精准解决了 TUI 控制序列导致的滚动跳动问题,思路清晰——在 terminal.write() 前后保存/恢复用户的视口偏移量。代码注释规范(英文注释 ✅),变更范围小且集中。主要建议集中在 xterm.js 异步写入时序的健壮性上。


🎯 问题详细分析 · 📊 点击展开查看

📊 **👆 点击此处展开详细统计和图表分析**

📈 问题趋势分析

本次审查: 🟡🔵🔵⚪⚪ (发现 3 个问题)
代码质量: ████████░░ 78%
安全评分: ██████████ 100% ✅
性能评分: ██████████ 100% ✅
🏆 质量指标 📊 当前分数 🎯 目标分数 📈 改进方向
代码质量 78/100 90+ ⬆️ 时序健壮性可改进
安全等级 100/100 100 ✅ 优秀
性能优化 100/100 100 ✅ 无额外开销

🐛 问题清单与修复指南 · 📋 点击展开查看

📋 **👆 点击此处展开详细问题列表 (共 3 个问题) · 一键复制追踪 📌**

🚀 快速开始


⚠️ 中等级别 (Major) 建议修复

🟡 #1 src/renderer/hooks/useXterm.ts:627-636

🏷️ 类型: 时序隐患 | ⏰ 优先级: P1 - 本版本修复 | 🎯 影响范围: 滚动恢复功能

  • 🔍 问题描述terminal.write() 在 xterm.js 中通过内部 WriteBuffer 调度处理,不保证同步执行。当写入缓冲区繁忙时,数据会被排队异步处理,导致 viewport 位置在恢复代码执行时尚未变化,scroll 恢复变成空操作。

    当前代码结构:

    // src/renderer/hooks/useXterm.ts:617-636
    const buffer = terminal.buffer.active;
    const offsetFromBottom = buffer.baseY - buffer.viewportY;
    const shouldLockViewport = offsetFromBottom > 0;
    const savedOffsetFromBottom = shouldLockViewport ? offsetFromBottom : 0;
    
    terminal.write(bufferedData);  // ← 异步写入!
    
    // 这段代码可能在 write 处理完成前执行
    if (shouldLockViewport) {
      const targetViewportY = terminal.buffer.active.baseY - savedOffsetFromBottom;
      const currentViewportY = terminal.buffer.active.viewportY;
      if (targetViewportY !== currentViewportY) {
        terminal.scrollLines(targetViewportY - currentViewportY);
      }
    }

    在实践中,由于本代码已有 30ms 的缓冲合并机制,前一批写入通常已处理完毕,新写入大概率同步执行。但这依赖 xterm.js 内部实现细节,不够稳健。

  • 🛠️ 解决方案:使用 terminal.write(data, callback) 的回调形式,确保在写入处理完毕后再恢复滚动位置:

    // src/renderer/hooks/useXterm.ts — 推荐修改
    terminal.write(bufferedData, () => {
      // Restore viewport if it was moved by the write
      if (shouldLockViewport) {
        const targetViewportY = terminal.buffer.active.baseY - savedOffsetFromBottom;
        const currentViewportY = terminal.buffer.active.viewportY;
        if (targetViewportY !== currentViewportY) {
          terminal.scrollLines(targetViewportY - currentViewportY);
        }
      }
    });

    Fix this →

  • ⚠️ 潜在影响:在 xterm.js 写入缓冲区繁忙时,滚动恢复可能失效,用户仍会看到内容跳动


⚡ 轻微级别 (Minor) 优化建议

🔵 #2 src/renderer/hooks/useXterm.ts:617-620

🏷️ 类型: 边界条件 | ⏰ 优先级: P2 - 后续版本 | 🎯 影响范围: Alternate Buffer 场景

  • 🔍 问题描述:当 TUI 写入触发 alternate buffer 切换时(如 \x1b[?1049h),写入前后 terminal.buffer.active 指向不同的 buffer 对象。写入前在 normal buffer 中记录的 offsetFromBottom,在写入后用 alternate buffer 的 baseY(通常为 0)计算 targetViewportY,会得到负值并触发无意义的 scrollLines 调用。

    // src/renderer/hooks/useXterm.ts:620
    // 写入前:normal buffer, baseY=200, viewportY=180, offset=20
    const offsetFromBottom = buffer.baseY - buffer.viewportY; // 20
    
    terminal.write(bufferedData); // 包含 alternate buffer 切换序列
    
    // 写入后:alternate buffer, baseY=0
    const targetViewportY = terminal.buffer.active.baseY - savedOffsetFromBottom;
    // 0 - 20 = -20 ← 无意义
  • 🛠️ 解决方案:在恢复逻辑中增加 buffer 类型检查,确保前后操作的是同一类型的 buffer:

    if (shouldLockViewport) {
      const activeBuffer = terminal.buffer.active;
      // Only restore if still in the same buffer type (normal vs alternate)
      if (activeBuffer.type === buffer.type) {
        const targetViewportY = activeBuffer.baseY - savedOffsetFromBottom;
        const currentViewportY = activeBuffer.viewportY;
        if (targetViewportY !== currentViewportY) {
          terminal.scrollLines(targetViewportY - currentViewportY);
        }
      }
    }
  • ⚠️ 潜在影响:实际影响较小——alternate buffer 中 baseY 通常为 0,scrollLines 对越界值有钳位保护。但逻辑上不严谨


🔵 #3 src/renderer/hooks/useXterm.ts:627

🏷️ 类型: 视觉闪烁 | ⏰ 优先级: P2 - 后续版本 | 🎯 影响范围: 用户体验

  • 🔍 问题描述:即使 terminal.write() 同步处理成功,在 write 和 scrollLines 之间可能有一帧的渲染间隔,导致用户看到短暂的滚动跳变再恢复。这在大量数据写入时更明显。

  • 🛠️ 解决方案:这个问题会被 #1 的回调方案自然缓解。如果需要进一步优化,可以结合 requestAnimationFrame 在同一帧内完成写入和恢复。当前的 30ms 缓冲机制已经在一定程度上合并了碎片写入,实际闪烁概率较低。

  • ⚠️ 潜在影响:用户在高频输出场景下可能偶尔感知到微小的内容跳动


📊 修复优先级矩阵

🎯 优先级 🚨 严重 ⚠️ 中等 ⚡ 轻微 📈 总计
🔴 P0 0 个 - - 0 个
🟡 P1 - 1 个 - 1 个
🔵 P2 - - 2 个 2 个
📊 合计 0 1 2 3 个

🎯 合并决策建议

📋 合并评估结果

📊 评估维度 ⭐ 得分 🎯 状态 💭 说明
🔐 安全性 10/10 ✅ 通过 无安全风险,仅涉及 viewport 滚动
📊 代码质量 7/10 ⚠️ 一般 异步时序依赖内部实现,建议用回调加固
⚡ 性能影响 10/10 ✅ 无影响 仅增加轻量的偏移量计算,无额外开销
🛠️ 功能完整性 8/10 ✅ 完整 核心场景覆盖良好,边界条件可优化
🧪 测试覆盖 6/10 ⚠️ 基本 TypeScript 编译通过,依赖手动测试验证

📈 综合评分: 41/50 分 · 等级: 良好


🚦 最终建议

⚠️ 条件合并

📝 详细理由

  • 🟢 修复思路正确,精准定位了 TUI 控制序列导致滚动跳变的根因
  • 🟢 变更范围小且集中,风险可控
  • 🟢 代码注释清晰规范
  • 🟡 terminal.write() 的异步特性可能导致恢复逻辑在部分场景下失效
  • 🟡 建议在合并前修复 #1(使用回调形式),确保修复的可靠性

🎯 具体行动建议

  1. 立即可执行的改进

    • 将 scroll 恢复逻辑移入 terminal.write() 的回调函数中(#1
    • 在回调中增加 buffer type 检查(#2
  2. 后续版本改进

    • 观察实际使用中是否存在闪烁,必要时引入 rAF 优化(#3
    • 考虑为 exit handler 中的 terminal.write() 也加上 scroll lock(一致性)

  • 收集上下文信息,分析变更内容
  • 深入代码审查
  • 生成审查报告

@J3n5en J3n5en merged commit ad7f0bd into J3n5en:main Apr 11, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants