Skip to content

TUI: terminal resize causes infinite scroll/render loop (screen flicker + status bar duplicates) #19216

@fangegege

Description

@fangegege

Bug Description

After resizing the terminal window (shrink then enlarge, or vice versa), the Hermes TUI enters an infinite render loop:

  • The status bar line duplicates/scrolls repeatedly
  • Screen flickers as fullReset is called every frame
  • The only escape is Ctrl+C to kill the process

Reproduction Steps

  1. Start hermes in macOS Terminal (or iTerm2)
  2. Wait for the welcome banner + prompt to appear
  3. Shrink the terminal window (drag edge to reduce height)
  4. Enlarge it back to original size
  5. Observe: status bar lines start stacking, screen flickers, prompt becomes unusable

Environment

  • Hermes version: v0.12.0 (2026.4.30), upstream 5d3be89
  • OS: macOS 15.x on MacBook Air
  • Terminal: macOS Terminal.app and iTerm2 (both reproduce)
  • Shell: zsh 5.9

Root Cause Analysis

The root cause is in ui-tui/packages/hermes-ink/src/ink/log-update.ts around line 230:

if (prev.screen.height >= prev.viewport.height && prev.screen.height > 0 && cursorAtBottom && !isGrowing) {
    // scrollback-diff check
    if (scrollbackChangeY >= 0) {
        return fullResetSequence_CAUSES_FLICKER(next, "offscreen", stylePool, ...)
    }
}

This block detects changes in scrollback rows (off-screen rows above the viewport) and triggers fullResetSequence_CAUSES_FLICKER. The loop:

  1. Resize triggers fullReset (line ~150, viewport.height < prev.viewport.height) — expected and correct.
  2. fullReset clears the entire terminal and repaints. After repaint, cursor-restore LF may push rows into scrollback differently than before.
  3. Next frame: scrollback-diff block detects that scrollback rows changed (because fullReset repositioned the cursor), triggers another fullReset.
  4. Infinite loop: each fullReset causes scrollback changes that trigger the next fullReset.

The loop is especially triggered when:

  • A spinner/timer/progress element updates in a scrollback row
  • The status bar content changes between frames (e.g. timer display ⏲ Ns)
  • Any dynamic content exists in what the renderer considers scrollback

The scrollback-diff block at line ~230 has NO altScreen guard, so it fires in main-screen mode too, where scrollback rows are truly off-screen and invisible.

Suggested Fix

Option A: Gate the scrollback-diff fullReset behind altScreen mode. In main-screen mode, scrollback row changes are invisible to the user (above the visible viewport), so incremental diff of visible rows is sufficient.

Option B: After a resize-triggered fullReset, add a one-frame cooldown that suppresses the scrollback-diff check, preventing the immediate re-trigger.

Additional Context

This affects daily usability — any terminal resize during a session makes the TUI unusable until restart. Confirmed with debug logging showing repeated fullReset(offscreen) calls triggered by scrollback-diff matching spinner/status-bar row changes.

Debug log evidence:

  • Every frame: [SCROLL-DBG] resize fullReset followed by fullReset(offscreen) on next frame
  • The offscreen trigger shows scrollback rows differing due to previous frame fullReset repositioning content
  • Loop continues until process is killed

Workaround

No known workaround other than avoiding terminal resize while Hermes TUI is running.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium — degraded but workaround existscomp/tuiTerminal UI (ui-tui/ + tui_gateway/)type/bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions