Skip to content

TUI flickers during terminal resize — pi-tui destructive full renders presented mid-drag / 终端 resize 过程中 TUI 持续闪烁 #2324

Description

@winoooops

What version of Kimi Code is running?

0.29.2

Which open platform/subscription were you using?

Kimi Code Allegretto

Which model were you using?

K3 (thinking: high)

What platform is your computer?

Darwin 25.5.0 arm64 arm

What issue are you seeing?

While dragging to resize the terminal (window edge or a pane divider), the whole TUI flickers continuously: visible clear-then-rewrite frames are presented for the duration of the drag. View position and scrollback are preserved in our terminals (this is not #347's jump-to-top — in terminals whose scrollback survives the clear sequences, the same root shows up as pure flicker), but every intermediate frame of the redraw cycle is visible, reading as the transcript collapsing and re-appearing several times per second.

Reproduces identically in two independent hosts, ruling the terminal out:

  1. stock Ghostty.app:

ghostty-app-resize

  1. a libghostty NSView runtime app (winoooops/vimeflow):

libghostty-resize

Root cause upstream (pi-tui), with fix status — kimi-code's TUI is built on pi-tui, whose fullRender path emits a destructive clear whenever any change lands above the viewport (packages/tui/src/tui.ts):

\x1b[2J\x1b[H\x1b[3J   // clear screen + home + clear scrollback

A resize reflows everything, so firstChanged < viewportTop holds on every step of a drag → one destructive full render per step → the flicker. The trail upstream (earendil-works/pi):

What steps can reproduce the bug?

  1. Run kimi in Ghostty.app (any terminal works); generate some transcript — a few tool calls suffice
  2. Continuously drag the window/pane edge to resize width, both directions
  3. Observe sustained flicker for the duration of the drag

What is the expected behavior?

No destructive intermediates presented during a resize burst. For contrast, an alt-screen agent (Claude Code) resized with the same gesture in the same terminals does not flicker, because its repaints replace the frame atomically instead of clearing first. The shape of that approach, for whoever picks this up:

# destructive (pi-tui fullRender today)
write("\x1b[2J\x1b[H\x1b[3J")        # clear + home + clear scrollback
for row in frame: write(row)          # <- every stage of this can be presented

# atomic full-frame replace (what the non-flickering agents do)
frame = render_offscreen(cols, rows)  # build the WHOLE next frame first
out  = "\x1b[?2026h"                  # begin synchronized update (DEC 2026; no-op if unsupported)
out += "\x1b[H"                       # home — never clear
for row in frame:
    out += row + "\x1b[K"             # overwrite in place, erase each line's tail
out += "\x1b[0J"                      # erase anything left below the new frame
out += "\x1b[?2026l"                  # end synchronized update
write(out)                            # ONE buffered write: no half-frame can be presented

Key properties: never 2J/3J; overwrite + per-line EL and a final ED instead of clearing first; a single buffered write; the DEC 2026 brackets make it atomic even mid-drag on terminals that support them (Ghostty does).

Additional information

Happy to test patches, and can verify fixes in both stock Ghostty and the embedded libghostty path quickly.


中文说明

拖动改变终端大小(窗口边缘或分屏分隔条)的过程中,整个 TUI 持续闪烁:整段"清空→重写"的中间帧在拖拽全程被呈现出来。在ghostty终端里视图位置与滚回历史都还在 —— 这不是 #347 的"跳回顶部":在清屏序列不清除 scrollback 的终端里,同一个根因表现为纯闪烁 —— 但重绘循环的每个中间帧都可见,视觉上就是转录内容每秒塌陷又重现好几次。

该现象在两个独立宿主中完全一致地复现(见上方两段录屏:原生 Ghostty.app 与一个基于 libghostty NSView 的运行时应用),因此可以排除终端本身的因素。

根因在上游 pi-tui:它的 fullRender 路径在任何变化落在视口上方时都会发出破坏性清屏(\x1b[2J\x1b[H\x1b[3J,清屏+归位+清滚回)。而 resize 会让全部内容重排,拖拽的每一步都满足 firstChanged < viewportTop,于是每一步都触发一次破坏性整绘 —— 这就是闪烁。上游相关线索:earendil-works/pi#6050(已关闭,但 7 月的调试日志显示 fullRender 仍在例行触发)、#6502(同一发射序列的 ESC[3J 面,被归为 Windows Terminal 问题搁置)、#7194(open 且活跃:工具卡滚出视口时每秒整绘,同一病理的另一个触发器)。下游本仓库的 #2039 / #2205 / #2133 正在落地"视口上方变化跳过破坏性重绘",但都没有覆盖 resize 触发的场景,所以提出这个报告。

期望的行为:resize 期间不呈现破坏性中间帧。对照组是 alt-screen 型 agent(Claude Code):同样的手势、同样的终端,它完全不闪 —— 因为它的重绘是"原子整帧替换"而不是"先清再写"。思路见上方伪代码,关键性质:绝不发 2J/3J;用就地覆写 + 每行 EL + 收尾 ED 取代先清屏;整帧一次性缓冲写出;?2026 同步更新括号在支持的终端(Ghostty 支持)上保证即使拖拽中也是原子呈现。

我在 libghostty NSView 运行时中集成了libghostty, 乐意协助测试补丁, 也可以同时在原生 Ghostty 验证修复效果。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions