Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/git-diff-attributes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Collapse files marked `linguist-generated` in repository `.gitattributes` rules while keeping explicitly visible files such as English and German translations expanded by default.
1 change: 1 addition & 0 deletions packages/kilo-vscode/src/agent-manager/local-diff-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export function createDiffCache(load: Loader) {
meta.additions,
meta.deletions,
meta.binary,
meta.generatedLike,
meta.stamp,
)

Expand Down
73 changes: 20 additions & 53 deletions packages/kilo-vscode/src/agent-manager/local-diff.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as fs from "fs/promises"
import { classifyGenerated, generatedLike, gitGeneratedFiles } from "../diff/shared/git-attributes"
import { imageMime, loadImage, readImageFile } from "../diff/shared/image"
import { resolveInside } from "../diff/shared/path"
import type { GitOps } from "./GitOps"
Expand Down Expand Up @@ -32,54 +33,7 @@ const MAX_SUMMARY_FILES = 32

/** Ported from `packages/opencode/src/file/ignore.ts` — identical patterns,
* no runtime dependency on minimatch/picomatch. */
const FOLDERS = new Set([
"node_modules",
"bower_components",
".pnpm-store",
"vendor",
".npm",
"dist",
"build",
"out",
".next",
"target",
"bin",
"obj",
".git",
".svn",
".hg",
".vscode",
".idea",
".turbo",
".output",
"desktop",
".sst",
".cache",
".webkit-cache",
"__pycache__",
".pytest_cache",
"mypy_cache",
".history",
".gradle",
])

const SUFFIXES = [".swp", ".swo", ".pyc", ".log"]
const BASENAMES = new Set([".DS_Store", "Thumbs.db"])
const CONTAINS_SEGMENTS = ["logs", "tmp", "temp", "coverage", ".nyc_output"]

export function generatedLike(file: string): boolean {
const parts = file.split(/[/\\]/)
for (const part of parts) {
if (FOLDERS.has(part)) return true
if (CONTAINS_SEGMENTS.includes(part)) return true
}
for (const suffix of SUFFIXES) {
if (file.endsWith(suffix)) return true
}
const base = parts[parts.length - 1] ?? ""
if (BASENAMES.has(base)) return true
return false
}
export { generatedLike } from "../diff/shared/git-attributes"

const BASE_CANDIDATES = ["main", "master", "dev", "develop"]

Expand Down Expand Up @@ -173,6 +127,18 @@ function statusFromCode(code: string): Status {
}

