fix: fall back to local git watcher for chat diff drawer#24512
Conversation
|
The failing That test downloads and verifies HashiCorp's published Evidence:
Happy to rebase once the key bundle is refreshed (or leave this to a follow-up if someone is already on it).
|
The Ctrl+D diff drawer in `coder exp agents` only rendered PR-backed
diffs returned by `/api/experimental/chats/{id}/diff`. Local working
tree changes in a chat's workspace produced an empty response, which
showed "No diff contents" in the drawer and no file summary.
Centralise diff loading behind a single `fetchChatDiffContents` helper
that first hits `/diff`, then falls back to the chat git watcher
WebSocket (`/stream/git`) when the remote diff is empty. Aggregate the
agent's `WorkspaceAgentRepoChanges` into a `ChatDiffContents` value so
the drawer can derive the file summary and styled body from the local
unified diff. Treat missing workspaces, missing agents, and watcher
timeouts as graceful fallbacks that render the empty-diff placeholder
instead of a hard error.
6991a5e to
7f05c14
Compare
|
@codex review
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7f05c14333
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…-quotes
The previous parser split the unquoted header with strings.Fields, so any
path containing spaces (e.g., 'a/foo bar.txt b/foo bar.txt') was treated
as four separate tokens and marked as a rename with truncated filenames.
For binary or mode-only diffs that lack '--- '/'+++ ' lines, those wrong
paths then leaked into the diff drawer summary.
The new parser recognizes both header forms:
- Quoted form ('diff --git "a/<old>" "b/<new>"') via strconv.Unquote,
which handles C-escaped paths (e.g., octal byte escapes).
- Unquoted form by searching for a ' b/' split point where the two
halves are equal after stripping a/ and b/ (the non-rename case),
falling back to the single-space split for simple renames.
When the header is ambiguous (rename with spaces in the path), the parser
now returns ok=false. The caller still creates a placeholder entry so the
follow-up 'rename from'/'rename to'/'--- '/'+++ ' lines can fill in the
correct paths; flush() already drops entries with an empty FilePath.
|
@codex review
|
mafredri
left a comment
There was a problem hiding this comment.
First-pass review (Netero). This is a mechanical scan; the full review panel has not yet reviewed this PR.
The diff-fetching fallback and unified-diff parser are well-structured, and the test coverage is solid. One P2 finding: dead code left behind after removing the gitChangesMsg producer.
🤖 This review was automatically generated with Coder Agents.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a94843fc15
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
shouldIgnoreLocalDiffFallbackError only recognized two of the 400 responses watchChatGit can return. When the chat pointed at a deleted workspace the server returned 'Chat workspace not found.' and the diff drawer regressed from showing 'No diff to display.' to a hard error. Treat all of the watchChatGit 400 variants (no workspace bound, workspace deleted, no agents, agent not connected) as ignorable and fall back to the empty remote diff, matching the pre-fallback behavior of the drawer. Add subtests covering each message.
The earlier fetchChatDiffContents refactor dropped the GetChatGitChanges dispatch from toggleDiffDrawerCmd, which orphaned the whole gitChangesMsg consumer chain. m.chat.gitChanges is never populated, so every downstream consumer worked against a permanently nil slice and resolvedChatDiffChanges always fell through to parseChatGitChangesFromUnifiedDiff. Drop: - gitChangesMsg struct and its chat handler branch - gitChanges field on chatViewModel and its reset in setChat - Draft/gitChangesMsg stale-generation test case - resolvedChatDiffChanges helper now that the parsed-from-diff path is the only one we ever hit - changes parameter from renderChatDiffSummary and renderDiffDrawer (both are now driven purely by the ChatDiffContents input) Callers and tests are updated accordingly. No behavior change: the diff drawer still derives its summary from the unified diff via parseChatGitChangesFromUnifiedDiff.
|
@codex review
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 957cfc3fc3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Git emits C-quoted paths on 'rename from'/'rename to' lines whenever
the name contains non-ASCII bytes or control characters (with the
default core.quotepath setting). The parser was storing the raw
token, so a quoted rename like
rename from "b\\303\\244r old.txt"
rename to "b\\303\\244r new.txt"
overwrote the already-decoded header paths and the diff summary
showed the octal-escaped form. Route these lines through
trimUnifiedDiffPath just like '--- '/'+++ ' so the paths decode to
'bär old.txt' and 'bär new.txt'.
|
@codex review
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 873d208dbd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Two follow-ups from the Codex review of the earlier rename-path fix: 1. Added or removed lines inside a hunk can legitimately start with '+++ ' or '--- ' (the content happens to begin with '++ ' or '-- '), so treating every such line as a file header would overwrite FilePath and OldPath and miscount additions/deletions. Track an inHunk flag that flips on the first '@@' marker after each 'diff --git' header and gate the header-only cases (new/deleted file mode, rename from/rename to, ---, +++) on !inHunk. Inside the hunk we instead credit lines starting with '+' or '-' to the addition/deletion counters. 2. 'rename from'/'rename to' paths are repository-relative and never carry the a/ or b/ prefix, so trimUnifiedDiffPath (which strips a/ and b/) would truncate real directories. Introduce a decodeQuotedDiffLinePath helper that only handles C-quote decoding and TrimSpace, and route the rename-line parsing through it. Added tests covering a rename that really lives under a literal 'a/' directory and a hunk body that contains '--- '/'+++ ' content lines.
mafredri
left a comment
There was a problem hiding this comment.
Panel review (12 reviewers). DEREM-1 from Round 1 was cleanly addressed in 957cfc3.
The consolidation from two parallel API calls into a single sequential fetch with WebSocket fallback is well-structured. The unified diff parser handles quoted paths, renames, and multi-repo aggregation correctly. Test coverage is solid across the main paths.
1 P2, 3 P3, 5 Nit. The P2 is a parser bug that corrupts file summaries for diffs containing SQL-style comments. The P3s are a missing 403 fallback case, server message coupling, and test coverage gaps.
Takumi verified all concurrency paths are clean (goroutine lifecycle, timeout handling, generation guards, race detector). Pariston confirmed the problem framing and solution alignment are correct. Mafu-san observed the clean response to R1 feedback.
"A deleted line whose original content starts with
--becomes--- Comment textin the diff. The parser matches this as a file header, setscurrent.OldPathto garbage, and skips the deletion counter." (Hisoka)
🤖 This review was automatically generated with Coder Agents.
|
@codex review
|
mafredri
left a comment
There was a problem hiding this comment.
Review blocked. DEREM-4 (P2, inHunk parser bug) was addressed in 59a1318, confirmed via the parallel Codex thread.
10 findings from Round 2 have no author response or code change. Further review is blocked until the author responds or pushes fixes.
P3 findings needing response:
- DEREM-5: Server message substring matching (no shared constants or error codes)
- DEREM-6: HTTP 403 not treated as graceful fallback
- DEREM-7: Four test coverage gaps (non-empty remote diff path, error propagation, edge cases)
- DEREM-8: Missing doc comment on
dialChatGitexplaining whyClient.Dialcan't be used
Nit findings needing response:
- DEREM-2: Stale
tc := tcloop capture - DEREM-3: Stale
message := messageloop capture - DEREM-9:
time.Sleepin test handler - DEREM-10:
context.Background()vst.Context() - DEREM-11:
strings.Splitvsstrings.SplitSeq - DEREM-12:
1 << 22without// 4MiBannotation
Acknowledging findings (with or without fixing) unblocks the next round.
🤖 This review was automatically generated with Coder Agents.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 59a1318884
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
shouldIgnoreLocalDiffFallbackError matched the server-side 400 messages from watchChatGit as lowercased substrings, so a server-side reword would silently stop the fallback and surface the raw error in the TUI diff drawer. It also only handled 404 and the 400 variants, so a 403 from authorizeChatWorkspaceExec when the chat owner's workspace permissions were revoked still bubbled up as a hard error for a scenario the remote diff endpoint handles gracefully. Expose the ignorable messages as shared codersdk constants, add ChatGitWatchAgentStateMessage for the formatted agent-state case, and IsChatGitWatchFallbackMessage that both the server and client now reference. shouldIgnoreLocalDiffFallbackError calls the predicate and also treats 403 as ignorable to match the remote diff endpoint's graceful behavior. Addresses mafredri Round 2 DEREM-5, DEREM-6. > Mux is running the Codex review loop on behalf of Mike.
- drop the redundant tc := tc loop-variable capture in parseUnifiedDiffHeaderPaths table test; Go 1.22+ scopes range variables per iteration (DEREM-2). - replace time.Sleep in the watcher timeout test with conn.Read on the accepted WebSocket so the handler drains cleanly when the client disconnects instead of blocking on a fixed wait (DEREM-9). - thread t.Context() through the fallback tests so the handlers and requests auto-cancel on test cleanup (DEREM-10). - replace strings.Split with strings.SplitSeq in the unified-diff parser so a 4 MiB diff does not allocate a full []string just to iterate once (DEREM-11). - expand IgnoresMissingWorkspaceFallbackErrors into a subtest per ignorable message so each 400 variant has coverage. > Mux is running the Codex review loop on behalf of Mike.
renderDiffDrawer called renderChatDiffSummary on every View() pass, and that helper walked the full unified diff through parseChatGitChangesFromUnifiedDiff each time. For 4 MiB local diffs (the watcher's read limit) routine redraw triggers like keypresses, resize, and status ticks repeatedly re-split and re-parse the same text and can stall the TUI. Cache the rendered summary on chatViewModel.diffSummary when the diffContentsMsg arrives and pass it into renderDiffDrawer instead. setChat clears the cache alongside diffContents so a new chat does not inherit a stale summary. Addresses Codex P2 feedback on 59a1318. > Mux is running the Codex review loop on behalf of Mike.
…r paths Add four subtests for the branches that were previously uncovered: - ReturnsRemoteDiffWithoutDialingWatcher asserts the fast path at fetchChatDiffContents returns the non-empty remote diff without dialing the WebSocket fallback. The git stream handler fails the request loudly if it ever fires, so inverting the condition regresses noisily. - PropagatesRemoteDiffAPIErrors asserts a 500 from /diff is returned verbatim and the local watcher is skipped. - SurfacesNonIgnorableWatcherErrors asserts a 500 from /stream/git is returned verbatim instead of collapsing to the empty remote diff. - Split TestBuildLocalChatDiffContents into subtests and add two edge cases covering the empty-repositories early return and the removed/empty UnifiedDiff filter, plus the all-skipped variant. Addresses mafredri Round 2 DEREM-7. > Mux is running the Codex review loop on behalf of Mike.
|
Round 7: addressed all 5 findings from @mafredri's latest review on head 759c7ab in 6aa1579:
Validation:
|
|
@codex review |
|
@codex review |
mafredri
left a comment
There was a problem hiding this comment.
Re-review (3 reviewers). All 20 prior findings addressed. CI green. Netero clean. Mafuuu and Meruem: no findings.
20 findings raised across 5 rounds: 2 P2 correctness bugs (dead code chain, hunk body misparse), 8 P3 (403 fallback, string coupling, test gaps, dead SDK method, diff body caching, metadata backfill), 7 Nits. All addressed with dedicated commits.
One remaining P3: the 404 branch in shouldIgnoreLocalDiffFallbackError lacks a test (same class as DEREM-16). Otherwise the PR is clean and ready for human review.
🤖 This review was automatically generated with Coder Agents.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6aa1579e04
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ackfill Addresses two findings from the Codex + mafredri review on 6aa1579: - [Codex P2] Metadata backfill was gated on `localDiff.Branch != nil || localDiff.RemoteOrigin != nil`, but that is an unreliable proxy for "single contributing repository". A repo in detached HEAD with no origin remote legitimately leaves both fields nil even though exactly one repository produced the diff. Codex correctly pointed out that this case silently dropped remote Provider/PullRequestURL from the drawer. Replace the proxy with an explicit `singleRepo` flag returned from buildLocalChatDiffContents so the backfill invariant ("single contributing repo is safe to backfill, multi-repo is not") is expressed in the type. Propagate the flag through fetchLocalChatDiffContents and gate the merge block on it. Update the block comment so it documents the invariant rather than the old proxy. - [DEREM-21 P3, mafredri] shouldIgnoreLocalDiffFallbackError's 404 branch had no test even though every other ignorable-error branch (context.DeadlineExceeded, 403, and each 400 fallback message) now has a dedicated subtest. Add IgnoresNotFoundWatcherFallbackErrors mirroring the 403 coverage added for DEREM-16 so the branch is mechanically covered and a future regression cannot silently remove it. Also add BackfillsRemoteMetadataWhenSingleRepoHasBlankBranchAndOrigin to cover the singleRepo edge case end-to-end: single contributing repo with empty Branch/RemoteOrigin strings plus non-empty remote metadata asserts that every remote field is backfilled. > Mux is Mike's coding agent. This commit was made on Mike's behalf.
|
Round 8: addressed the two review findings on head 6aa1579 in 0d3f417:
Also added Updated the four Pre-commit passed (gen/fmt/lint/build).
|
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0d3f417c38
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Addresses Codex P2 on head 0d3f417: the local chat git watcher set a 4 MiB websocket read limit but agentgit caps each repository's UnifiedDiff at 3 MiB (maxTotalDiffSize), so a Changes message aggregating two or more maxed-out repos legitimately exceeds 4 MiB and triggers a coder/websocket close with StatusMessageTooBig. Because wsjson.Decoder swallowed the read error (log at Debug, close the channel), fetchLocalChatDiffContents surfaced a generic "git watch connection closed" error and the diff drawer crashed with a hard error in exactly the large-multi-repo case the local fallback was meant to improve: a regression from the pre-fallback behavior. Fix it with a narrowly scoped, two-layered degrade path: 1. Bump the client-side read limit from 4 MiB to 32 MiB (localChatDiffReadLimit), which covers ~10 maxed-out repos and makes realistic multi-repo worktrees just work without any fallback. 2. For pathological payloads beyond 32 MiB, treat the specific StatusMessageTooBig close as ignorable. fetchLocalChatDiffContents now reads from the websocket directly (bypassing wsjson.NewStream) so it can inspect websocket.CloseStatus(err), returning the narrow errLocalDiffMessageTooLarge sentinel on read-limit violations. shouldIgnoreLocalDiffFallbackError maps that sentinel onto the same remote-empty-diff fallback we already use for 404/403/400. Generic websocket close / decode / protocol errors continue to surface as hard errors so real regressions do not silently disappear behind the fallback. Tests: - IgnoresWatcherMessageTooBigCloses accepts the websocket, reads the refresh, then closes with StatusMessageTooBig. The client must degrade to the empty remote diff without error. - SurfacesUnexpectedWatcherCloseErrors closes with StatusInternalError instead and pins that the fallback is scoped to MessageTooBig only, so a future attempt to blanket-ignore every close reason immediately breaks the test. Pre-commit (gen/fmt/lint/build) passed. Full cli/ tests pass in 60.9s. > Mux is Mike's coding agent. This commit was made on Mike's behalf.
|
Round 9: addressed Codex P2 on head 0d3f417 in fcd8b7b. [Codex P2] The local chat git watcher set a 4 MiB websocket read limit but agentgit caps each repo's Fixed with a two-layered degrade path:
Tests:
Pre-commit (gen/fmt/lint/build) passed. Full
|
|
@codex review |
mafredri
left a comment
There was a problem hiding this comment.
Status: APPROVED
All 23 findings resolved across 6 rounds. CI green. Netero clean. Panel (Mafuuu, Meruem, Hisoka): no findings.
Over 6 rounds this review raised 2 P2 correctness bugs, 9 P3 defects, 7 Nits, and 5 dropped findings. Every finding was addressed with dedicated commits. The author improved the code beyond the original findings: shared constants replaced string coupling, diff body caching was added alongside summary caching, and the metadata backfill was refactored from a nil-check proxy to an explicit singleRepo flag.
937 test lines cover 666 production lines. Every branch in shouldIgnoreLocalDiffFallbackError has a dedicated subtest. The unified diff parser handles quoted paths, renames, spaces, and hunk-body lookalikes. Concurrency paths are clean.
🤖 This review was automatically generated with Coder Agents.
|
Codex Review: Didn't find any major issues. 🎉 ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fcd8b7b858
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Mirrors Round 7 DEREM-15 on the frontend side. site/src/api/api.ts
defined ExperimentalApiMethods.getChatGitChanges (plus the internal
ChatGitChangeResponse interface backing it) that targeted
GET /api/experimental/chats/{id}/git-changes, but:
- No call site exists anywhere under site/src/. The method was a pure
orphan, exactly like codersdk.ExperimentalClient.GetChatGitChanges
was on the Go side before Round 7 removed it.
- The HTTP route is not registered in coderd's chat router. The
frontend's live diff panels rely on GET /diff (RemoteDiffPanel via
chatDiffContents query) and the /stream/git websocket (useGitWatcher
+ GitPanel + LocalDiffPanel). /git-changes was never part of that
wiring.
Drop the method and the ChatGitChangeResponse interface. The
TypesGen.ChatGitChange generated type stays because the Go codersdk
type is still referenced by parseChatGitChangesFromUnifiedDiff in cli.
No behavior change.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 03d5021385
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…-repo diff Round 10 addresses both Codex findings on head 03d5021. [Codex P1] Fallback on upstream oversize close statuses. fetchLocalChatDiffContents previously degraded only on StatusMessageTooBig, but the coderd watchChatGit proxy (coderd/exp_chats.go) always closes the client stream with StatusGoingAway regardless of why the upstream agent->coderd hop failed. In particular, when that hop's 4 MiB read limit (workspacesdk/agentconn.go SetReadLimit) is exceeded, the agent closes its end with StatusMessageTooBig but the proxy does not propagate that status. So for diffs between the 4 MiB upstream limit and our 32 MiB client read limit the client only ever sees StatusGoingAway and Round 9's narrow match fails to degrade, returning a hard error in exactly the multi-repo worktree case that local fallback was meant to improve. Broaden the close-status match to cover both StatusMessageTooBig and StatusGoingAway while keeping every other close status (StatusInternalError, StatusProtocolError, ...) and every non-close read error propagating so real protocol regressions still reach the user. Rename the narrow errLocalDiffMessageTooLarge sentinel to errLocalDiffWatchClosed to reflect the broader semantic. Pin the new behavior with TestFetchChatDiffContents/IgnoresWatcherGoingAwayCloses alongside the existing MessageTooBig test; the SurfacesUnexpectedWatcherCloseErrors test (which uses StatusInternalError) continues to enforce that unrelated close reasons still surface. [Codex P2] Flag partially unparsable diffs in summary. renderChatDiffSummary only fell back to 'Changes present but could not be summarized.' when parseChatGitChangesFromUnifiedDiff returned zero entries, but a multi-repo aggregate assembled by buildLocalChatDiffContents can legitimately mix real 'diff --git' chunks from small repos with agent/agentgit's 'Total diff too large to show...' placeholder for repos that exceed maxTotalDiffSize (3 MiB). In that case the function counted the real chunks, returned a normal 'N files changed' summary, and silently dropped the oversized repo. The drawer then underreported the changeset in exactly the multi-repo case this feature is meant to improve. Detect the placeholder prefix separately via hasOversizedRepoPlaceholder and append '(some repositories omitted: diff too large to summarize)' to the summary whenever one is present. Scoped to lines that start with the placeholder prefix so a legitimate '+' added line that happens to contain the phrase cannot trigger a false positive. Covered by TestExpAgentsRender/RenderDiffDrawer/FlagsPartiallyUnparsableMultiRepoDiff alongside the existing ShowsFallbackForUnparsableNonEmptyDiff case (placeholder-only input still goes through the zero-changes branch).
|
Round 10: addressed both Codex findings on head 03d5021 in 2668c33.
Local validation: relevant cli tests (TestFetchChatDiffContents, TestExpAgentsRender, TestBuildLocalChatDiffContents) all pass. make pre-commit green.
@codex review |
|
Codex Review: Didn't find any major issues. More of your lovely PRs please. ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
There was a problem hiding this comment.
Status: APPROVED
All clear. Netero clean. Panel (Mafuuu, Meruem, Hisoka): no findings.
The two new commits since R6 (dead getChatGitChanges TS removal, oversized watcher payload graceful degradation with 32 MiB read limit + StatusMessageTooBig sentinel) are clean. Test coverage now includes the oversized-payload fallback path.
23 findings raised and resolved across 7 rounds. No open findings. CI green. Ready for human review.
🤖 This review was automatically generated with Coder Agents.
The Ctrl+D diff drawer in
coder exp agentsonly rendered PR-backeddiffs returned by
/api/experimental/chats/{id}/diff. Local workingtree changes in a chat's workspace returned an empty diff, so the
drawer showed "No diff contents" with no file summary.
Centralise diff loading behind a single
fetchChatDiffContentshelperthat first hits
/diff, then falls back to the chat git watcherWebSocket (
/stream/git) when the remote diff is empty. Aggregate theagent's
WorkspaceAgentRepoChangesinto aChatDiffContentsvalue sothe drawer can derive the file summary and styled body from the local
unified diff. Missing workspaces, missing agents, and watcher timeouts
are treated as graceful fallbacks that render the empty-diff
placeholder instead of a hard error.