diff --git a/.changeset/quiet-binary-diffs.md b/.changeset/quiet-binary-diffs.md new file mode 100644 index 00000000000..90432a14b79 --- /dev/null +++ b/.changeset/quiet-binary-diffs.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Keep binary and audio files collapsed in diff reviews instead of showing empty diff panels. diff --git a/packages/kilo-vscode/src/agent-manager/local-diff.ts b/packages/kilo-vscode/src/agent-manager/local-diff.ts index c58189dace2..b0832c06a2e 100644 --- a/packages/kilo-vscode/src/agent-manager/local-diff.ts +++ b/packages/kilo-vscode/src/agent-manager/local-diff.ts @@ -1,5 +1,6 @@ import * as fs from "fs/promises" import * as path from "path" +import { binaryFile } from "../diff/shared/binary" import type { GitOps } from "./GitOps" import type { WorktreeDiffEntry } from "./types" @@ -12,6 +13,7 @@ type Meta = { status: Status tracked: boolean generatedLike: boolean + binary: boolean stamp: string } @@ -118,7 +120,7 @@ async function numstat(git: GitOps, dir: string, base: string, file?: string) { const args = ["-c", "core.quotepath=false", "diff", "--numstat", "--no-renames", base] if (file) args.push("--", file) const result = await git.execGit(args, dir) - const map = new Map() + const map = new Map() if (result.code !== 0) return map for (const line of result.stdout.trim().split("\n")) { if (!line) continue @@ -130,6 +132,7 @@ async function numstat(git: GitOps, dir: string, base: string, file?: string) { map.set(name, { additions: add === "-" ? 0 : parseInt(add || "0", 10) || 0, deletions: del === "-" ? 0 : parseInt(del || "0", 10) || 0, + binary: add === "-" || del === "-", }) } return map @@ -179,7 +182,7 @@ async function list(git: GitOps, dir: string, anc: string, log?: Log): Promise undefined) if (!exists) continue + const binary = await binaryFile(full) result.push({ file, - additions: await lineCount(full), + additions: binary ? 0 : await lineCount(full), deletions: 0, status: "added", tracked: false, generatedLike: generatedLike(file), + binary, stamp: await statStamp(dir, file), }) } @@ -230,7 +236,7 @@ function summarize(meta: Meta): WorktreeDiffEntry { status: meta.status, tracked: meta.tracked, generatedLike: meta.generatedLike, - summarized: true, + summarized: !meta.binary, stamp: meta.stamp, } } @@ -254,13 +260,15 @@ async function detailMeta(git: GitOps, dir: string, anc: string, file: string): const full = path.join(dir, file) const exists = await fs.stat(full).catch(() => undefined) if (!exists) return undefined + const binary = await binaryFile(full) return { file, - additions: await lineCount(full), + additions: binary ? 0 : await lineCount(full), deletions: 0, status: "added", tracked: false, generatedLike: generatedLike(file), + binary, stamp: await statStamp(dir, file), } } @@ -278,7 +286,7 @@ async function detailMeta(git: GitOps, dir: string, anc: string, file: string): if (!code) return undefined const counts = await numstat(git, dir, anc, file) - const stat = counts.get(file) ?? counts.get(pathPart) ?? { additions: 0, deletions: 0 } + const stat = counts.get(file) ?? counts.get(pathPart) ?? { additions: 0, deletions: 0, binary: false } const status = statusFromCode(code) return { file: pathPart, @@ -287,6 +295,7 @@ async function detailMeta(git: GitOps, dir: string, anc: string, file: string): status, tracked: true, generatedLike: generatedLike(pathPart), + binary: stat.binary, stamp: status === "deleted" ? `deleted:${anc}` : await statStamp(dir, pathPart), } } @@ -345,6 +354,7 @@ export async function diffFile( if (!anc) return null const meta = await detailMeta(git, dir, anc, file) if (!meta) return null + if (meta.binary) return summarize(meta) // Cheap size probe before materializing content — protects the extension // host from OOM on huge tracked files. `git cat-file -s` returns the blob diff --git a/packages/kilo-vscode/src/diff/shared/binary.ts b/packages/kilo-vscode/src/diff/shared/binary.ts new file mode 100644 index 00000000000..43875e35af6 --- /dev/null +++ b/packages/kilo-vscode/src/diff/shared/binary.ts @@ -0,0 +1,29 @@ +import * as fs from "fs/promises" + +const SAMPLE_BYTES = 8_192 + +function binary(bytes: Uint8Array): boolean { + if (bytes.length === 0) return false + + let controls = 0 + for (const byte of bytes) { + if (byte === 0) return true + if (byte < 9 || (byte > 13 && byte < 32)) controls++ + } + return controls / bytes.length > 0.3 +} + +export async function binaryFile(file: string): Promise { + const stat = await fs.lstat(file).catch(() => undefined) + if (!stat?.isFile()) return false + + const handle = await fs.open(file, "r").catch(() => undefined) + if (!handle) return false + const sample = Buffer.alloc(SAMPLE_BYTES) + const read = await handle + .read(sample, 0, sample.length, 0) + .catch(() => undefined) + .finally(() => handle.close()) + if (!read) return false + return binary(sample.subarray(0, read.bytesRead)) +} diff --git a/packages/kilo-vscode/src/diff/sources/git-status.ts b/packages/kilo-vscode/src/diff/sources/git-status.ts index f2463cf0606..44cf3001ac7 100644 --- a/packages/kilo-vscode/src/diff/sources/git-status.ts +++ b/packages/kilo-vscode/src/diff/sources/git-status.ts @@ -17,6 +17,7 @@ export interface FileEntry { additions: number deletions: number tracked: boolean + binary: boolean stamp?: string } @@ -35,16 +36,17 @@ export function parseNameStatus(stdout: string): { file: string; status: Status } /** Parse `git diff --numstat` output into a per-file `{additions, deletions}` map. */ -export function parseNumstat(stdout: string): Map { - const map = new Map() +export function parseNumstat(stdout: string): Map { + const map = new Map() for (const line of stdout.split("\n")) { if (!line.trim()) continue const parts = line.split("\t") if (parts.length < 3) continue - const additions = parts[0] === "-" ? 0 : parseInt(parts[0]!, 10) || 0 - const deletions = parts[1] === "-" ? 0 : parseInt(parts[1]!, 10) || 0 + const binary = parts[0] === "-" || parts[1] === "-" + const additions = binary ? 0 : parseInt(parts[0]!, 10) || 0 + const deletions = binary ? 0 : parseInt(parts[1]!, 10) || 0 const file = parts.slice(2).join("\t") - if (file) map.set(file, { additions, deletions }) + if (file) map.set(file, { additions, deletions, binary }) } return map } @@ -69,7 +71,8 @@ export function summarize(entry: FileEntry): DiffFile { status: entry.status, tracked: entry.tracked, generatedLike: generatedLike(entry.file), - summarized: true, + // Binary metadata is complete because no deferred text body exists. + summarized: !entry.binary, // Synthetic stamp keyed on the stats we actually polled: any change to // the file's diff produces new additions/deletions, which invalidates // the webview-side cached detail via mergeWorktreeDiffs. Callers can diff --git a/packages/kilo-vscode/src/diff/sources/session.ts b/packages/kilo-vscode/src/diff/sources/session.ts index b70f4f715d7..c5c16242cd2 100644 --- a/packages/kilo-vscode/src/diff/sources/session.ts +++ b/packages/kilo-vscode/src/diff/sources/session.ts @@ -77,6 +77,8 @@ export function toSessionDiffFile(raw: SnapshotFileDiff): DiffFile { status: raw.status, tracked: true, generatedLike: false, - summarized: raw.patch === "", + // A zero-stat empty patch has no text body to fetch; nonzero stats + // indicate a deferred large-file summary. + summarized: raw.patch === "" && (raw.additions !== 0 || raw.deletions !== 0), } } diff --git a/packages/kilo-vscode/src/diff/sources/staged.ts b/packages/kilo-vscode/src/diff/sources/staged.ts index 03a54198ff7..c47cc552fb3 100644 --- a/packages/kilo-vscode/src/diff/sources/staged.ts +++ b/packages/kilo-vscode/src/diff/sources/staged.ts @@ -53,6 +53,7 @@ export function createStagedDiffSource(): DiffSource { additions: counts.get(item.file)?.additions ?? 0, deletions: counts.get(item.file)?.deletions ?? 0, tracked: true, + binary: counts.get(item.file)?.binary ?? false, })) } @@ -80,6 +81,8 @@ export function createStagedDiffSource(): DiffSource { const entry = await fileEntry(git, dir, file, log) if (!entry) return null + if (entry.binary) return summarize(entry) + const beforeBytes = entry.status === "added" ? 0 : await blobSize(git, dir, "HEAD", file) const afterBytes = entry.status === "deleted" ? 0 : await blobSize(git, dir, INDEX_REF, file) if (beforeBytes > MAX_DETAIL_BYTES || afterBytes > MAX_DETAIL_BYTES) { @@ -147,5 +150,6 @@ async function fileEntry( additions: stats.get(item.file)?.additions ?? 0, deletions: stats.get(item.file)?.deletions ?? 0, tracked: true, + binary: stats.get(item.file)?.binary ?? false, } } diff --git a/packages/kilo-vscode/src/diff/sources/unstaged.ts b/packages/kilo-vscode/src/diff/sources/unstaged.ts index 2441bb7f85b..3aeb5db1704 100644 --- a/packages/kilo-vscode/src/diff/sources/unstaged.ts +++ b/packages/kilo-vscode/src/diff/sources/unstaged.ts @@ -3,6 +3,7 @@ import * as vscode from "vscode" import { GitOps } from "../../agent-manager/GitOps" import { generatedLike } from "../../agent-manager/local-diff" import { appendOutput, getWorkspaceRoot } from "../../review-utils" +import { binaryFile } from "../shared/binary" import type { DiffFile } from "../types" import type { DiffSource, DiffSourceDescriptor, DiffSourceFetch } from "./types" import { @@ -56,6 +57,7 @@ export function createUnstagedDiffSource(): DiffSource { additions: counts.get(item.file)?.additions ?? 0, deletions: counts.get(item.file)?.deletions ?? 0, tracked: true, + binary: counts.get(item.file)?.binary ?? false, })) } @@ -82,6 +84,7 @@ export function createUnstagedDiffSource(): DiffSource { additions: 0, deletions: 0, tracked: false, + binary: await binaryFile(full), // Untracked entries always have additions/deletions = 0 (numstat // can't compute them without an index blob), so fold size+mtime // into the stamp. Editing the file changes mtime → the webview @@ -118,6 +121,8 @@ export function createUnstagedDiffSource(): DiffSource { const entry = await fileEntry(git, dir, file, log) if (!entry) return null + if (entry.binary) return summarize(entry) + const beforeBytes = !entry.tracked || entry.status === "added" ? 0 : await blobSize(git, dir, INDEX_REF, file) const afterBytes = entry.status === "deleted" ? 0 : await fileSize(dir, file) if (beforeBytes > MAX_DETAIL_BYTES || afterBytes > MAX_DETAIL_BYTES) { @@ -191,6 +196,7 @@ async function fileEntry( additions: stats.get(item.file)?.additions ?? 0, deletions: stats.get(item.file)?.deletions ?? 0, tracked: true, + binary: stats.get(item.file)?.binary ?? false, } } } @@ -215,6 +221,7 @@ async function fileEntry( additions: 0, deletions: 0, tracked: false, + binary: await binaryFile(full), stamp: `added:untracked:${stat.size}:${stat.mtimeMs}`, } } diff --git a/packages/kilo-vscode/tests/unit/agent-manager-diff-state.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-diff-state.test.ts index 8e80715a477..1e220230c9f 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-diff-state.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-diff-state.test.ts @@ -7,6 +7,8 @@ import { eagerDiffFiles, expandableOpenFiles, initialOpenFiles, + isDiffExpandable, + sanitizeOpenFiles, toggleOpenFiles, } from "../../webview-ui/diff-viewer/diff-open-policy" import type { WorktreeFileDiff } from "../../webview-ui/src/types/messages" @@ -72,6 +74,7 @@ describe("agent manager diff state", () => { initialOpenFiles([ diff({ file: "src/app.ts", generatedLike: false, additions: 3 }), diff({ file: "node_modules/pkg/index.js", generatedLike: true, additions: 3 }), + diff({ file: "audio/notification.wav", summarized: false, additions: 0 }), diff({ file: "src/huge.ts", additions: EXTREME_DIFF_CHANGED_LINES + 1 }), ]), ).toEqual(["src/app.ts"]) @@ -85,6 +88,7 @@ describe("agent manager diff state", () => { expandableOpenFiles([ diff({ file: "src/app.ts", generatedLike: false, additions: 3 }), diff({ file: "src/generated.ts", generatedLike: true, additions: 3 }), + diff({ file: "assets/archive.zip", summarized: false, additions: 0 }), diff({ file: "src/huge.ts", additions: EXTREME_DIFF_CHANGED_LINES + 1 }), ]), ).toEqual(["src/app.ts"]) @@ -95,6 +99,7 @@ describe("agent manager diff state", () => { diff({ file: "src/app.ts" }), diff({ file: "src/panel.ts" }), diff({ file: "src/generated.ts", generatedLike: true }), + diff({ file: "audio/alert.mp3", summarized: false, additions: 0 }), diff({ file: "src/huge.ts", additions: EXTREME_DIFF_CHANGED_LINES + 1 }), ] @@ -109,6 +114,15 @@ describe("agent manager diff state", () => { expect(toggleOpenFiles(diffs, ["src/app.ts"])).toEqual(["src/app.ts", "src/panel.ts"]) expect(toggleOpenFiles(diffs, ["src/app.ts", "src/panel.ts"])).toEqual([]) }) + + it("prevents non-text diffs from entering open state", () => { + const audio = diff({ file: "audio/alert.wav", summarized: false, additions: 0 }) + const text = diff({ file: "src/app.ts" }) + + expect(isDiffExpandable(audio)).toBe(false) + expect(isDiffExpandable(text)).toBe(true) + expect(sanitizeOpenFiles([audio, text], [audio.file, text.file])).toEqual([text.file]) + }) }) describe("eager diff files", () => { diff --git a/packages/kilo-vscode/tests/unit/diff-session-source.test.ts b/packages/kilo-vscode/tests/unit/diff-session-source.test.ts index f8a24d24c5c..4db1b322db7 100644 --- a/packages/kilo-vscode/tests/unit/diff-session-source.test.ts +++ b/packages/kilo-vscode/tests/unit/diff-session-source.test.ts @@ -57,13 +57,20 @@ describe("createSessionDiffSource.fetch", () => { deletions: 0, status: "modified", }, + { + file: "large.txt", + patch: "", + additions: 500, + deletions: 200, + status: "modified", + }, ] const { fetch } = recording(raw) const source = createSessionDiffSource("s2", fetch, "/repo") const result = await source.fetch() - expect(result.diffs).toHaveLength(2) + expect(result.diffs).toHaveLength(3) const foo = result.diffs[0]! expect(foo.file).toBe("foo.ts") @@ -78,9 +85,10 @@ describe("createSessionDiffSource.fetch", () => { expect(foo.summarized).toBe(false) const big = result.diffs[1]! - expect(big.summarized).toBe(true) + expect(big.summarized).toBe(false) expect(big.before).toBe("") expect(big.after).toBe("") + expect(result.diffs[2]?.summarized).toBe(true) }) it("propagates errors from the underlying fetch", async () => { diff --git a/packages/kilo-vscode/tests/unit/local-diff.test.ts b/packages/kilo-vscode/tests/unit/local-diff.test.ts index cd9e35f9490..682b8caa717 100644 --- a/packages/kilo-vscode/tests/unit/local-diff.test.ts +++ b/packages/kilo-vscode/tests/unit/local-diff.test.ts @@ -182,6 +182,21 @@ describe("diffSummary", () => { }) }) + it("classifies untracked files from content rather than their extension", async () => { + await withRepo(async (dir, base) => { + await fs.writeFile(path.join(dir, "tone.wav"), Buffer.from([0x52, 0x49, 0x46, 0x46, 0x00, 0x01, 0x02, 0x03])) + await fs.writeFile(path.join(dir, "notes.bin"), "plain text\n") + + const result = await diffSummary(git(), dir, base) + expect(result.find((entry) => entry.file === "tone.wav")?.additions).toBe(0) + expect(result.find((entry) => entry.file === "notes.bin")?.additions).toBe(1) + + const detail = await diffFile(git(), dir, base, "tone.wav") + expect(detail?.summarized).toBe(false) + expect(detail?.patch).toBe("") + }) + }) + it("all entries are summarized with empty before/after/patch", async () => { await withRepo(async (dir, base) => { await fs.writeFile(path.join(dir, "untracked.txt"), "x\n") @@ -200,6 +215,21 @@ describe("diffSummary", () => { }) }) + it("uses git numstat metadata for tracked binary files", async () => { + await withRepo(async (dir, base) => { + await fs.writeFile(path.join(dir, "tone.wav"), Buffer.from([0x52, 0x49, 0x46, 0x46, 0x00, 0x01, 0x02, 0x03])) + runSync(dir, ["add", "tone.wav"]) + runSync(dir, ["commit", "-m", "add audio"]) + + const summary = await diffSummary(git(), dir, base) + expect(summary.find((entry) => entry.file === "tone.wav")?.additions).toBe(0) + + const detail = await diffFile(git(), dir, base, "tone.wav") + expect(detail?.summarized).toBe(false) + expect(detail?.patch).toBe("") + }) + }) + it("marks generated-like files via generatedLike flag", async () => { await withRepo(async (dir, base) => { await fs.mkdir(path.join(dir, "dist"), { recursive: true }) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx index a0a2bd68c3f..e7c4bc26e32 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx @@ -47,7 +47,9 @@ import { allOpenFiles, eagerDiffFiles, initialOpenFiles, + isDiffExpandable, isLargeDiffFile, + sanitizeOpenFiles, toggleOpenFiles, } from "../diff-viewer/diff-open-policy" import { DiffEndMarker } from "../diff-viewer/DiffEndMarker" @@ -216,8 +218,11 @@ export const DiffPanel: Component = (props) => { // Already initialized for this key — preserve manual expand/collapse, // only prune files that no longer exist (e.g. deleted during session) setOpen((prev) => { - const filtered = prev.filter((file) => fileSet.has(file)) - if (filtered.length === prev.length && prev.every((f) => fileSet.has(f))) return prev + const filtered = sanitizeOpenFiles( + diffs, + prev.filter((file) => fileSet.has(file)), + ) + if (filtered.length === prev.length && prev.every((file) => filtered.includes(file))) return prev return filtered }) }, @@ -252,7 +257,7 @@ export const DiffPanel: Component = (props) => { for (const file of next) { if (loading.has(file)) continue const diff = props.diffs.find((item) => item.file === file) - if (!diff || diff.summarized !== true) continue + if (!diff || !isDiffExpandable(diff) || diff.summarized !== true) continue const value = diffToken(diff) if (requested.get(file) === value) continue requested.set(file, value) @@ -440,8 +445,8 @@ export const DiffPanel: Component = (props) => { files: props.diffs.length, additions: props.diffs.reduce((sum, diff) => sum + diff.additions, 0), deletions: props.diffs.reduce((sum, diff) => sum + diff.deletions, 0), - large: props.diffs.filter((diff) => isLargeDiffFile(diff)).length, - collapsed: Math.max(props.diffs.length - open().length, 0), + large: props.diffs.filter((diff) => isDiffExpandable(diff) && isLargeDiffFile(diff)).length, + collapsed: props.diffs.filter((diff) => isDiffExpandable(diff) && !open().includes(diff.file)).length, })) const allOpen = createMemo(() => allOpenFiles(props.diffs, open())) const openLabel = () => (allOpen() ? t("ui.sessionReview.collapseAll") : t("ui.sessionReview.expandAll")) @@ -527,7 +532,7 @@ export const DiffPanel: Component = (props) => { 0}>
- + setOpen(sanitizeOpenFiles(props.diffs, files))}> {(diff) => { const isAdded = () => diff.status === "added" @@ -629,9 +634,11 @@ export const DiffPanel: Component = (props) => { /> - - - + + + + +
diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx index f31eedec301..7441dae313a 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx +++ b/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx @@ -49,7 +49,9 @@ import { allOpenFiles, eagerDiffFiles, initialOpenFiles, + isDiffExpandable, isLargeDiffFile, + sanitizeOpenFiles, toggleOpenFiles, } from "./diff-open-policy" import { DiffEndMarker } from "./DiffEndMarker" @@ -220,8 +222,11 @@ export const FullScreenDiffView: Component = (props) => // Already initialized for this key — preserve manual expand/collapse, // only prune files that no longer exist (e.g. deleted during session) setOpen((prev) => { - const filtered = prev.filter((file) => fileSet.has(file)) - if (filtered.length === prev.length && prev.every((f) => fileSet.has(f))) return prev + const filtered = sanitizeOpenFiles( + diffs, + prev.filter((file) => fileSet.has(file)), + ) + if (filtered.length === prev.length && prev.every((file) => filtered.includes(file))) return prev return filtered }) }, @@ -256,7 +261,7 @@ export const FullScreenDiffView: Component = (props) => for (const file of next) { if (loading.has(file)) continue const diff = props.diffs.find((item) => item.file === file) - if (!diff || diff.summarized !== true) continue + if (!diff || !isDiffExpandable(diff) || diff.summarized !== true) continue const value = diffToken(diff) if (requested.get(file) === value) continue requested.set(file, value) @@ -438,7 +443,8 @@ export const FullScreenDiffView: Component = (props) => const handleFileSelect = (path: string) => { setActiveFile(path) // Ensure the accordion is open for this file - if (!open().includes(path)) { + const diff = props.diffs.find((item) => item.file === path) + if (diff && isDiffExpandable(diff) && !open().includes(path)) { setOpen((prev) => [...prev, path]) } // Scroll to the file in the diff viewer @@ -515,8 +521,8 @@ export const FullScreenDiffView: Component = (props) => files: props.diffs.length, additions: props.diffs.reduce((s, d) => s + d.additions, 0), deletions: props.diffs.reduce((s, d) => s + d.deletions, 0), - large: props.diffs.filter((diff) => isLargeDiffFile(diff)).length, - collapsed: Math.max(props.diffs.length - open().length, 0), + large: props.diffs.filter((diff) => isDiffExpandable(diff) && isLargeDiffFile(diff)).length, + collapsed: props.diffs.filter((diff) => isDiffExpandable(diff) && !open().includes(diff.file)).length, })) const allOpen = createMemo(() => allOpenFiles(props.diffs, open())) const openLabel = () => (allOpen() ? t("ui.sessionReview.collapseAll") : t("ui.sessionReview.expandAll")) @@ -618,7 +624,7 @@ export const FullScreenDiffView: Component = (props) => 0}>
- + setOpen(sanitizeOpenFiles(props.diffs, files))}> {(diff) => { const isAdded = () => diff.status === "added" @@ -720,9 +726,11 @@ export const FullScreenDiffView: Component = (props) => /> - - - + + + + +
diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/diff-open-policy.ts b/packages/kilo-vscode/webview-ui/diff-viewer/diff-open-policy.ts index 64a52ac71ef..f10fd1b1bc8 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/diff-open-policy.ts +++ b/packages/kilo-vscode/webview-ui/diff-viewer/diff-open-policy.ts @@ -28,8 +28,19 @@ export function eagerDiffFiles(diffs: WorktreeFileDiff[]): Set { return eager } +export function isDiffExpandable(diff: WorktreeFileDiff): boolean { + return diff.summarized === true || Boolean(diff.patch || diff.before || diff.after) +} + +export function sanitizeOpenFiles(diffs: WorktreeFileDiff[], open: string[]): string[] { + const blocked = new Set(diffs.filter((diff) => !isDiffExpandable(diff)).map((diff) => diff.file)) + return open.filter((file) => !blocked.has(file)) +} + export function expandableOpenFiles(diffs: WorktreeFileDiff[]): string[] { - return diffs.filter((diff) => !isLargeDiffFile(diff) && diff.generatedLike !== true).map((diff) => diff.file) + return diffs + .filter((diff) => isDiffExpandable(diff) && !isLargeDiffFile(diff) && diff.generatedLike !== true) + .map((diff) => diff.file) } export function initialOpenFiles(diffs: WorktreeFileDiff[]): string[] {