Skip to content

fix(tui): re-layout on every terminal resize, not just clamped ones - #1374

Open
addyCooks wants to merge 3 commits into
Nano-Collective:mainfrom
addyCooks:fix/terminal-resize-reactive-1328
Open

addyCooks wants to merge 3 commits into
Nano-Collective:mainfrom
addyCooks:fix/terminal-resize-reactive-1328

Conversation

@addyCooks

@addyCooks addyCooks commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Closes #1328

Description

useResponsiveTerminal returned actualWidth as a raw render-time read of process.stdout.columns, with no subscription of its own. The only reactive state was the clamped boxWidth (max(min(columns - 4, 200), 40)), so a resize that left boxWidth unchanged produced no re-render at all:

  • below 44 columns, boxWidth pins to 40
  • above 204 columns, it pins to 200

Resize anywhere inside those bands and every consumer welcome screen, input box, status bar, session selector kept rendering for the old terminal, so lines got cut off mid-word and boxes sat misaligned.

Fix: the raw column count becomes the reactive state, and boxWidth derives from it. useTerminalWidth and useResponsiveTerminal now both read the same subscribed value, and nothing reads process.stdout.columns during render. The single shared resize listener is unchanged, so there is still exactly one stdout listener regardless of how many consumers mount. boxWidth had been computed in two places; it is now computed once.

Recording

B07-FIXED-live-resize

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Changeset

  • Added a changeset (pnpm changeset) describing this change for the changelog

Testing

Automated Tests

  • New features include passing tests in .spec.ts/tsx files
  • All existing tests pass (pnpm test:all completes successfully)
  • Tests cover both success and error scenarios

Manual Testing

  • Tested with Ollama
  • Tested with OpenRouter
  • Tested with OpenAI-compatible API
  • Tested MCP integration (if applicable)

Checklist

  • If this was for an open issue, I was assigned to it
  • Code follows project style guidelines
  • Self-review completed
  • Documentation updated (if needed)
  • No breaking changes (or clearly documented)
  • Appropriate logging added using structured logging

Layout reactivity hung off the clamped box width, while actualWidth was a raw render-time read of process.stdout.columns with no subscription of its own. A resize that left boxWidth unchanged - anything below 44 or above 204 columns - produced no re-render, so the welcome screen, input box, status bar and session selector kept rendering for a terminal size that no longer existed. The raw column count is now the reactive state and boxWidth derives from it, so every resize reaches every consumer through the same single shared listener. Closes Nano-Collective#1328.
@github-actions

Copy link
Copy Markdown
Contributor

nc-review: comments — 3 nits

@addyCooks — a few things worth a look, none blocking.

The PR correctly resolves #1328 by making the raw column count the reactive state and deriving boxWidth from it, so any resize — even one that lands inside the 40/200 clamp — re-renders all consumers. The diff is minimal, the changeset names the workspace package correctly, and useResponsiveTerminal no longer reads process.stdout.columns during render. Two nits on the tests: one is named like a clamp-inside regression test but uses 100→140 (neither value hits the clamp), and both new tests use a 20ms setTimeout to wait for the re-render, which is timing-based and can flake.

⚪ nit · tests · source/hooks/useTerminalWidth.spec.tsx

The new useTerminalWidth tracks a resize inside the clamp test does not actually exercise a clamp-inside scenario: it resizes from 100 to 140 columns, neither of which clamps (boxWidth would change 96 → 136, so the pre-fix code would have re-rendered correctly). To actually cover the regression this PR fixes for direct useTerminalWidth callers, pick values that hit the clamp on both ends — e.g. 30 → 50 (both clamp to 40) or 250 → 350 (both clamp to 200) — and assert the post-resize boxWidth is unchanged but the render fired anyway (e.g. by counting widths.length ≥ 2).

⚪ nit · tests · source/hooks/useTerminalWidth.spec.tsx

Both new tests wait for the post-resize re-render with await new Promise(resolve => setTimeout(resolve, 20)). This is timing-based and can flake on a slow CI runner (or pass despite the listener not having fired if React hasn't scheduled yet). Prefer an ink-testing-library waitFor/rerender helper, or assert on the captured widths sequence length once the listener has clearly run — for example by emitting the resize and then calling the listener directly rather than going through process.stdout.emit.

⚪ nit · completeness · source/hooks/useTerminalWidth.tsx

useResponsiveTerminal now derives boxWidth from actualWidth locally (const boxWidth = calculateBoxWidth(actualWidth)) instead of taking it from useTerminalWidth(). The two are mathematically identical, but boxWidth for the same consumer is now computed in two places (here and inside useTerminalWidth). Not a bug; just noting that any future change to the clamp formula has to keep both call sites in sync, or calculateBoxWidth itself has to stay the single source of truth.


🔴 blocking · 🟠 a reviewer would ask for a change · ⚪ optional

Automated code review — correctness, security, design, tests, plus duplicates and scope. A human still decides; this is not a substitute for review and is not exhaustive. The required status checks separately cover lint, formatting, types, unused dependencies, the test suite and the build. This bot never merges. Maintainers can rerun with /re-review.

@github-actions github-actions Bot added the agent:comments nc-review left non-blocking findings label Sep 17, 2026
The second resize test resized 100 to 140 columns, which neither clamps, so it never covered the regression for direct useTerminalWidth callers. It now resizes 250 to 350 - both clamp to 200 - and counts renders rather than distinct values, since an effect keyed on an unchanged width would never fire. Both tests now poll for the resize to propagate instead of sleeping a fixed 20ms. Verified both fail against the pre-fix hook. Also records why useResponsiveTerminal derives boxWidth from its own subscription rather than calling useTerminalWidth.
The unit-test job failed in source/commands/stats.spec.tsx, a pre-existing
stale-closure bug in StatsDisplay arrow-key handling that this branch does
not touch. Empty commit to get a clean run.
@addyCooks

Copy link
Copy Markdown
Contributor Author

The failing Unit Tests & Coverage Analysis check here isn't from this PR.

StatsDisplay changes range with arrow keys and closes on Escape (stats.spec.tsx:121) wedges on a pre-existing stale-closure bug in StatsDisplay:
the arrow handler steps the range from its captured closure value,
but Ink re-registers useInput handlers in a passive effect that runs after the frame is painted.

The test polls the frame as its readiness signal, so its second lands in that window, recomputes the tab it's already on,
and React bails out.

Nothing on this branch touches /stats or input handling,
it reproduces on main too, so it can block any PR.

Fix is up separately in #1379;
once that merges I'll bring main into this branch for a green run here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent:comments nc-review left non-blocking findings area:tui Terminal UI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Resizing the terminal sometimes doesn't re-layout the UI at all

1 participant