把窗格拖窄一点,给第二个智能体腾地方,再往上翻,想重读第一个智能体写好的计划。结果同一个答案出现了三次,每一份的换行宽度还不一样。
![]()
这个现象被记在 Claude Code 的追踪系统里,编号 #51828,2026 年 4 月 22 日创建,标签是 has repro(可复现)。macOS、Linux 配 tmux、Windows Terminal、tmux 下的 xterm.js,都有人复现出来。
这个 bug 不玄乎。它来自终端改变尺寸时必然跑过的一串小步骤,每一步都有文档可查。看清这条链,就能分清一次尺寸故障该算在智能体头上、终端头上,还是外面那层窗口头上。
一次 ioctl,一个信号,一次重绘
终端模拟器握着伪终端的主端,你的 shell 和从它启动的智能体握着另一端。拖动分隔条时,模拟器算出新的行数和列数,用 TIOCSWINSZ 这个 ioctl 告诉内核。node-pty 的 PtyResize 函数就是这么做的:一次 ioctl(fd, TIOCSWINSZ, &winp;) 调用。
内核把尺寸存进一个 struct winsize,字段是 ws_row 和 ws_col。按 ioctl 手册页的说法,窗口尺寸变化时,SIGWINCH 信号会发给前台进程组。SIGWINCH 的默认动作是忽略,所以从没问过自己尺寸的程序照常跑,毫不知情。
关心尺寸的程序会用 TIOCGWINSZ 把新尺寸读回来,然后重绘。有意思的事全在最后这一步,因为“重绘”在两个不同的屏幕上含义完全不同。
三条命令看清整条链
在 shell 里就能逐环验证。先问内核它认为尺寸是多少,stty size 打印的是“内核记录的行数和列数”。跑一次,拖动窗口,再跑一次,两个数字变了,因为模拟器告诉了内核,而不是 stty 量出了什么。
还能证明内核只是照单全收:stty cols 40 手动把列数设成 40,下一次 stty size 就报 40,哪怕窗口更宽。再拖窗口,模拟器又把它覆盖回去。
接着抓信号。写一个 winch.sh,用 trap 捕获 WINCH,每次尺寸变化打印一行。这些行可能比拖动慢最多一秒,bash 手册页写明了原因:bash 在等一条命令完成时收到已设 trap 的信号,trap 要等命令结束才执行。这里等的命令是 sleep 1。
如果智能体是 Node 程序,第三个版本才是它真正看到的。Node 在 process.stdout 上发 'resize' 事件,条件是 columns 或 rows 变了;进程文档说 SIGWINCH 本身“在控制台被调整尺寸时投递”。写个 winch.mjs 同时监听两者,慢慢拖分隔条,数一数打印了几行。一次拖动产生多少事件取决于你的终端,不取决于 Node,全屏智能体可能每个事件都重绘一次。
Windows 上有个前提:Node 进程文档说 SIGWINCH 只在“向控制台写入且光标移动时,或可读 tty 处于 raw 模式时”投递。
两块屏幕,两种坏法
终端有两个屏幕缓冲区。普通屏保留回滚历史;备用屏用 xterm 私有模式 1049 进入,没有回滚。xterm 控制序列把 CSI ? 1049 h 描述为:保存光标,切换到备用屏并先清空。vim、less、htop 都住在那里。
这个区分决定了尺寸变化时会发生什么。xterm.js 是 VS Code 集成终端背后的库,它的 buffer set 构建备用缓冲区时关掉了回滚(“备用缓冲区永远不该有回滚”),buffer 代码只在有回滚时才启用重排。所以在普通屏上,终端会替你重新折行旧内容;在备用屏上不会,程序必须自己重绘每一个单元格。
普通屏上,硬换行会破坏重排。终端能重新接上自己折的行,接不上程序写换行符断开的行,因为终端只标记自己折的行,程序写的换行看起来和程序本意的换行一模一样。#51828 的评论者描述 Claude Code 的经典渲染器就是这么干的:按当前宽度折行输出,尺寸变化时把光标上移上一帧的高度再重绘。光标上移会停在可见屏顶部。帧比视口高时,已经推进回滚区的行够不着,新帧落在它们下面,一次尺寸变化,尤其是变窄,就可能多留一份副本。这个分析来自报告者,有字节级追踪佐证,不是 Anthropic 的说法,issue 至今开着,没有关联修复。
备用屏上,你失去终端自己的历史。Claude Code 的全屏渲染画在备用屏上,“像 vim 或 htop”,只渲染可见消息。文档把代价说得很清楚:Cmd+f 和 tmux 搜索“看不到对话,因为它住在备用屏缓冲区,不在原生回滚里”,所以应用自带滚动、自带搜索(Ctrl+o 再按 /),还有一个键([)按需把记录写回原生回滚。/tui fullscreen 命令在对话中途切换渲染器,/tui default 切回去。#51828 的讨论串把 "tui": "fullscreen" 列为消除重复的变通办法。
两种选择都不算错。普通屏保住终端的搜索和选择,代价是重排 bug;备用屏重绘干净,代价是在应用内部重建回滚。
第三层:终端外面的窗口
一次尺寸变化还有一方参与,就是你正在拖的那个东西。承载终端的 IDE 在终端回滚之上有自己的视口,尺寸变化会挪动脚下的地面:行数变了,重排可能改写行位置,普通屏上的智能体可能同时在重绘。如果你当时正往上翻着读,宿主得决定你的视图最后停在哪。
Hermes IDE 的 2.3.4 版本在 2026 年 10 月 15 日修的就是这个:“窗口调整尺寸时,回滚查看终端不再被打断。”同一版本还修了“会话窗格在某些窗口宽度下不再停止工作”,并在窄窗格里把会话上下两条栏折进头部,给智能体多留几行。
这个修复不碰智能体的渲染器。普通屏上的智能体如果在尺寸变化时重新吐出记录,重复的行就是回滚区里真实的字节,任何宿主都分不清它们和智能体有意打印的输出。
尺寸出问题时按链排查
尺寸不对或没有:在同一个窗格里跑 stty size。和你看到的不一致,说明模拟器没发 TIOCSWINSZ,或者中间某一环(tmux、SSH 跳转)没传过去。
没有重绘:在那个窗格里跑 winch.mjs。没有 resize 行,说明信号没到前台进程。Windows 上注意上面的前提,脚本可能一声不吭但调整尺寸其实是好的。
尺寸变化后回滚区出现副本:那是普通屏渲染器重新吐了帧。Claude Code 可以试 /tui fullscreen,看是否消失。
残留乱码:全屏模式下,Claude Code 文档说 Ctrl+L 重绘且不丢对话,CLAUDE_CODE_ALT_SCREEN_FULL_REPAINT=1 每帧重绘所有单元格,针对 Windows Terminal 这类处理增量写入有问题的终端。
滚动位置乱跳:那是宿主的视口,不是智能体。先更新宿主,再怪智能体。
把 winch.mjs 留着。下次智能体把输出抹得满回滚区都是,它几秒钟就能告诉你信号到底到没到。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.