Skip to content

fix(desktop): re-send terminal PTY dims when a hidden pane becomes visible - #5725

Merged
Kitenite merged 2 commits into
mainfrom
shell-aardvark
Jul 17, 2026
Merged

Kitenite merged 2 commits into
mainfrom
shell-aardvark

Conversation

@Kitenite

@Kitenite Kitenite commented Jul 17, 2026 •

Copy link
Copy Markdown
Member

Summary

  • v2 terminal panes hidden via CSS (zero-size container) and shown again now re-send their PTY dimensions on reveal, even when the fitted dims are unchanged — mirroring VS Code's setVisible → _resize behavior.
  • Fixes the case where the PTY was resized elsewhere while the pane was hidden (another window on the same terminal, host-side drift): the re-send is then a real size change, the program gets its SIGWINCH, and the TUI repaints at the correct size on reveal.
  • Implementation: the ResizeObserver scheduler's post-fit notification is now unconditional (forceNotify) instead of gated on changed dims. The observer only fires on genuine container changes, so this covers the reveal edge with no state — dims either changed (a resize was being sent anyway) or it's a reveal / sub-cell wiggle, where a same-size re-send is a kernel no-op on both ends (host modeTracker.resize also early-returns on same dims). VS Code's resize debouncer likewise has no changed-dims gate. Net +9/−2 in one file.

Notes

  • When both ends already agree on the size, the re-send is a kernel no-op by design — a same-size TIOCSWINSZ delivers no SIGWINCH (verified against a real pty + Node child), so idle reveals stay invisible to running programs.
  • Deliberately minimal: SIGWINCH-jiggle variants (host-side nudge on attach, a new nudge protocol message, a rows−1→rows resize dance, a reveal-edge latch) were prototyped and rejected in favor of this VS Code-parity re-send.

Testing

  • CDP against the real dev app (live PTY, 136×48): idle/hidden → 0 resize sends; reveal → exactly 1 send with unchanged dims (the forced path — old code sent nothing); 3× rapid hide/show → 1 send (debounced). See PR comment for the run against the first revision; re-ran identically on the final one.
  • bun test src/renderer/lib/terminal/ (264 tests), desktop typecheck, repo lint all green

Summary by CodeRabbit

  • Bug Fixes
    • Improved terminal resizing behavior when switching from a hidden or collapsed view.
    • Ensured resize notifications are reliably triggered even when terminal dimensions remain unchanged, so layout updates continue to apply after non-visible observations.

…sible

The v2 resize scheduler already sees both edges of a CSS hide/show (the
ResizeObserver reports 0x0 on hide, real dims on reveal) but only sent a
resize when the fitted dimensions changed. Track the hide edge and force
the notification on reveal even when dims are unchanged — VS Code's
setVisible -> _resize parity — so the host session resyncs a PTY that was
resized elsewhere (e.g. another window on the same terminal) while this
pane was hidden. The re-send is a kernel no-op when both ends already
agree (same-size TIOCSWINSZ delivers no SIGWINCH; verified against a real
pty), so idle reveals stay silent.
@ghost

ghost commented Jul 17, 2026 •

Copy link
Copy Markdown

Ready to review this PR? Stage has broken it down into 2 individual chapters for you:

Title
1 Add forceNotify option to terminal resize logic
2 Force PTY re-sync on terminal reveal
Open in Stage

Chapters generated by Stage for commit 375f928 on Jul 17, 2026 1:19am UTC.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Terminal resize handling adds an optional forced-notification flag and enables it for scheduled measurements, allowing onResize to run even when terminal dimensions are unchanged.

Changes

Terminal resize notification flow

Layer / File(s) Summary
Force resize notification during scheduled measurements
apps/desktop/src/renderer/lib/terminal/terminal-runtime.ts
measureAndResize accepts forceNotify, includes it in the resize callback condition, and the scheduler enables it for each scheduled run.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: avipeltz

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Title check ✅ Passed The title is concise, conventional, and accurately summarizes the main terminal resize behavior change.
Description check ✅ Passed The description covers what changed, why it changed, and how it was tested; only the template checklist is left incomplete.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch shell-aardvark

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.

@greptile-apps

