Skip to content

[Bug]: Terminal output disappears on window resize in Ghostty (ZSH plugin and interactive mode) #2893

@hessnd

Description

@hessnd

Bug Description

When using Forge in Ghostty terminal emulator, resizing the window causes streamed output to disappear. This is most noticeable via the ZSH plugin (: prompt) but also affects interactive TUI mode during streaming. Content does not reflow or adapt to the new size — it simply vanishes.

Root cause analysis from source inspection:

  1. No SIGWINCH handling in the codebase. The StreamdownRenderer captures term_width() once at construction (crates/forge_main/src/stream_renderer.rs, ensure_renderer()) via
    terminal_size::terminal_size() and stores it as a fixed width: usize in the Renderer struct (crates/forge_markdown_stream/src/renderer.rs). This width is never re-queried. The
    re is no SIGWINCH signal handler, no crossterm::event::Event::Resize listener, and no periodic re-check of terminal dimensions anywhere in the codebase (confirmed via GitHub code sea
    rch for SIGWINCH, window_change, and Event::Resize — zero results).

  2. Spinner cursor manipulation conflicts with terminal reflow. The SpinnerManager (crates/forge_spinner/src/lib.rs) uses indicatif 0.18.4. The StreamDirectWriter in stream _renderer.rs pauses the spinner before writing content and resumes it after writes ending with \n. On resize, Ghostty reflows content in its buffer, invalidating the spinner's curso
    r position assumptions. When indicatif's finish_and_clear() (called in SpinnerManager::stop()) fires, it uses carriage-return and clear-to-end-of-line sequences based on stale positions, erasing visible response content.

  3. ZSH plugin launches Forge from a ZLE widget context. The _forge_exec_interactive function (shell-plugin/lib/helpers.zsh) runs "${cmd[@]}" </dev/tty >/dev/tty from within the forge-accept-line ZLE widget. The plugin does not trap or forward SIGWINCH. Whether forge actually receives SIGWINCH in this context may depend on terminal/OS-specific process group behavior and needs further investigation — but the primary issue is that even if the signal is delivered, there is no handler to act on it.

Related: PR #2827 ("fix: prevent short responses from being erased by spinner in Ghostty") addresses a narrower symptom where short responses without trailing newlines are erased by the spinner's finish_and_clear(). The resize issue described here is a broader problem.


I'm willing to submit a PR for this fix. The changes would span multiple crates:

  • crates/forge_main/src/stream_renderer.rs: Register a tokio::signal::unix::signal(SignalKind::window_change()) handler; expose a mechanism to update the active renderer's width (e.g., via Arc<AtomicUsize>)
  • crates/forge_markdown_stream/src/renderer.rs: Accept dynamic width so current_width() reflects the live terminal size
  • crates/forge_spinner/src/lib.rs: Pause spinner on resize to prevent stale cursor erasure
  • shell-plugin/lib/helpers.zsh: Investigate whether SIGWINCH forwarding or process group changes are needed in the ZLE widget context

Happy to discuss the approach before starting implementation.

Steps to Reproduce

  1. Install Forge and set up the ZSH plugin via forge zsh setup
  2. Open Ghostty terminal emulator
  3. Run a prompt using the ZSH plugin: : explain how async works in Rust
  4. While the response is streaming (or after it completes), resize the Ghostty window by dragging the edge
  5. Observe that the rendered output disappears

Also reproducible in interactive TUI mode (forge with no args) — resize during streaming output.

Expected Behavior

Streamed output should remain visible after a terminal resize. New content after resize should ideally render at the updated terminal width. Already-rendered content should at minimum remain visible, even if not perfectly reflowed.

Actual Behavior

Previously rendered Forge output disappears when the Ghostty window is resized. The spinner's finish_and_clear() erases content due to stale cursor position assumptions after Ghostty reflows terminal content. After forge exits, the shell prompt returns normally but the response content is gone from the scroll buffer.

Forge Version

2.8.0

Operating System & Version

macOS 15, Ghostty terminal emulator

AI Provider

Anthropic

Model

Any (display-layer bug, not model-specific)

Installation Method

Other

Configuration

Installed via curl -fsSL https://forgecode.dev/cli | sh

Metadata

Metadata

Assignees

No one assigned

    Labels

    type: bugSomething isn't working.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions