From 6414e64b291619457c13d089102059e4e0f29b06 Mon Sep 17 00:00:00 2001 From: kirillk Date: Tue, 4 Aug 2026 09:38:14 -0400 Subject: [PATCH] fix(jetbrains): render multi-hunk diffs --- .changeset/jetbrains-multihunk-diff-viewer.md | 5 +++ .../client/diff/DiffPatchReconstruct.kt | 13 ++++--- .../client/diff/DiffPatchReconstructTest.kt | 37 ++++++++++++++++++- .../client/diff/KiloDiffEditorContentTest.kt | 25 +++++++++++++ 4 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 .changeset/jetbrains-multihunk-diff-viewer.md diff --git a/.changeset/jetbrains-multihunk-diff-viewer.md b/.changeset/jetbrains-multihunk-diff-viewer.md new file mode 100644 index 00000000000..b4a871eebb9 --- /dev/null +++ b/.changeset/jetbrains-multihunk-diff-viewer.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Render multi-hunk modified-file diffs correctly in the JetBrains diff viewer. diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/diff/DiffPatchReconstruct.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/diff/DiffPatchReconstruct.kt index 654ef86da03..ca2c92cf46e 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/diff/DiffPatchReconstruct.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/diff/DiffPatchReconstruct.kt @@ -53,11 +53,14 @@ internal object DiffPatchReconstruct { } } } - // Both producers (CLI snapshot and branchDiff) emit a single full-context hunk. A patch with - // several hunks, or one whose header lengths don't match the reconstructed body, has elided - // context: reconstructing would place every line at the wrong number, so fall back to the - // raw-patch view (renderable = false) instead of showing a misaligned side-by-side diff. - if (hunks != 1 || oldSeen != oldLen || newSeen != newLen) return DiffSides("", "", false) + // A patch may carry several hunks (limited-context git output) or a single full-context hunk. + // We concatenate every hunk body into contiguous before/after text: unchanged context lines + // anchor each region so the resulting side-by-side still colors adds/removes correctly. The + // elided gaps between hunks collapse (line numbers restart at 1), which is acceptable for a + // "what changed" view and far better than the all-green raw-patch fallback. We still bail when + // there is no hunk, or when the header lengths don't match the reconstructed body (truncated + // context), because that would place lines against the wrong side. + if (hunks < 1 || oldSeen != oldLen || newSeen != newLen) return DiffSides("", "", false) val left = if (added(patch)) "" else before.toString().removeSuffix("\n") val right = if (deleted(patch)) "" else after.toString().removeSuffix("\n") return DiffSides(left, right, true) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/diff/DiffPatchReconstructTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/diff/DiffPatchReconstructTest.kt index 24ab5b67c5a..0215144879a 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/diff/DiffPatchReconstructTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/diff/DiffPatchReconstructTest.kt @@ -133,7 +133,42 @@ class DiffPatchReconstructTest { } @Test - fun `multi hunk partial context patch is not renderable`() { + fun `multi hunk patch reconstructs concatenated changed regions`() { + // Turn/tool diffs are ordinary limited-context git output with several hunks. The reconstruction + // stitches each hunk body into contiguous before/after text so the diff editor colors the + // changes instead of dumping the raw patch as all-added lines. Inter-hunk gaps collapse. + val dto = DiffFileDto( + file = "src/A.kt", + additions = 2, + deletions = 2, + patch = """ + diff --git a/src/A.kt b/src/A.kt + --- a/src/A.kt + +++ b/src/A.kt + @@ -1,3 +1,3 @@ + one + -two + +TWO + three + @@ -20,3 +20,3 @@ + twenty + -x + +X + z + """.trimIndent(), + ) + + val sides = DiffPatchReconstruct.sides(dto) + + assertTrue(sides.renderable) + assertEquals("one\ntwo\nthree\ntwenty\nx\nz", sides.before) + assertEquals("one\nTWO\nthree\ntwenty\nX\nz", sides.after) + } + + @Test + fun `multi hunk patch with truncated context is not renderable`() { + // header claims 3 old / 3 new lines per hunk but the body carries only 2 of each: reconstructing + // would misalign the sides, so fall back to the raw-patch view. val dto = DiffFileDto( file = "src/A.kt", additions = 2, diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/diff/KiloDiffEditorContentTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/diff/KiloDiffEditorContentTest.kt index 7bf4d3c12bd..de63af8b033 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/diff/KiloDiffEditorContentTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/diff/KiloDiffEditorContentTest.kt @@ -291,6 +291,31 @@ class KiloDiffEditorContentTest : BasePlatformTestCase() { assertEquals("hello\nworld", contents[1]) } + fun `test diff request reconstructs multi hunk modified patch`() { + // Regression: multi-hunk modified diffs used to fall back to an empty original + raw patch on + // the right, rendering every line as added. They now reconstruct into a real side-by-side diff. + val patch = """ + diff --git a/src/App.kt b/src/App.kt + --- a/src/App.kt + +++ b/src/App.kt + @@ -1,3 +1,3 @@ + one + -two + +TWO + three + @@ -20,3 +20,3 @@ + twenty + -x + +X + z + """.trimIndent() + val request = diffRequest(project, file("src/App.kt", 2, 2, patch = patch)) as SimpleDiffRequest + val contents = request.contents.map(::content) + + assertEquals("one\ntwo\nthree\ntwenty\nx\nz", contents[0]) + assertEquals("one\nTWO\nthree\ntwenty\nX\nz", contents[1]) + } + fun `test tree displays absolute files relative to workspace`() { val parent = Disposer.newDisposable() try {