Skip to content

fix: 补全会话建立时的终端尺寸同步,修复内嵌终端画面错位 - #8

Open
LeaFluorine wants to merge 2 commits into
Mai-with-u:mainfrom
LeaFluorine:fix-pty-size-sync
Open

fix: 补全会话建立时的终端尺寸同步,修复内嵌终端画面错位#8
LeaFluorine wants to merge 2 commits into
Mai-with-u:mainfrom
LeaFluorine:fix-pty-size-sync

Conversation

@LeaFluorine

Copy link
Copy Markdown

现象

开启「使用应用内终端」时,终端会出现光标跳到中间某一行、随后把下面的内容覆盖掉的画面错乱,
在服务启动阶段(依赖检查、pip / uv 进度条)尤其明显。

把「使用应用内终端」关掉、让服务跑独立的 cmd 窗口后完全正常 —— 因为外部终端模式走
startExternalTerminal(),用 child_process.spawn 开真正的控制台窗口,完全不经过 node-pty
这个对照把范围锁定在 node-pty + xterm 这条链上。

根因

174a871038 fix: 稳定终端尺寸同步(2026-05-20)给 terminal.onResize 加了缓存比对后提前返回:

const currentSession = sessionsRef.current.get(sessionId);
if (currentSession?.cols === cols && currentSession.rows === rows) {
  return;
}

这个改动本身是对的,但漏掉了一条关键路径:onResize 只在 xterm 自身尺寸发生变化时才触发
而 PTY 是主进程按固定尺寸创建的 —— 创建 PTY 并不会改变 xterm 的尺寸。于是:

  1. 主进程按 SERVICE_TERMINAL_COLS = 260 / SERVICE_TERMINAL_ROWS = 36
    src/main/services/service-manager.ts)创建 PTY;
  2. xterm 此时早已按终端面板尺寸 fit 完毕(常见约 110×30),尺寸不会再变化;
  3. onResize 不触发 → pty.resize 从不发出;
  4. bridge.pty.onSnapshot 的处理函数只记录快照并加载缓冲区,没有任何尺寸回传

结果 PTY 长期停留在 260×36:子进程按 260 列排版,而 xterm 按约 110 列渲染,一条 200 字符的行会被
折成 2 个视觉行。凡是按行数做相对光标移动的输出(tqdm / uv / rich 等的多行进度条,ESC[nA
上移 + \r 重绘)都会算错行 —— 子进程以为上移 3 行,视觉上是 7 行,于是覆盖了中间的内容。

只有用户手动拉窗口 / 改字号(真的让 xterm 尺寸变了)时 onResize 才触发、尺寸才被纠正。
这也解释了为什么这个 bug 表现为「时好时坏、和窗口操作有关」。

修复

1. 会话建立时把渲染层真实尺寸推给 PTY(TerminalPanel.tsx

         sessionsRef.current.set(snapshot.id, snapshot);
         upsertUserTerminal(snapshot);
         void loadSessionBuffer(snapshot.id);
+
+        // PTY 由主进程按固定尺寸创建(service-manager 的 SERVICE_TERMINAL_COLS/ROWS)。
+        // 此时 xterm 通常已按面板尺寸 fit 完毕,尺寸不会再变化,terminal.onResize
+        // 也就不会触发,PTY 会长期停留在主进程的默认尺寸上:子进程按错误宽度排版,
+        // 依赖行数的相对光标移动(tqdm/rich 等进度条重绘)会算错行,画面被覆盖。
+        // 快照是在 spawn 之后、子进程尚未输出时发出的,在这里同步一次真实尺寸最干净。
+        const snapshotInstance = terminalsRef.current.get(snapshot.id);
+        if (snapshotInstance?.opened) {
+          const { cols, rows } = snapshotInstance.terminal;
+          if (cols > 0 && rows > 0 && (snapshot.cols !== cols || snapshot.rows !== rows)) {
+            void bridge.pty.resize({ sessionId: snapshot.id, cols, rows }).catch(() => undefined);
+          }
+        }
+
         notifySessionsChanged();
       }),

快照是在 spawn() 之后、子进程尚未产生任何输出时发出的(pty-session-manager.tsemitSnapshot()),
因此这次 resize 是干净的重排,不会触发 reflow 损坏。主进程 PtySession.resize() 之后会回发快照,
届时尺寸已一致、条件不成立,所以不会形成循环。

附带的收益:SERVICE_TERMINAL_COLS 的取值不再影响正确性,不需要改动它

2. 为 xterm 声明 ConPTY 后端(TerminalPanel.tsx + 导出 platform.tsdetectPlatformSync

主进程是无条件 useConpty: truesrc/main/pty/pty-session-manager.ts),但 xterm 没有被告知这一点,
windowsPty 默认为 {},导致 Buffer.resize 在行数增长时走「消耗 scrollback 上移整屏」分支
而不是 Windows 的「底部追加」,与 ConPTY 的语义不一致:

if (windowsMode || windowsPty.backend !== undefined || windowsPty.buildNumber !== undefined) {
  lines.push(new BufferLine(cols, nullCell));   // Windows:底部追加
} else {
  if (ybase > 0 && lines.length <= ybase + y + o2 + 1) { ybase--; ... }  // 非 Windows:上移整屏
}

已按平台条件化设置,macOS / Linux 行为不变

验证

  • bun run typecheckbun run build 通过
  • 复现与对照步骤:
    1. 启动 MaiBot 服务,观察启动阶段(依赖检查 / 进度条)是否出现光标跳到中间行并覆盖内容
    2. 拉大 / 缩小窗口、改终端字号、切换会话标签页
    3. 关闭「使用应用内终端」,让服务跑外部 cmd 窗口 → 完全正常(对照组)
  • 环境:Windows 11 Pro(build 26200)+ v0.5.0
  • 如果需要,我可以补一段复现录屏

影响面

  • 只涉及渲染层终端面板与一个平台判断工具的导出;不改 IPC 协议
    PtyResizeRequest 已有 sessionId / cols / rows),不新增依赖,不改 bun.lock
  • 不改 SERVICE_TERMINAL_COLS / ROWS
  • macOS / Linux 行为不变(windowsPty 仅在 Windows 设置)

后续(可选,未包含在本 PR)

xterm 的 _isReflowEnabled 只有在同时提供 buildNumber 时才会按 ConPTY 版本决定是否允许自身 reflow
>= 21376 才开启)。若希望 Windows 10(build < 21376,旧 ConPTY 有 rewrap 问题)也走到
「关闭 xterm reflow + 开启 wrapping heuristics」这个组合,需要把系统构建号透出到渲染层
(例如给 DesktopSnapshot 加一个 osRelease)。这超出本 PR 的范围,如需要我可以另开一个。

@LeaFluorine

Copy link
Copy Markdown
Author

本地验证结果(首次向本仓库提交,CI 需要维护者批准后才会执行,先附上本地跑的结果):

$ bun install --frozen-lockfile
424 packages installed [299.64s]

$ bun run typecheck
$ tsc --noEmit -p tsconfig.json
(无输出,退出码 0)

$ bun run build
$ tsc --noEmit -p tsconfig.json && electron-vite build
out/main/index.cjs                  597.16 kB
out/preload/index.cjs                12.78 kB
out/renderer/assets/index-*.js    2,044.96 kB
✓ built in 3.94s(退出码 0)

环境:Windows 11 Pro(build 26200)、bun 1.4.2、基点 main @ 3659093

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.

1 participant