Skip to content

fix(source-control): keep huge change sets responsive - #9477

Merged
brennanb2025 merged 7 commits into
stablyai:mainfrom
OrcaWin:OrcaWin/issue-7991-source-control-freeze-repro
Jul 20, 2026
Merged

brennanb2025 merged 7 commits into
stablyai:mainfrom
OrcaWin:OrcaWin/issue-7991-source-control-freeze-repro

Conversation

@OrcaWin

@OrcaWin OrcaWin commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • stream porcelain status incrementally on relay hosts and terminate Git as soon as Orca has enough entries
  • cap source-control data at 1,000 entries before IPC or SSH/relay transfer, preserving an explicit didHitLimit signal
  • keep huge-repository watcher suppression sticky while adding an explicit Retry path that recovers after the repository drops below the cap

ELI5

A branch with thousands of changes made Orca carry and render a giant list all at once, briefly freezing the window even though only a capped list was shown. Orca now stops reading as soon as it has the capped result, sends only that bounded result across process and host boundaries, pauses automatic refresh churn, and lets the user retry explicitly after cleaning up files.

Electron evidence

Exact fixture in both runs: one committed file plus exactly 12,000 untracked TypeScript files in 120 directories.

Measurement Before After
Initial Source Control activation 1601.6 ms 1516.9 ms
Renderer max event-loop lag during activation 1113.6 ms 664.4 ms
Explicit status scan 529.6 ms 284.0 ms
Warm rescan 146.4 ms 196.9 ms
IPC payload 850,222 bytes 85,221 bytes
Entries crossing IPC 10,000 1,000
Post-activation max lag 29.2 ms 20.7 ms
Render update — 2.0 ms

The exact Electron flow passed twice after the fix, including a fresh rebuild. Removing all 12,000 untracked files and choosing Retry cleared capped mode, proving explicit recovery.

Validation

  • 429 focused parser, status, relay, IPC, provider, runtime, store, and polling tests
  • additional relay/component run: 192 passed, 10 skipped
  • typecheck:node
  • typecheck:web
  • changed-file oxlint and max-lines ratchet
  • localization catalog and coverage checks
  • all six relay target builds plus the WSL hook relay
  • exact 12,000-file Electron E2E

Trade-off / user-visible change

Repositories with more than 1,000 changes now enter capped mode earlier, show the first 1,000 entries, pause automatic watcher and safety refreshes, and expose Retry. The smaller bound is what keeps renderer projection and cross-host transfer responsive; explicit Retry preserves recovery after cleanup.

Fixes #7991

@OrcaWin

OrcaWin commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator Author

Electron before/after proof for the exact 12,000-file fixture: https://htmlpreview.github.io/?https://gist.githubusercontent.com/OrcaWin/6647ab6a294ab47bc1898334076e66b6/raw/orca-7991-electron-proof.html

The artifact embeds both temporary screenshots and their SHA-256 hashes; neither the screenshots nor fixture are committed to the repository. The measured IPC boundary changed from 10,000 entries / 850,222 bytes to 1,000 entries / 85,221 bytes, and deleting the fixture followed by Retry verified recovery.

@coderabbitai

coderabbitai Bot commented Jul 19, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: bb35f38e-07e2-43fa-aaa3-275b8b4f396e

📥 Commits

Reviewing files that changed from the base of the PR and between eff7a34 and 8c41c8f.

📒 Files selected for processing (1)
  • src/renderer/src/components/right-sidebar/SourceControl.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/renderer/src/components/right-sidebar/SourceControl.tsx

📝 Walkthrough

Walkthrough

The change streams Git status output into an incremental porcelain parser, enabling early termination and capped status metadata. Unmerged records now count toward limits and preserve output order across main and relay status paths. Shared subprocess-tree termination handles streamed Git and agent execution cleanup. Capped metadata propagates through IPC, runtime, polling, submodule, and editor state. Source Control adds retry and truncated-submodule UI states, with end-to-end coverage for payload size, responsiveness, recovery, and cache behavior.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers summary and testing, but it misses required template sections like Screenshots, AI Review Report, Security Audit, and Notes. Add the missing template sections and briefly summarize screenshots, AI review findings, security review, and any platform-specific notes.
Docstring Coverage ⚠️ Warning Docstring coverage is 15.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: making large source-control change sets responsive.
Linked Issues check ✅ Passed The PR implements the linked issue’s core fix: earlier capping, early Git termination, sticky huge-state suppression, and explicit retry recovery.
Out of Scope Changes check ✅ Passed No clear out-of-scope code changes stand out; the added relay, IPC, renderer, and test updates support the large-status responsiveness fix.

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

