Skip to content

Prioritize active terminal renderer writes#4792

Merged
nwparker merged 2 commits into
mainfrom
terminal-renderer-active-output-priority
Jun 7, 2026
Merged

Prioritize active terminal renderer writes#4792
nwparker merged 2 commits into
mainfrom
terminal-renderer-active-output-priority

Conversation

@nwparker
Copy link
Copy Markdown
Contributor

@nwparker nwparker commented Jun 7, 2026

Summary

This PR adds renderer-side active terminal output priority. It complements #4791, which prioritizes the active local PTY before output reaches the renderer. This slice handles the next boundary: once output is already queued in the renderer, the terminal output scheduler now drains the active xterm target before older background terminal queues.

The intent is to protect typing and foreground TUI responsiveness when many agents/OpenCode-style TUIs are producing output at once, including remote/SSH terminals whose output does not use the local main-process PTY scheduler hint.

What changed

  • Tracks active renderer terminal targets in pane-terminal-output-scheduler.
  • During queued output drains, picks a drainable active terminal entry first, then falls back to existing insertion-order background draining.
  • Keeps the hint scoped to the visible active terminal surface:
    • visible active tab marks exactly one pane active;
    • hidden/inactive tab clears every pane and local PTY hint;
    • split focus changes update priority only when the terminal tab is visible and active;
    • remote PTYs skip the local main IPC hint but still get renderer-side active priority.
  • Clears stale active hints when terminal output is discarded or a pane is closed.
  • Extends the pane close callback payload with the closed terminal reference so teardown can clear the exact xterm object from the scheduler WeakSet.

This does not stop reading terminals and does not increase output chunk sizes. Background terminals keep reading and queuing under the existing caps; this only changes which queued renderer target gets first chance to write when the scheduler is under load.

Why this direction

This is a small step toward the Ghostty-shaped model/view architecture we have been discussing: keep terminal state/output ingestion alive everywhere, but make foreground view work explicitly higher priority than background render work. It is intentionally narrower than a full model/view rewrite, while still moving in the same direction.

Benchmark

Command:

SKIP_BUILD=1 npx playwright test tests/e2e/artificial-opencode-terminal-load.spec.ts \
  --config tests/playwright.config.ts \
  --project electron-headless \
  --workers=1 \
  --reporter=json > /tmp/orca-opencode-renderer-active-priority-final.json

Final run on this branch:

Scenario Panes Median typing latency Worst typing latency Max timer drift Notes
Baseline active terminal 1 3.4ms 5.5ms 0.7ms Single visible active terminal baseline
Same-workspace OpenCode-style redraw 5 3.6ms 6.5ms 4.6ms 253 deferred foreground enqueues, 239 deferred foreground writes, 60 scheduled drains
Cross-workspace OpenCode-style stream 4 2.9ms 9.8ms 1.9ms 455 hidden skips, 164,349 hidden skipped chars

