Skip to content

emrg: guard TUI terminal size probe against 0x0 winsize - #1080

Merged
argszero merged 1 commit into
masterfrom
feature/tui-terminal-size-zero-guard
Aug 31, 2026
Merged

emrg: guard TUI terminal size probe against 0x0 winsize#1080
argszero merged 1 commit into
masterfrom
feature/tui-terminal-size-zero-guard

Conversation

@argszero

Copy link
Copy Markdown
Owner

Problem

Host-reported bug (rant 2026-08-31T11:36:22): typing Chinese in the TUI right after opening macOS Terminal still misaligns ("中文错行") even after PR #1071 — and resizing the window once fixes it. That pointed at a terminal-size initialization race, a different root cause than #1071's row-count overflow.

Root cause (evidence chain)

  1. os.get_terminal_size() returns os.terminal_size(columns=0, lines=0) when the PTY winsize has not been reported yet (macOS Terminal at startup) — it does not raise, so the existing except (OSError, ValueError) guard never fires.
  2. _probe_terminal() (emrg/client/python_tui/terminal.py) blindly overwrites the 80×24 default with those 0s → caps.width=0viewport.resizeviewport_width=0RenderContext.width=0.
  3. In InputWidget.render, available = max(1, ctx.width - len(prompt)) = max(1, 0-2) = 1. Every CJK char has cell_len=2 > 1, so the wrap loop breaks immediately and end == pos → end = pos+1each Chinese character is rendered on its own line, blowing up the composer height.
  4. "Resize fixes it" matches exactly: SIGWINCH re-probes, Terminal.app now reports the real winsize, full re-render corrects the layout. handle_resize() had the same unguarded path.

Fix

  • _probe_terminal(): use shutil.get_terminal_size(fallback=(80, 24)) — it rejects 0 values (guard 0 < columns < 255) and falls back to COLUMNS/LINES env then the explicit fallback, never returning 0×0.
  • handle_resize(): same probe but falling back to the last known-good dimensions (self.caps), so a premature SIGWINCH cannot shrink the layout either.
  • +2 tests: 0×0 probe → 80×24 fallback; 0×0 resize probe → last-known-good dims kept.

Verification

  • uv run pytest tests/1179 passed + 1 skipped (1180 total, Agent.md doc-count synced 1178 → 1180)
  • Import + CLI checks green
  • No workflow/CI file changes (actionlint gate unaffected)

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle

Independent review of this cycle:

  • Root-cause chain verified: os.get_terminal_size() returns 0x0 (no exception) when the PTY winsize isn't reported yet → width=0 overwrites the 80x24 default → ctx.width=0 → available=max(1, -2)=1 → every CJK char (cell_len=2) split onto its own line. 'Resize once fixes it' matches the SIGWINCH re-probe path.
  • Fix: shutil.get_terminal_size(fallback=(80,24)) in _probe_terminal (rejects 0 values — 0 < col < 255 guard); handle_resize falls back to last-known-good dims instead of the defaults, so a premature SIGWINCH cannot shrink the layout. Covers the Windows 500ms poll path too (app.py → handle_resize).
  • Tests: both new tests exercise the 0x0 monkeypatch path — probe → 80x24 fallback; resize → keeps 120x40. shutil's internal exception+range guard makes the fallback deterministic.
  • CI green: test + test-windows (run 33355317364), incl. actionlint + doc-count guard; Agent.md 1178→1180 synced. Local: pytest 1179 passed + 1 skipped.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle (2/3)

Independent re-verification this cycle (distinct from the 1/3 cycle):

  • Head byte-identical to the 1/3 review (379096e) — no force-push, no history rewrite.
  • CI still green: test + test-windows pass (run 33355317364), incl. actionlint gate + doc-count guard.
  • Behavior re-confirmed live: with os.get_terminal_size() monkeypatched to 0x0, shutil.get_terminal_size(fallback=(80,24)) returns columns=80 lines=24 — the guard deterministically rejects 0.
  • Branch file check: tests/test_terminal_render.py has the 2 new tests (7 total vs 5 on master), exercising both the probe fallback (80x24) and resize last-known-good (120x40) paths.
  • MERGEABLE / CLEAN. No ❌ in between. 2 consecutive LGTMs from distinct cycles.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle (3/3)

Third consecutive LGTM from a distinct evolution cycle:

  • Head byte-identical 379096e (no force-push / history rewrite since 1/3).
  • Diff re-verified against master: exactly 3 files (Agent.md 1178→1180 doc-count sync, terminal.py shutil guard in _probe_terminal + last-known-good fallback in handle_resize, +2 tests) — 62 insertions / 13 deletions, no scope creep.
  • CI green: test + test-windows (run 33355317364), actionlint + doc-count guard included.
  • MERGEABLE / CLEAN. No ❌ between LGTMs.
    Merge condition met (3 consecutive ✅ from different cycles).

@argszero
argszero merged commit 392a37e into master Aug 31, 2026
2 checks passed
argszero added a commit that referenced this pull request Aug 31, 2026
…d, GUI tool/text order, LLM stream retry, session message_count) (#1085)

Co-authored-by: EMRG Evolution <emrg@argszero.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant