Skip to content

fix(ui): cap expanded context source reads#367

Merged
benvinegar merged 1 commit into
mainfrom
fix/cap-source-expansion
May 25, 2026
Merged

fix(ui): cap expanded context source reads#367
benvinegar merged 1 commit into
mainfrom
fix/cap-source-expansion

Conversation

@benvinegar

Copy link
Copy Markdown
Member

Summary

  • Cap expandable source reads at 1MB for filesystem, Git blob, and Git index sources
  • Kill over-limit git show source reads while streaming instead of buffering the whole blob
  • Show a clear “Source too large to expand…” collapsed-row status instead of freezing the UI
  • Add regression coverage for capped source reads and large-file expansion cases

Test plan

  • bun test src/core/fileSource.test.ts src/core/loaders.test.ts src/ui/diff/expandCollapsedRows.test.ts src/ui/hooks/useReviewController.test.tsx
  • bun run typecheck

This PR description was generated by Pi using OpenAI GPT-5

@greptile-apps

greptile-apps Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR prevents large files from freezing the UI during inline context expansion by capping source reads at 1 MB across all three source kinds (filesystem, git-blob, git-index), killing over-limit git show subprocesses while streaming rather than buffering the whole blob first, and surfacing a clear "Source too large to expand" label in collapsed gap rows.

  • fileSource.ts: Introduces SourceTextTooLargeError, DEFAULT_SOURCE_TEXT_MAX_BYTES, and readStreamTextWithLimit — a streaming reader that kills the subprocess and throws as soon as the byte budget is exceeded.
  • expandCollapsedRows.ts / useReviewController.ts: Threads the reason: \"too-large\" discriminant through the FileSourceStatus error variant so the UI can display a distinct, actionable label instead of the generic "Could not load" message.
  • Tests: Adds regression coverage for the cap at the unit, integration, and hook levels.

Confidence Score: 4/5

Safe to merge — the cap correctly prevents memory exhaustion and the UI path is well-covered by new tests; two minor logic gaps are worth addressing in a follow-up.

The streaming kill logic, the fs size-check, and the new UI label all work correctly and are tested end-to-end. Two issues are worth noting: the toggleGap skip-fetch guard doesn't include the too-large terminal state, so every subsequent gap interaction on an over-limit file spawns and immediately kills a fresh git show subprocess; and a SourceTextTooLargeError thrown from the 64 KB stderr reader would be misreported as 'source too large' even when the actual source content was within budget. Neither issue causes data loss or a crash, but both are real misbehaviors on the changed paths.

src/core/fileSource.ts and src/ui/hooks/useReviewController.ts warrant a second look for the two logic gaps described in the inline comments.

Important Files Changed

Filename Overview
src/core/fileSource.ts Adds SourceTextTooLargeError, per-read byte caps for fs/git-blob/git-index, and streaming limit logic with process kill; minor logic issues around stderr overflow mapping and missing result caching on error
src/ui/hooks/useReviewController.ts Classifies SourceTextTooLargeError in the catch handler and forwards reason to status; the skip-fetch guard doesn't include the too-large terminal state, allowing redundant re-fetches
src/ui/diff/expandCollapsedRows.ts Adds optional reason field to error status and a new errorRowText branch for "too-large"; clean, minimal change
src/core/fileSource.test.ts New tests for fs and git byte-cap rejection via SourceTextTooLargeError; coverage looks correct
src/core/loaders.test.ts Adds a large-file integration test that verifies the source cap is enforced end-to-end and sourceFetcher assertions on skipped files
src/ui/diff/expandCollapsedRows.test.ts New test for "too-large" reason renders correct label; straightforward and correct
src/ui/hooks/useReviewController.test.tsx New test confirms toggleGap sets status to { kind: "error", reason: "too-large" } when SourceTextTooLargeError is thrown; test is correct for the first-toggle path but doesn't cover re-fetch on second toggle
CHANGELOG.md Entry added to Unreleased section describing the source-read cap

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[toggleGap called] --> B{currentStatus?}
    B -- loaded / loading --> C[Skip fetch — return early]
    B -- error / undefined --> D[Spawn source load request]
    D --> E[file.sourceFetcher.getFullText]
    E --> F{Result?}
    F -- text !== null --> G[setSettledStatus loaded]
    F -- text === null --> H[setSettledStatus error]
    F -- throws SourceTextTooLargeError --> I[setSettledStatus error reason=too-large]
    F -- throws other error --> J[console.error + setSettledStatus error]

    subgraph fileSource.ts
        K[readFsSpec] --> L{file.size > maxBytes?}
        L -- yes --> M[throw SourceTextTooLargeError]
        L -- no --> N[Bun.file.text]

        O[readGitObjectSpec] --> P[git show subprocess]
        P --> Q[readStreamTextWithLimit stdout]
        Q --> R{totalBytes > maxBytes?}
        R -- yes --> S[proc.kill + throw SourceTextTooLargeError]
        R -- no --> T[accumulate chunks → decode]
    end

    E -.-> K
    E -.-> O