greptile-apps Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a PTY dimension desync that occurs when a terminal pane hidden via CSS becomes visible again. By latching a revealPending flag in createResizeScheduler when the container collapses to 0×0, the next reveal unconditionally re-sends PTY dimensions to the host via onResize, even when fitAddon.fit() reports no size change.

  • measureAndResize gains an options.forceNotify parameter that bypasses the prevCols/prevRows equality guard, triggering onResize?.() on every reveal after a hide.
  • createResizeScheduler latches revealPending = true on any zero-size observation and clears it atomically inside run(), so multiple hides before a reveal are safe and no extra notifications are sent on idle reveals when both sides already agree on size.

Confidence Score: 4/5

Safe to merge — the change is minimal, correctly scoped, and the forceNotify path is a no-op at the kernel level when both sides already agree on terminal size.

One file is touched with 13 lines added. The hide→reveal flag logic is straightforward: revealPending is latched on a zero-size observation and consumed atomically in run(), so no state leaks across scheduler lifetimes. The only note is that createResizeScheduler and its new revealPending branch have no dedicated unit tests, making future regressions harder to catch early.

apps/desktop/src/renderer/lib/terminal/terminal-runtime.ts — specifically the untested hide/reveal cycle through the new revealPending flag.

Important Files Changed

Filename Overview
apps/desktop/src/renderer/lib/terminal/terminal-runtime.ts Adds revealPending flag to createResizeScheduler that forces onResize to fire on reveal even when terminal dims are unchanged, mirroring VS Code's setVisible behavior. Logic is correct and well-scoped.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant RO as ResizeObserver
    participant RS as createResizeScheduler
    participant MAR as measureAndResize
    participant CB as onResize

    Note over RO,CB: Pane hidden via CSS (container collapses to 0x0)
    RO->>RS: "observe entry width=0 height=0"
    RS->>RS: "revealPending = true"
    RS->>RS: dispose clears debounce timeout
    RS-->>RO: return early no setTimeout

    Note over RO,CB: Pane becomes visible again
    RO->>RS: "observe entry width=W height=H"
    RS->>RS: dispose then setTimeout run 75ms
    RS->>RS: "run reads forceNotify=true resets revealPending=false"
    RS->>MAR: "measureAndResize with forceNotify=true"
    MAR->>MAR: fitAddon.fit may or may not change cols/rows
    MAR->>CB: onResize called unconditionally via forceNotify
    CB-->>MAR: PTY dims re-sent to host TIOCSWINSZ
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant RO as ResizeObserver
    participant RS as createResizeScheduler
    participant MAR as measureAndResize
    participant CB as onResize

    Note over RO,CB: Pane hidden via CSS (container collapses to 0x0)
    RO->>RS: "observe entry width=0 height=0"
    RS->>RS: "revealPending = true"
    RS->>RS: dispose clears debounce timeout
    RS-->>RO: return early no setTimeout

    Note over RO,CB: Pane becomes visible again
    RO->>RS: "observe entry width=W height=H"
    RS->>RS: dispose then setTimeout run 75ms
    RS->>RS: "run reads forceNotify=true resets revealPending=false"
    RS->>MAR: "measureAndResize with forceNotify=true"
    MAR->>MAR: fitAddon.fit may or may not change cols/rows
    MAR->>CB: onResize called unconditionally via forceNotify
    CB-->>MAR: PTY dims re-sent to host TIOCSWINSZ
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
apps/desktop/src/renderer/lib/terminal/terminal-runtime.ts:197-215
**No test coverage for `revealPending` path**

`createResizeScheduler` and `measureAndResize` are not covered by any test file in the `terminal/` directory — the 264-test suite exercises other modules. The hide→reveal→`forceNotify` flow is the core invariant of this fix, and it's easy to accidentally regress (e.g., `revealPending` not being reset before `measureAndResize` is called, or the flag surviving a scheduler recreation). A lightweight unit test that stubs `ResizeObserver` entries and asserts `onResize` is called after a 0→positive cycle would anchor the behavior.

Reviews (1): Last reviewed commit: "fix(desktop): re-send terminal PTY dims ..." | Re-trigger Greptile

Comment on lines 197 to 215

const run = () => {
timeoutId = null;
measureAndResize(runtime, onResize);
const forceNotify = revealPending;
revealPending = false;
measureAndResize(runtime, onResize, { forceNotify });
};

