Skip to content

[codex] Fix renderer stress degradation#4500

Merged
Kitenite merged 25 commits into
mainfrom
stress-test-degradation
May 14, 2026
Merged

[codex] Fix renderer stress degradation#4500
Kitenite merged 25 commits into
mainfrom
stress-test-degradation

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented May 13, 2026

Summary

This PR hardens the desktop renderer and host-service paths that degraded during heavy workspace usage and rapid workspace switching.

  • Adds renderer stress coverage for workspace switching, route sweeps, heavy pane/tab/browser/diff actions, large realistic git fixtures, and terminal/WebGL pressure.
  • Keeps workspace switching responsive by preserving navigation state, reducing inactive sidebar git/host churn, and timing out slow host identity shell calls.
  • Preserves browser and terminal state across workspace switches while unloading heavy terminal image/WebGL addons when terminals are parked.
  • Adds immediate terminal WebGL fallback so forced/lost contexts switch to DOM rendering without the xterm 3s blank-window delay.
  • Hardens git status and pull-request runtime behavior for missing worktrees and external git-status contention cases.

Root Cause

Rapid workspace switching was forcing too much synchronous renderer work while retaining many heavy panes. Terminal WebGL and image addons could also retain GPU/WASM pressure across parked runtimes, and xterm's built-in WebGL context-loss notification waits several seconds before falling back, which left terminals visually blank during context loss.

Validation

  • bun run lint
  • bun run typecheck
  • bun --cwd apps/desktop test src/renderer/lib/terminal/terminal-image-addon-controller.test.ts src/renderer/lib/terminal/terminal-webgl-addon-controller.test.ts
  • bun --cwd apps/desktop stress:renderer -- --port 9333 --scenario terminal-heavy --iterations 120 --terminal-iterations 120 --terminal-tab-count 24 --terminal-panes-per-tab 4 --terminal-lines 40 --terminal-payload-bytes 1024 --progress-every 20 --timeout-ms 240000 --max-heartbeat-delay-ms 1500 --max-long-task-ms 2000 --settle-ms 1000
  • Direct renderer probe confirmed forced WebGL loss moved active terminals from canvas-only rendering to visible DOM rows within 150ms.

Summary by cubic

Fixes renderer slowdowns and freezes under heavy workspace switching and terminal pressure by adding stress tooling, browser layout scheduling, tab/changes windowing, and tighter terminal/host-service lifecycles. Adds CDP profiling hooks and disables session recording to keep the UI responsive.

  • Performance and Stability

    • Workspace switching: queue/coalesce navigation, show pending selection instantly via a small store, and remount WorkspaceProvider per workspace to isolate pane state (tests added).
    • Terminal resilience: explicit image/WebGL addon controllers with immediate WebGL→DOM fallback; renderer-side PTY output backpressure/chunking; focus active runtime; hidden/contained parking; WS transport listener cleanup (tests for addons/transport/runtime).
    • Browser/runtime layout: coalesce layout work with a per-frame budget and idle timeout; limit layouts per frame; destroy browser runtimes when panes close.
    • Fewer re-renders and DOM: memoized TerminalPane, tab items, and sortable items; windowed TabBar (visible range + overscan); virtualized “Changes” list (folder grouping) and cheaper overflow‑fade measuring; system font discovery batched on idle.
    • Lighter data/compute: sidebar uses git.getDiffStats; gate useGitStatus, changeset, review, task search, external‑editor lookup, and diff refs by active tab/search.
    • Host-service hardening: coalesce main‑workspace setup during parallel creates; retryable .git/config writes via gitConfigWrite; PullRequestRuntimeManager.stop() awaits background tasks; close filesystem on shutdown; bounded machine‑id timeouts; new git.getDiffStats for sidebar totals; concurrent workspace.destroy and create/delete churn leave no duplicates or stale worktrees (tests).
    • Robustness: ignore non‑string workspace run commands (tests); navigation queuing is unit‑tested.
    • Telemetry: disable PostHog session recording and block terminal surfaces/canvas capture (tests). React DevTools are skipped when CDP profiling is enabled.
  • New Features

    • Renderer stress tooling: stress:renderer and stress:renderer:fixtures with route‑sweep, terminal‑heavy, workspace‑switch, and heavy workspace scenarios; CDP control via SUPERSET_RENDERER_STRESS_CDP_PORT; CPU profiling and React render probes; dev‑only window.__SUPERSET_RENDERER_STRESS_NAVIGATE__.
    • Workspace stress bridge exposes pane/tab/terminal metrics and actions (incl. forced WebGL loss); workspace rows include data hooks for automation.

