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:
-
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).
-
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.
-
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
- Install Forge and set up the ZSH plugin via
forge zsh setup
- Open Ghostty terminal emulator
- Run a prompt using the ZSH plugin:
: explain how async works in Rust
- While the response is streaming (or after it completes), resize the Ghostty window by dragging the edge
- 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
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:
No SIGWINCH handling in the codebase. The
StreamdownRenderercapturesterm_width()once at construction (crates/forge_main/src/stream_renderer.rs,ensure_renderer()) viaterminal_size::terminal_size()and stores it as a fixedwidth: usizein theRendererstruct (crates/forge_markdown_stream/src/renderer.rs). This width is never re-queried. There is no SIGWINCH signal handler, no
crossterm::event::Event::Resizelistener, and no periodic re-check of terminal dimensions anywhere in the codebase (confirmed via GitHub code search for
SIGWINCH,window_change, andEvent::Resize— zero results).Spinner cursor manipulation conflicts with terminal reflow. The
SpinnerManager(crates/forge_spinner/src/lib.rs) usesindicatif0.18.4. TheStreamDirectWriterinstream _renderer.rspauses 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 cursor position assumptions. When
indicatif'sfinish_and_clear()(called inSpinnerManager::stop()) fires, it uses carriage-return and clear-to-end-of-line sequences based on stale positions, erasing visible response content.ZSH plugin launches Forge from a ZLE widget context. The
_forge_exec_interactivefunction (shell-plugin/lib/helpers.zsh) runs"${cmd[@]}" </dev/tty >/dev/ttyfrom within theforge-accept-lineZLE 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 atokio::signal::unix::signal(SignalKind::window_change())handler; expose a mechanism to update the active renderer's width (e.g., viaArc<AtomicUsize>)crates/forge_markdown_stream/src/renderer.rs: Accept dynamic width socurrent_width()reflects the live terminal sizecrates/forge_spinner/src/lib.rs: Pause spinner on resize to prevent stale cursor erasureshell-plugin/lib/helpers.zsh: Investigate whether SIGWINCH forwarding or process group changes are needed in the ZLE widget contextHappy to discuss the approach before starting implementation.
Steps to Reproduce
forge zsh setup: explain how async works in RustAlso reproducible in interactive TUI mode (
forgewith 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