Skip to content

fix(terminal): polish pass — node-pty diagnostics, Ctrl+W guards, black flash, split grouping - #5170

Open
StiensWout wants to merge 23 commits into
pingdotgg:mainfrom
StiensWout:terminal-polish
Open

fix(terminal): polish pass — node-pty diagnostics, Ctrl+W guards, black flash, split grouping#5170
StiensWout wants to merge 23 commits into
pingdotgg:mainfrom
StiensWout:terminal-polish

Conversation

@StiensWout

@StiensWout StiensWout commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Terminal polish pass

Five terminal fixes that share one theme: the terminal backend fails loudly instead of silently, and the terminal UI stops eating destructive or surprising interactions.

Fixes #4924
Fixes #2787
Addresses #5129

Server: node-pty load failures are loud and actionable

A broken node-pty install (missing toolchain, wrong Node ABI) died as a bare startup defect that headless logging could swallow — npx t3 exited cleanly with no output. The failure stays typed in the adapter, and the CLI entrypoint now renders its diagnostic to stderr (what node-pty is, the two usual causes, the reinstall steps) and forces a non-zero exit code.

This makes the failure diagnosable; it does not solve the underlying missing-toolchain/prebuild problem in #5129, and it overlaps with #5132 — happy to rebase on whichever lands first.

Server: spawn-helper exec-bit repair and diagnosis

When npm extracts node-pty without the exec bit on spawn-helper, every shell candidate fails with posix_spawnp failed and the final error read like no working shell existed on the machine. Now:

  • the chmod repair retries instead of disabling itself after one failure, and is skipped entirely when the helper is already executable (so a read-only package store does not re-log the same warning on every spawn);
  • executability is judged against the calling process's uid/gids rather than "any exec bit", since an owner-only helper is not executable for anyone else and FileSystem.access cannot test X_OK;
  • the diagnosis is a typed SpawnHelperNotExecutableError carrying helperPath, and the shell-candidate fallback short-circuits on its tag rather than on message text.

Web + desktop: held Ctrl+W cannot close the window

The terminal close shortcut never called preventDefault in the terminal key handler, and after the last terminal closes, held-key auto-repeats resolve no command and leak to Electron's windowMenu close accelerator — one hold could close the window (and on Windows/Linux, quit the app). The web handler now prevents the default like its sibling shortcuts, and the desktop window drops auto-repeated CmdOrCtrl+W in before-input-event. Deliberate single presses still close terminals and windows as before.

Web: brand-new terminals no longer flash black

The ghostty surface appends an opaque (alpha: false) canvas to the drawer before awaiting webfonts and the WASM core, and an opaque backing store is pure black until the first paint. The canvas is now prefilled with the theme background as soon as the 2D context exists.

Web: bottom-bar splits stay in their group

The drawer's reconcile-against-server-metadata pass compared raw server session ids while the store filters suppressed (closed) ids. One stale server session defeated the "server list is lagging" guard on every local change: a fresh split's id is not server-known yet, so reconcile dropped it and the next metadata update re-added it as its own group. The guard decision is now a pure store helper that filters suppressed ids before comparing, with regression tests. The right panel never ran this reconcile pass, which is why splitting only misbehaved in the bottom bar.

Web: PTY and grid resize in lockstep, sequenced through the stream

Resizing a narrow pane left stranded fragments of the wrapped prompt above the live one. The PTY resize was trailing-debounced while the local grid reflowed immediately, so the shell's redraw sequences were computed for one width and interpreted at another; each mismatched redraw orphaned the previous prompt. The grid now requests the resize and applies it only when the PTY acknowledges — the acknowledgement arrives on the same socket after all old-width output, so every byte is interpreted at the width it was generated for. Verified in a live paired session: wrap-crossing resize sequences that previously stacked a fragment per redraw now leave a single correctly wrapped prompt.

Web: panel opens reserved, interrupted panel closes settled by metadata

Follow-ups from the re-check: the right-panel add/split paths now allocate against the in-flight reservation registry and open through the shared rollback helper, and an interrupted panel close parks its surface snapshot until authoritative session metadata either restores the terminal into its original surface (session survived) or drops it (session closed), instead of guessing.