🧹 Nitpick comments (2)
src/relay/git-stdout-stream.ts (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Explicitly type child and use optional chaining for stream access.

To ensure type safety and prevent implicit any errors under strict TypeScript configurations, consider explicitly typing child as ChildProcess and adding optional chaining to stdout and stderr (since Node types them as Readable | null).

♻️ Proposed refactor

Update the import and variable declaration:

-import { spawn } from 'node:child_process'
+import { spawn, type ChildProcess } from 'node:child_process'
-    let child
+    let child: ChildProcess
     try {

Add optional chaining in cleanup and during listener setup:

     const cleanup = (): void => {
-      child.stdout.off('data', onStdoutData)
-      child.stderr.off('data', onStderrData)
+      child.stdout?.off('data', onStdoutData)
+      child.stderr?.off('data', onStderrData)
       child.off('error', onError)
       child.off('close', onClose)
       options.signal?.removeEventListener('abort', onAbort)
-    child.stdout.on('data', onStdoutData)
-    child.stderr.on('data', onStderrData)
+    child.stdout?.on('data', onStdoutData)
+    child.stderr?.on('data', onStderrData)
     child.on('error', onError)
     child.on('close', onClose)

Also applies to: 42-42, 65-69, 135-138

src/relay/agent-exec-handler.ts (1)

237-252: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider using StringDecoder instead of chunk.toString('utf-8').

Decoding arbitrary stream chunks with .toString('utf-8') can corrupt multi-byte characters if they happen to be split across buffer boundaries. Since the new git-stdout-stream.ts file elegantly solves this by using node:string_decoder, consider adopting the same approach here for robustness.

♻️ Proposed refactor

Add the import at the top of the file:

import { StringDecoder } from 'node:string_decoder'

Instantiate the decoders and replace .toString() in the data handlers:

+      const stdoutDecoder = new StringDecoder('utf8')
+      const stderrDecoder = new StringDecoder('utf8')
+
       const onStdoutData = (chunk: Buffer): void => {
         stdoutBytes += chunk.byteLength
         if (stdoutBytes > MAX_OUTPUT_BYTES) {
           terminateRelaySubprocessTree(child)
           return
         }
-        stdout += chunk.toString('utf-8')
+        stdout += stdoutDecoder.write(chunk)
       }
       const onStderrData = (chunk: Buffer): void => {
         stderrBytes += chunk.byteLength
         if (stderrBytes > MAX_OUTPUT_BYTES) {
           terminateRelaySubprocessTree(child)
           return
         }
-        stderr += chunk.toString('utf-8')
+        stderr += stderrDecoder.write(chunk)
       }

(Note: Ensure you call .end() on both decoders during cleanup if the final trailing bytes are needed).


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 04668d81-4542-437b-8f3e-9522f2b30020

📥 Commits

Reviewing files that changed from the base of the PR and between 624c8d4 and 77fd7cd.

📒 Files selected for processing (20)
  • src/main/git/status-porcelain-parser.test.ts
  • src/main/git/status.ts
  • src/main/ipc/filesystem.test.ts
  • src/main/providers/ssh-git-provider.test.ts
  • src/main/runtime/rpc/methods/git.test.ts
  • src/relay/agent-exec-handler.ts
  • src/relay/git-handler-status-ops.test.ts
  • src/relay/git-handler-status-ops.ts
  • src/relay/git-handler.ts
  • src/relay/git-stdout-stream.test.ts
  • src/relay/git-stdout-stream.ts
  • src/relay/subprocess-tree-termination.ts
  • src/renderer/src/components/right-sidebar/SourceControl.tsx
  • src/renderer/src/components/right-sidebar/useGitStatusPolling.test.ts
  • src/renderer/src/store/slices/editor.test.ts
  • src/shared/git-status-limit.ts
  • src/shared/git-status-porcelain-parser.ts
  • src/shared/git-uncommitted-line-stats.test.ts
  • tests/e2e/large-file-count-fixtures.ts
  • tests/e2e/source-control-large-file-count.spec.ts

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
README.md (1)

24-25: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update translated Windows release guidance.

The main README now points Windows users to the latest installer and removes the RC warning, but docs/readme/README.fr.md Line 220 still recommends pinned RC v1.4.147-rc.3. Update the translated documentation in the same change so localized users are not directed to stale or conflicting builds.

Also applies to: 212-216


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a18d9335-c6ea-4d21-9636-a54ae5c8b422

📥 Commits

Reviewing files that changed from the base of the PR and between 77fd7cd and a618e25.

📒 Files selected for processing (3)
  • README.md
  • src/main/git/status.ts
  • src/main/ipc/filesystem.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/git/status.ts

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

🧹 Nitpick comments (1)
src/main/git/status.ts (1)

308-315: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Simplify the unmerged limit logic to robustly handle skipped conflicts.

If parseUnmergedEntry returns null (e.g., for a submodule conflict), it skips that entry. Because unmergedLimit is pre-calculated and slices the array upfront, skipping an entry means we might return fewer than limit total entries, even if there are additional unused unmerged lines in the parser that could have filled the gap.

Checking entries.length inside the loop simplifies the logic and ensures we fill up to the exact limit.
(Note: You may want to apply this same simplification to src/relay/git-handler-status-ops.ts, which uses the identical pattern).

♻️ Proposed refactor
-  // Why: cap conflict lookups too so a conflict-heavy merge cannot bypass the status bound.
-  const unmergedLimit = didHitLimit
-    ? Math.max(0, limit - entries.length)
-    : parser.unmergedLines.length
-  for (const line of parser.unmergedLines.slice(0, unmergedLimit)) {
+  // Why: cap conflict lookups too so a conflict-heavy merge cannot bypass the status bound.
+  for (const line of parser.unmergedLines) {
+    if (didHitLimit && entries.length >= limit) {
+      break
+    }
     const unmergedEntry = await parseUnmergedEntry(worktreePath, line)
     if (unmergedEntry) {
       entries.push(unmergedEntry)

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 73541675-44a1-4b36-a99e-e4542ec78c11

📥 Commits

Reviewing files that changed from the base of the PR and between a618e25 and 28f81b6.

📒 Files selected for processing (7)
  • src/main/git/status-porcelain-parser.test.ts
  • src/main/git/status.test.ts
  • src/main/git/status.ts
  • src/relay/git-handler-status-ops.test.ts
  • src/relay/git-handler-status-ops.ts
  • src/shared/git-status-porcelain-parser.ts
  • tests/e2e/source-control-large-file-count.spec.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/relay/git-handler-status-ops.ts
  • tests/e2e/source-control-large-file-count.spec.ts
  • src/relay/git-handler-status-ops.test.ts

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

🧹 Nitpick comments (1)
src/relay/git-handler-status-ops.ts (1)

42-61: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Prefer asynchronous file system checks to avoid blocking the event loop.

Node.js existsSync is synchronous and blocks the thread. Since this function is already async and runs on a relay server that handles concurrent requests, it is better practice to use asynchronous file system operations (e.g., fs.promises.access) to check for file existence without blocking the event loop.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a199157b-2c51-4a3e-92ff-2903ac69a203

📥 Commits

Reviewing files that changed from the base of the PR and between 28f81b6 and eff7a34.

📒 Files selected for processing (29)
  • src/main/git/runner-command-exec.test.ts
  • src/main/git/runner.ts
  • src/main/git/status-porcelain-parser.test.ts
  • src/main/git/status.test.ts
  • src/main/git/status.ts
  • src/relay/agent-exec-handler.test.ts
  • src/relay/git-handler-status-ops.test.ts
  • src/relay/git-handler-status-ops.ts
  • src/relay/git-handler.ts
  • src/relay/git-stdout-stream.test.ts
  • src/relay/git-stdout-stream.ts
  • src/relay/subprocess-tree-termination.test.ts
  • src/relay/subprocess-tree-termination.ts
  • src/renderer/src/components/right-sidebar/SourceControl.tsx
  • src/renderer/src/components/right-sidebar/source-control-submodule-expansion.test.ts
  • src/renderer/src/components/right-sidebar/source-control-submodule-expansion.ts
  • src/renderer/src/components/right-sidebar/source-control-too-many-changes-banner.test.tsx
  • src/renderer/src/components/right-sidebar/useSourceControlSubmoduleStatus.test.tsx
  • src/renderer/src/components/right-sidebar/useSourceControlSubmoduleStatus.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/store/slices/editor.test.ts
  • src/renderer/src/store/slices/editor.ts
  • src/shared/git-status-limit.test.ts
  • src/shared/git-status-limit.ts
  • src/shared/git-status-porcelain-parser.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/relay/git-handler.ts
  • src/relay/git-handler-status-ops.test.ts

@brennanb2025
brennanb2025 merged commit e109e78 into stablyai:main Jul 20, 2026
2 checks passed
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
* fix(source-control): keep huge change sets responsive

* Fix cancellation and retry handling for capped status

* Harden capped status for conflict-heavy repositories

* Harden capped status recovery and cancellation

* fix(source-control): preserve capped status correctness

* fix(source-control): translate submodule status at render time

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
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.

Source control freezes when a branch has too many changed files

2 participants