fix(desktop): refetch Changes pane patches when git state changes

Merged
SA
2mo ago
+4-0

Problem

A user has the Changes pane open while an agent (or they themselves, outside the pane) keeps editing a file that is already in the changeset. When they then expand context or enter edit mode on that file, @pierre/diffs throws trailing context mismatch (additions=N, deletions=M) inside a React layout effect. The throw escapes to the dashboard content boundary, so the whole workspace view is replaced by "This view hit an error" until they navigate away. 57 events across DESKTOP-158 and DESKTOP-154 on 1.26.0 in four days; both issues are the same invariant reached from two library call sites.

Root cause

The pane renders from per-category patches (git.getDiffPatch, #6968). Their query key carries the changed-file list and the ref pair, not the working-tree state, and nothing invalidates them: useGitStatus invalidates git.getDiff and git.getBaseBranch on git:changed, and DiffPane invalidates git.readDiffSideFile, but the successor of getDiff was never added. So a further edit to an already-changed file refreshes the status counts in the header but leaves the parsed hunks as they were when the pane opened.

Hydration (loadDiffFiles) reads the file as it is now through the uncached trpcClient.git.getDiff.query. The library merges those fresh contents into the stale hunks and, on the next layout pass, checks that the unchanged region after the last hunk has the same length on both sides. It never does once the file has moved on, and the invariant throws.

The hunks we build are not at fault. Reproduced against the shipped 1.3.6 build by parsing a git-shaped patch and hydrating it:

matching-contents             OK   { mismatch: false }
new-side-grew-after-patch     THROWS computeEstimatedDiffHeights: trailing context mismatch (additions=62, deletions=17)
old-side-changed-after-patch  THROWS computeEstimatedDiffHeights: trailing context mismatch (additions=18, deletions=17)
no-trailing-newline-new-side  OK   { mismatch: false }
crlf                          OK   { mismatch: false }

\ No newline at end of file and CRLF hunks pass; only contents that no longer match the patch fail. Upstream 1.4.1 keeps the same invariant, so it is the library's contract that hydrated contents describe the same two sides as the hunks, and we were breaking it.

Fix

Invalidate git.getDiffPatch for the workspace on git:changed, in useGitStatus, next to the getDiff invalidation it replaced. The partial-input filter matches the per-group keys useDiffCodeViewItems builds with getQueryKey (checked with a throwaway QueryClient test: a ws-1 group query is marked stale, a ws-2 one is not). A refetch replaces the group's FileDiffMetadata objects, which is what already happens today whenever the set of changed paths shifts; the library keeps an item's editor document by id across version changes, so an in-progress edit is not discarded.

No typed cause and no new capture: nothing reads one, and the error is the library's own. No catch around the render, no padding of hunks.

Remaining window: between the watcher's 300 ms debounce firing and the patch refetch resolving, a hydration can still land on the previous hunks. That is the same read-then-read race every refetch has; it is not what the 57 events are, which is a patch that was never refreshed at all.

Reality check

  1. Harm. The workspace content area crashes to an error page when someone expands or edits a diff whose file changed after the pane loaded it.
  2. Reach. Events come from 1.26.0; this ships in the desktop release cut from main (1.27.0, apps/desktop/package.json), and reaches every desktop user since the hook is renderer-side with no host-service dependency.
  3. Instrument. Archiving leaves the crash. A Sentry filter is barred by the contract. Catching in the loader would hide a stale pane. The code path is not being replaced (#7244 and #7166 build on it). Refreshing the patch on the event the rest of the git cache already refreshes on is the smallest change that makes the hunks and the hydrated contents describe the same file.

Verification

$ bunx biome check apps/desktop/src/renderer/hooks/host-service/useGitStatus/useGitStatus.ts
Checked 1 file in 13ms. No fixes applied.

$ cd apps/desktop && bun run typecheck
$ tsc --noEmit
(clean)

$ cd apps/desktop && bun test src/renderer/hooks/host-service/useGitStatus src/no-main-process-blocking.test.ts
 7 pass
 0 fail
 13 expect() calls
Ran 7 tests across 2 files. [152.00ms]

Not verified in the running app: reproducing needs a workspace with an agent editing a changed file while the pane is open.

Refs DESKTOP-158
Refs DESKTOP-154

https://claude.ai/code/session_01QJYrL9eWkq6piM3K4oiGPZ


Summary by cubic

Fixes the Changes pane crashing to an error page when a file in the changeset is edited after the pane loads it.

The pane renders from cached git.getDiffPatch queries that were never invalidated on git:changed, so further edits left the hunks stale while hydration read the file as it is now, violating @pierre/diffs' trailing-context invariant. Now the patch query is invalidated next to the getDiff invalidation it replaced, so hunks always match the current file contents. Refetches replace the FileDiffMetadata objects; in-progress editor documents are kept by id. Closes DESKTOP-158 and DESKTOP-154.

Summary by CodeRabbit

  • Bug Fixes
    • Updated Git status refresh behavior to keep file diff views synchronized with the latest workspace changes.
    • Prevented stale cached diff hunks from being displayed after files are modified.