Written for commit 8a6efae. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Renderer stress testing framework and dev helpers for workload simulation and diagnostics
    • Fast git diff‑stats endpoint for sidebar change summaries
    • Terminal image/WebGL addon controllers with explicit lifecycle controls
  • Performance Improvements

    • Conditional data fetching to avoid work for inactive tabs/hooks
    • Safer background-task shutdown for pull‑request processing
    • Provider remounting to isolate workspace state
  • Bug Fixes

    • More reliable workspace navigation with pending-state handling
    • Improved WebGL context‑loss handling and terminal cleanup
  • Chores

    • Added local CLI scripts to prepare/run renderer stress fixtures
  • Tests

    • New renderer stress, terminal addon, navigation, git, and workspace lifecycle tests

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a renderer stress fixture generator and CDP harness, refactors terminal addon lifecycle into controller modules, extends terminal runtime/registry for stress operations, introduces pending V2 workspace navigation with serialized navigation, gates tab-specific queries, and implements a git diff-stats TRPC endpoint plus tests.

Changes

Renderer Stress Testing Feature

Layer / File(s) Summary
Stress fixtures, harness, and main process integration
apps/desktop/package.json, apps/desktop/scripts/prepare-renderer-stress-fixtures.ts, apps/desktop/scripts/stress-renderer.ts, apps/desktop/src/main/index.ts, apps/desktop/src/renderer/index.tsx
CLI generates multi-worktree git repos and seeds TanStack/host DBs; CDP harness attaches to renderer and injects stress runner; package scripts added; dev-only remote-debugging and renderer navigation helper exposed.
Renderer stress workspace bridge (dev-only)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useRendererStressWorkspaceBridge/*, page.tsx
Dev-only hook attaches window bridge to mutate workspace store, generate synthetic panes/layouts, write stress output to terminals, and force WebGL context loss while returning debug summaries.

Terminal Addon Controller Architecture Refactoring

Layer / File(s) Summary
Image & WebGL addon controllers + tests
apps/desktop/src/renderer/lib/terminal/terminal-image-addon-controller.ts, terminal-image-addon-controller.test.ts, apps/desktop/src/renderer/lib/terminal/terminal-webgl-addon-controller.ts, terminal-webgl-addon-controller.test.ts
New controllers manage lazy loading, lifecycle, and failure/fallback for xterm ImageAddon and WebglAddon; tests cover load/dispose and context-loss behavior.
Addon integration, runtime, and cache
apps/desktop/src/renderer/lib/terminal/terminal-addons.ts, terminal-runtime.ts, apps/desktop/src/renderer/screens/main/.../Terminal/helpers.ts, v1-terminal-cache.ts
loadAddons returns enable/disable controller hooks; TerminalRuntime wires these callbacks into attach/detach and deferred disposal; cached terminals call enable/disable on attach/detach.
Terminal runtime-registry stress APIs
apps/desktop/src/renderer/lib/terminal/terminal-runtime-registry.ts
Adds writeForStress, forceWebglContextLossForStress, and getStressDebugInfo for stress scenarios and diagnostics.

V2 Workspace Navigation State Management and UI Updates

Layer / File(s) Summary
V2 navigation store & serialized navigation
apps/desktop/src/renderer/stores/v2-workspace-navigation.ts, apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/workspace-navigation.ts, workspace-navigation.test.ts
Zustand store for pendingWorkspaceId; navigateToV2Workspace queues/coalesces/plain-vs-focused logic and clears pending state; tests validate coalescing and overrides.
Sidebar/workspaces UI and shortcuts
dashboard sidebar components, V2WorkspacesList/*, SortableWorkspaceItem/*, workspace item hooks, and shortcuts
Sidebar derives active workspace from pending-or-current, threads activeWorkspaceId/isActive, replaces hover callbacks with onWorkspaceHover, adds stress-data attributes, and uses pending state for keyboard navigation targets.

Tab-Based Query Optimization and Efficient Data Fetching

Layer / File(s) Summary
Git status/diff and tab gating hooks
useGitStatus, useDiffStats, useChangesTab, useReviewTab, useChangeset, useOpenInExternalEditor, useSidebarDiffRef, useHybridSearch
Hooks accept enabled flags to gate React Query and expensive work to active tabs; useDiffStats now queries host-service git.getDiffStats.
WorkspaceSidebar gating
WorkspaceSidebar.tsx
Computes active tab flags and enables Changes/Review hooks only when those tabs are active.

Git Diff Stats Query and Numstat Parsing

Layer / File(s) Summary
Numstat parsing and getDiffStats endpoint
packages/host-service/src/trpc/router/git/utils/git-helpers.ts, packages/host-service/src/trpc/router/git/git.ts, tests
parseNumstatRecords returns structured records preserving rename oldPath; gitRouter.getDiffStats aggregates additions/deletions across against-base, staged, unstaged, and untracked; tests validate parsing and totals.

Pull Request Runtime Graceful Shutdown & App Disposal

Layer / File(s) Summary
PR runtime and app dispose
packages/host-service/src/runtime/pull-requests/pull-requests.ts, tests, packages/host-service/src/app.ts
PullRequestRuntimeManager tracks background tasks and implements async stop() that drains in-flight work; createApp.dispose awaits stop() and attempts filesystem.close() with error handling; tests added for shutdown sequencing.

Misc, Tests, and Infra

Layer / File(s) Summary
Misc infra and tests
apps/desktop/src/shared/workspace-run-definition.ts, tests, packages/host-service/src/trpc/router/git/utils/config-write.ts, git-config usages, packages/shared/src/host-info.ts, integration tests
Workspace run commands accept unknown[] and filter strings; gitConfigWrite typing used and applied; machine-id commands bounded by timeout; ensureMainWorkspace coalesces concurrent ensures; integration/concurrency tests updated to assert expected warnings/behaviors.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

🐰 I nibble at code with eager paws,

Fixtures sprout and CDP hums,
Addons tamed by careful laws,
Workspaces churn — the test drum drums.
Hop, carrot, debug — joy becomes!

✨ 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 stress-test-degradation

@capy-ai
Copy link
Copy Markdown

capy-ai Bot commented May 13, 2026

Capy auto-review is paused for this organization because the monthly auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 57 files

You’re at about 91% of the monthly review limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/scripts/stress-renderer.ts">

<violation number="1" location="apps/desktop/scripts/stress-renderer.ts:1200">
P1: `rendererStress` is evaluated in a different runtime, so this outer constant is out of scope and will throw at runtime in the WebGL recovery branch.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/desktop/scripts/stress-renderer.ts Outdated
Copy link
Copy Markdown
Contributor

@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: 5

🧹 Nitpick comments (2)
apps/desktop/src/renderer/lib/terminal/terminal-runtime.ts (1)

123-138: ⚡ Quick win

Consider extracting the double-RAF pattern to a shared utility.

This function uses the same double-RAF pattern as afterPendingXtermRefresh in terminal-webgl-addon-controller.ts (lines 13-21). Both defer work until after the next paint with identical fallback logic for missing requestAnimationFrame.

♻️ Refactoring suggestion

Extract to a shared utility module (e.g., terminal-utils.ts or terminal-scheduling.ts):

export function afterPendingRefresh(callback: () => void): void {
  if (typeof requestAnimationFrame !== "function") {
    setTimeout(callback, 0);
    return;
  }
  requestAnimationFrame(() => {
    requestAnimationFrame(callback);
  });
}

Then both modules can import and use this helper.

🤖 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 `@apps/desktop/src/renderer/lib/terminal/terminal-runtime.ts` around lines 123
- 138, The double-RAF scheduling logic duplicated in
disposeTerminalAfterPendingRefresh and afterPendingXtermRefresh should be
extracted into a shared helper (e.g., afterPendingRefresh) and imported where
needed; create a new function afterPendingRefresh(callback: () => void) that
implements the requestAnimationFrame -> requestAnimationFrame fallback to
setTimeout behavior, replace the inline double-RAF in
disposeTerminalAfterPendingRefresh (and in terminal-webgl-addon-controller.ts
where afterPendingXtermRefresh exists) to call afterPendingRefresh(() =>
terminal.dispose()) or afterPendingRefresh(yourCallback), and remove the
duplicated code so both modules import and use the single utility.
packages/host-service/test/integration/git.integration.test.ts (1)

62-75: ⚡ Quick win

Add a same-path staged+unstaged case for getDiffStats.

This test only covers disjoint files. Add one case where the same file has both staged and unstaged edits, then assert totals include both contributions.

🤖 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 `@packages/host-service/test/integration/git.integration.test.ts` around lines
62 - 75, Add a new same-path staged+unstaged scenario in the "getDiffStats
returns sidebar totals without full status payload" test: create a file (e.g.,
"mixed.txt"), write initial lines, stage those lines with
scenario.repo.git.add("mixed.txt"), then modify/append the file with additional
lines without staging the second change, and then call
scenario.host.trpc.git.getDiffStats.query({ workspaceId: scenario.workspaceId })
and update the expect(stats).toEqual(...) to include additions from both the
staged and unstaged edits; refer to the existing helpers used in the test such
as writeFileSync, scenario.repo.git.add and
scenario.host.trpc.git.getDiffStats.query to locate where to insert the new
writes and assertion.
🤖 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 `@apps/desktop/src/renderer/lib/terminal/terminal-runtime-registry.ts`:
- Around line 174-183: The listEntries method currently treats an
instanceId-only call as unscoped and returns all entries; update
listEntries(terminalId?: string, instanceId?: string) so that if instanceId is
provided without terminalId it returns an empty array (guard the instanceId-only
case) instead of falling through to Array.from(this.entries.values()); modify
the conditional flow inside listEntries (the function name and the parameters
terminalId/instanceId and use of this.getEntry / this.getEntries / this.entries)
to check for instanceId && !terminalId and return [] early.
- Around line 66-71: getWebglContext currently probes canvases by calling
canvas.getContext("webgl2" / "webgl" / "experimental-webgl"), which can allocate
new GPU contexts; stop probing in getWebglContext and instead rely on the
runtime to consult already-tracked active WebGL contexts or the xterm WebGL
addon state. Replace the getWebglContext logic so it does not call
canvas.getContext; either accept an existing
WebGLRenderingContext/WebGL2RenderingContext passed in from the terminal runtime
wrapper or query a registry/flag maintained by the runtime (or the WebGL addon)
that tracks active contexts, and update any code that invokes getWebglContext to
provide or look up the pre-existing context rather than creating one.

In `@packages/host-service/src/trpc/router/git/git.ts`:
- Around line 67-77: The current applyNumstatToStatsMap function (and the
similar blocks at the other occurrences) overwrites existing DiffStats for a
path with Map.set, losing prior additions/deletions when a file appears in
multiple buckets; change the logic to read the existing entry
(byPath.get(record.path) || {additions:0,deletions:0}), sum record.additions and
record.deletions into the existing totals, and then put the aggregated object
back into the map (byPath.set(record.path, { additions: existing.additions +
record.additions, deletions: existing.deletions + record.deletions })); apply
the same aggregation fix to the analogous code blocks referenced in the review
so totals accumulate instead of being replaced.

In
`@packages/host-service/src/trpc/router/workspace-creation/shared/git-config.ts`:
- Line 9: Summary: Remove the ad-hoc cast `git as Parameters<typeof
gitConfigWrite>[0]` by fixing the underlying type mismatch between GitClient and
SimpleGit. Fix: update the type definitions so they align — either redefine
GitClient to be an alias of SimpleGit (e.g., export type GitClient = SimpleGit |
Awaited<ReturnType<GitFactory>>> simplified to SimpleGit) or change the
gitConfigWrite signature to accept the GitClient alias (or a union like
SimpleGit | GitClient) so callers like gitConfigWrite(git, ...) no longer need
casts; apply the same change consistently for usages in git-config.ts,
adopt-existing-worktree.ts, and workspaces.ts. Ensure the chosen approach
preserves type safety and removes the need for `as Parameters<typeof
gitConfigWrite>[0]` casts.

In `@packages/host-service/src/trpc/router/workspaces/workspaces.ts`:
- Around line 911-919: The gitConfigWrite call is writing to the main repo
because it lacks the worktree '-C' argument; update the gitConfigWrite
invocation (the one that writes branch.${resolvedBranch}.base with
baseShortName) to include the worktreePath via the '-C' option so it targets the
worktree git config (match the pattern used in recordBaseBranchConfig and
enablePushAutoSetupRemote), i.e. pass the worktreePath as the first CLI args
array element after '-C' to gitConfigWrite so the config is written to the
correct worktree instead of the main repo.

---

Nitpick comments:
In `@apps/desktop/src/renderer/lib/terminal/terminal-runtime.ts`:
- Around line 123-138: The double-RAF scheduling logic duplicated in
disposeTerminalAfterPendingRefresh and afterPendingXtermRefresh should be
extracted into a shared helper (e.g., afterPendingRefresh) and imported where
needed; create a new function afterPendingRefresh(callback: () => void) that
implements the requestAnimationFrame -> requestAnimationFrame fallback to
setTimeout behavior, replace the inline double-RAF in
disposeTerminalAfterPendingRefresh (and in terminal-webgl-addon-controller.ts
where afterPendingXtermRefresh exists) to call afterPendingRefresh(() =>
terminal.dispose()) or afterPendingRefresh(yourCallback), and remove the
duplicated code so both modules import and use the single utility.

In `@packages/host-service/test/integration/git.integration.test.ts`:
- Around line 62-75: Add a new same-path staged+unstaged scenario in the
"getDiffStats returns sidebar totals without full status payload" test: create a
file (e.g., "mixed.txt"), write initial lines, stage those lines with
scenario.repo.git.add("mixed.txt"), then modify/append the file with additional
lines without staging the second change, and then call
scenario.host.trpc.git.getDiffStats.query({ workspaceId: scenario.workspaceId })
and update the expect(stats).toEqual(...) to include additions from both the
staged and unstaged edits; refer to the existing helpers used in the test such
as writeFileSync, scenario.repo.git.add and
scenario.host.trpc.git.getDiffStats.query to locate where to insert the new
writes and assertion.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: e44c6128-48bc-41e2-8feb-d046640c1592

📥 Commits

Reviewing files that changed from the base of the PR and between f3e3e93 and f126521.

📒 Files selected for processing (57)
  • apps/desktop/package.json
  • apps/desktop/scripts/prepare-renderer-stress-fixtures.ts
  • apps/desktop/scripts/stress-renderer.ts
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/renderer/hooks/host-service/useDiffStats/useDiffStats.ts
  • apps/desktop/src/renderer/hooks/host-service/useGitStatus/useGitStatus.ts
  • apps/desktop/src/renderer/index.tsx
  • apps/desktop/src/renderer/lib/terminal/terminal-addons.ts
  • apps/desktop/src/renderer/lib/terminal/terminal-image-addon-controller.test.ts
  • apps/desktop/src/renderer/lib/terminal/terminal-image-addon-controller.ts
  • apps/desktop/src/renderer/lib/terminal/terminal-runtime-registry.ts
  • apps/desktop/src/renderer/lib/terminal/terminal-runtime.ts
  • apps/desktop/src/renderer/lib/terminal/terminal-webgl-addon-controller.test.ts
  • apps/desktop/src/renderer/lib/terminal/terminal-webgl-addon-controller.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarHoverCardOverlay/DashboardSidebarHoverCardOverlay.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/DashboardSidebarProjectSection.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarCollapsedProjectContent/DashboardSidebarCollapsedProjectContent.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarExpandedProjectContent/DashboardSidebarExpandedProjectContent.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/SortableWorkspaceItem/SortableWorkspaceItem.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useHybridSearch/useHybridSearch.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useTasksData/useTasksData.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/workspace-navigation.test.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/workspace-navigation.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useReviewTab/useReviewTab.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useChangeset/useChangeset.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useOpenInExternalEditor/useOpenInExternalEditor.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useRendererStressWorkspaceBridge/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useRendererStressWorkspaceBridge/useRendererStressWorkspaceBridge.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useSidebarDiffRef/useSidebarDiffRef.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/V2WorkspacesList.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/helpers.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/v1-terminal-cache.ts
  • apps/desktop/src/renderer/stores/v2-workspace-navigation.ts
  • apps/desktop/src/shared/workspace-run-definition.test.ts
  • apps/desktop/src/shared/workspace-run-definition.ts
  • packages/host-service/src/app.ts
  • packages/host-service/src/runtime/pull-requests/pull-requests.test.ts
  • packages/host-service/src/runtime/pull-requests/pull-requests.ts
  • packages/host-service/src/trpc/router/git/git.ts
  • packages/host-service/src/trpc/router/git/utils/git-helpers.test.ts
  • packages/host-service/src/trpc/router/git/utils/git-helpers.ts
  • packages/host-service/src/trpc/router/workspace-creation/shared/git-config.ts
  • packages/host-service/src/trpc/router/workspaces/workspaces.ts
  • packages/host-service/test/integration/bug-hunt-3.integration.test.ts
  • packages/host-service/test/integration/git.integration.test.ts
  • packages/panes/src/react/components/Workspace/Workspace.tsx
  • packages/shared/src/host-info.ts

Comment thread apps/desktop/src/renderer/lib/terminal/terminal-runtime-registry.ts Outdated
Comment thread apps/desktop/src/renderer/lib/terminal/terminal-runtime-registry.ts
Comment thread packages/host-service/src/trpc/router/git/git.ts
Comment thread packages/host-service/src/trpc/router/workspace-creation/shared/git-config.ts Outdated
Comment thread packages/host-service/src/trpc/router/workspaces/workspaces.ts Outdated
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 13, 2026

Greptile Summary

This PR hardens the desktop renderer and host-service against degradation during heavy usage and rapid workspace switching, addressing WebGL/WASM pressure from parked terminals, slow host-identity shell calls, and racing git-status syncs.

  • Terminal lifecycle: WebGL and image addons are now lazily enabled on attachToContainer and disabled on detachFromContainer, keeping parked terminals cheap; a new DOM-renderer fallback fires within a setTimeout(0) instead of xterm's built-in ~3 s delay, and terminal.dispose() is deferred two rAF frames to avoid viewport-sync races.
  • Navigation serialisation: navigateToV2Workspace now serialises plain workspace switches through an in-memory queue so rapid clicks coalesce to a single trailing transition; a Zustand store records the pending destination for optimistic sidebar highlighting before the route settles.
  • Host-service hardening: PullRequestRuntimeManager.stop() is now async and drains in-flight background tasks before clearing state; getDiffStats moves server-side the additions/deletions aggregation previously done in the client hook, and ioreg/reg.exe machine-ID calls are bounded to 1500 ms.

Confidence Score: 3/5

Safe to merge with one functional edge case in the WebGL controller worth resolving first.

The WebGL controller's fallbackToDom uses an uncancellable setTimeout, so if a genuine GPU context-loss fires in the same event-loop turn as a terminal park (workspace switch), the global suggestedRendererType is permanently set to dom for the session — exactly the kind of heavy-switching scenario this PR optimises for. The rest of the changes are well-structured with good test coverage.

terminal-webgl-addon-controller.ts needs attention for the uncancelled fallback timeout; git-helpers.ts has a minor oldPath type inconsistency in parseNumstatRecords.

Important Files Changed

Filename Overview
apps/desktop/src/renderer/lib/terminal/terminal-webgl-addon-controller.ts New WebGL addon controller with per-terminal enable/disable lifecycle; a deferred fallbackToDom setTimeout is not cancelled by disable(), risking permanent session-wide DOM fallback on rapid workspace parking during a context loss.
packages/host-service/src/trpc/router/git/utils/git-helpers.ts Extracts parseNumstatRecords from parseNumstat to avoid double-counting renames; pushes oldPath as empty string instead of omitting it when the git entry has no old path.
packages/host-service/src/runtime/pull-requests/pull-requests.ts Adds stopped flag, backgroundTasks tracking, and a drainInFlightWork drain loop to stop(); prevents new work from starting post-stop and awaits in-flight tasks up to 10 passes before clearing caches.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/workspace-navigation.ts Adds a serialised navigation queue for plain workspace switches to prevent route-transition races; module-level queue variables are not reset between test runs, making tests fragile if any promise fails to settle.
packages/host-service/src/trpc/router/git/git.ts Adds getDiffStats endpoint that aggregates against-base, staged, unstaged, and untracked counts server-side using parseNumstatRecords, removing the duplicate computation from the client hook.
apps/desktop/src/renderer/lib/terminal/terminal-runtime.ts Wires enable/disable hooks for image and WebGL addons into attach/detach lifecycle; defers terminal.dispose() two rAF frames to avoid xterm viewport sync races on unmount.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx Guards relaxed so cached workspace data renders during loading; adds a React key on the provider so pane state resets fully on each workspace switch.
packages/shared/src/host-info.ts Adds a 1500 ms timeout to the ioreg and reg.exe shell calls used to read the machine ID, preventing host-identity lookup from blocking the renderer during workspace switches.
apps/desktop/src/renderer/stores/v2-workspace-navigation.ts New Zustand store for tracking the pending workspace ID during navigation, used to optimistically highlight the destination workspace in the sidebar before the route settles.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[navigateToV2Workspace called] --> B{in-flight navigation?}
    B -- No --> C[startV2WorkspaceNavigation]
    B -- Yes, plain switch --> D[queueV2WorkspaceNavigation - coalesce]
    B -- Yes, replace or search params --> E[cancelQueuedNavigation + startV2WorkspaceNavigation]
    C --> F[setPendingWorkspaceId in store]
    D --> F
    E --> F
    F --> G[Sidebar highlights destination immediately]
    C --> H[Router transition begins]
    D --> I[wait for in-flight to settle, then run last queued target]
    I --> H
    E --> H
    H --> J[WorkspaceProvider remounts with new key]
    J --> K[clearPendingWorkspaceId]
Loading
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
apps/desktop/src/renderer/lib/terminal/terminal-webgl-addon-controller.ts:65-72
**`disable()` does not cancel the deferred `fallbackToDom` timeout**

`fallbackToDom` schedules `setTimeout(() => disposeAddon({ markDomFallback: true }), 0)` and guards against double-scheduling via `fallbackScheduled`. However `disable()` resets `fallbackScheduled = false` without cancelling the already-queued `setTimeout`. As a result, if a genuine WebGL context-loss fires and then `disable()` is called within the same event-loop turn (as happens during the rapid workspace parking this PR optimises for), the `setTimeout` still fires and calls `disposeAddon({ markDomFallback: true })` — permanently setting the module-level `suggestedRendererType = "dom"` and preventing all subsequent terminals in the session from using WebGL, even though the `disable()` signal was meant only to park the terminal, not to declare a permanent GPU failure.

Storing the `setTimeout` handle and clearing it in `disable()` / `dispose()` would prevent the leak.

### Issue 2 of 3
packages/host-service/src/trpc/router/git/utils/git-helpers.ts:115-118
`result.push({ path: newPath, oldPath, ...stats })` unconditionally includes `oldPath` even when it is the empty-string fallback from `?? ""`. The `NumstatRecord` interface declares `oldPath` as optional (`oldPath?: string`), so an empty string is semantically wrong and any consumer that checks `record.oldPath !== undefined` instead of truthiness would incorrectly treat the record as a rename. Guard with a truthy check before including the field.

```suggestion
		if (pathMaybe === "") {
			const oldPath = entries[++i] ?? "";
			const newPath = entries[++i] ?? "";
			if (newPath)
				result.push({
					path: newPath,
					...(oldPath ? { oldPath } : {}),
					...stats,
				});
```

### Issue 3 of 3
apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/workspace-navigation.ts:31-34
**Module-level navigation state is not reset between tests**

`inFlightV2WorkspaceNavigation` and `queuedV2WorkspaceNavigation` live at module scope. The new test suite resets the Zustand store in `beforeEach` but has no mechanism to reset these two variables. If any test assertion fails before all navigation promises have settled, the stale `inFlightV2WorkspaceNavigation` will cause the first plain-switch in the next test to be queued rather than started immediately, masking the bug under test. Exporting a `_resetNavigationStateForTesting()` helper (or using a factory function) would make the tests hermetic.

Reviews (1): Last reviewed commit: "Simplify terminal WebGL fallback" | Re-trigger Greptile

Comment on lines +65 to +72
const fallbackToDom = () => {
if (fallbackScheduled) return;
fallbackScheduled = true;
console.info("[terminal:webgl] context lost; falling back to DOM renderer");
setTimeout(() => {
disposeAddon({ markDomFallback: true });
}, 0);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 disable() does not cancel the deferred fallbackToDom timeout

fallbackToDom schedules setTimeout(() => disposeAddon({ markDomFallback: true }), 0) and guards against double-scheduling via fallbackScheduled. However disable() resets fallbackScheduled = false without cancelling the already-queued setTimeout. As a result, if a genuine WebGL context-loss fires and then disable() is called within the same event-loop turn (as happens during the rapid workspace parking this PR optimises for), the setTimeout still fires and calls disposeAddon({ markDomFallback: true }) — permanently setting the module-level suggestedRendererType = "dom" and preventing all subsequent terminals in the session from using WebGL, even though the disable() signal was meant only to park the terminal, not to declare a permanent GPU failure.

Storing the setTimeout handle and clearing it in disable() / dispose() would prevent the leak.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/lib/terminal/terminal-webgl-addon-controller.ts
Line: 65-72

Comment:
**`disable()` does not cancel the deferred `fallbackToDom` timeout**

`fallbackToDom` schedules `setTimeout(() => disposeAddon({ markDomFallback: true }), 0)` and guards against double-scheduling via `fallbackScheduled`. However `disable()` resets `fallbackScheduled = false` without cancelling the already-queued `setTimeout`. As a result, if a genuine WebGL context-loss fires and then `disable()` is called within the same event-loop turn (as happens during the rapid workspace parking this PR optimises for), the `setTimeout` still fires and calls `disposeAddon({ markDomFallback: true })` — permanently setting the module-level `suggestedRendererType = "dom"` and preventing all subsequent terminals in the session from using WebGL, even though the `disable()` signal was meant only to park the terminal, not to declare a permanent GPU failure.

Storing the `setTimeout` handle and clearing it in `disable()` / `dispose()` would prevent the leak.

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

Comment on lines +115 to +118
if (pathMaybe === "") {
const oldPath = entries[++i] ?? "";
const newPath = entries[++i] ?? "";
if (newPath) result.set(newPath, stats);
if (oldPath) result.set(oldPath, stats);
if (newPath) result.push({ path: newPath, oldPath, ...stats });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 result.push({ path: newPath, oldPath, ...stats }) unconditionally includes oldPath even when it is the empty-string fallback from ?? "". The NumstatRecord interface declares oldPath as optional (oldPath?: string), so an empty string is semantically wrong and any consumer that checks record.oldPath !== undefined instead of truthiness would incorrectly treat the record as a rename. Guard with a truthy check before including the field.

Suggested change
if (pathMaybe === "") {
const oldPath = entries[++i] ?? "";
const newPath = entries[++i] ?? "";
if (newPath) result.set(newPath, stats);
if (oldPath) result.set(oldPath, stats);
if (newPath) result.push({ path: newPath, oldPath, ...stats });
if (pathMaybe === "") {
const oldPath = entries[++i] ?? "";
const newPath = entries[++i] ?? "";
if (newPath)
result.push({
path: newPath,
...(oldPath ? { oldPath } : {}),
...stats,
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/host-service/src/trpc/router/git/utils/git-helpers.ts
Line: 115-118

Comment:
`result.push({ path: newPath, oldPath, ...stats })` unconditionally includes `oldPath` even when it is the empty-string fallback from `?? ""`. The `NumstatRecord` interface declares `oldPath` as optional (`oldPath?: string`), so an empty string is semantically wrong and any consumer that checks `record.oldPath !== undefined` instead of truthiness would incorrectly treat the record as a rename. Guard with a truthy check before including the field.

```suggestion
		if (pathMaybe === "") {
			const oldPath = entries[++i] ?? "";
			const newPath = entries[++i] ?? "";
			if (newPath)
				result.push({
					path: newPath,
					...(oldPath ? { oldPath } : {}),
					...stats,
				});
```

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

Comment on lines +31 to +34
interface QueuedV2WorkspaceNavigation extends V2WorkspaceNavigationRequest {
waiters: Array<{
resolve: () => void;
reject: (error: unknown) => void;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Module-level navigation state is not reset between tests

inFlightV2WorkspaceNavigation and queuedV2WorkspaceNavigation live at module scope. The new test suite resets the Zustand store in beforeEach but has no mechanism to reset these two variables. If any test assertion fails before all navigation promises have settled, the stale inFlightV2WorkspaceNavigation will cause the first plain-switch in the next test to be queued rather than started immediately, masking the bug under test. Exporting a _resetNavigationStateForTesting() helper (or using a factory function) would make the tests hermetic.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/utils/workspace-navigation.ts
Line: 31-34

Comment:
**Module-level navigation state is not reset between tests**

`inFlightV2WorkspaceNavigation` and `queuedV2WorkspaceNavigation` live at module scope. The new test suite resets the Zustand store in `beforeEach` but has no mechanism to reset these two variables. If any test assertion fails before all navigation promises have settled, the stale `inFlightV2WorkspaceNavigation` will cause the first plain-switch in the next test to be queued rather than started immediately, masking the bug under test. Exporting a `_resetNavigationStateForTesting()` helper (or using a factory function) would make the tests hermetic.

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

Kitenite added 3 commits May 13, 2026 09:15
# Conflicts:
#	apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
Kitenite added 2 commits May 13, 2026 14:45
# Conflicts:
#	apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts
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