Merge pull request #8754 from shssoichiro/issue-7861

ux(cli): visually show when applied patch skips sections
This commit is contained in:
Catriel Müller
2026-05-12 11:57:23 -03:00
committed by GitHub
5 changed files with 280 additions and 59 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@kilocode/cli": patch
"kilo-code": patch
---
Fix TUI diff rendering when header-like content lines appear inside a unified diff hunk.
@@ -83,6 +83,8 @@ import * as Model from "../../util/model"
import { formatTranscript } from "../../util/transcript"
import { UI } from "@/cli/ui.ts"
import { useTuiConfig } from "../../context/tui-config"
import { splitDiffHunks } from "@/kilocode/tui/diff" // kilocode_change
import { formatMarkdownTables } from "../../util/markdown" // kilocode_change
import { bell } from "@/kilocode/bell" // kilocode_change
import { SessionIndexing } from "@/kilocode/components/session-indexing" // kilocode_change
@@ -2228,32 +2230,46 @@ function Edit(props: ToolProps<typeof EditTool>) {
const ft = createMemo(() => filetype(props.input.filePath))
const diffContent = createMemo(() => props.metadata.diff)
const hunks = createMemo(() => splitDiffHunks(diffContent() ?? "")) // kilocode_change
return (
<Switch>
<Match when={props.metadata.diff !== undefined}>
<BlockTool title={"← Edit " + normalizePath(props.input.filePath!)} part={props.part}>
<box paddingLeft={1}>
<diff
diff={diffContent()}
view={view()}
filetype={ft()}
syntaxStyle={syntax()}
showLineNumbers={true}
width="100%"
wrapMode={ctx.diffWrapMode()}
fg={theme.text}
addedBg={theme.diffAddedBg}
removedBg={theme.diffRemovedBg}
contextBg={theme.diffContextBg}
addedSignColor={theme.diffHighlightAdded}
removedSignColor={theme.diffHighlightRemoved}
lineNumberFg={theme.diffLineNumber}
lineNumberBg={theme.diffContextBg}
addedLineNumberBg={theme.diffAddedLineNumberBg}
removedLineNumberBg={theme.diffRemovedLineNumberBg}
/>
{/* kilocode_change start */}
<box paddingLeft={1} flexDirection="column">
<For each={hunks()}>
{(hunk, i) => (
<>
<Show when={i() > 0}>
<text fg={theme.textMuted} alignSelf="center" height={2}>
...
</text>
</Show>
<diff
diff={hunk}
view={view()}
filetype={ft()}
syntaxStyle={syntax()}
showLineNumbers={true}
width="100%"
wrapMode={ctx.diffWrapMode()}
fg={theme.text}
addedBg={theme.diffAddedBg}
removedBg={theme.diffRemovedBg}
contextBg={theme.diffContextBg}
addedSignColor={theme.diffHighlightAdded}
removedSignColor={theme.diffHighlightRemoved}
lineNumberFg={theme.diffLineNumber}
lineNumberBg={theme.diffContextBg}
addedLineNumberBg={theme.diffAddedLineNumberBg}
removedLineNumberBg={theme.diffRemovedLineNumberBg}
/>
</>
)}
</For>
</box>
{/* kilocode_change end */}
<Diagnostics diagnostics={props.metadata.diagnostics} filePath={props.input.filePath ?? ""} />
</BlockTool>
</Match>
@@ -2279,29 +2295,43 @@ function ApplyPatch(props: ToolProps<typeof ApplyPatchTool>) {
})
function Diff(p: { diff: string; filePath: string }) {
// kilocode_change start
const hunks = createMemo(() => splitDiffHunks(p.diff))
return (
<box paddingLeft={1}>
<diff
diff={p.diff}
view={view()}
filetype={filetype(p.filePath)}
syntaxStyle={syntax()}
showLineNumbers={true}
width="100%"
wrapMode={ctx.diffWrapMode()}
fg={theme.text}
addedBg={theme.diffAddedBg}
removedBg={theme.diffRemovedBg}
contextBg={theme.diffContextBg}
addedSignColor={theme.diffHighlightAdded}
removedSignColor={theme.diffHighlightRemoved}
lineNumberFg={theme.diffLineNumber}
lineNumberBg={theme.diffContextBg}
addedLineNumberBg={theme.diffAddedLineNumberBg}
removedLineNumberBg={theme.diffRemovedLineNumberBg}
/>
<box paddingLeft={1} flexDirection="column">
<For each={hunks()}>
{(hunk, i) => (
<>
<Show when={i() > 0}>
<text fg={theme.textMuted} alignSelf="center" height={2}>
...
</text>
</Show>
<diff
diff={hunk}
view={view()}
filetype={filetype(p.filePath)}
syntaxStyle={syntax()}
showLineNumbers={true}
width="100%"
wrapMode={ctx.diffWrapMode()}
fg={theme.text}
addedBg={theme.diffAddedBg}
removedBg={theme.diffRemovedBg}
contextBg={theme.diffContextBg}
addedSignColor={theme.diffHighlightAdded}
removedSignColor={theme.diffHighlightRemoved}
lineNumberFg={theme.diffLineNumber}
lineNumberBg={theme.diffContextBg}
addedLineNumberBg={theme.diffAddedLineNumberBg}
removedLineNumberBg={theme.diffRemovedLineNumberBg}
/>
</>
)}
</For>
</box>
)
// kilocode_change end
}
function title(file: { type: string; relativePath: string; filePath: string; deletions: number }) {
@@ -19,6 +19,7 @@ import { useDialog } from "../../ui/dialog"
import { getScrollAcceleration } from "../../util/scroll"
import { useTuiConfig } from "../../context/tui-config"
import { ConfigProtection } from "@/kilocode/permission/config-paths" // kilocode_change
import { splitDiffHunks } from "@/kilocode/tui/diff" // kilocode_change
import { normalizeUrls } from "@/kilocode/util/url" // kilocode_change
type PermissionStage = "permission" | "always" | "reject"
@@ -67,6 +68,7 @@ function EditBody(props: { request: PermissionRequest }) {
const ft = createMemo(() => filetype(filepath()))
const scrollAcceleration = createMemo(() => getScrollAcceleration(config))
const hunks = createMemo(() => splitDiffHunks(diff())) // kilocode_change
return (
<box flexDirection="column" gap={1}>
@@ -81,25 +83,38 @@ function EditBody(props: { request: PermissionRequest }) {
},
}}
>
<diff
diff={diff()}
view={view()}
filetype={ft()}
syntaxStyle={syntax()}
showLineNumbers={true}
width="100%"
wrapMode="word"
fg={theme.text}
addedBg={theme.diffAddedBg}
removedBg={theme.diffRemovedBg}
contextBg={theme.diffContextBg}
addedSignColor={theme.diffHighlightAdded}
removedSignColor={theme.diffHighlightRemoved}
lineNumberFg={theme.diffLineNumber}
lineNumberBg={theme.diffContextBg}
addedLineNumberBg={theme.diffAddedLineNumberBg}
removedLineNumberBg={theme.diffRemovedLineNumberBg}
/>
{/* kilocode_change start */}
<box flexDirection="column">
<For each={hunks()}>
{(hunk, i) => (
<>
<Show when={i() > 0}>
<text fg={theme.textMuted}>...</text>
</Show>
<diff
diff={hunk}
view={view()}
filetype={ft()}
syntaxStyle={syntax()}
showLineNumbers={true}
width="100%"
wrapMode="word"
fg={theme.text}
addedBg={theme.diffAddedBg}
removedBg={theme.diffRemovedBg}
contextBg={theme.diffContextBg}
addedSignColor={theme.diffHighlightAdded}
removedSignColor={theme.diffHighlightRemoved}
lineNumberFg={theme.diffLineNumber}
lineNumberBg={theme.diffContextBg}
addedLineNumberBg={theme.diffAddedLineNumberBg}
removedLineNumberBg={theme.diffRemovedLineNumberBg}
/>
</>
)}
</For>
</box>
{/* kilocode_change end */}
</scrollbox>
</Show>
<Show when={!diff()}>
@@ -0,0 +1,79 @@
export function splitDiffHunks(diff: string): string[] {
const parse = (line: string) => {
const match = line.match(/^@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@/)
if (!match) return
return {
old: Number(match[1] ?? "1"),
next: Number(match[2] ?? "1"),
}
}
const split = (section: string[]) => {
const start = section.findIndex((line) => line.startsWith("@@"))
if (start === -1) return [section.join("\n")]
const prefix = section.slice(0, start)
const hunks = section.slice(start).reduce((acc, line) => {
if (line.startsWith("@@")) return [...acc, [line]]
if (acc.length === 0) return [[line]]
return [...acc.slice(0, -1), [...acc.at(-1)!, line]]
}, [] as string[][])
const head = prefix.join("\n")
return hunks.map((hunk) => [head, ...hunk].join("\n"))
}
const lines = diff.split("\n")
const files = lines.reduce(
(acc, line, index) => {
const hunk = parse(line)
if (hunk) {
return { files: acc.files, old: hunk.old, next: hunk.next }
}
if (
acc.old !== 0 &&
acc.next !== 0 &&
line.startsWith("--- ") &&
lines[index + 1]?.startsWith("+++ ") &&
lines[index + 2]?.startsWith("@@")
) {
return { files: [...acc.files, index], old: 0, next: 0 }
}
if (acc.old === 0 && acc.next === 0) {
if (line.startsWith("--- ") && lines[index + 1]?.startsWith("+++ ")) {
return { files: [...acc.files, index], old: 0, next: 0 }
}
return acc
}
if (line.startsWith("\\ ")) return acc
if (line.startsWith("+")) {
return { files: acc.files, old: acc.old, next: acc.next - 1 }
}
if (line.startsWith("-")) {
return { files: acc.files, old: acc.old - 1, next: acc.next }
}
return { files: acc.files, old: acc.old - 1, next: acc.next - 1 }
},
{ files: [] as number[], old: 0, next: 0 },
).files
if (files.length === 0) {
const hunks = split(lines)
if (hunks.length <= 1) return [diff]
return hunks
}
const hunks = files
.map((line, index) => {
const start = index === 0 ? 0 : line
const end = files[index + 1] ?? lines.length
return split(lines.slice(start, end))
})
.flat()
if (hunks.length <= 1) return [diff]
return hunks
}
@@ -0,0 +1,91 @@
import { describe, expect, test } from "bun:test"
import { splitDiffHunks } from "../../src/kilocode/tui/diff"
describe("splitDiffHunks", () => {
test("returns original diff when there are no hunks", () => {
const diff = "--- a/file.ts\n+++ b/file.ts"
expect(splitDiffHunks(diff)).toEqual([diff])
})
test("returns original diff when there is one hunk", () => {
const diff = ["--- a/file.ts", "+++ b/file.ts", "@@ -1,2 +1,2 @@", "-one", "+ONE", " two"].join("\n")
expect(splitDiffHunks(diff)).toEqual([diff])
})
test("ignores header-like content lines inside a hunk", () => {
const diff = [
"--- a/file.ts",
"+++ b/file.ts",
"@@ -1,1 +1,1 @@",
"--- not-a-header",
"+++ still-not-a-header",
].join("\n")
expect(splitDiffHunks(diff)).toEqual([diff])
})
test("splits multi-hunk diff and preserves headers", () => {
const diff = [
"--- a/file.ts",
"+++ b/file.ts",
"@@ -1,2 +1,2 @@",
"-one",
"+ONE",
" two",
"@@ -10,2 +10,2 @@",
"-ten",
"+TEN",
" eleven",
].join("\n")
expect(splitDiffHunks(diff)).toEqual([
["--- a/file.ts", "+++ b/file.ts", "@@ -1,2 +1,2 @@", "-one", "+ONE", " two"].join("\n"),
["--- a/file.ts", "+++ b/file.ts", "@@ -10,2 +10,2 @@", "-ten", "+TEN", " eleven"].join("\n"),
])
})
test("splits concatenated multi-file diff with per-file headers", () => {
const diff = [
"--- a/one.ts",
"+++ b/one.ts",
"@@ -1,2 +1,2 @@",
"-one",
"+ONE",
"--- a/two.ts",
"+++ b/two.ts",
"@@ -1,2 +1,2 @@",
"-two",
"+TWO",
].join("\n")
expect(splitDiffHunks(diff)).toEqual([
["--- a/one.ts", "+++ b/one.ts", "@@ -1,2 +1,2 @@", "-one", "+ONE"].join("\n"),
["--- a/two.ts", "+++ b/two.ts", "@@ -1,2 +1,2 @@", "-two", "+TWO"].join("\n"),
])
})
test("splits multi-file diff when first file has multiple hunks", () => {
const diff = [
"--- a/one.ts",
"+++ b/one.ts",
"@@ -1,2 +1,2 @@",
"-one",
"+ONE",
"@@ -10,2 +10,2 @@",
"-ten",
"+TEN",
"--- a/two.ts",
"+++ b/two.ts",
"@@ -3,2 +3,2 @@",
"-two",
"+TWO",
].join("\n")
expect(splitDiffHunks(diff)).toEqual([
["--- a/one.ts", "+++ b/one.ts", "@@ -1,2 +1,2 @@", "-one", "+ONE"].join("\n"),
["--- a/one.ts", "+++ b/one.ts", "@@ -10,2 +10,2 @@", "-ten", "+TEN"].join("\n"),
["--- a/two.ts", "+++ b/two.ts", "@@ -3,2 +3,2 @@", "-two", "+TWO"].join("\n"),
])
})
})