Title
SIGWINCH-based window resize silently breaks when process.stdout is not the user's real terminal (e.g. wrapped by 1Password's op run, screen recorders, or any stdio-proxying wrapper)
Summary
CliRenderer only registers its SIGWINCH listener when _usesProcessStdout is true (renderer.ts:1205). _usesProcessStdout is set to stdout === process.stdout (line 1039), which is identity, but the documented rationale at line 985-986 is "terminal-driven resize only fires for process.stdout."
When the child process is launched by a wrapper that proxies stdio — most commonly 1Password CLI's op run, which always pipes the child's stdout/stderr (even with --no-masking) — the renderer's stdout is no longer attached to the user's terminal. In that case:
process.on("SIGWINCH", …) is gated by _usesProcessStdout. However, the gate is stdout === process.stdout, which is true even when stdout is a pipe. So the listener is registered in the wrapper case.
- On every SIGWINCH, the handler reads
this.stdout.columns / this.stdout.rows (renderer.ts:891-892). On a pipe (non-TTY) those are undefined, so the handler falls back to 80×24 and calls handleResize(80, 24). The renderer resizes to a tiny hardcoded grid on every window resize — which from the user's perspective looks like "the TUI doesn't resize when I resize the window" (layout collapses to ~80×24, sometimes interpreted as "stuck" if the original was wider).
- The initial size at startup is correct because
CliRenderer is constructed with explicit width/height from the caller (line 1046). Only subsequent resizes are wrong.
The net effect: no TUI using CliRenderer correctly resizes when launched through any wrapper that proxies stdio — op run, script -f (script), asciinema, screen recorders, etc.
Reproduction (opencode 1.18.x on macOS, Bun 1.3.14 standalone)
Minimal: launch ~/.opencode/bin/opencode (any modern opencode TUI which uses OpenTUI) via 1Password's op run:
op run --env-file ~/.config/opencode/qwen.env -- ~/.opencode/bin/opencode
Inside the running TUI, resize the terminal window: the layout does not reflow. Manually sending kill -WINCH <opencode-pid> produces the same effect (no real resize).
Confirm the cause:
lsof -a -d 1,2 -p <opencode-pid> # fd 1,2 are PIPE under op run, not the TTY
stty -f /dev/ttysNNN size # PTY size is updated correctly by the host
Other TUI apps launched the same way (htop, vim) resize fine in the same terminal, so the kernel signal path works — the bug is specific to OpenTUI's resolver.
Expected
The TUI should detect window resizes and reflow correctly regardless of whether the parent process proxied stdio, as long as at least one of stdin/stdout is the user's TTY (typically stdin remains a TTY under op run).
Proposed fix
In packages/core/src/renderer.ts:
-
When this.stdout.isTTY is false, fall back to reading the TTY size from process.stdin (which is usually still a TTY under wrappers like op run). process.stdin.rows / columns reflect the user's terminal.
-
Either:
- register
SIGWINCH whenever any of process.stdout.isTTY or process.stdin.isTTY is true, or
- keep the existing
_usesProcessStdout identity gate (for custom Writable cases) but in the handler, fall back to process.stdin.columns/rows when this.stdout columns/rows are undefined.
-
Keep the current guard for non-process custom Writables (tests, embedded use), where SIGWINCH is genuinely meaningless.
A small change, likely under 10 lines, and would unblock every Bun-OpenTUI app run through op run (or any stdio proxy) — an increasingly common deployment shape for credential-bearing dev tools.
Environment
- OpenTUI: latest (
anomalyco/opentui, checked out 2026-08-05)
- opencode: 1.18.13 (Bun 1.3.14 standalone, macOS aarch64, Ghostty/cmux)
- Repro: 1Password CLI
op run (any modern version)
Related
Title
SIGWINCH-based window resize silently breaks when
process.stdoutis not the user's real terminal (e.g. wrapped by 1Password'sop run, screen recorders, or any stdio-proxying wrapper)Summary
CliRendereronly registers itsSIGWINCHlistener when_usesProcessStdoutis true (renderer.ts:1205)._usesProcessStdoutis set tostdout === process.stdout(line 1039), which is identity, but the documented rationale at line 985-986 is "terminal-driven resize only fires forprocess.stdout."When the child process is launched by a wrapper that proxies stdio — most commonly 1Password CLI's
op run, which always pipes the child's stdout/stderr (even with--no-masking) — the renderer's stdout is no longer attached to the user's terminal. In that case:process.on("SIGWINCH", …)is gated by_usesProcessStdout. However, the gate isstdout === process.stdout, which is true even when stdout is a pipe. So the listener is registered in the wrapper case.this.stdout.columns/this.stdout.rows(renderer.ts:891-892). On a pipe (non-TTY) those areundefined, so the handler falls back to80×24and callshandleResize(80, 24). The renderer resizes to a tiny hardcoded grid on every window resize — which from the user's perspective looks like "the TUI doesn't resize when I resize the window" (layout collapses to ~80×24, sometimes interpreted as "stuck" if the original was wider).CliRendereris constructed with explicitwidth/heightfrom the caller (line 1046). Only subsequent resizes are wrong.The net effect: no TUI using
CliRenderercorrectly resizes when launched through any wrapper that proxies stdio —op run,script -f(script),asciinema, screen recorders, etc.Reproduction (opencode 1.18.x on macOS, Bun 1.3.14 standalone)
Minimal: launch
~/.opencode/bin/opencode(any modern opencode TUI which uses OpenTUI) via 1Password'sop run:Inside the running TUI, resize the terminal window: the layout does not reflow. Manually sending
kill -WINCH <opencode-pid>produces the same effect (no real resize).Confirm the cause:
Other TUI apps launched the same way (
htop,vim) resize fine in the same terminal, so the kernel signal path works — the bug is specific to OpenTUI's resolver.Expected
The TUI should detect window resizes and reflow correctly regardless of whether the parent process proxied stdio, as long as at least one of stdin/stdout is the user's TTY (typically stdin remains a TTY under
op run).Proposed fix
In
packages/core/src/renderer.ts:When
this.stdout.isTTYis false, fall back to reading the TTY size fromprocess.stdin(which is usually still a TTY under wrappers likeop run).process.stdin.rows/columnsreflect the user's terminal.Either:
SIGWINCHwhenever any ofprocess.stdout.isTTYorprocess.stdin.isTTYis true, or_usesProcessStdoutidentity gate (for custom Writable cases) but in the handler, fall back toprocess.stdin.columns/rowswhenthis.stdoutcolumns/rows areundefined.Keep the current guard for non-process custom Writables (tests, embedded use), where SIGWINCH is genuinely meaningless.
A small change, likely under 10 lines, and would unblock every Bun-OpenTUI app run through
op run(or any stdio proxy) — an increasingly common deployment shape for credential-bearing dev tools.Environment
anomalyco/opentui, checked out 2026-08-05)op run(any modern version)Related
handleResizeis producing bad sizes.