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.
This commit is contained in:
Kyle Carberry
2026-03-11 13:04:58 +00:00
committed by GitHub
parent f766ad064d
commit c72d3e4919
@@ -559,13 +559,20 @@ export const FilesChangedPanel: FC<FilesChangedPanelProps> = ({
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) => {