fix(web, mobile): a failed terminal resize is sent again#32047

open7
main
fix/issue-31013
opened2d agobyJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 2d ago

Problem

When a terminal.resize request fails while the WebSocket stays up, the client never sends the size again. The terminal surface only reports a grid when its measured size changes, and the resize result was dropped (reportFailure: false, result ignored). The PTY then keeps the old width indefinitely: stty size disagrees with the drawn pane, full-width output wraps, and spinners staircase, until the next drag to a new width. That matches the report: stty size printed 51 110 while the panel showed about 86 columns, and dragging again fixed it.

Reproduced on main (6577c6b618), web dev server, right-panel bash terminal, a 100 ms spinner, divider dragged mid-run. With every resize succeeding the PTY follows the pane (41 68 → 41 57). With exactly one resize request rejected by the server while the socket stays connected, the pane draws 44–46 columns but stty size stays at the previous width (53/60 columns) and the full-width line wraps, still stale more than a minute later with nothing touched.

This closes the failed-request path. What made the request fail on the reporter's macOS desktop is not confirmed (a node-pty resize throw surfaces as TerminalResizeError); the reproduction injects the failure.

Change

After a resize request fails, and the failure is not an interruption, the client resends the terminal's current grid once:

  • Web and desktop, ThreadTerminalDrawer: on failure the drawer records the writable session generation and calls the surface's resendSize(), so the retry carries the current measured grid through the existing 150 ms debounce (resize commands are a latest-wins lane, so the failed size is never replayed over a newer one). The next report consumes the record; a retry's failure is not retried, and a record never carries over to another session generation.
  • Mobile, createTerminalSession in packages/client-runtime: sendResize(retry) calls itself once with retry = true after a non-interrupt failure, re-checking the attachment generation and operate access. No shared state survives a detach or reconnect.

Failures stay silent. A request that keeps failing is sent at most twice; the existing per-generation replay on reconnect still applies. No server, contract or settings change.

Scope and approval

Fixes pingdotgg/t3code#31013

Triaged via the automated issue flow; reproduced on main with one injected resize failure.

Verification

Focused tests, each failing on main first:

  • apps/web/src/components/ThreadTerminalDrawer.browser.test.tsx: resends the terminal size after a resize request fails (main: only one 57×41 resize reached the command) and resends a failed resize once in each writable session (a retry suppressed after the session stopped being writable, and the bound on persistent failure). 4/4 pass.
  • packages/client-runtime/src/state/terminal.test.ts: sends each failed resize again once, in every attachment generation (a failure after detach is not resent, generation 2's failure is resent once, its retry's failure is not, a new grid gets its own retry). 13/13 pass.
  • A mutation check removing the retry bound makes both suites fail (unbounded resend).
  • vp run check:changed: format, lint, knip, typecheck, tests and React compiler pass.

Real-app recordings, web dev server in Chrome 156 on Linux, 1440×900, DPR 2, dark theme, fresh database per side, same driver on both sides. Each recording first shows a successful drag (68 → 53 columns), then one drag narrower whose resize frame is rewritten in the page to cols: 0 so the server rejects it ("Expected a value greater than or equal to 1"), with the socket open throughout:

Before 6577c6b618After db28804cc9
Rejected 44×46 resizefailure, no further resize sentfailure, then a valid 44×46 resize 184 ms later, success
stty size after idle (+60 s)46 53 in a 44-column pane; full-width line wraps46 44; full-width line ends at the pane edge

The head adds only f6525a2d4c after the captured build, which swaps in the equivalent isAtomCommandInterrupted helper and edits tests. An independent rebuild at the head reproduced the recovery (retry 178 ms after the rejected send, PTY back at 44 columns). The recordings (as GIFs) are in a PR comment; the final before/after stills and a zero-threshold diff are in the reply to the CodeRabbit summary. The differences between the final stills are the probe output and wrapping, the completed spinner glyphs and the sidebar thread age.

Not verified: the reporter's natural failure on the macOS desktop app; mobile on a device (covered by the client-runtime test; no mobile rendering changed); the server path where resizeLocked ignores a resize from an unregistered viewer while another viewer has a size, which returns success and is not covered by this retry.

AI-assisted; verified with the tests and tools listed above.

Checks

24 successful · 11 skipped
All checks passed
f6525a2

