From ba764a24ea67d220b04b818dc0e3d969463d15f3 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 10 Mar 2026 07:18:42 -0700 Subject: [PATCH] fix(site): upgrade @pierre/diffs to 1.1.0-beta.19 (#22895) Fixes a race condition in `DiffHunksRenderer` where a stale async highlight callback overwrites the render cache with an old diff, causing a hunk count mismatch: ``` DiffHunksRenderer.renderHunks: lineHunk doesn't exist ``` ## Root cause The `DiffHunksRenderer` in `@pierre/diffs@1.0.11` caches highlighted AST results keyed by diff object reference. When the shiki highlighter isn't fully loaded, it fires `asyncHighlight(diff)` which captures the current diff in a closure. If the diff changes before that promise resolves, `onHighlightSuccess` unconditionally overwrites `renderCache` with the stale diff/result pair. The subsequent `rerender()` then iterates the new diff's hunks against the old result's `code.hunks` array, crashing at an out-of-bounds index. ## Fix Upgrades `@pierre/diffs` from `1.0.11` to `1.1.0-beta.19`, which completely refactors the rendering pipeline: - Replaces the per-hunk `code.hunks[hunkIndex]` lookup with flat `additionLines`/`deletionLines` arrays indexed directly by line index - Uses a new `iterateOverDiff` callback pattern instead of the `renderHunks` method - The `lineHunk doesn't exist` error is gone from the codebase entirely The only code change on our side is adapting `extractDiffContent()` in `FilesChangedPanel.tsx` to the new `ChangeContent`/`ContextContent` types where `deletions`, `additions`, and `lines` are now counts with index pointers into top-level `FileDiffMetadata.deletionLines`/`additionLines` arrays. --- site/package.json | 2 +- site/pnpm-lock.yaml | 19 +++++++++------ .../pages/AgentsPage/FilesChangedPanel.tsx | 23 ++++++++++++------- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/site/package.json b/site/package.json index 191d9678fe..4c61f3bec1 100644 --- a/site/package.json +++ b/site/package.json @@ -53,7 +53,7 @@ "@mui/material": "5.18.0", "@mui/system": "5.18.0", "@mui/x-tree-view": "7.29.10", - "@pierre/diffs": "1.0.11", + "@pierre/diffs": "1.1.0-beta.19", "@radix-ui/react-avatar": "1.1.11", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index f2343291ac..38b2eae0fc 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -74,8 +74,8 @@ importers: specifier: 7.29.10 version: 7.29.10(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.2))(@types/react@19.2.7)(react@19.2.2))(@mui/material@5.18.0(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.2))(@types/react@19.2.7)(react@19.2.2))(@types/react@19.2.7)(react-dom@19.2.2(react@19.2.2))(react@19.2.2))(@mui/system@5.18.0(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.2))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.2))(@types/react@19.2.7)(react@19.2.2))(@types/react@19.2.7)(react@19.2.2))(@types/react@19.2.7)(react-dom@19.2.2(react@19.2.2))(react@19.2.2) '@pierre/diffs': - specifier: 1.0.11 - version: 1.0.11(react-dom@19.2.2(react@19.2.2))(react@19.2.2) + specifier: 1.1.0-beta.19 + version: 1.1.0-beta.19(react-dom@19.2.2(react@19.2.2))(react@19.2.2) '@radix-ui/react-avatar': specifier: 1.1.11 version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.2(react@19.2.2))(react@19.2.2) @@ -1596,12 +1596,16 @@ packages: cpu: [x64] os: [win32] - '@pierre/diffs@1.0.11': - resolution: {integrity: sha512-j6zIEoyImQy1HfcJqbrDwP0O5I7V2VNXAaw53FqQ+SykRfaNwABeZHs9uibXO4supaXPmTx6LEH9Lffr03e1Tw==, tarball: https://registry.npmjs.org/@pierre/diffs/-/diffs-1.0.11.tgz} + '@pierre/diffs@1.1.0-beta.19': + resolution: {integrity: sha512-XxGPKkVW+1t2KJQfgjmSnS+93nI9+ACJl1XjhF3Lo4BdQJOxV3pHeyix31ySn/m/1llq6O/7bXucE0OYCK6Kog==, tarball: https://registry.npmjs.org/@pierre/diffs/-/diffs-1.1.0-beta.19.tgz} peerDependencies: react: ^18.3.1 || ^19.0.0 react-dom: ^18.3.1 || ^19.0.0 + '@pierre/theme@0.0.22': + resolution: {integrity: sha512-ePUIdQRNGjrveELTU7fY89Xa7YGHHEy5Po5jQy/18lm32eRn96+tnYJEtFooGdffrx55KBUtOXfvVy/7LDFFhA==, tarball: https://registry.npmjs.org/@pierre/theme/-/theme-0.0.22.tgz} + engines: {vscode: ^1.0.0} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==, tarball: https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz} engines: {node: '>=14'} @@ -7941,10 +7945,9 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.14.0': optional: true - '@pierre/diffs@1.0.11(react-dom@19.2.2(react@19.2.2))(react@19.2.2)': + '@pierre/diffs@1.1.0-beta.19(react-dom@19.2.2(react@19.2.2))(react@19.2.2)': dependencies: - '@shikijs/core': 3.22.0 - '@shikijs/engine-javascript': 3.22.0 + '@pierre/theme': 0.0.22 '@shikijs/transformers': 3.22.0 diff: 8.0.3 hast-util-to-html: 9.0.5 @@ -7953,6 +7956,8 @@ snapshots: react-dom: 19.2.2(react@19.2.2) shiki: 3.22.0 + '@pierre/theme@0.0.22': {} + '@pkgjs/parseargs@0.11.0': optional: true diff --git a/site/src/pages/AgentsPage/FilesChangedPanel.tsx b/site/src/pages/AgentsPage/FilesChangedPanel.tsx index d18dbb31c1..c65543c3ff 100644 --- a/site/src/pages/AgentsPage/FilesChangedPanel.tsx +++ b/site/src/pages/AgentsPage/FilesChangedPanel.tsx @@ -89,6 +89,7 @@ function extractDiffContent( const file = parsedFiles.find((f) => f.name === fileName); if (!file) return ""; + const lines = side === "additions" ? file.additionLines : file.deletionLines; const collected: string[] = []; for (const hunk of file.hunks) { let addLine = hunk.additionStart; @@ -96,10 +97,14 @@ function extractDiffContent( for (const block of hunk.hunkContent) { if (block.type === "context") { - for (const line of block.lines) { + for (let i = 0; i < block.lines; i++) { const ln = side === "additions" ? addLine : delLine; if (ln >= startLine && ln <= endLine) { - collected.push(line); + const idx = + side === "additions" + ? block.additionLineIndex + i + : block.deletionLineIndex + i; + if (lines[idx] != null) collected.push(lines[idx]); } addLine++; delLine++; @@ -107,23 +112,25 @@ function extractDiffContent( } else { // ChangeContent block. if (side === "deletions") { - for (const line of block.deletions) { + for (let i = 0; i < block.deletions; i++) { if (delLine >= startLine && delLine <= endLine) { - collected.push(line); + const line = lines[block.deletionLineIndex + i]; + if (line != null) collected.push(line); } delLine++; } // Addition lines in a change block still advance // the addition counter. - addLine += block.additions.length; + addLine += block.additions; } else { // side === "additions" // Deletion lines in a change block still advance // the deletion counter. - delLine += block.deletions.length; - for (const line of block.additions) { + delLine += block.deletions; + for (let i = 0; i < block.additions; i++) { if (addLine >= startLine && addLine <= endLine) { - collected.push(line); + const line = lines[block.additionLineIndex + i]; + if (line != null) collected.push(line); } addLine++; }