From c72d3e4919b86400e71ae8679efd2a59790a7f85 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 11 Mar 2026 06:04:58 -0700 Subject: [PATCH] fix(site): include diff length in cache key to prevent stale highlight reuse (#22942) When a PR diff update arrives via SSE, the diff content query is invalidated and re-fetched. `parsePatchFiles` was called with the same cache key prefix (`chat-{chatId}`) regardless of content, so the `@pierre/diffs` worker pool's LRU cache returned the old highlighted AST. The stale `code.additionLines`/`code.deletionLines` arrays no longer matched the new diff's line structure, causing `DiffHunksRenderer.processDiffResult` to throw: ``` DiffHunksRenderer.processDiffResult: deletionLine and additionLine are null, something is wrong ``` **Root cause:** The rendering pipeline has two phases that both call `iterateOverDiff` but with different `diffStyle` parameters. Phase 1 (highlighting) uses `diffStyle: "both"` to populate `code.deletionLines[]` and `code.additionLines[]`. Phase 2 (DOM construction in `processDiffResult`) uses `diffStyle: "unified"` or `"split"` to consume those arrays. When the cache returned stale phase 1 output for new diff content, the line indices from phase 2 pointed to entries that didn't exist in the stale arrays. **Fix:** Append `diff.length` to the cache key prefix so that content changes produce a cache miss and trigger fresh highlighting. While not collision-proof, it's vanishingly unlikely that two sequential PR diff updates have the exact same byte length. --- site/src/pages/AgentsPage/FilesChangedPanel.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/site/src/pages/AgentsPage/FilesChangedPanel.tsx b/site/src/pages/AgentsPage/FilesChangedPanel.tsx index c65543c3ff..eb40448f21 100644 --- a/site/src/pages/AgentsPage/FilesChangedPanel.tsx +++ b/site/src/pages/AgentsPage/FilesChangedPanel.tsx @@ -559,13 +559,20 @@ export const FilesChangedPanel: FC = ({ try { // The cacheKeyPrefix enables the worker pool's LRU cache // so highlighted ASTs are reused across re-renders instead - // of being re-computed on every render cycle. - const patches = parsePatchFiles(diff, `chat-${chatId}`); + // of being re-computed on every render cycle. We include + // dataUpdatedAt so that when the diff content changes + // (e.g. new commits pushed) the old cached highlight AST + // is not reused with mismatched line indices, which would + // cause DiffHunksRenderer.processDiffResult to throw. + const patches = parsePatchFiles( + diff, + `chat-${chatId}-${diffContentsQuery.dataUpdatedAt}`, + ); return patches.flatMap((p) => p.files); } catch { return []; } - }, [diffContentsQuery.data?.diff, chatId]); + }, [diffContentsQuery.data?.diff, diffContentsQuery.dataUpdatedAt, chatId]); const handleSubmitComment = useCallback( (text: string) => {