Added 7 commits

  • fix(mobile): a failed terminal resize is sent again6ada42e
  • fix(web): a failed terminal resize is sent againb466929
  • fix(web): a failed terminal resize is retried in every sessiondb28804
  • test(web): failed terminal resize is resent (reproduces #31013)de3b36e
  • fix(mobile): a failed terminal resize is retried in every attachment generatione5a44fd
  • test(client-runtime): failed terminal resize is resent (reproduces #31013)eb78c4a
  • refactor(web): reuse the interrupt helper for the resize retryf6525a2
Synced from GitHub

Approvability

Verdict: Approved at f6525a2

Macroscope's review found this PR approvable — Failed terminal resizes are retried once using the current dimensions, while retaining session and write-access checks. The change is limited to resize recovery and presents no obvious material risk requiring human judgment.

You can add or adjust custom eligibility rules. Learn more.

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

The client runtime retries eligible failed terminal resize commands once. The terminal drawer also resends its measured size after a resize failure for a writable session generation. Tests cover retry behavior, reconnects, and changed grid sizes.

Changes: Terminal resize retry flow

Layer / File(s)Summary
Client runtime resize retry
packages/client-runtime/src/state/terminal.ts, packages/client-runtime/src/state/terminal.test.ts
The runtime retries a failed resize command once unless the failure is an interruption. The retry uses the current grid size. Tests cover failures across attachment generations and after the grid size changes.
Drawer resend by writable generation
apps/web/src/components/ThreadTerminalDrawer.tsx, apps/web/src/components/ThreadTerminalDrawer.browser.test.tsx
After an eligible resize failure, the drawer resends the terminal surface’s measured size. It tracks the retry by writable session generation. Browser tests check that the request retains its parameters and that retries occur after a writable session reconnects.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Low

Possibly related PRs

  • pingdotgg/t3code#25458: Updates drawer resizing when the writable session generation changes; this PR adds failure retries to that resize path.
  • pingdotgg/t3code#24352: Changes how measured terminal sizes reach the server and how viewer resize requests are applied; this PR retries failures in that path.

Suggested labels: bugfix

Suggested reviewers: saphid

Merge Risk: External image Minimal · up to f6525

No actionable issue remains from this review; the change is mergeable after normal checks.

Pre-merge checks: Passed 3 · Inconclusive 1

Failed Failed checks: 1 inconclusive

Check nameStatusExplanationResolution
Linked Issues checkInconclusive[#31013] The drawer retries a failed resize with the current grid. Focused tests cover bounded retries and session changes. The reported injected-failure test shows the PTY width matches the pane afte…Provide evidence of the original failure mechanism, or evidence that the affected resize path now keeps the PTY width aligned with the pane.
Passed Passed checks: 3 passed

Check nameStatusExplanation
Title checkPassedThe title clearly describes the failed terminal resize retry.
Out of Scope Changes checkPassed[#31013] The web retry, mobile runtime retry, and their tests address stale terminal dimensions after resize failures. The mobile change supports the same terminal behavior on another client.
Description checkPassedThe description covers the problem, changes, scope, and verification. It identifies unverified cases. The screenshots and recordings are referenced but not linked directly, and the AI model and harnes…
Full details: Description check

Explanation

The description covers the problem, changes, scope, and verification. It identifies unverified cases. The screenshots and recordings are referenced but not linked directly, and the AI model and harness are not named.

Full details: Linked Issues check

Explanation

[#31013] The drawer retries a failed resize with the current grid. Focused tests cover bounded retries and session changes. The reported injected-failure test shows the PTY width matches the pane after a retry. The cause of the original macOS failure is unknown. The retry does not cover a resize that the server ignores while returning success, so the evidence does not establish that the reported failure is fixed.

Resolution

Provide evidence of the original failure mechanism, or evidence that the affected resize path now keeps the PTY width aligned with the pane.

Finishing Touches

Generate docstrings

  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
External image Recent review info

External image Run configuration

External image Commits

Reviewing files that changed from the base of the PR and between 6577c6b6185167c0e502fa01f9e8e8f2dee6ff25 and f6525a2d4c05a731fc1b1715fb672bee6683f92b.

External image Files selected for processing: 4

  • apps/web/src/components/ThreadTerminalDrawer.browser.test.tsx
  • apps/web/src/components/ThreadTerminalDrawer.tsx
  • packages/client-runtime/src/state/terminal.test.ts
  • packages/client-runtime/src/state/terminal.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 5 remain after this review.

On the inconclusive linked-issue check: this PR closes the path where a terminal.resize request fails while the connection stays up, which leaves the PTY at the old width until the next drag to a new width. That is the symptom in #31013. On main (6577c6b618), a single rejected resize in the web app kept stty size at 53 columns in a 44-column pane. With this branch, the client sends the current 44×46 grid once more about 150 ms after the failure, and stty size matches the pane.

What failed on the reporter's macOS desktop is not confirmed, as the PR body says. The server path that ignores a resize while returning success only applies when another viewer holds a size. The reporter had a single client, so that path is out of scope here.

Recordings for the Verification section: web dev server, Chrome 156 on Linux, 1440×900, DPR 2, dark theme, a fresh database per side, the same driver on both sides. Shown here as 4 fps GIFs scaled to 960 px.

Steps, the same on both sides:

  1. A 100 ms spinner runs while the divider is dragged once (68 → 53 columns). The resize succeeds.
  2. One outgoing terminal.resize frame is rewritten to cols: 0, so the server rejects it while the socket stays open. The spinner runs again and the divider is dragged narrower once (44 columns). Nothing is dragged or reconnected after that.
  3. The width probe (stty size and a line as wide as the PTY reports) runs again after about 40 s idle.
Before 6577c6b618After db28804cc9
Rejected 44×46 resize+29.9 s, no resize follows+25.6 s, valid 44×46 resize at +25.8 s (184 ms later), success
Probe after idle+78 s: 46 53, the line wraps+63 s: 46 44, the line ends at the edge

The only production change between db28804cc9 and the head (f6525a2d4c) swaps in the equivalent isAtomCommandInterrupted helper.