async function list(git: GitOps, dir: string, anc: string, log?: Log): Promise<Meta[]> {
const markGenerated = async (entries: Meta[]) => {
const configured = await gitGeneratedFiles(
git,
dir,
entries.map((entry) => entry.file),
)
return entries.map((entry) => ({
...entry,
generatedLike: classifyGenerated(entry.file, configured),
}))
}

const [tracked, untracked] = await Promise.all([
git.execGit(["-c", "core.quotepath=false", "diff", "--raw", "--numstat", "--no-renames", anc], dir, {
priority: true,
Expand Down Expand Up @@ -224,11 +190,11 @@ async function list(git: GitOps, dir: string, anc: string, log?: Log): Promise<M

if (untracked.code !== 0) {
log?.("git ls-files --others failed", { code: untracked.code, stderr: untracked.stderr.trim() })
return result
return markGenerated(result)
}

const files = untracked.stdout.trim()
if (!files) return result
if (!files) return markGenerated(result)
const paths = files.split("\n").filter((file) => file && !seen.has(file))

for (let index = 0; index < paths.length; index += MAX_SUMMARY_FILES) {
Expand All @@ -255,7 +221,7 @@ async function list(git: GitOps, dir: string, anc: string, log?: Log): Promise<M
}
}

return result
return markGenerated(result)
}

/**
Expand Down Expand Up @@ -295,6 +261,7 @@ async function detailMeta(
): Promise<Meta | undefined> {
const full = resolveInside(dir, file)
if (!full) return undefined
const configured = await gitGeneratedFiles(git, dir, [file], { signal })
const tracked = await git.execGit(["ls-files", "--error-unmatch", "--", file], dir, { signal, priority: true })
check(signal)
if (tracked.code !== 0) {
Expand All @@ -312,7 +279,7 @@ async function detailMeta(
deletions: 0,
status: "added",
tracked: false,
generatedLike: generatedLike(file),
generatedLike: classifyGenerated(file, configured),
binary: value.binary,
stamp: value.stamp,
}
Expand Down Expand Up @@ -341,7 +308,7 @@ async function detailMeta(
deletions: stat.deletions,
status,
tracked: true,
generatedLike: generatedLike(pathPart),
generatedLike: classifyGenerated(pathPart, configured),
binary: stat.binary,
stamp:
status === "deleted"
Expand Down
92 changes: 92 additions & 0 deletions packages/kilo-vscode/src/diff/shared/git-attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import type { GitOps } from "../../agent-manager/GitOps"

const ATTRIBUTE = "linguist-generated"
const FOLDERS = new Set([
"node_modules",
"bower_components",
".pnpm-store",
"vendor",
".npm",
"dist",
"build",
"out",
".next",
"target",
"bin",
"obj",
".git",
".svn",
".hg",
".vscode",
".idea",
".turbo",
".output",
"desktop",
".sst",
".cache",
".webkit-cache",
"__pycache__",
".pytest_cache",
"mypy_cache",
".history",
".gradle",
])

const SUFFIXES = [".swp", ".swo", ".pyc", ".log"]
const BASENAMES = new Set([".DS_Store", "Thumbs.db"])
const CONTAINS_SEGMENTS = ["logs", "tmp", "temp", "coverage", ".nyc_output"]

export type GeneratedAttributes = ReadonlyMap<string, boolean>
export type GeneratedFiles = (files: readonly string[]) => Promise<GeneratedAttributes>

export function generatedLike(file: string): boolean {
const parts = file.split(/[/\\]/)
for (const part of parts) {
if (FOLDERS.has(part)) return true
if (CONTAINS_SEGMENTS.includes(part)) return true
}
for (const suffix of SUFFIXES) {
if (file.endsWith(suffix)) return true
}
const base = parts[parts.length - 1] ?? ""
return BASENAMES.has(base)
}

export function classifyGenerated(file: string, attrs?: GeneratedAttributes): boolean {
return attrs?.get(file) ?? generatedLike(file)
}

/**
* Read the repository's generated-file attributes for a set of paths.
* GitHub Linguist uses `linguist-generated`, and repositories already keep
* those rules in `.gitattributes` for pull-request diffs.
*/
export async function gitGeneratedFiles(
git: GitOps,
dir: string,
files: readonly string[],
options: { cached?: boolean; signal?: AbortSignal } = {},
): Promise<Map<string, boolean>> {
const paths = [...new Set(files.filter(Boolean))]
if (paths.length === 0) return new Map()

const args = ["check-attr"]
if (options.cached) args.push("--cached")
args.push("-z", "--stdin", ATTRIBUTE)
const result = await git.execGit(args, dir, {
stdin: `${paths.join("\0")}\0`,
signal: options.signal,
priority: true,
})
if (result.code !== 0) return new Map()

const attrs = new Map<string, boolean>()
const fields = result.stdout.split("\0")
for (let field = 0; field + 2 < fields.length; field += 3) {
if (fields[field + 1] !== ATTRIBUTE) continue
const value = fields[field + 2]
if (value === "true" || value === "set") attrs.set(fields[field]!, true)
if (value === "false" || value === "unset") attrs.set(fields[field]!, false)
}
return attrs
}
29 changes: 22 additions & 7 deletions packages/kilo-vscode/src/diff/sources/catalog.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as vscode from "vscode"
import type { KiloConnectionService } from "../../services/cli-backend"
import { GitOps } from "../../agent-manager/GitOps"
import { gitGeneratedFiles } from "../shared/git-attributes"
import { resolveLocalDiffTarget } from "../shared/target"
import { appendOutput, getWorkspaceRoot } from "../../review-utils"
import type { BranchListItem } from "../../agent-manager/git-import"
Expand Down Expand Up @@ -73,6 +74,8 @@ export class DiffSourceCatalog implements vscode.Disposable {
// owned by the catalog so it survives source swaps.
private branchGit: GitOps | undefined
private branchOutput: vscode.OutputChannel | undefined
private attributeGit: GitOps | undefined
private attributeOutput: vscode.OutputChannel | undefined

constructor(
private readonly connection: KiloConnectionService,
Expand Down Expand Up @@ -100,6 +103,10 @@ export class DiffSourceCatalog implements vscode.Disposable {

build(id: string, ctx: PanelContext): DiffSource {
const opts = { dir: () => ctx.dir, strictDir: ctx.strictDir, git: ctx.git, log: ctx.log }
const dir = ctx.dir ?? ctx.workspaceRoot
const generated = dir
? (files: readonly string[]) => gitGeneratedFiles(ctx.git ?? this.ensureAttributeGit(), dir, files)
: undefined
if (id === WORKSPACE_SOURCE_ID) {
return createWorktreeDiffSource({
...opts,
Expand All @@ -118,18 +125,13 @@ export class DiffSourceCatalog implements vscode.Disposable {
if (!sessionId || !messageId) {
throw new Error(`DiffSourceCatalog.build: malformed turn id "${id}" (expected turn:<sessionId>:<messageId>)`)
}
return createTurnDiffSource(sessionId, messageId, this.turnFetch, ctx.workspaceRoot)
return createTurnDiffSource(sessionId, messageId, this.turnFetch, dir, generated)
}

if (id.startsWith(SESSION_PREFIX)) {
const sessionId = id.slice(SESSION_PREFIX.length)
if (!sessionId) throw new Error(`DiffSourceCatalog.build: empty session id in "${id}"`)
return createSessionDiffSource(
sessionId,
this.sessionFetch,
ctx.dir ?? ctx.workspaceRoot,
this.checkSnapshotsEnabled,
)
return createSessionDiffSource(sessionId, this.sessionFetch, dir, this.checkSnapshotsEnabled, generated)
}

throw new Error(`DiffSourceCatalog.build: unknown source id "${id}"`)
Expand Down Expand Up @@ -164,8 +166,12 @@ export class DiffSourceCatalog implements vscode.Disposable {
dispose(): void {
this.branchGit?.dispose()
this.branchGit = undefined
this.attributeGit?.dispose()
this.attributeGit = undefined
this.branchOutput?.dispose()
this.branchOutput = undefined
this.attributeOutput?.dispose()
this.attributeOutput = undefined
}

private readonly branchLog = (...args: unknown[]) => {
Expand All @@ -179,4 +185,13 @@ export class DiffSourceCatalog implements vscode.Disposable {
this.branchGit = new GitOps({ log: this.branchLog })
return this.branchGit
}

private ensureAttributeGit(): GitOps {
if (this.attributeGit) return this.attributeGit
this.attributeOutput = vscode.window.createOutputChannel("Kilo Diff: Attributes")
this.attributeGit = new GitOps({
log: (...args) => appendOutput(this.attributeOutput!, "DiffSourceCatalog", ...args),
})
return this.attributeGit
}
}
38 changes: 36 additions & 2 deletions packages/kilo-vscode/src/diff/sources/git-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import * as fs from "fs/promises"
import type { GitOps } from "../../agent-manager/GitOps"
import { generatedLike } from "../../agent-manager/local-diff"
import { classifyGenerated, generatedLike, gitGeneratedFiles } from "../shared/git-attributes"
import { imageMime, readImageFile } from "../shared/image"
import { resolveInside } from "../shared/path"
import type { DiffFile } from "../types"
Expand All @@ -19,9 +19,43 @@ export interface FileEntry {
deletions: number
tracked: boolean
binary: boolean
generatedLike?: boolean
stamp?: string
}

export async function applyGeneratedAttributes(
git: GitOps,
dir: string,
entries: FileEntry[],
cached = false,
): Promise<FileEntry[]> {
const configured = await gitGeneratedFiles(
git,
dir,
entries.map((entry) => entry.file),
{ cached },
)
return entries.map((entry) => ({
...entry,
generatedLike: classifyGenerated(entry.file, configured),
}))
}

export function createFileEntry(
item: { file: string; status: Status },
stats: Map<string, { additions: number; deletions: number; binary: boolean }>,
): FileEntry {
const stat = stats.get(item.file)
return {
file: item.file,
status: item.status,
additions: stat?.additions ?? 0,
deletions: stat?.deletions ?? 0,
tracked: true,
binary: stat?.binary ?? false,
}
}

/** Parse `git diff --name-status` output into entries (status code + path). */
export function parseNameStatus(stdout: string): { file: string; status: Status }[] {
const out: { file: string; status: Status }[] = []
Expand Down Expand Up @@ -85,7 +119,7 @@ export function summarize(entry: FileEntry): DiffFile {
deletions: entry.deletions,
status: entry.status,
tracked: entry.tracked,
generatedLike: generatedLike(entry.file),
generatedLike: entry.generatedLike ?? generatedLike(entry.file),
// Binary metadata is complete because no deferred text body exists.
// Images are the exception: their encoded sides load lazily on expansion.
summarized: image || !entry.binary,
Expand Down
Loading
Loading