Skip to content

fix: give a new PTY the terminal's real size, not the 80x24 spawn default - #552

Merged
Anton-Horn merged 1 commit into
0-AI-UG:mainfrom
superkim0610:agent/fix-pty-initial-size
Jul 27, 2026
Merged

fix: give a new PTY the terminal's real size, not the 80x24 spawn default#552
Anton-Horn merged 1 commit into
0-AI-UG:mainfrom
superkim0610:agent/fix-pty-initial-size

Conversation

@superkim0610

@superkim0610 superkim0610 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Symptom

The on-screen grid fits the panel, but the PTY stays at 80 columns. The shell wraps at 80, and any TUI started in that terminal draws its frame to 80.

Found via a restored Claude Code session rendering its welcome banner into the left ~75% of the panel with dead space beside it.

freshly opened terminal              tput cols → 80
  ↳ nudge the panel border by 1px    tput cols → 89   (fixed permanently)

That "nudge fixes it forever" behaviour is what made it look cosmetic.

Cause

getOrCreate registers the entry before awaiting the spawn, so that concurrent callers share one object:

registry.set(panelId, entry)                        // concurrent callers share this
...
const ptyId = await electronAPI.terminalCreate(...) // ← hundreds of ms
wireTerminalListeners(...)                          // onResize is registered here

During that wait attach() fits the xterm to its real container. The terminal.resize() those fits perform fires onResize before the listener that forwards it to the PTY exists, so the size lands on xterm and is dropped for the PTY.

Nothing corrects it afterwards, because fit() only resizes on a change. The grid already matches the container, so every later fit is a no-op and the PTY is never told. Nudging the panel is the one thing that makes the size change again.

Instrumenting both ends made the ordering unambiguous:

safeFit calling resize {"probeId":"UNWIRED", "80x24" → "96x27"}   ← no listener yet (×6)
wiring listeners       {"...rpty-1-local", cols:107, rows:27}      ← registers already-resized
main resizeTerminal    → 0 calls                                    ← nothing reached the PTY

Three layers swallow the failure silently — runtimeForTerminal(id)?., RemoteRuntime's .catch(() => {}), and the daemon's ptys.get(id)?. — so nothing was logged anywhere. That made it look like a main-process or runtime problem when in fact the renderer never made the IPC call at all.

Fix

Push the terminal's current size once, right after the listeners are wired:

if (terminal.cols !== cols || terminal.rows !== rows) {
  electronAPI.terminalResize(ptyId, terminal.cols, terminal.rows)
}

The guard keeps it silent when the terminal genuinely is still at the spawn size (panel not laid out yet), so no pointless SIGWINCH reaches an already-correct PTY.

Tests

Two regression tests, and I checked they actually fail without the fix:

fix removed  → 1 failed | 39 passed
restored     → 40 passed
  • a terminal fitted while the spawn was in flight gets its real size through once the spawn resolves
  • a terminal still at the spawn size sends nothing

Typecheck passes; 232 terminal/panel tests pass.

Verification

Instrumented a dev build on the session-restore path: main resizeTerminal went from 0 calls to carrying the real sizes (107x27, 89x24) through to the PTY, and tput cols changed from 80 to the panel's actual width.

Other entry paths — new window, detach, dock switch — were not exercised. The fix sits on the common path every fresh spawn takes, so they should be covered by construction, but the empirical check is one path only.

Relationship to #551

Independent, and they do not conflict. Both touch terminalRegistry.test.ts but in different regions; git merge-tree confirms a clean automatic merge. Either can land first.

The causes are unrelated too — #551 is about render scaling under canvas zoom, this one is about PTY size at spawn time.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WsMgXwPNJJ4SgXgRff5wJk

…ault

A terminal the user never happened to resize by hand kept its PTY at 80x24 for
its entire life. The on-screen grid looked right, but the shell wrapped at 80
columns and any TUI started in it drew its frame to 80 — a restored Claude Code
session rendered its welcome banner into the left ~75% of the panel with dead
space beside it. Nudging the panel border by a pixel fixed it permanently, which
is what made the bug look cosmetic.

getOrCreate registers the registry entry BEFORE awaiting the spawn, so that
concurrent callers share one object. That also means attach() can — and in
practice does — fit the xterm to its real container while the spawn is still in
flight. Those fits call terminal.resize(), which fires onResize before
wireTerminalListeners has attached the listener that forwards it to the PTY, so
the size is applied to xterm and dropped for the PTY. Nothing corrects it
afterwards, because fit() only resizes when the size CHANGES: the grid already
matches the container, so every later fit is a no-op and the PTY never hears
about it.

Instrumenting both ends made the ordering unambiguous — six resizes logged
against an unwired terminal, then the listeners attaching to a terminal already
at 107x27, and zero resize IPCs for the whole restore.

Push the terminal's current size once, right after the listeners are wired. The
guard keeps it silent when the terminal really is still at the spawn size, so no
pointless SIGWINCH reaches an already-correct PTY.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WsMgXwPNJJ4SgXgRff5wJk
@Anton-Horn

Copy link
Copy Markdown
Contributor

Looks good. The resize is sent after the listener is wired, and the guard avoids an unnecessary SIGWINCH when 80x24 is still correct. The regression coverage is solid.

@Anton-Horn
Anton-Horn added this pull request to the merge queue Jul 27, 2026
Merged via the queue into 0-AI-UG:main with commit f1a63d3 Jul 27, 2026
3 checks passed
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.

2 participants