Skip to content

fix(terminal): stop stale PTY resize after worktree reveal#8502

Merged
brennanb2025 merged 3 commits into
mainfrom
brennanb2025/fix-tui-resize-glitch
Jul 13, 2026
Merged

fix(terminal): stop stale PTY resize after worktree reveal#8502
brennanb2025 merged 3 commits into
mainfrom
brennanb2025/fix-tui-resize-glitch

Conversation

@brennanb2025

Copy link
Copy Markdown
Contributor

Summary

Fixes the "bottom of the Claude Code TUI garbles after returning to a worktree; resizing repairs it" regression that became frequent in v1.4.136 (refs #7951, #7240).

Mechanism. On reveal, noteVisibilityResume() requests a PTY size readback that captures xterm's pre-reveal grid as its resize target (pty-size-reassertion.ts). The applied-size read is an async daemon round-trip; the reveal fit refits xterm and resizes the PTY while it is in flight, with no follow-up request queued. Instrumented traces show this stale-capture interleaving on every worktree reveal — correctness relied entirely on the read being processed before the fit's resize (local FIFO luck). When the read loses that ordering — daemon busy serializing reveal snapshots (common since the #7214 hidden-delivery pipeline), SSH/relay round-trips — the resolved callback sees applied ≠ captured target and "repairs" the PTY back to the pre-reveal grid. An idle TUI then redraws for the wrong grid, and nothing heals it: no output means no grid-drift check, and no later layout change means no ResizeObserver request. Reproduced live on an unpatched build (e2e read-delay seam sweeping 15–60ms): FORWARD {123x46} issued while xterm and the PTY were correctly at 81x36.

Fix. In forwardIfDrifted, re-measure xterm at resolve time; if the grid moved while the read was in flight, suppress the forward and re-run against the fresh grid through the existing pending coalescing. Under the same adversarial sweep the fixed build logs the guard engaging repeatedly with zero stale forwards.

Why it was hard to reproduce locally (also why #7961 couldn't confirm): on a fast idle machine the daemon read wins the FIFO race, and even when the stale forward fires, a ResizeObserver-driven follow-up reassertion heals it within ~30ms. The field lacks both escapes.

Related: #7961 (community PR by @gatsby74) independently proposed the same-shaped guard against #7951 — this PR adds the live reproduction, the read-delay seam, and regression coverage; credit to them for the early diagnosis.

Screenshots

No visual change. (The fix prevents the garbled-bottom-TUI frames; there is no new UI.)

Testing

  • pnpm lint
  • pnpm typecheck
  • pnpm test — 28 failures, all pre-existing on base 550c41a35 (A/B-verified via stash: identical 28 failed / 3 passed without this diff; PR-comments + Linear-skill-prompt areas, disjoint from this change). One additional pty-connection.test.ts failure under full-suite load is a contention flake: passes 3/3 in isolation. Zero new failures from this PR; scoped terminal-pane + pane-manager suites: 2458 passed.
  • pnpm buildbuild:desktop passes; build:native fails at codesign (errSecInternalComponent) identically on base — local signing environment, unrelated to this renderer-only diff.
  • Added or updated high-quality tests that would catch regressions, or explained why tests were not needed

Targeted validation:

  • New unit tests in pty-size-reassertion.test.ts reproduce the race and fail on unpatched main (stale forwardResize(80, 24) fires); pass with the fix. This is the deterministic regression guard.
  • New e2e spec tests/e2e/terminal-reveal-stale-pty-resize-repro.spec.ts: hidden-resize/reveal cycles with an idle WINCH-reactive bottom-bar TUI, natural ordering + slow-read (seam) variants; asserts main's applied PTY size converges to xterm's grid every cycle.
  • A/B live proof on this machine: unpatched build fired the stale FORWARD (PTY yanked to the pre-reveal grid) under the seam sweep; fixed build logged 5× guard suppressions, zero stale forwards, all cycles converged.
  • Full terminal-pane + pane-manager unit suites: 2458 passed.
  • Perf lane pnpm run test:e2e:terminal-perf: 20 passed, 0 failed (1 skip: missing local OpenCode capture fixture, environment-gated). Typing latency, redraw-freeze, output-scheduler, and worktree-revisit backpressure budgets all green.
  • terminal-hidden-tui-visual-restore.spec.ts: 4/4 passed.
  • Pre-existing main breakage (not from this PR, A/B-verified via stash at base 550c41a35): terminal-column-desync-repro.spec.ts "vertical split reparent" fails 3/3 on base (2/3 with this PR — noise); terminal-hidden-view-parking.spec.ts fails 4/5 on base.

AI Review Report

Reviewed the reassertion flow end to end: target capture, in-flight coalescing, the new resolve-time re-measure, rejection-path fallback, disposal, and PTY-rebinding guards. Key risks checked: (1) unbounded re-run loops — a re-run only occurs when the grid moved during that read's flight and converges once layout is stable for one flight; no timers or retained state. (2) Behavior on the common path — when the grid is unchanged mid-flight the guard is two property reads and behavior is byte-identical. (3) Readback-failure fallback — still performs the one guarded resize, now at the fresh grid instead of a stale one. (4) Fit semantics — the guard re-queues with pending only, so no unrequested fits occur (visibility-resume's fit-avoidance against the transient hidden DOM fallback is preserved).

Cross-platform: renderer-only logic, identical on macOS, Linux, and Windows; no shortcuts, labels, paths, shell behavior, or Electron platform APIs. Provider coverage: local/daemon and app-SSH PTYs flow through the same guarded path; remote: runtime PTYs remain excluded by the existing isRemotePtyId gate; mobile presence-lock and fit-override suppression (shouldSuppressDesktopResize) are checked before any forward, unchanged.

Performance: the reassertion path runs only on visibility resume, settled layout changes, and drift checks — never on typing/output hot paths. Per resolve the guard adds two synchronous property reads; the extra applied-size IPC happens only when the grid moved mid-flight, and each prevented stale forward saves a resize IPC + SIGWINCH + full TUI redraw. The e2e read-delay seam is gated on the e2e build config (exposeStore) and compiles to a 0-delay early return in packaged builds (one extra microtask hop on an IPC-bound path).

Security Audit

No new input handling, command execution, filesystem/path access, auth, secrets, dependencies, or IPC channels. The e2e seam reads a window global only when the e2e build flag is set and can only add latency to an existing read in test builds; it cannot redirect or amplify any privileged operation. The guard strictly narrows which resizes are forwarded to the PTY.

Notes

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5bd11864-91e2-470c-a4b9-a58c57bb325a

📥 Commits

Reviewing files that changed from the base of the PR and between 4e66514 and 53bd6af.

📒 Files selected for processing (1)
  • tests/e2e/terminal-reveal-stale-pty-resize-repro.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/e2e/terminal-reveal-stale-pty-resize-repro.spec.ts

📝 Walkthrough

Walkthrough

The change prevents PTY resize reassertion from forwarding dimensions captured before a terminal grid change. It adds unit regressions for mid-flight refits and readback failures, plus an E2E-only delay seam for applied-size reads. A new E2E specification cycles worktree visibility, viewport sizes, and terminal state while checking immediate desynchronization and eventual convergence under natural and delayed read ordering.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: preventing stale PTY resizes after worktree reveal.
Description check ✅ Passed The description matches the required template and includes summary, screenshots, testing, AI review, security audit, and notes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The visibility-resume size readback captures xterm's pre-reveal grid as
its resize target while the applied-size read is in flight. A reveal fit
or snapshot-restore resize can change the grid mid-flight without
queuing a newer request, so the resolved callback "repaired" the PTY
back to the pre-reveal grid — an idle TUI (Claude Code) then redraws
for the wrong grid until a manual resize (refs #7951, #7240).
Instrumented traces show the stale capture on every reveal; correctness
relied on the read winning FIFO against the fit's resize, which busy
daemons and SSH/relay round-trips lose.

Re-measure xterm at resolve time and re-run against the fresh grid
instead of forwarding a stale target. Adds an e2e seam that delays the
readback dispatch to reproduce the losing ordering, unit repros that
fail without the guard, and a hidden-resize/reveal-cycle e2e spec.
@brennanb2025
brennanb2025 force-pushed the brennanb2025/fix-tui-resize-glitch branch from 20e0d3b to ce54ff3 Compare July 13, 2026 07:02
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Live SSH verification (brennan-test-ssh pass): land.

  • Target: throwaway Ubuntu 24.04 Docker container (orca-ssh-test-1783927044, sshd on :42250, temp ed25519 key), not ssh localhost. demo-project cloned inside via git-bundle fallback (container had no GitHub auth).
  • App: this exact PR worktree (manatee @ brennanb2025/fix-tui-resize-glitch, identity verified over CDP), isolated ORCA_DEV_USER_DATA_PATH, relay built via pnpm build:relay and deployed for linux on connect.
  • Remote repo action: registered /work/demo-project in Orca over SSH and created + later removed a real remote worktree/branch (orca-ssh-test-8502-flip), verified via git worktree list --porcelain / branch --list in the container.
  • Changed behavior exercised: idle WINCH-only bottom-bar TUI in an app-SSH PTY (ssh:<target>@@pty-2); 10 hide → layout-change (sidebar width/right-panel, cols 135↔140↔160) → reveal cycles: 4 natural ordering, 4 with the applied-size read delayed 40ms, 2 at 120ms (the e2e seam, arming the exact losing ordering the fix guards).
  • Result: 10/10 cycles converged; zero desync samples at 100ms sampling; container-side ground truth stty -F /dev/pts/1 size = 58×140 matching xterm; TUI bottom bar redrawn on the true last row at the real grid (rows=58 cols=140), no garble. pty:getSize works for app-SSH PTYs, so the guard's re-measure path (not the readback-failure fallback) is the active one over SSH.
  • Cleanup: remote worktree/branch/script/bundle removed and verified absent, SSH target disconnected/removed from Orca, dev session stopped, container and temp keys deleted.

Remaining gap (pre-existing, accepted): remote: runtime (relay viewer) PTYs are excluded from reassertion by the existing gate — untouched by this PR either way.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Windows verification (agent on Windows 11 Pro 25H2, PR head 4e6651458): the PR's behavior passed everywhere it could run.

  • Unit: 435/435 pass (both reassertion + pty-connection suites).
  • E2E: all 6 target specs initially failed in the shared fixture before any terminal assertion — a pre-existing Windows harness bug (store returns C:/..., fixture strict-compares C:\...). Fix: fix(e2e): compare store repo paths with normalized separators on Windows #8520. With an equivalent normalization diagnostic in place, both PR-specific stale-resize tests passed (8 natural-ordering + 12 delayed-read cycles); 5/6 overall. The sixth failure is an unrelated PowerShell OSC-title quoting issue in the hidden-TUI helper: [e2e] Hidden-TUI OSC title assertions fail on Windows: side-effect burst quoting breaks under PowerShell #8521.
  • Supplemental automated hide/resize/reveal cycles on Windows found no PTY-size divergence and no garbled frames. Interactive 10-cycle human QA (incl. confirmed DOM-renderer mode) was not possible in that agent session and remains the one open Windows item.

Combined with the live SSH pass above and the macOS evidence, remaining risk is confined to: (1) eyeballs-on-Windows DOM-renderer QA, (2) the pre-existing remote: relay path outside this code.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Windows DOM-renderer QA (Win11 25H2, DOM renderer confirmed active): mixed — flagging before merge.

Open question before this lands: is that stale-child behavior pre-existing on Windows (base 06ea55181) or introduced by this fix (a plausible mechanism exists: the guard's re-run can see getSize already matching xterm because onResize updated main's record, and skip a forward that base main would have sent). Running an A/B on base main + a fresh-process layer probe to decide. Holding the "no Windows regression" conclusion until that comes back. macOS A/B and the live SSH pass are unaffected.

…ruth

The reveal repro asserts on pty:getSize convergence; the bottom-bar TUI
only makes the pane WINCH-reactive. Record that a long-lived process can
miss Node's stdout 'resize' event under Windows ConPTY even when the OS
PTY was resized, so future readers don't add a flaky bar-content check.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Windows flag resolved — not a regression. Cleared to land.

A/B on Windows (idle-TUI reveal cycles) decisively attributes the stale-bar symptom to the test probe, not this PR:

  • D1: reproduces with the terminal visible the whole time (no hide/reveal) → not reveal-related.
  • D2: identical on base 06ea55181 and fix ce54ff3aa → not introduced by this fix.
  • D3 (decisive): after killing the stale bar process, a fresh Node process in the same terminal reads the correct grid (114×60) on both builds → ConPTY applied the resize; the long-lived process simply never received Node's stdout resize notification under this pseudoconsole (no Unix SIGWINCH).

So: Orca→ConPTY resize delivery works, the reveal logic isn't suppressing anything, and the earlier 10/10 xterm↔getSize convergence stands. The 0/10 bar result was a synthetic-probe artifact (process.stdout.on('resize') is unreliable for a long-lived process under Windows ConPTY; a real Windows TUI re-queries console dims). Added a comment on the repro spec so nobody mistakes the illustrative bar for the assertion target (which is getSize convergence).

Full status: macOS A/B + perf lane green · live SSH pass 10/10 · Windows units 435/435, e2e 5/5 (with #8520), renderer convergence 10/10 · oscillation unit-tested. Remaining out-of-scope: the pre-existing remote: relay path.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Windows regression question — RESOLVED: not a regression. The prior bar-stale result was a probe artifact.

A/B on Windows (visible-only D1, base 06ea55181 vs fix ce54ff3aa D2, fresh-process OS check D3):

  • D1: the stale-bar symptom reproduces with the terminal visible the whole time — no hide/reveal involved, so it is not a reveal-path effect.
  • D2: identical behavior on base and fix — initial child 132×60; xterm and getSize both resize correctly to 179×60 / 114×60; the long-lived Node script stays at 132 and wraps on both builds.
  • D3 (decisive): after killing the stale process, a fresh Node process in the same terminal reads 114×60 on both builds → ConPTY had applied the resize. The already-running process simply never observed Node's stdout resize event under this pseudoconsole (no Unix SIGWINCH).

Conclusion: Orca→ConPTY resize delivery works; this PR's reveal logic does not suppress the resize; the symptom predates the PR and occurs without reveal; the 10/10 xterm↔getSize convergence result stands. The synthetic WINCH bar is not a valid Windows ground truth — pushed a comment on the repro spec so nobody adds a bar-content assertion (it asserts on getSize, not the bar).

Net Windows status: units 435/435, e2e 5/5 (with the #8520 fixture fix), renderer convergence 10/10, no regression. Combined with the macOS A/B and the live SSH pass, I have no remaining regression concern for this PR. Only out-of-scope item is the pre-existing remote: relay viewport path (excluded from reassertion by an existing gate).

@brennanb2025
brennanb2025 merged commit 65c65c3 into main Jul 13, 2026
2 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.

1 participant