Skip to content

Commit dd3d5d4

Browse files
claude-code-bestdeepseek-v4-pro
andcommitted
fix(ink): 主屏幕模式周期性终端重绘, 防止长时间运行 TUI 显示腐蚀
- 添加 lastMainScreenHealTime 字段, 每 5 秒触发一次全量终端重绘 - 使用 wall-clock 时间替代帧计数, 避免 drain frames (250fps) 加速周期 - 添加 isTTY 守卫, 防止非 TTY 环境泄漏 ANSI 转义序列 - 扩展 needsEraseBeforePaint 到主屏幕模式, BSU/ESU 确保原子性无闪烁 - 修复 log-update cursor 漂移和 blit ghosting 导致的文字重叠/残留 Co-Authored-By: deepseek-v4-pro <deepseek-ai@claude-code-best.win>
1 parent bca2758 commit dd3d5d4

1 file changed

Lines changed: 35 additions & 0 deletions

File tree

packages/@ant/ink/src/core/ink.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,12 @@ export default class Ink {
165165
private frontFrame: Frame;
166166
private backFrame: Frame;
167167
private lastPoolResetTime = performance.now();
168+
/** Timestamp of last periodic full-redraw in main screen mode. Used to
169+
* recover from accumulated cursor drift / blit ghosting. Wall-clock
170+
* based (not frame-count) so drain scroll frames (250fps) don't
171+
* accelerate the cycle. Alt-screen doesn't need this — CSI H resets
172+
* cursor every frame. */
173+
private lastMainScreenHealTime = performance.now();
168174
private drainTimer: ReturnType<typeof setTimeout> | null = null;
169175
private lastYogaCounters: {
170176
ms: number;
@@ -521,7 +527,25 @@ export default class Ink {
521527
// an extra React re-render cycle.
522528
this.options.onBeforeRender?.();
523529

530+
// Periodic self-healing: every ~5s in main screen mode, force a full
531+
// terminal redraw to recover from accumulated cursor drift / blit
532+
// ghosting. Alt-screen doesn't need this — CSI H resets cursor to
533+
// (0,0) every frame. Wall-clock based so drain scroll frames (250fps)
534+
// don't accelerate the cycle. Guarded by isTTY so ANSI escape
535+
// sequences are not leaked into pipes / redirected output.
524536
const renderStart = performance.now();
537+
if (
538+
!this.altScreenActive &&
539+
!this.isPaused &&
540+
this.options.stdout.isTTY &&
541+
renderStart - this.lastMainScreenHealTime > 5000
542+
) {
543+
this.lastMainScreenHealTime = renderStart;
544+
this.repaint();
545+
this.prevFrameContaminated = true;
546+
this.needsEraseBeforePaint = true;
547+
}
548+
525549
const terminalWidth = this.options.stdout.columns || 80;
526550
const terminalRows = this.options.stdout.rows || 24;
527551

@@ -725,6 +749,10 @@ export default class Ink {
725749
const optimized = optimize(diff);
726750
const optimizeMs = performance.now() - tOptimize;
727751
const hasDiff = optimized.length > 0;
752+
// Periodic self-healing: for main-screen mode, emit ERASE_SCREEN + HOME
753+
// to clear the terminal before the diff. Alt-screen has its own CSI H
754+
// anchor + cursor park below. BSU/ESU wraps erase+paint atomically on
755+
// supported terminals (main-screen always uses sync markers).
728756
if (this.altScreenActive && hasDiff) {
729757
// Prepend CSI H to anchor the physical cursor to (0,0) so
730758
// log-update's relative moves compute from a known spot (self-healing
@@ -752,6 +780,13 @@ export default class Ink {
752780
optimized.unshift(CURSOR_HOME_PATCH);
753781
}
754782
optimized.push(this.altScreenParkPatch);
783+
} else if (this.needsEraseBeforePaint && hasDiff) {
784+
// Main-screen periodic self-healing: clear visible terminal before
785+
// painting the diff. Without this, rows past the new frame's height
786+
// would retain stale content from the previous frame. BSU/ESU keeps
787+
// old content visible until the full erase+paint is flushed atomically.
788+
this.needsEraseBeforePaint = false;
789+
optimized.unshift(ERASE_THEN_HOME_PATCH);
755790
}
756791

757792
// Native cursor positioning: park the terminal cursor at the declared

0 commit comments

Comments
 (0)