Server: replayable query traffic stripped from terminal history

Reopening a terminal replays its history, and stored capability queries made the terminal answer again — the shell echoed the replies as visible junk at the prompt (the 2026;2$y…1$r0m / 1;2c / 11;rgb:… gibberish from #1238). The history sanitizer now also strips DECRQM/DECRPM ($p/$y), XTVERSION and kitty-keyboard queries, and DECRQSS/XTGETTCAP DCS traffic, while setters sharing final bytes (DECSTR, DECSCL, DECSCUSR, restore-cursor) survive. This prevents new junk at the source; residue already flattened into stored scrollback as plain text is deliberately left to #3508, which tackles that broader live-stream stripping.

Work done by Claude Fable 5 via Claude Code.

🤖 Generated with Claude Code


Note

Medium Risk
Changes span PTY spawn/exec-bit logic, terminal event ordering, and optimistic UI rollback across drawer and right panel; regressions could affect terminal open/close, resize timing, or split grouping, though coverage is strong in tests.

Overview
This PR tightens terminal backend failures and web/desktop terminal UX so broken installs and optimistic UI do not strand users.

Server: The CLI now writes typed startup defects (e.g. node-pty load) to stderr and sets a non-zero exit. node-pty spawn-helper repair retries chmod, judges exec against the caller’s uid/gids, and surfaces SpawnHelperNotExecutableError with a chmod fix instead of retrying every shell. Resize queues a flush sentinel so resize replies publish after old-width output. History sanitization strips more replayable CSI/DCS query traffic (DECRQM, XTVERSION, kitty keyboard, DECRQSS/XTGETTCAP) while keeping setters like DECSCUSR.

Web: The ghostty canvas is prefilled with the theme background and grid resize waits on PTY ack (single-flight with retry). Terminal UI state adds pending vs suppressed ids, reconcilableServerTerminalIds, and rollback via unsuppressTerminal / abandonPendingTerminal. Open reservations avoid id collisions; failed/interrupted closes restore drawer tabs or park panel snapshots until metadata settles (terminalPendingPanelCloses). Ctrl+W auto-repeat is swallowed at window and chat level; terminal close uses preventDefault in the surface.

Desktop: Electron blocks auto-repeated Cmd/Ctrl+W so held close shortcuts do not close the window after the last terminal unmounts.

Reviewed by Cursor Bugbot for commit 013841c. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix terminal black flash, Ctrl+W auto-repeat, split grouping, and node-pty spawn diagnostics

  • Fills the terminal canvas with the theme background color immediately after acquiring the 2D context, eliminating the black flash before fonts load
  • Suppresses CmdOrCtrl+W auto-repeat events in both the Electron before-input-event handler and the web keydown handler, preventing accidental window/tab closure when holding the close shortcut as a terminal unmounts
  • Delays the terminal grid resize until the PTY acknowledges the new dimensions, keeping rendering aligned with the shell's redrawn width
  • Adds per-thread in-flight open reservations (terminalOpenReservations.ts) to prevent ID collisions across drawer and panel opens; failed opens abandon the optimistic ID and roll back any optimistic UI
  • Records interrupted right-panel terminal closes as pending and restores the pane on failure via a new restoreTerminal action in the right-panel store
  • Tracks pending terminal IDs in useTerminalUiStateStore so unconfirmed locally opened terminals stay visible across reconcile cycles; adds unsuppressTerminal and abandonPendingTerminal actions
  • Surfaces a SpawnHelperNotExecutableError with a chmod remedy when node-pty spawn fails due to a non-executable spawn-helper, skipping shell retries and reporting diagnostics to stderr on startup
  • Strips additional CSI/DCS query-reply sequences (DECRQM, DECRPM, XTVERSION, kitty-keyboard, DECRQSS, XTGETTCAP) from stored terminal history to reduce replay noise

Macroscope summarized 013841c.

StiensWout and others added 3 commits August 1, 2026 07:56
A broken node-pty install used to take the whole server down as a bare
defect that headless startup logging could swallow, so npx t3 exited
cleanly with no output. Write a diagnosis straight to stderr (toolchain
hints, reinstall steps) and force a failing exit code before dying.

When spawn-helper ships without its exec bit, every shell candidate
fails with posix_spawnp and the error read like no shell existed on the
machine. The chmod repair no longer disables itself after one failed
attempt, failures are logged with the manual remedy, and spawn errors
name the non-executable helper directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ault

The window-level handler owns terminal.close but bails without
preventDefault when the palette is open or no thread is active, letting
mod+W fall through to the tab/window close default while the terminal is
focused. Prevent the default at the xterm key handler like the sibling
shortcuts already do.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closing the last terminal disposes the xterm mount, and while focus is
in flight the held key's auto-repeats resolve no command, leak past the
renderer guards, and hit the windowMenu close accelerator - one hold
could take out the window (and on Windows/Linux the whole app). Drop
auto-repeated CmdOrCtrl+W presses in before-input-event; deliberate
single presses still work everywhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3ecffecf-bd70-4a26-8764-8c074afff743

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@github-actions github-actions Bot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Aug 1, 2026
Comment thread apps/server/src/terminal/NodePtyAdapter.ts Outdated

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the Effect service changes in apps/server/src/terminal/NodePtyAdapter.ts (plus its test) against the service conventions. Two findings, both in the new spawn-failure diagnostics path.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/terminal/NodePtyAdapter.ts Outdated
Comment thread apps/server/src/terminal/NodePtyAdapter.ts Outdated
Review follow-up: the not-executable spawn-helper wrapper still carried
posix_spawnp in its cause chain, so the manager retried every shell
candidate (re-attempting chmod and logging a warning per candidate)
before surfacing the actionable error. Treat that diagnosis as
non-retryable. Also stop latching the ensure flag when the helper path
resolves to null, since resolution failures are transient and folded
into null.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One new finding in this revision. The previously raised points about the manufactured plain Error and the process-exit side effects in the adapter still stand and are not re-posted here.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/terminal/Manager.ts Outdated
Comment thread apps/server/src/terminal/NodePtyAdapter.ts
StiensWout and others added 2 commits August 1, 2026 08:38
The ghostty surface appends an opaque canvas to the drawer before
awaiting webfonts and the WASM core, and an alpha:false backing store is
pure black until the first paint. Prefill the canvas with the theme
background as soon as the context exists so a brand-new terminal opens
straight into its themed background.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The drawer reconciles its terminal list against server session metadata,
but its lag guard compared raw server ids while the store filters
suppressed (closed) ids. One stale server session was enough to defeat
the guard on every local change: a fresh split's id is not server-known
yet, so reconcile dropped it and the next metadata update re-added it as
its own group. The right panel has no such reconcile pass, which is why
only the bottom bar misbehaved.

The guard decision now lives in the store as a pure helper that filters
suppressed ids before comparing, with regression tests for the stale-
session split scenario.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the terminal-polish changes for Effect service conventions. One leftover from the ChatView.tsx helper extraction; the earlier findings on NodePtyAdapter.ts/Manager.ts still stand and are not re-posted here.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/web/src/components/ChatView.tsx Outdated
…y to the CLI

Review follow-ups from Macroscope:
- model the non-executable spawn-helper failure as SpawnHelperNotExecutableError
  with a helperPath attribute, and key the terminal manager's retry decision off
  the tag instead of substring-matching a message
- skip the chmod when the helper is already executable, so a read-only package
  store no longer logs the same warning on every spawn
- render the node-pty load diagnostic and set the exit code in the CLI
  entrypoint rather than inside the adapter constructor
- drop a docblock left behind in ChatView by the helper extraction

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/server/src/terminal/NodePtyAdapter.ts

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One convention finding on the new SpawnHelperNotExecutableError declaration. The earlier feedback on error modeling and process-exit policy has been addressed.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/terminal/PtyAdapter.ts Outdated
Review follow-ups from Macroscope: a bare `mode & 0o111` calls an
owner-only helper (0o100) executable for every other user, which skips
the chmod repair and suppresses the actionable diagnosis for exactly the
users hitting the failure. Effect's FileSystem.access cannot test X_OK,
so derive executability from the mode against the process uid/gids.

Also make the SpawnHelperNotExecutableError cause required, since every
construction wraps a real spawn failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@StiensWout StiensWout changed the title fix(terminal): polish pass — actionable node-pty failures and Ctrl+W guards fix(terminal): polish pass — node-pty diagnostics, Ctrl+W guards, black flash, split grouping Aug 1, 2026
@StiensWout
StiensWout marked this pull request as ready for review August 1, 2026 07:06
@macroscopeapp

macroscopeapp Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces significant new terminal state management (pending/suppressed id tracking, async resize acknowledgment, new error handling paths) spanning server and client. While each fix addresses real issues, the aggregate introduces new coordination patterns and state concepts that warrant human review.

You can customize Macroscope's approvability policy. Learn more.

@t3-code t3-code Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

found four actionable issues in this pass.

Comment thread apps/web/src/components/ThreadTerminalDrawer.tsx
Comment thread apps/web/src/terminalUiStateStore.ts
Comment thread apps/web/src/terminalUiStateStore.ts Outdated
Comment thread apps/server/src/terminal/Manager.ts
…longer lose state

Four review findings:
- swallow auto-repeats of the close chord at the window handler, so a hold that
  unmounts the last terminal cannot fall through to the browser tab-close
  default once the shortcut stops resolving
- roll back the optimistic close suppression when both the close request and
  the fallback exit write fail, instead of hiding a still-running terminal for
  the rest of the session; the right-panel close had no failure handling at all
- merge pending local ids during reconcile rather than adopting the server list
  wholesale, so an update that carries an unknown server id no longer drops a
  split opened moments earlier
- publish the diagnosed spawn failure to the client, so the actionable chmod +x
  remedy is no longer hidden behind the generic PtySpawnError wrapper

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/web/src/terminalUiStateStore.ts Outdated
Comment thread apps/web/src/components/ChatView.tsx
Review follow-up: the previous merge kept every client id, so a terminal
that ended server-side was never removed once any new id arrived in the
same update, leaving a dead tab on screen for good.

A client-only id is ambiguous — either a local open the server has not
registered yet, or a session that ended remotely — and id sets alone
cannot tell them apart. Track ids introduced by split/new/ensure in a
pending set, clear an id once the server reports it, and let reconcile
keep only pending or server-visible ids. An empty server list is still
treated as unloaded metadata so a reconnect cannot clear the drawer.

The merge decision moves into the store, where the pending set lives.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 1, 2026
Comment thread apps/web/src/terminalUiStateStore.ts
Comment thread apps/web/src/components/ChatView.tsx Outdated
Review follow-up: pending ids are immune to reconcile, and every caller
fired openTerminal without handling rejection, so a failed open left a
terminal tab in the drawer that nothing could ever clean up.

Add abandonPendingTerminal and call it from every path that marks an id
pending: both split directions, new terminal, the drawer toggle, and the
project-script runner. It clears the id without suppressing it, so a
session that did get created after all can still be adopted.

Also rolls back the optimistic close suppression in the third close path,
matching the drawer and right-panel handlers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/web/src/terminalUiStateStore.ts
Two review findings on the close-rollback path:
- a failed right-panel close re-surfaced the live terminal in the bottom
  drawer, because the rollback only cleared drawer-store suppression while
  the panel surface stayed removed; restore the panel surface as well
- the drawer reconcile effect only watched the server session list, so a
  suppression rollback did not resurface the terminal until unrelated
  metadata changed; subscribe to the suppression and pending sets that
  feed the store's reconcile decision

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/web/src/components/ChatView.tsx
…ight

Review follow-up: closing an optimistic tab while its open request is
pending freed the id for the next terminal, so the first request's
failure handler could tear down the second terminal's tab. Suppressed and
pending ids now stay reserved during allocation in both the drawer and
the chat-level handlers, so a stale failure rollback can only ever touch
an id that no longer exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/web/src/components/ChatView.tsx Outdated

@t3-code t3-code Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

re-check: the original server, reconciliation, and held-shortcut fixes look good. 4 web edge cases remain.

Comment thread apps/web/src/components/ChatView.tsx Outdated
Comment thread apps/web/src/components/ChatView.tsx Outdated
Comment thread apps/web/src/components/ChatView.tsx Outdated
Comment thread apps/web/src/components/ChatView.tsx Outdated
…tcomes

Four review findings on the rollback machinery:
- a failed panel close now restores the terminal into its original surface
  at its original pane position and split direction via a pre-close
  snapshot and rightPanelStore.restoreTerminal, instead of a standalone
  surface that stranded split members
- id reservation now tracks only opens actually in flight (a small
  per-thread registry held for the request window), instead of reserving
  every suppressed id, which would have skipped every terminal ever
  closed in the session and grown without bound
- an interrupted open now abandons the optimistic id like a failed one:
  interruption is not confirmation, and if the session was created after
  all, reconcile re-adopts it from server metadata
- an interrupted close now rolls back the optimistic suppression in all
  drawer, chat-level, and panel close paths, converging on server truth
  instead of treating interruption as success

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@t3-code t3-code Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

re-check on e72a: the 4 reported fixes are materially addressed, but 2 related web edge cases remain.

Comment thread apps/web/src/lib/terminalOpenReservations.ts
Comment thread apps/web/src/components/ChatView.tsx
Review re-check follow-ups plus a reported reflow artifact:
- panel add/split now allocate against the in-flight reservation registry and
  open through the shared helper, so closing a panel terminal mid-open can no
  longer free its id for the next panel open while the request is unresolved
- an interrupted panel close now parks its surface snapshot instead of
  guessing: authoritative session metadata restores the terminal into its
  original surface if the session survived, or drops the snapshot if it did not
- the PTY now hears the leading edge of a resize burst as well as the settled
  size. Trailing-only notification left the shell on its old width for the
  whole drag while the grid rewrapped already-printed output, stranding
  fragments of a wrapped prompt above the live one on narrow panes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:XL 500-999 changed lines (additions + deletions). labels Aug 1, 2026
Comment thread apps/web/src/lib/terminalPendingPanelCloses.ts
The previous leading-edge notify only helped the first step of a drag:
the timer swallowed every intermediate step, so the shell's redraw
sequences were still computed for one width and interpreted at another,
and each mismatched redraw orphaned the previous prompt as a stranded
fragment. Notify on every grid change instead, like a native terminal's
SIGWINCH — fit() already coalesces to one call per crossed cell
boundary, so a drag cannot flood the PTY.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/web/src/components/ChatView.tsx
Lockstep notification was not enough: the shell's redraw bytes are
generated against the width the server pty had at generation time, and
during a drag the client grid had already moved on, so each mismatched
redraw still orphaned the previous prompt as a stranded fragment.

Sequence the resize into the output stream instead. fit() requests the
resize and keeps the old grid; the acknowledgement arrives on the same
socket after all old-width output, so core.resize lands exactly where
the server pty flipped and every byte is interpreted at the width it was
generated for. Superseded acknowledgements are ignored via a sequence
counter, and a failed request still applies locally so the grid cannot
get stuck.

Verified in a live paired session: clear + six wrap-crossing viewport
steps leave a single correctly wrapped prompt with no stale fragments,
where the debounced code stacked one fragment per redraw.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/web/src/terminal/ghostty/surface.ts Outdated
StiensWout and others added 2 commits August 1, 2026 11:22
Codex-assisted diagnosis of the remaining resize artifact: the resize RPC
layer coalesces with mode 'latest', and applyGridSize discarded
acknowledgements superseded by a newer measurement. When a drag produced
several measurements while one request was in flight, the server PTY
visited widths the client grid skipped — the shell redrew for a width
the emulator never occupied, which is exactly the stranded-fragment
mechanism. This is why coarse jumps still failed while slow fine steps
were clean.

Resizes are now single-flight: one request at a time, its exact size
always applied on settle, then the newest measurement is requested if it
moved on. The emulator steps through every width the PTY entered, in
order.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…esizes

Two review findings:
- recording a pending panel close now notifies subscribers, and the
  settling effect subscribes via useSyncExternalStore. Without this, a
  close recorded after its settling metadata already arrived — or one
  whose session survived, so the list never changes — left the terminal
  suppressed and paneless forever
- a denied PTY resize no longer moves the grid: the drawer resolves the
  acknowledgement with the request outcome, and the surface keeps the
  grid at the PTY's size and re-aligns the desired size so the same
  measurement retries instead of being skipped. The pre-grant exception
  keeps mount bookkeeping aligned with the constructed core

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/web/src/lib/terminalPendingPanelCloses.ts
Comment thread apps/web/src/terminal/ghostty/surface.ts
Comment thread apps/web/src/components/ThreadTerminalDrawer.tsx
…ndling

- pending panel closes settle only after a grace window, so a close whose
  metadata has not propagated yet cannot be restored from a stale cached
  session list and pin a dead pane; entries stay pending until post-grace
  metadata decides, with tests for the stale-cache window
- the first-settle bookkeeping exception applies exactly once: repeated
  failed resizes before any grant no longer keep moving the grid while
  the PTY stays put
- a resize while the terminal process is not running now records the
  session dimensions, so restart (which falls back to session.cols/rows)
  spawns at the size the client was acknowledged for instead of the
  stale one

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@t3-code t3-code Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

rechecked at e38c250. the previous fixes are present, but 7 actionable edge cases remain. focused web tests and web/server typechecks pass.

Comment thread apps/web/src/components/ChatView.tsx Outdated
Comment thread apps/web/src/lib/terminalPendingPanelCloses.ts Outdated
Comment thread apps/web/src/lib/terminalPendingPanelCloses.ts
Comment thread apps/web/src/components/ChatView.tsx
Comment thread apps/web/src/components/ThreadTerminalDrawer.tsx
Comment thread apps/server/src/terminal/Manager.ts
Comment thread apps/web/src/terminal/ghostty/surface.ts
…llback gaps

Review follow-ups, the first being the root ordering gap:
- the server resize reply now orders itself behind every output byte
  queued before the resize, via a flush sentinel drained through the same
  per-session event queue; queue wipes release sentinels so awaiters
  cannot hang. The client can therefore apply its grid on the reply
  knowing no old-width bytes follow, with a Manager ordering test
- a failed or interrupted resize now schedules a retry of the measured
  size until the PTY grants it, instead of leaving the emulator on a size
  the server never confirmed
- a definite open failure now also rolls back the optimistic right-panel
  surface or split pane, not just the drawer-store pending id
- an authoritative empty session list settles pending panel closes (the
  closed terminal may have been the last session), distinguished from
  unloaded metadata via the query's loaded state
- surviving snapshots restore in reverse close order so nested split
  closes reinsert at their original pane positions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/server/src/terminal/Manager.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8026b6a. Configure here.

Comment thread apps/web/src/components/ChatView.tsx
Comment thread apps/server/src/terminal/Manager.ts
StiensWout and others added 2 commits August 1, 2026 20:22
Reopening a terminal replays its history, and stored capability queries
made the terminal answer again — the shell then echoed the replies as
junk at the prompt (the 2026;2$y…1$r0m / 1;2c / 11;rgb:… gibberish
from the field reports). The history sanitizer already dropped DSR, CPR,
DA, and OSC colour traffic; it now also strips DECRQM queries and DECRPM
replies ($-intermediate p/y), XTVERSION and kitty-keyboard queries, and
DECRQSS/XTGETTCAP DCS queries and replies. Setters sharing final bytes —
DECSTR, DECSCL, DECSCUSR, restore-cursor — survive, covered by a
regression test.

Prevents new junk at the source; residue already flattened into stored
scrollback as plain text is out of scope here (PR pingdotgg#3508 tackles that
broader stripping).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-up: dequeue compacts the pending queue before the dequeued
chunk is published, so a resize landing in that window saw an empty
queue, skipped the flush sentinel, and acked ahead of the old-width
bytes. A running drain now also gates the flush — the sentinel is picked
up when the drain loop re-enters, behind the in-flight publication.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

1 participant