For comparison, the previous merged main-side active PTY priority PR (#4791) reported:

Scenario Panes Median typing latency Worst typing latency
Baseline active terminal 1 3.0ms 6.8ms
Same-workspace OpenCode-style redraw 5 3.2ms 7.1ms
Cross-workspace OpenCode-style stream 4 2.8ms 4.9ms

The numbers are in the same low-ms band. The meaningful improvement in this PR is not a broad benchmark drop for local PTYs already helped by #4791; it is closing the renderer-side gap so active visible xterm writes win even when queued renderer work comes from transports that cannot use the local PTY scheduler hint, including remote/SSH paths.

Validation

  • pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler.test.ts src/renderer/src/lib/pane-manager/pane-split-close.test.ts src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.test.ts src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.test.ts
    • 4 files passed, 59 tests passed
  • pnpm typecheck
  • pnpm exec oxlint ...touched files...
  • pnpm exec oxfmt --check ...touched files...
  • git diff --check
  • SKIP_BUILD=1 npx playwright test tests/e2e/artificial-opencode-terminal-load.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --reporter=json
    • 3 scenarios passed
  • Subagent review loop:
    • first review found lifecycle stale-active risks;
    • fixed hidden/inactive cleanup, visibility gating, discard cleanup, and close cleanup;
    • final re-review reported no issues found.

Summary by CodeRabbit

  • New Features

    • Smarter terminal output targeting so visible/active panes receive prioritized output and background panes are drained appropriately.
    • Improved handling so closing or switching panes updates which pane receives renderer output.
  • Bug Fixes

    • Resolved cases where active terminal state could be left inconsistent after pane close, hide, or switch.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 7, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b5e3083e-bbfc-4260-9456-deafb3fad49b

📥 Commits

Reviewing files that changed from the base of the PR and between 1a5d770 and 9b00490.

📒 Files selected for processing (1)
  • src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.test.ts

Walkthrough

Adds scheduler active-targeting and coordinates pane lifecycle and global effects to set/clear active terminal output targets and update main-process PTY active state across visibility, activation, and close events.

Changes

Terminal output targeting and scheduler coordination

Layer / File(s) Summary
Scheduler active-targeting API
src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler.ts, pane-terminal-output-scheduler.test.ts
activeOutputTargets WeakSet tracks eligible terminals; setActiveTerminalOutputTarget controls membership; takeNextDrainableEntry filters queued entries to active terminals; discardTerminalOutput cleans up the set. Tests verify drain ordering respects active priority and discard clears the active flag.
ClosedPaneInfo type expansion
src/renderer/src/lib/pane-manager/pane-manager-types.ts
ClosedPaneInfo gains optional terminal field so close handlers can access the terminal directly.
Pane lifecycle coordination
src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts, use-terminal-pane-lifecycle.test.ts
Exported reportActiveRendererPtyForPane now accepts PaneManager and activeAllowed flag to set active targets via scheduler and update main-process PTY state for local PTYs. On close, clears scheduler target and main-process PTY state for the closed pane, then refreshes active state for remaining panes. Tests verify active visible pane receives scheduler target (excluding remote PTYs) and inactive state clears targets and inactivates local PTYs.
Pane close callback
src/renderer/src/lib/pane-manager/pane-split-close.ts, pane-split-close.test.ts
closeManagedPane callback reformatted to multi-line form passing paneId, leafId, and terminal. Test suite for closeManagedPane verifies callback receives expected pane/leaf/terminal payload.
Global effects tab visibility sync
src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.ts, use-terminal-pane-global-effects.test.ts
syncActiveOutputTargets helper clears all targets when tab inactive/hidden, otherwise sets targets for all panes and updates main-process PTY state for local PTYs based on active pane id; includes cleanup on unmount. Test mocks and assertions validate scheduler coordination.

Possibly related PRs

  • stablyai/orca#4791: Main-process implementation that records active renderer PTYs from window.api.pty.setActiveRendererPty calls and prioritizes draining for the active PTY, complementing this PR's renderer-side coordination.
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes — adds renderer-side active terminal output prioritization so the active xterm's queued output drains before background terminal queues, complementing #4791's main-process PTY hint.

  • Active terminal output priority in takeNextDrainableEntry — scans the activeOutputTargets WeakSet first during queued-output drains, falling through to insertion-order background draining when no active terminal has drainable output.
  • Lifecycle management of active hintsuseTerminalPaneGlobalEffects syncs active targets for all panes when the tab becomes active/visible, clears all when hidden/inactive, and cleans up on unmount. useTerminalPaneLifecycle's reportActiveRendererPtyForPane updates priority on split focus changes and pane close, gated by tab visibility.
  • closedPane.terminal in close callbackClosedPaneInfo now carries the terminal reference so teardown can clear the exact xterm from the scheduler's WeakSet.
  • Discard cleanupdiscardTerminalOutput also removes the terminal from activeOutputTargets.

Cleanup is covered at every boundary: pane close, PTY close, output discard, tab hide/inactive, and effect unmount. The WeakSet provides automatic GC safety for replaced terminal objects, and the explicit delete calls handle the live-teardown paths.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.ts (1)

141-167: ⚡ Quick win

Consolidate duplicated active-targeting logic.

The syncActiveOutputTargets helper (lines 141-152) duplicates the logic from reportActiveRendererPtyForPane in use-terminal-pane-lifecycle.ts. Both iterate over panes/transports and set active state identically.

♻️ Refactor to use the exported helper
+import { reportActiveRendererPtyForPane } from './use-terminal-pane-lifecycle'
+
 export function useTerminalPaneGlobalEffects({
   // ...
 }: UseTerminalPaneGlobalEffectsArgs): void {
   // ...
   useEffect(() => {
     const manager = managerRef.current
-    const syncActiveOutputTargets = (activePaneId: number | null): void => {
-      for (const pane of manager?.getPanes() ?? []) {
-        setActiveTerminalOutputTarget(pane.terminal, pane.id === activePaneId)
-      }
-      for (const [paneId, transport] of paneTransportsRef.current) {
-        const ptyId = transport.getPtyId()
-        if (!ptyId || ptyId.startsWith('remote:')) {
-          continue
-        }
-        window.api.pty.setActiveRendererPty?.(ptyId, paneId === activePaneId)
-      }
-    }
 
     if (!isActive || !isVisible || !manager) {
-      syncActiveOutputTargets(null)
+      reportActiveRendererPtyForPane(paneTransportsRef.current, manager, null, false)
       return
     }
 
     const activePane = manager.getActivePane()
     const activePaneId = activePane?.id ?? null
-    // Why: active output hints must clear every pane when a tab hides; split
-    // focus can change after this effect's active-pane snapshot.
-    syncActiveOutputTargets(activePaneId)
+    reportActiveRendererPtyForPane(paneTransportsRef.current, manager, activePaneId, true)
     return () => {
-      syncActiveOutputTargets(null)
+      reportActiveRendererPtyForPane(paneTransportsRef.current, manager, null, false)
     }
   }, [isActive, isVisible, managerRef, paneTransportsRef])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.ts`
around lines 141 - 167, Replace the duplicated syncActiveOutputTargets
implementation with the shared helper reportActiveRendererPtyForPane from
use-terminal-pane-lifecycle.ts: remove the local syncActiveOutputTargets
function and instead import reportActiveRendererPtyForPane and call it with the
same inputs (manager.getPanes()/manager, paneTransportsRef.current and the
activePaneId or null) so setActiveTerminalOutputTarget and
window.api.pty.setActiveRendererPty logic is centralized; ensure the effect
still calls reportActiveRendererPtyForPane(activePaneId) when active and the
cleanup/hidden path calls reportActiveRendererPtyForPane(null).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.test.ts`:
- Line 191: The test currently only asserts window.api.pty.setActiveRendererPty
was not called with ('remote:env@@pty-2', true) but can still be invoked with
other args; change the assertion to ensure no main IPC call for that remote PTY
by using
expect(window.api.pty.setActiveRendererPty).not.toHaveBeenCalledWith('remote:env@@pty-2',
expect.anything()), so any invocation carrying the 'remote:env@@pty-2' id is
rejected; update the assertion in use-terminal-pane-lifecycle.test.ts
accordingly referencing window.api.pty.setActiveRendererPty.

---

Nitpick comments:
In
`@src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.ts`:
- Around line 141-167: Replace the duplicated syncActiveOutputTargets
implementation with the shared helper reportActiveRendererPtyForPane from
use-terminal-pane-lifecycle.ts: remove the local syncActiveOutputTargets
function and instead import reportActiveRendererPtyForPane and call it with the
same inputs (manager.getPanes()/manager, paneTransportsRef.current and the
activePaneId or null) so setActiveTerminalOutputTarget and
window.api.pty.setActiveRendererPty logic is centralized; ensure the effect
still calls reportActiveRendererPtyForPane(activePaneId) when active and the
cleanup/hidden path calls reportActiveRendererPtyForPane(null).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d8a35a67-e7e4-4559-a97e-0cd017cf06c1

📥 Commits

Reviewing files that changed from the base of the PR and between 3704c76 and 1a5d770.

📒 Files selected for processing (9)
  • src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.test.ts
  • src/renderer/src/components/terminal-pane/use-terminal-pane-global-effects.ts
  • src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.test.ts
  • src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts
  • src/renderer/src/lib/pane-manager/pane-manager-types.ts
  • src/renderer/src/lib/pane-manager/pane-split-close.test.ts
  • src/renderer/src/lib/pane-manager/pane-split-close.ts
  • src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler.test.ts
  • src/renderer/src/lib/pane-manager/pane-terminal-output-scheduler.ts

Comment thread src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.test.ts Outdated
@nwparker nwparker merged commit b88ff28 into main Jun 7, 2026
2 checks passed
@nwparker nwparker deleted the terminal-renderer-active-output-priority branch June 7, 2026 14:19
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