feat(desktop,host-service): render the Changes pane from patches
Stacked on #6955 — review that one first; this PR's diff is against it.
@pierre/diffs is built around a model we weren't using. You parse a patch into partial per-file metadata, render the hunks, and hand the library a loadDiffFiles callback it calls only when it needs the rest of a file. Their own DiffsHub demo (apps/diffshub in pierrecomputer/pierre) drives arbitrarily large GitHub diffs that way, with createGitHubDiffFileLoader as the loader and viewer.addItems as files stream in.
We were doing the opposite: git show twice per changed file, two complete copies over the wire, parsed on the renderer's main thread. A CPU profile of the old path put ~1 second in payload decode alone, which no amount of deferral touches.
What changed
Host service. git.getDiffPatch returns one git diff per category, built inside the worker pool that already runs the bulk path. The ref pair per category mirrors loadFileDiffContent exactly, so a patch and a hydrated file always describe the same two sides. Untracked files get their own --no-index section, since git diff won't report them.
Renderer. useDiffCodeViewItems groups files by (category, baseBranch, commitHash, fromHash) — usually 1-3 groups for a pane — issues one patch query per group, and parses it with parsePatchFiles. DiffPane passes loadDiffFiles into codeViewOptions, so whole files cross the wire only for files somebody expands or edits. Parsed metadata is cached per group and reused by identity, because the library hydrates partial diffs by upgrading that object in place.
Payload
| bytes | |
|---|---|
| patch for this branch's changeset (22 files) | 3.4 MB |
the same changeset as getDiff contents | 13.2 MB |
| 25.5% |
A one-line edit in a 5,000-line file comes out under 10% (asserted in get-diff-patch.test.ts).
What this removes and what it keeps
Hunks are cheap enough that the size-based deferral from #6955 is gone: a 34,971-line messages.po now renders inline where #6955 hid it behind "Load diff". Verified in the running app.
Kept: generated artifacts stay behind the button — their patches are still tens of thousands of lines of noise, and the compiled ones (locales/*/messages.ts) are single multi-megabyte lines, which the Pierre writeup lists as an unsolved case for their renderer too. The 500k-char guard survives as the ceiling on hydration rather than a gate on rendering. Binary placeholders and per-file loading/error/retry states are unchanged.
Verification
apps/desktop:bun test→ 2956 pass, 0 fail.packages/host-service: git router suite → 129 pass, 0 fail, including six newgetDiffPatchcases pinning each category's ref pair, untracked sections, path filtering, and the payload ratio.tsc --noEmitclean in both packages;biome checkclean;bun run --cwd packages/i18n checkexits 0 with ja and zh-CN at 0 missing.- Timing, CDP, same workspace, before and after. Patch build: first content 2342/2279 ms, longest task 191/179 ms, blocking 420/383 ms. The same workspace with this branch's
DiffPanereverted to #6955: 2299/2417 ms, 185/190 ms, 343/350 ms. Equal within noise — but #6955 is not rendering the 34,971-linemessages.poat all and this build renders it inline. Paired screenshots captured. - Hydration, live. Clicking "5 unmodified lines" in the running app fired two
git.getDiffrequests, the pane content grew, and the expanded lines rendered. That isloadDiffFilesend to end. - Untracked and unstaged, real worktree. Ran the host-side patch build against this worktree's live state (one modified tracked file, two untracked files) and parsed the result with
parsePatchFiles: the tracked edit comes backtype: "change", both untracked files come backtype: "new"with hunks, and every name is repo-relative, which is what the renderer matchesChangesetFile.pathagainst. - Edit mode on a partial diff resolves in the library, not in our code:
FileDiff.attachEditorcallsloadFilesIfNecessary()whenfileDiff.isPartial(dist/components/FileDiff.js:636), the same path expanding context uses (:505), andcanHydrateDifflimits hydration tochange/rename-changed/rename-pure—newanddeletedalready carry their side, which is why our loader is never asked for them. Verified by reading the shipped source; not yet exercised through the editor UI.
Older host-service compatibility
Desktop and host-service ship from one version, but a remote or cloud workspace runs host-service inside its own sandbox and can be older than the app talking to it, and there is no capability negotiation between them. A desktop on this build pointed at such a host would have called a procedure that isn't there and rendered every file as "Unable to load diff".
The patch query now catches a missing-procedure error and falls back to per-file git.getDiff, parsed into complete metadata — what the pane did before this branch. Verified by renaming the procedure server-side to simulate an old host:
| host | getDiffPatch calls | per-file getDiff calls | pane |
|---|---|---|---|
| simulated old (procedure absent) | 2, both 404 | 22 | renders, no error placeholders |
| current | 2, both 200 | 0 | renders identically |
Not done here
The item list is still controlled items rather than initialItems + addItems, so files appear when the patch resolves rather than streaming in as it arrives. That's the remaining half of the DiffsHub pattern and wants its own PR.
Two paths remain unexercised through the UI: entering edit mode on a partial diff (the library source says it hydrates; nobody has typed into one), and an untracked file rendering in the pane (verified at the patch and parse layers only). Both need a workspace with uncommitted changes; the app's command palette searches commands rather than workspaces, so I could not drive the running instance to one.
If loadDiffFiles rejects — a file over the 500k-char hydration ceiling — the library logs and leaves the diff partial. For expansion that degrades cleanly. For edit mode on such a file it would mean editing against un-hydrated content, so the ceiling and the generated-file gate should stay aligned.
https://claude.ai/code/session_01Pkvq2izakEnyAXphgz176E
Summary by cubic
Renders the Changes pane from per-category git diff patches instead of pulling two complete copies of every changed file, so a changeset now ships about a quarter of the bytes (3.4 MB vs 13.2 MB here). Diffs render as per-file placeholders: small, non-generated files auto-load on scroll, while large and generated files show a "Load diff" button (or "Open file" past the 500k-char hydration ceiling); a failed fetch retries per file instead of showing a pane-wide error.
Host service
- Adds
git.getDiffPatch, one patch per category built in the existing worker pool; a failedgit diffnow throws so the query reports an error instead of an empty patch. - Untracked files get their own
--no-indexpatch section, sincegit diffwon't report them. git.getDiffBulkhas no renderer caller left; the procedure stays for older desktop builds.
Renderer
- Groups files by ref pair, requests one patch per group, and parses hunks into per-file metadata; a patch missing a file's section shows that file's error state, not a generated-file placeholder, and the load/retry button refetches on the first click.
- Expanding context or editing fetches full contents for just that file.
- Diff cache keys use react-query's
dataUpdatedAtinstead of hashing both file bodies. - If the host lacks
getDiffPatch(an older remote or cloud host), the pane falls back to per-filegetDiffcontents so files don't render as permanent errors. - Files appear when the patch resolves rather than streaming in as it arrives; that's left for a follow-up.
Summary by CodeRabbit
- New Features
- Diff views now load file contents on demand, improving performance for large or generated files.
- Added clear placeholders for hidden, loading, or failed diffs, with Load diff and Retry actions.
- Large files are protected from excessive loading.
- Bug Fixes
- Staging, discarding, refreshing, and saving changes now reliably update displayed status and diffs.
- Localization
- Added translated messages for new diff-loading states and actions.
- Tests
- Added coverage for diff safeguards and Git diff retrieval scenarios.