Loading

Comments Outside Diff (1)

  1. src/ui/hooks/useReviewController.ts, line 440-443 (link)

    P2 Too-large status not treated as terminal — redundant re-fetches on every toggle

    The skip-fetch guard only bails out for "loaded" and "loading", but not for { kind: "error", reason: "too-large" }. Once a file has been identified as over-limit, every subsequent call to toggleGap (collapsing, re-opening, or opening a different gap in the same file) passes the guard and spawns a brand-new git show subprocess that streams up to 1 MB before being killed — an expensive operation guaranteed to fail identically each time. The fix is to also short-circuit when the settled status is a permanent "too-large" error.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/ui/hooks/useReviewController.ts
    Line: 440-443
    
    Comment:
    **Too-large status not treated as terminal — redundant re-fetches on every toggle**
    
    The skip-fetch guard only bails out for `"loaded"` and `"loading"`, but not for `{ kind: "error", reason: "too-large" }`. Once a file has been identified as over-limit, every subsequent call to `toggleGap` (collapsing, re-opening, or opening a different gap in the same file) passes the guard and spawns a brand-new `git show` subprocess that streams up to 1 MB before being killed — an expensive operation guaranteed to fail identically each time. The fix is to also short-circuit when the settled status is a permanent "too-large" error.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
src/ui/hooks/useReviewController.ts:440-443
**Too-large status not treated as terminal — redundant re-fetches on every toggle**

The skip-fetch guard only bails out for `"loaded"` and `"loading"`, but not for `{ kind: "error", reason: "too-large" }`. Once a file has been identified as over-limit, every subsequent call to `toggleGap` (collapsing, re-opening, or opening a different gap in the same file) passes the guard and spawns a brand-new `git show` subprocess that streams up to 1 MB before being killed — an expensive operation guaranteed to fail identically each time. The fix is to also short-circuit when the settled status is a permanent "too-large" error.

### Issue 2 of 2
src/core/fileSource.ts:186-201
**`SourceTextTooLargeError` from stderr reader surfaces as wrong UI message**

Both stdout and stderr reads share the same `SourceTextTooLargeError` type, and the catch block re-throws any instance of it without distinguishing which stream originated it. If `git show` writes more than 64 KB to stderr (e.g., hook output, verbose trace logging), the error from the stderr `readStreamTextWithLimit` call will be caught and re-thrown, causing the caller to display "Source too large to expand" even though the actual source content may have been well within the 1 MB limit. The stderr overflow should instead be treated as a generic read failure (logged and return `null`) rather than mapped to `SourceTextTooLargeError`.

Reviews (1): Last reviewed commit: "fix(ui): cap expanded context source rea..." | Re-trigger Greptile

Comment thread src/core/fileSource.ts
Comment on lines 186 to 201
try {
output = await Promise.all([
proc.exited,
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
readStreamTextWithLimit(proc.stdout, maxSourceBytes, () => proc.kill()),
readStreamTextWithLimit(proc.stderr, 64 * 1024),
]);
} catch (error) {
if (error instanceof SourceTextTooLargeError) {
proc.kill();
await proc.exited.catch(() => undefined);
throw error;
}

logSourceDiagnostic(`failed to collect Git source ${objectName}`, error);
return null;
}

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 SourceTextTooLargeError from stderr reader surfaces as wrong UI message

Both stdout and stderr reads share the same SourceTextTooLargeError type, and the catch block re-throws any instance of it without distinguishing which stream originated it. If git show writes more than 64 KB to stderr (e.g., hook output, verbose trace logging), the error from the stderr readStreamTextWithLimit call will be caught and re-thrown, causing the caller to display "Source too large to expand" even though the actual source content may have been well within the 1 MB limit. The stderr overflow should instead be treated as a generic read failure (logged and return null) rather than mapped to SourceTextTooLargeError.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/core/fileSource.ts
Line: 186-201

Comment:
**`SourceTextTooLargeError` from stderr reader surfaces as wrong UI message**

Both stdout and stderr reads share the same `SourceTextTooLargeError` type, and the catch block re-throws any instance of it without distinguishing which stream originated it. If `git show` writes more than 64 KB to stderr (e.g., hook output, verbose trace logging), the error from the stderr `readStreamTextWithLimit` call will be caught and re-thrown, causing the caller to display "Source too large to expand" even though the actual source content may have been well within the 1 MB limit. The stderr overflow should instead be treated as a generic read failure (logged and return `null`) rather than mapped to `SourceTextTooLargeError`.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch — fixed. Oversized stderr now creates a generic diagnostics-limit error instead of SourceTextTooLargeError, so the UI only shows “Source too large…” for over-limit source stdout. I also added a regression test for oversized git stderr returning a generic source failure.

This comment was generated by Pi using OpenAI GPT-5

@benvinegar benvinegar force-pushed the fix/cap-source-expansion branch from c438eb3 to 63443a6 Compare May 25, 2026 00:29
@benvinegar benvinegar merged commit e04133e into main May 25, 2026
5 checks passed
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