Skip to content

fix(terminal): keep the reattach grid push alive for a hidden pane - #13155

Merged
brennanb2025 merged 2 commits into
mainfrom
brennanb2025/fix-floating-terminal-restart
Aug 9, 2026
Merged

brennanb2025 merged 2 commits into
mainfrom
brennanb2025/fix-floating-terminal-restart

Conversation

@brennanb2025

Copy link
Copy Markdown
Contributor

Summary

After an app restart, the Floating Workspace panel always mounts closed, so its restored terminal panes are display:none for the entire PTY reattach. Reattach pins xterm to the snapshot's grid and writes the replay there — correct, and required, because xterm's serialize format is width-implicit (#7279). The step that then pushes the client's real grid back to the PTY and sends the explicit SIGWINCH that makes a restored TUI fully repaint after the replay — fitAfterReattachRestore — was permanently dropped by the display:none guard in safeFitAndThen.

The guard exists for a real reason: structuralReplayCoordinator holds live PTY bytes behind that continuation's promise, so simply parking it would stall output for a closed panel indefinitely. This splits the two concerns instead — the completion promise still settles immediately, but the continuation moves to a per-pane deferred bucket that the first measurable fit drains.

Deferral is opt-in (deferIfHidden) and used only by the reattach grid push. The other retryIfUnmeasurable caller (hidden-snapshot-pty-resize, which hits display:none on ordinary background tabs) keeps its current behavior rather than being changed implicitly.

What this does not fix. On macOS the shell is spawned under /usr/bin/login for TCC attribution (macos-tcc-login-shell.ts:321), and sendSignal targets that pid (local-pty-provider.ts:1205). login neither handles nor forwards SIGWINCH, so the explicit repaint signal is swallowed before it reaches the shell. That is pre-existing and also affects the visible reattach path, so a restored TUI on macOS still may not repaint cleanly. Filed separately; this PR is the prerequisite half (the signal has to be sent before it can be delivered).

pane-fit.ts was already exactly at the 300-line max-lines cap, so the pending-continuation registry moved to pane-fit-continuation-registry.ts rather than adding a disable.

Screenshots

No visual change — this restores a signal and a resize on a path that previously sent neither. Behavioral evidence is the A/B below rather than a screenshot.

Testing

  • pnpm lint
  • pnpm typecheck
  • pnpm test
  • pnpm build
  • Added or updated high-quality tests that would catch regressions, or explained why tests were not needed

Live A/B on a restarted app, with the panel sized identically before and after the restart so the kernel contributes no SIGWINCH of its own — isolating the explicit one. A shell trapping WINCH counted deliveries on panel open:

fix disabled fix present
stty size before quit 32 113 32 113
panel rect before / after restart 920×560 / 920×560 920×560 / 920×560
SIGWINCH on opening the panel 0 1

Run on an isolated profile (own daemon) against a throwaway repo, with ORCA_DISABLE_MACOS_LOGIN_SHELL=1 so the signal was observable at all — see the macOS caveat above. Verified per arm that the pane's ancestor chain really was display:none for the whole reattach, and that the served module was actually patched (no HMR trust).

Five regression tests, each individually confirmed to fail with its specific fix patched out:

  • pty-connection.test.ts — forwards the destination grid on reveal after a display:none reattach. Deliberately a near-copy of the existing "waits for a recovered destination fit…" test above it, differing only in the hidden ancestor.
  • pty-connection.test.ts — suppresses the deferred push when mobile claims the PTY while the pane is hidden.
  • pane-fit.test.ts — runs a display:none continuation on the first fit after reveal; also asserts the completion promise still resolves false immediately, that no terminal_safe_fit_retry_exhausted breadcrumb is logged, and that the continuation is one-shot.
  • pane-fit.test.ts — parks on one pane object and drains on a different one. PTY connections hold a toPublicPane() wrapper while PaneManager drains via internal panes, so a pane-identity key is silently unreachable; this is the trap pane-metric-options-deferral.ts:16-18 already documents. An earlier revision of this PR had exactly that bug and its tests could not see it.
  • pane-fit.test.ts — cancel() after deferral, and same-key replacement not double-firing.

Local full-suite has 18 pre-existing failures across src/main, src/relay and a root-directory guard — zero overlap with this diff, all environment-shaped (real HOME / ORCA_* leaking into env-assertion tests).

AI Review Report

Two independent passes: an adversarial review of the diff, and a history sweep of every prior PR that established an invariant in this code. Both found real defects, all fixed here:

Security Audit

No new IPC surface, no input handling, no command execution, no path handling, no auth or secrets, no dependency changes. The only privileged call reached is the existing pty:signal IPC with a literal 'SIGWINCH' and a pty id the transport already owns, double-guarded by a generation check and a pty-id equality check. Net change is renderer-local bookkeeping of a callback that already existed.

Notes

  • For a pane that goes display:none → measure-lease (laid out full size but still hidden) without becoming visible, the deferred continuation can fire while hidden. That matches fitAfterReattachRestore's existing intent — it deliberately bypasses the visibility gate because it is the reattach repair — and the measure-lease reasoning settled in fix(terminal): restore paired snapshots at the source grid #12768. Flagging it because hidden SIGWINCH resetting full-screen TUIs is a known hazard here.
  • Follow-up: macOS sendSignal should target the foreground process group rather than the login wrapper pid, or the explicit reattach repaint never lands on macOS.
  • Follow-up: floatingTerminalOpen is useState(false) in App.tsx and never persisted, so the Floating Workspace always returns closed regardless of how you left it. That is what puts these panes on the hidden path.
  • Follow-up: the 40-frame onExhausted path drops the reattach continuation the same way this fixes for display:none; an existing test asserts that contract deliberately.

A restored Floating Workspace pane is display:none for its whole PTY
reattach, because the panel always mounts closed after a restart. The
display:none guard in safeFitAndThen dropped the reattach continuation
outright, so the PTY never received the client grid and no explicit
SIGWINCH was sent to repaint the restored TUI after the replay.

Parking the continuation is not an option: the structural replay
coordinator holds live PTY bytes behind its promise, so a closed panel
would stall output indefinitely. Settle the promise immediately instead
and move the continuation to a per-pane deferred bucket that the first
measurable fit drains.

Deferral is opt-in so the bounded-degradation contract still covers every
other caller, and the continuation re-checks mobile PTY ownership at fire
time now that the wait is unbounded.
@coderabbitai

coderabbitai Bot commented Aug 8, 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 Plus

Run ID: 4546997a-de57-4fbc-9577-883a48b6fd7c

📥 Commits

Reviewing files that changed from the base of the PR and between 5ddc11e and af85ab6.

📒 Files selected for processing (3)
  • src/renderer/src/lib/pane-manager/pane-fit-continuation-registry.ts
  • src/renderer/src/lib/pane-manager/pane-fit-deferred-continuations.ts
  • src/renderer/src/lib/pane-manager/pane-fit.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/renderer/src/lib/pane-manager/pane-fit-deferred-continuations.ts
  • src/renderer/src/lib/pane-manager/pane-fit.test.ts
  • src/renderer/src/lib/pane-manager/pane-fit-continuation-registry.ts

📝 Walkthrough

Walkthrough

The change adds pane-scoped and terminal-keyed registries for deferred fit continuations. Hidden panes retain continuations until they become measurable. safeFit and safeFitAndThen use the registries for flushing, retry cleanup, cancellation, and settlement. PTY reattach fitting validates connection state, applies the measured grid, sends SIGWINCH, and suppresses desktop updates during mobile ownership. Tests cover reveal, cancellation, replacement, teardown, and mobile takeover behavior.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% 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
Description check ✅ Passed The description includes all required sections, explains the user-visible change, documents testing and known failures, and provides AI review, security, and follow-up details.
Title check ✅ Passed The title clearly and concisely describes preserving the reattach grid push for hidden terminal panes.
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 💡 1
📝 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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e5ac4692-e96e-4365-89c1-b7fae5dc3b5c

📥 Commits

Reviewing files that changed from the base of the PR and between c991bb2 and 5ddc11e.

📒 Files selected for processing (6)
  • src/renderer/src/components/terminal-pane/pty-connection.test.ts
  • src/renderer/src/components/terminal-pane/pty-connection.ts
  • src/renderer/src/lib/pane-manager/pane-fit-continuation-registry.ts
  • src/renderer/src/lib/pane-manager/pane-fit-deferred-continuations.ts
  • src/renderer/src/lib/pane-manager/pane-fit.test.ts
  • src/renderer/src/lib/pane-manager/pane-fit.ts

Comment on lines +220 to +225
if (isPendingSafeFitContinuationCurrent(pane, operationKey, pending)) {
if (!safeFit(pane) && options.retryIfUnmeasurable) {
if (isManagedPaneDisplayNone(pane)) {
cancel()
// Why not the frame retry: a zero-box pane can stay hidden indefinitely, so
// burning the budget only logs an exhaustion crumb. Hand it to the reveal instead.
releaseSafeFitContinuationUntilMeasurable(pane, operationKey, pending)

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.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Keep hidden-pane retry behavior for non-opt-in continuations.

These branches release every display:none continuation. releaseSafeFitContinuationUntilMeasurable resolves it as incomplete and only defers it when deferIfHidden is true. Therefore, callers such as hidden-snapshot-pty-resize, which set retryIfUnmeasurable: true without deferIfHidden, no longer use the bounded retry path.

  • src/renderer/src/lib/pane-manager/pane-fit.ts#L220-L225: only release when pending.deferIfHidden is true. Otherwise, arm the retry.
  • src/renderer/src/lib/pane-manager/pane-fit.ts#L236-L240: apply the same condition in the direct fit path.
  • src/renderer/src/lib/pane-manager/pane-fit-continuation-registry.ts#L120-L125: only release hidden entries when pending.deferIfHidden is true. Keep other entries pending for retry.
📍 Affects 2 files
  • src/renderer/src/lib/pane-manager/pane-fit.ts#L220-L225 (this comment)
  • src/renderer/src/lib/pane-manager/pane-fit.ts#L236-L240
  • src/renderer/src/lib/pane-manager/pane-fit-continuation-registry.ts#L120-L125

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Electron QA — PASS (af85ab6229)

Validated the exact PR worktree with an isolated profile and CDP-only Electron automation. window.api.app.getIdentity() matched pr-13155-hidden-pane-grid-push on brennanb2025/fix-floating-terminal-restart.

  • Before restart: Floating Workspace 920×560, PTY/stty 32×113, WINCH counter reset to 0.
  • After a full app restart with the same isolated profile/daemon: PTY remained alive at 32×113; while the panel was closed, the xterm had offsetParent=false, a display:none ancestor, and a 0×0 box.
  • Opening Show floating workspace through the real UI delivered exactly one deferred signal: POST_OPEN_WINCH_COUNT=1, still 32×113.
  • No output stall, duplicate signal, terminal_safe_fit_retry_exhausted, or target-related console/log error.
  • Disposable profile, repo, daemon, and app processes were cleaned up.

Before restart

Before restart: Floating Workspace terminal at 32x113 with WINCH counter reset to zero

After restart and reveal

After restart and reveal: Floating Workspace terminal reports POST_OPEN_WINCH_COUNT=1 at 32x113

@brennanb2025
brennanb2025 merged commit 98cc730 into main Aug 9, 2026
44 checks passed
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…tablyai#13155)

* fix(terminal): keep the reattach grid push alive for a hidden pane

A restored Floating Workspace pane is display:none for its whole PTY
reattach, because the panel always mounts closed after a restart. The
display:none guard in safeFitAndThen dropped the reattach continuation
outright, so the PTY never received the client grid and no explicit
SIGWINCH was sent to repaint the restored TUI after the replay.

Parking the continuation is not an option: the structural replay
coordinator holds live PTY bytes behind its promise, so a closed panel
would stall output indefinitely. Settle the promise immediately instead
and move the continuation to a per-pane deferred bucket that the first
measurable fit drains.

Deferral is opt-in so the bounded-degradation contract still covers every
other caller, and the continuation re-checks mobile PTY ownership at fire
time now that the wait is unbounded.

* fix(terminal): preserve replacement deferred grid push
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