const observe: ResizeObserverCallback = (entries) => {
if (
entries.some(
(entry) =>
entry.contentRect.width <= 0 || entry.contentRect.height <= 0,
)
) {
revealPending = true;
dispose();
return;
}

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.

P2 No test coverage for revealPending path

createResizeScheduler and measureAndResize are not covered by any test file in the terminal/ directory — the 264-test suite exercises other modules. The hide→reveal→forceNotify flow is the core invariant of this fix, and it's easy to accidentally regress (e.g., revealPending not being reset before measureAndResize is called, or the flag surviving a scheduler recreation). A lightweight unit test that stubs ResizeObserver entries and asserts onResize is called after a 0→positive cycle would anchor the behavior.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/lib/terminal/terminal-runtime.ts
Line: 197-215

Comment:
**No test coverage for `revealPending` path**

`createResizeScheduler` and `measureAndResize` are not covered by any test file in the `terminal/` directory — the 264-test suite exercises other modules. The hide→reveal→`forceNotify` flow is the core invariant of this fix, and it's easy to accidentally regress (e.g., `revealPending` not being reset before `measureAndResize` is called, or the flag surviving a scheduler recreation). A lightweight unit test that stubs `ResizeObserver` entries and asserts `onResize` is called after a 0→positive cycle would anchor the behavior.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 1 file

Architecture diagram
sequenceDiagram
    participant CSS as CSS Display
    participant RO as ResizeObserver
    participant Sched as createResizeScheduler
    participant RMS as measureAndResize
    participant Term as xterm.js Terminal
    participant PTY as PTY Process
    
    Note over CSS,PTY: Terminal pane visibility lifecycle
    
    RO->>RO: Observe container element
    
    alt Pane hidden via CSS (zero-size container)
        CSS-->>RO: contentRect = 0x0
        RO->>Sched: observe(entry)
        Sched->>Sched: Set revealPending = true
        Sched->>Sched: Debounce pause
        Note over Sched: No resize notification sent<br/>while hidden
    end
    
    alt Pane becomes visible again
        CSS-->>RO: contentRect = real dimensions
        RO->>Sched: observe(entry)
        Sched->>Sched: Schedule run()
        Sched->>RMS: run() with forceNotify=true
        Note over RMS: forceNotify overrides<br/>size-change check
        RMS->>Term: measure container dims
        Term->>Term: terminal.resize(cols, rows)
        alt forceNotify is true
            RMS->>RMS: Force emit onResize
            RMS->>PTY: Send TIOCSWINSZ
            Note over PTY: Resizes kernel pty
            alt PTY size actually changed while hidden
                PTY->>PTY: Deliver SIGWINCH
                PTY->>Term: Program repaints TUI
            else Same size (no change)
                Note over PTY: Kernel no-op,<br/>no SIGWINCH sent
            end
        else Size actually changed
            RMS->>RMS: Emit onResize (normal path)
            RMS->>PTY: Send TIOCSWINSZ
        end
    end
    
    Note over Term,PTY: Debounce and parser-idle gating unchanged<br/>from previous implementation
Loading

Re-trigger cubic

@Kitenite

Copy link
Copy Markdown
Member Author

CDP verification (real dev app, live PTY): instrumented WebSocket.prototype.send in the renderer, opened a terminal (136×48, transport open), then drove the container's visibility directly:

Scenario Resize messages sent
Control: visible, no changes, 800ms 0
While hidden (display:none, 400ms) 0
On reveal (same dims) 1 — {type:"resize", cols:136, rows:48}
3× rapid hide/show thrash 1 (debounce collapses the burst)

The reveal-edge send carries dims identical to pre-hide, i.e. the forceNotify path — the old code's changed-dims gate would have sent nothing. No spurious sends while idle or hidden.

…er observer-driven fits

The ResizeObserver only fires on genuine container changes, so an
unconditional post-fit notify covers the reveal edge without tracking it:
dims either changed (was already sent) or it's a reveal/sub-cell wiggle,
where a same-size re-send is a kernel no-op on both ends. Matches VS Code,
whose resize debouncer also has no changed-dims gate. CDP re-verified:
idle 0 sends, reveal 1 same-dims send, 3x hide/show thrash collapses to 1.
@Kitenite
Kitenite merged commit 953e8fb into main Jul 17, 2026
11 checks passed
@Kitenite
Kitenite deleted the shell-aardvark branch July 17, 2026 01:27
@github-actions

Copy link
Copy Markdown
Contributor

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ⚠️ Neon database branch

Thank you for your contribution! 🎉

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