From 056e06911b0bf4232021e8a56676ae9b3f888bb0 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Mon, 15 Jun 2026 16:00:12 +0200 Subject: [PATCH 1/3] feat(vscode): render images in diff viewer --- .changeset/render-image-diffs.md | 5 + .../kilo-vscode/src/agent-manager/GitOps.ts | 27 ++++-- .../src/agent-manager/local-diff.ts | 83 ++++++++++++---- .../kilo-vscode/src/agent-manager/types.ts | 3 + packages/kilo-vscode/src/diff/shared/image.ts | 69 ++++++++++++++ packages/kilo-vscode/src/diff/shared/path.ts | 9 ++ .../src/diff/sources/git-status.ts | 57 ++++++++--- .../kilo-vscode/src/diff/sources/session.ts | 48 ++++++++-- .../kilo-vscode/src/diff/sources/staged.ts | 71 +++++++++++--- .../kilo-vscode/src/diff/sources/unstaged.ts | 95 +++++++++++++++---- .../kilo-vscode/src/diff/sources/worktree.ts | 2 + packages/kilo-vscode/src/diff/types.ts | 16 ++++ packages/kilo-vscode/src/review-utils.ts | 18 ++-- .../tests/unit/agent-manager-arch.test.ts | 1 + .../unit/agent-manager-diff-state.test.ts | 24 ++++- .../kilo-vscode/tests/unit/diff-image.test.ts | 78 +++++++++++++++ .../tests/unit/diff-session-source.test.ts | 49 ++++++++++ .../kilo-vscode/tests/unit/local-diff.test.ts | 36 +++++++ .../tests/unit/review-utils.test.ts | 29 ++++++ .../webview-ui/agent-manager/DiffPanel.tsx | 87 ++++++++++------- .../agent-manager/agent-manager.css | 72 ++++++++++++++ .../webview-ui/agent-manager/i18n/en.ts | 6 ++ .../diff-viewer/FullScreenDiffView.tsx | 87 ++++++++++------- .../webview-ui/diff-viewer/ImageDiffView.tsx | 80 ++++++++++++++++ .../diff-viewer/diff-open-policy.ts | 4 +- .../webview-ui/diff-viewer/diff-state.ts | 6 +- .../src/types/messages/agent-manager.ts | 16 ++++ 27 files changed, 919 insertions(+), 159 deletions(-) create mode 100644 .changeset/render-image-diffs.md create mode 100644 packages/kilo-vscode/src/diff/shared/image.ts create mode 100644 packages/kilo-vscode/src/diff/shared/path.ts create mode 100644 packages/kilo-vscode/tests/unit/diff-image.test.ts create mode 100644 packages/kilo-vscode/tests/unit/review-utils.test.ts create mode 100644 packages/kilo-vscode/webview-ui/diff-viewer/ImageDiffView.tsx diff --git a/.changeset/render-image-diffs.md b/.changeset/render-image-diffs.md new file mode 100644 index 00000000000..7445a209c7d --- /dev/null +++ b/.changeset/render-image-diffs.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Render image changes in diff viewers and open images with VS Code's image preview. diff --git a/packages/kilo-vscode/src/agent-manager/GitOps.ts b/packages/kilo-vscode/src/agent-manager/GitOps.ts index c490a576411..92a2984b5a9 100644 --- a/packages/kilo-vscode/src/agent-manager/GitOps.ts +++ b/packages/kilo-vscode/src/agent-manager/GitOps.ts @@ -48,6 +48,12 @@ export interface ExecResult { stderr: string } +export interface ExecBufferResult { + code: number + stdout: Buffer + stderr: string +} + /** * Fixed SSH command injected by {@link nonInteractiveEnv} when the user has * not already configured their own. Exported so callers can check whether a @@ -532,12 +538,21 @@ export class GitOps { return this.exec(args, cwd, options) } - private exec(args: string[], cwd: string, options?: ExecOptions): Promise { + execGitBuffer(args: string[], cwd: string): Promise { + return this.execBuffer(args, cwd) + } + + private async exec(args: string[], cwd: string, options?: ExecOptions): Promise { + const result = await this.execBuffer(args, cwd, options) + return { code: result.code, stdout: result.stdout.toString("utf8"), stderr: result.stderr } + } + + private execBuffer(args: string[], cwd: string, options?: ExecOptions): Promise { if (this.controller.signal.aborted) { - return Promise.resolve({ code: 1, stdout: "", stderr: "GitOps disposed" }) + return Promise.resolve({ code: 1, stdout: Buffer.alloc(0), stderr: "GitOps disposed" }) } const invoke = () => - new Promise((resolve) => { + new Promise((resolve) => { const child = spawn("git", args, { cwd, env: options?.env, @@ -547,7 +562,7 @@ export class GitOps { if (options?.stdin !== undefined) { if (!child.stdin) { - resolve({ code: 1, stdout: "", stderr: "stdin not available for git process" }) + resolve({ code: 1, stdout: Buffer.alloc(0), stderr: "stdin not available for git process" }) return } child.stdin.end(options.stdin) @@ -559,12 +574,12 @@ export class GitOps { child.stderr?.on("data", (chunk: Buffer) => err.push(chunk)) child.on("error", (error) => { - resolve({ code: 1, stdout: "", stderr: error.message }) + resolve({ code: 1, stdout: Buffer.alloc(0), stderr: error.message }) }) child.on("close", (code) => { resolve({ code: code ?? 1, - stdout: Buffer.concat(out).toString("utf8"), + stdout: Buffer.concat(out), stderr: Buffer.concat(err).toString("utf8"), }) }) diff --git a/packages/kilo-vscode/src/agent-manager/local-diff.ts b/packages/kilo-vscode/src/agent-manager/local-diff.ts index 82ac3e2521b..e643e866270 100644 --- a/packages/kilo-vscode/src/agent-manager/local-diff.ts +++ b/packages/kilo-vscode/src/agent-manager/local-diff.ts @@ -1,6 +1,7 @@ import * as fs from "fs/promises" -import * as path from "path" import { binaryFile } from "../diff/shared/binary" +import { imageMime, loadImage, readImageFile } from "../diff/shared/image" +import { resolveInside } from "../diff/shared/path" import type { GitOps } from "./GitOps" import type { WorktreeDiffEntry } from "./types" @@ -139,16 +140,20 @@ async function numstat(git: GitOps, dir: string, base: string, file?: string) { } async function statStamp(dir: string, file: string): Promise { - const stat = await fs.stat(path.join(dir, file)).catch(() => undefined) + const full = resolveInside(dir, file) + if (!full) return `missing:${file}` + const stat = await fs.lstat(full).catch(() => undefined) if (!stat) return `missing:${file}` return `${stat.size}:${stat.mtimeMs}` } async function lineCount(file: string): Promise { - const stat = await fs.stat(file).catch(() => undefined) + const stat = await fs.lstat(file).catch(() => undefined) if (!stat || stat.size === 0) return 0 if (stat.size > MAX_UNTRACKED_BYTES) return 0 - const content = await fs.readFile(file, "utf-8").catch(() => "") + const content = stat.isSymbolicLink() + ? await fs.readlink(file).catch(() => "") + : await fs.readFile(file, "utf-8").catch(() => "") if (!content) return 0 if (content.endsWith("\n")) return content.split("\n").length - 1 return content.split("\n").length @@ -191,7 +196,8 @@ async function list(git: GitOps, dir: string, anc: string, log?: Log): Promise undefined) + const full = resolveInside(dir, file) + if (!full) continue + const exists = await fs.lstat(full).catch(() => undefined) if (!exists) continue const binary = await binaryFile(full) result.push({ @@ -226,6 +233,7 @@ async function list(git: GitOps, dir: string, anc: string, log?: Log): Promise => { const state = states.get(`${dir}\0${base}`) - const meta = state?.metas.get(file) - if (!state || !meta) return diffFile(git, dir, base, file, log) + if (!state) return diffFile(git, dir, base, file, log) + const meta = state.metas.get(file) + if (!meta) return null return materialize(git, dir, state.anc, meta, log) }, } } async function detailMeta(git: GitOps, dir: string, anc: string, file: string): Promise { + const full = resolveInside(dir, file) + if (!full) return undefined const tracked = await git.execGit(["ls-files", "--error-unmatch", "--", file], dir) if (tracked.code !== 0) { - const full = path.join(dir, file) - const exists = await fs.stat(full).catch(() => undefined) + const untracked = await git.execGit(["ls-files", "--others", "--exclude-standard", "--", file], dir) + if (untracked.code !== 0 || !untracked.stdout.split("\n").includes(file)) return undefined + const exists = await fs.lstat(full).catch(() => undefined) if (!exists) return undefined const binary = await binaryFile(full) return { @@ -323,7 +336,10 @@ async function detailMeta(git: GitOps, dir: string, anc: string, file: string): tracked: true, generatedLike: generatedLike(pathPart), binary: stat.binary, - stamp: status === "deleted" ? `deleted:${anc}` : await statStamp(dir, pathPart), + stamp: + status === "deleted" + ? `deleted:${anc}` + : `${imageMime(pathPart) ? `${anc}:` : ""}${await statStamp(dir, pathPart)}`, } } @@ -334,10 +350,25 @@ async function blobSize(git: GitOps, dir: string, anc: string, file: string): Pr } async function fileSize(dir: string, file: string): Promise { - const stat = await fs.stat(path.join(dir, file)).catch(() => undefined) + const full = resolveInside(dir, file) + if (!full) return 0 + const stat = await fs.lstat(full).catch(() => undefined) return stat?.size ?? 0 } +async function readBlob(git: GitOps, dir: string, ref: string, file: string): Promise { + const result = await git.execGitBuffer(["show", `${ref}:${file}`], dir) + return result.code === 0 ? result.stdout : undefined +} + +async function readFile(dir: string, file: string): Promise { + const full = resolveInside(dir, file) + if (!full) return undefined + const stat = await fs.lstat(full).catch(() => undefined) + if (!stat?.isFile()) return undefined + return readImageFile(full) +} + async function readBefore(git: GitOps, dir: string, anc: string, file: string, status: Status): Promise { if (status === "added") return "" const result = await git.execGit(["show", `${anc}:${file}`], dir) @@ -346,9 +377,12 @@ async function readBefore(git: GitOps, dir: string, anc: string, file: string, s async function readAfter(dir: string, file: string, status: Status): Promise { if (status === "deleted") return "" - const full = path.join(dir, file) - const exists = await fs.stat(full).catch(() => undefined) - if (!exists) return "" + const full = resolveInside(dir, file) + if (!full) return "" + const stat = await fs.lstat(full).catch(() => undefined) + if (!stat) return "" + if (stat.isSymbolicLink()) return fs.readlink(full).catch(() => "") + if (!stat.isFile()) return "" return fs.readFile(full, "utf-8").catch(() => "") } @@ -385,12 +419,21 @@ export async function diffFile( } async function materialize(git: GitOps, dir: string, anc: string, meta: Meta, log?: Log): Promise { - if (meta.binary) return summarize(meta) + const mime = imageMime(meta.file) + if (meta.binary && !mime) return summarize(meta) + const beforeBytes = meta.status === "added" ? 0 : await blobSize(git, dir, anc, meta.file) + const afterBytes = meta.status === "deleted" ? 0 : await fileSize(dir, meta.file) + if (mime) { + const image = await loadImage( + meta.file, + meta.status === "added" ? undefined : { bytes: beforeBytes, read: () => readBlob(git, dir, anc, meta.file) }, + meta.status === "deleted" ? undefined : { bytes: afterBytes, read: () => readFile(dir, meta.file) }, + ) + return { ...summarize(meta), summarized: false, image } + } // Cheap size probe before materializing content — protects the extension // host from OOM on huge tracked files. `git cat-file -s` returns the blob // size without streaming its contents, and `fs.stat` is a plain syscall. - const beforeBytes = meta.status === "added" ? 0 : await blobSize(git, dir, anc, meta.file) - const afterBytes = meta.status === "deleted" ? 0 : await fileSize(dir, meta.file) if (beforeBytes > MAX_DETAIL_BYTES || afterBytes > MAX_DETAIL_BYTES) { log?.("diffFile: file too large for detail view, returning summarized entry", { file: meta.file, diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts index 360b32c7217..ed766104333 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -8,6 +8,7 @@ */ import type { SnapshotFileDiff } from "@kilocode/sdk/v2/client" +import type { DiffImage } from "../diff/types" import type { Worktree, ManagedSession, Section } from "./WorktreeStateManager" import type { WorktreeStats, LocalStats } from "./GitStatsPoller" import type { ApplyConflict } from "./GitOps" @@ -33,6 +34,8 @@ export type WorktreeDiffEntry = SnapshotFileDiff & { generatedLike?: boolean summarized?: boolean stamp?: string + kind?: "image" + image?: DiffImage } // --------------------------------------------------------------------------- diff --git a/packages/kilo-vscode/src/diff/shared/image.ts b/packages/kilo-vscode/src/diff/shared/image.ts new file mode 100644 index 00000000000..6b3a887f152 --- /dev/null +++ b/packages/kilo-vscode/src/diff/shared/image.ts @@ -0,0 +1,69 @@ +import { createReadStream } from "fs" +import * as path from "path" +import type { DiffImage, DiffImageSide } from "../types" + +const MIMES: Record = { + ".avif": "image/avif", + ".bmp": "image/bmp", + ".gif": "image/gif", + ".ico": "image/x-icon", + ".jpe": "image/jpeg", + ".jpeg": "image/jpeg", + ".jpg": "image/jpeg", + ".png": "image/png", + ".svg": "image/svg+xml", + ".webp": "image/webp", +} + +export const MAX_IMAGE_BYTES = 5_000_000 + +export interface DiffImageSource { + bytes: number + read: () => Promise +} + +export function imageMime(file: string): string | undefined { + return MIMES[path.extname(file).toLowerCase()] +} + +export function encodeImageSide(mime: string, data: Buffer | undefined, bytes = data?.byteLength ?? 0): DiffImageSide { + if (bytes > MAX_IMAGE_BYTES) return { mime, bytes, error: "too-large" } + if (!data || data.byteLength === 0) return { mime, bytes, error: "unreadable" } + if (data.byteLength > MAX_IMAGE_BYTES) return { mime, bytes: data.byteLength, error: "too-large" } + return { mime, bytes: data.byteLength, data: data.toString("base64") } +} + +export function readImageFile(file: string): Promise { + return new Promise((resolve) => { + const chunks: Buffer[] = [] + let bytes = 0 + const stream = createReadStream(file, { end: MAX_IMAGE_BYTES, highWaterMark: 64 * 1024 }) + stream.on("data", (chunk) => { + const data = typeof chunk === "string" ? Buffer.from(chunk) : chunk + chunks.push(data) + bytes += data.byteLength + }) + stream.on("error", () => resolve(undefined)) + stream.on("end", () => resolve(Buffer.concat(chunks, bytes))) + }) +} + +async function load(mime: string, source: DiffImageSource): Promise { + if (source.bytes > MAX_IMAGE_BYTES) return { mime, bytes: source.bytes, error: "too-large" } + const data = await source.read().catch(() => undefined) + return encodeImageSide(mime, data, source.bytes) +} + +export async function loadImage( + file: string, + before?: DiffImageSource, + after?: DiffImageSource, +): Promise { + const mime = imageMime(file) + if (!mime) return undefined + const [left, right] = await Promise.all([ + before ? load(mime, before) : undefined, + after ? load(mime, after) : undefined, + ]) + return { before: left, after: right } +} diff --git a/packages/kilo-vscode/src/diff/shared/path.ts b/packages/kilo-vscode/src/diff/shared/path.ts new file mode 100644 index 00000000000..3d0f9d7e0df --- /dev/null +++ b/packages/kilo-vscode/src/diff/shared/path.ts @@ -0,0 +1,9 @@ +import * as path from "path" + +export function resolveInside(dir: string, file: string): string | undefined { + if (path.isAbsolute(file)) return undefined + const full = path.resolve(dir, file) + const base = path.resolve(dir) + if (full !== base && !full.startsWith(base + path.sep)) return undefined + return full +} diff --git a/packages/kilo-vscode/src/diff/sources/git-status.ts b/packages/kilo-vscode/src/diff/sources/git-status.ts index 44cf3001ac7..b6844af9aa1 100644 --- a/packages/kilo-vscode/src/diff/sources/git-status.ts +++ b/packages/kilo-vscode/src/diff/sources/git-status.ts @@ -2,9 +2,10 @@ // off `git diff` variants and need the same name-status + numstat stitching. import * as fs from "fs/promises" -import * as path from "path" import type { GitOps } from "../../agent-manager/GitOps" import { generatedLike } from "../../agent-manager/local-diff" +import { imageMime, readImageFile } from "../shared/image" +import { resolveInside } from "../shared/path" import type { DiffFile } from "../types" export { MAX_DETAIL_BYTES } from "../../agent-manager/local-diff" @@ -51,6 +52,19 @@ export function parseNumstat(stdout: string): Map { + const map = new Map() + for (const line of stdout.split("\n")) { + const tab = line.indexOf("\t") + if (!line.startsWith(":") || tab < 0) continue + const meta = line.slice(1, tab).split(" ") + const file = line.slice(tab + 1) + if (!file || !meta[2] || !meta[3]) continue + map.set(file, { before: meta[2], after: meta[3] }) + } + return map +} + function statusFromCode(code: string): Status { if (code.startsWith("A")) return "added" if (code.startsWith("D")) return "deleted" @@ -62,6 +76,7 @@ function statusFromCode(code: string): Status { * are left empty — the controller fetches detail lazily through `fetchFile`. */ export function summarize(entry: FileEntry): DiffFile { + const image = imageMime(entry.file) !== undefined return { file: entry.file, before: "", @@ -72,13 +87,15 @@ export function summarize(entry: FileEntry): DiffFile { tracked: entry.tracked, generatedLike: generatedLike(entry.file), // Binary metadata is complete because no deferred text body exists. - summarized: !entry.binary, + // Images are the exception: their encoded sides load lazily on expansion. + summarized: image || !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 // supply a custom stamp (see `FileEntry.stamp`) when additions/deletions // aren't a reliable change signal — notably untracked files. stamp: entry.stamp ?? `${entry.status}:${entry.additions}:${entry.deletions}`, + kind: image ? "image" : undefined, } } @@ -95,6 +112,11 @@ export async function showBlob(git: GitOps, dir: string, ref: string, file: stri return result.code === 0 ? result.stdout : "" } +export async function showBlobBytes(git: GitOps, dir: string, ref: string, file: string): Promise { + const result = await git.execGitBuffer(["show", `${ref}:${file}`], dir) + return result.code === 0 ? result.stdout : undefined +} + /** * Read a working-tree file off disk, matching git's blob semantics for * symlinks: when the entry is a symlink, return its target string (what git @@ -116,6 +138,14 @@ export async function readDisk(dir: string, file: string): Promise { return fs.readFile(full, "utf-8").catch(() => "") } +export async function readDiskBytes(dir: string, file: string): Promise { + const full = resolveInside(dir, file) + if (!full) return undefined + const stat = await fs.lstat(full).catch(() => undefined) + if (!stat?.isFile()) return undefined + return readImageFile(full) +} + // Used to size-cap detail reads without materializing the blob. Mirrors // local-diff.ts's `blobSize` helper. export async function blobSize(git: GitOps, dir: string, ref: string, file: string): Promise { @@ -124,6 +154,19 @@ export async function blobSize(git: GitOps, dir: string, ref: string, file: stri return parseInt(result.stdout.trim(), 10) || 0 } +export async function blobOid(git: GitOps, dir: string, ref: string, file: string): Promise { + const result = await git.execGit(["rev-parse", "--verify", `${ref}:${file}`], dir) + return result.code === 0 ? result.stdout.trim() : "missing" +} + +export async function diskStamp(dir: string, file: string): Promise { + const full = resolveInside(dir, file) + if (!full) return "missing" + const stat = await fs.lstat(full).catch(() => undefined) + if (!stat) return "missing" + return `${stat.size}:${stat.mtimeMs}` +} + /** * Size of the working-tree entry at `file`. Uses `lstat` so symlinks report * the link's own size (length of the target string) instead of resolving to @@ -135,13 +178,3 @@ export async function fileSize(dir: string, file: string): Promise { const stat = await fs.lstat(full).catch(() => undefined) return stat?.size ?? 0 } - -// Rejects absolute paths and any `..` traversal that would escape `dir`. -// Returns the resolved path when safe, `undefined` otherwise. -export function resolveInside(dir: string, file: string): string | undefined { - if (path.isAbsolute(file)) return undefined - const full = path.resolve(dir, file) - const base = path.resolve(dir) - if (full !== base && !full.startsWith(base + path.sep)) return undefined - return full -} diff --git a/packages/kilo-vscode/src/diff/sources/session.ts b/packages/kilo-vscode/src/diff/sources/session.ts index c5c16242cd2..58fc6d8bdd4 100644 --- a/packages/kilo-vscode/src/diff/sources/session.ts +++ b/packages/kilo-vscode/src/diff/sources/session.ts @@ -1,5 +1,7 @@ +import { createHash } from "crypto" import type { SnapshotFileDiff } from "@kilocode/sdk/v2/client" import { normalize, text } from "@kilocode/kilo-ui/session-diff" +import { encodeImageSide, imageMime } from "../shared/image" import type { DiffFile } from "../types" import type { DiffSource, DiffSourceDescriptor, DiffSourceFetch } from "./types" @@ -36,6 +38,7 @@ export function createSessionDiffSource( ): DiffSource { // Cached across fetches so subsequent polling ticks skip the config lookup. let snapshotsDisabled = false + let cache: { key: string; diffs: DiffFile[] } | undefined return { descriptor: sessionDescriptor(sessionId), @@ -54,24 +57,50 @@ export function createSessionDiffSource( } const raw = await fetch({ sessionID: sessionId, directory: workspaceRoot }) - return { diffs: raw.map(toSessionDiffFile) } + const key = raw.map(fingerprint).join("|") + if (cache?.key === key) return { diffs: cache.diffs } + const diffs = raw.map(toSessionDiffFile) + cache = { key, diffs } + return { diffs } }, } } +function fingerprint(raw: SnapshotFileDiff): string { + const patch = createHash("sha1") + .update(raw.patch ?? "") + .digest("hex") + return [raw.file, raw.status, raw.additions, raw.deletions, patch].join(":") +} + /** * Project a backend `SnapshotFileDiff` onto the `DiffFile` shape the viewer * expects. Shared with `createTurnDiffSource` since both hit the same endpoint. */ export function toSessionDiffFile(raw: SnapshotFileDiff): DiffFile { + const file = raw.file ?? "" + const mime = imageMime(file) // Empty patch means binary or summarized (>256 KB) — normalize() can't - // parse it, so short-circuit to empty strings. - const view = raw.patch === "" ? null : normalize(raw) + // parse it, so short-circuit to empty strings. Binary snapshot images do + // not retain their sides, while text-backed SVG patches can be rebuilt. + const view = raw.patch === "" || (mime && mime !== "image/svg+xml") ? null : normalize(raw) + const before = view ? text(view, "deletions") : "" + const after = view ? text(view, "additions") : "" + const image = (() => { + if (mime === "image/svg+xml" && view) { + return { + before: raw.status === "added" ? undefined : encodeImageSide(mime, Buffer.from(before)), + after: raw.status === "deleted" ? undefined : encodeImageSide(mime, Buffer.from(after)), + } + } + if (mime) return {} + return undefined + })() return { - file: raw.file ?? "", - before: view ? text(view, "deletions") : "", - after: view ? text(view, "additions") : "", - patch: raw.patch, + file, + before: mime ? "" : before, + after: mime ? "" : after, + patch: mime ? "" : raw.patch, additions: raw.additions, deletions: raw.deletions, status: raw.status, @@ -79,6 +108,9 @@ export function toSessionDiffFile(raw: SnapshotFileDiff): DiffFile { generatedLike: false, // 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), + summarized: !mime && raw.patch === "" && (raw.additions !== 0 || raw.deletions !== 0), + kind: mime ? "image" : undefined, + image, + stamp: mime ? fingerprint(raw) : undefined, } } diff --git a/packages/kilo-vscode/src/diff/sources/staged.ts b/packages/kilo-vscode/src/diff/sources/staged.ts index c47cc552fb3..d21a508902b 100644 --- a/packages/kilo-vscode/src/diff/sources/staged.ts +++ b/packages/kilo-vscode/src/diff/sources/staged.ts @@ -2,15 +2,20 @@ 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 { imageMime, loadImage } from "../shared/image" +import { resolveInside } from "../shared/path" import type { DiffFile } from "../types" import type { DiffSource, DiffSourceDescriptor, DiffSourceFetch } from "./types" import { + blobOid, blobSize, INDEX_REF, MAX_DETAIL_BYTES, parseNameStatus, parseNumstat, + parseRawOids, showBlob, + showBlobBytes, summarize, type FileEntry, } from "./git-status" @@ -24,6 +29,11 @@ export const STAGED_DESCRIPTOR: DiffSourceDescriptor = { capabilities: { revert: false, comments: true }, } +function stamp(entry: FileEntry, before: string, after: string): FileEntry { + if (!imageMime(entry.file)) return entry + return { ...entry, stamp: `${entry.status}:${before}:${after}` } +} + /** * Diff between the git index and HEAD — what `git diff --cached` would show. * Polls on the standard interval; revert isn't supported (use `git reset` from @@ -37,24 +47,36 @@ export function createStagedDiffSource(): DiffSource { const root = (): string | undefined => getWorkspaceRoot() const listEntries = async (dir: string): Promise => { - const [nameStatus, numstat] = await Promise.all([ + const [nameStatus, numstat, raw] = await Promise.all([ git.execGit(["-c", "core.quotepath=false", "diff", "--cached", "--name-status", "--no-renames", "HEAD"], dir), git.execGit(["-c", "core.quotepath=false", "diff", "--cached", "--numstat", "--no-renames", "HEAD"], dir), + git.execGit( + ["-c", "core.quotepath=false", "diff", "--cached", "--raw", "--abbrev=64", "--no-renames", "HEAD"], + dir, + ), ]) if (nameStatus.code !== 0) { log("git diff --cached --name-status failed", { code: nameStatus.code, stderr: nameStatus.stderr.trim() }) return [] } const counts = parseNumstat(numstat.code === 0 ? numstat.stdout : "") - const items = parseNameStatus(nameStatus.stdout) - return items.map((item) => ({ - file: item.file, - status: item.status, - additions: counts.get(item.file)?.additions ?? 0, - deletions: counts.get(item.file)?.deletions ?? 0, - tracked: true, - binary: counts.get(item.file)?.binary ?? false, - })) + const refs = parseRawOids(raw.code === 0 ? raw.stdout : "") + return parseNameStatus(nameStatus.stdout).map((item) => { + const ref = refs.get(item.file) + const entry = { + file: item.file, + status: item.status, + additions: counts.get(item.file)?.additions ?? 0, + deletions: counts.get(item.file)?.deletions ?? 0, + tracked: true, + binary: counts.get(item.file)?.binary ?? false, + } + return stamp( + entry, + item.status === "added" ? "missing" : (ref?.before ?? "missing"), + item.status === "deleted" ? "missing" : (ref?.after ?? "missing"), + ) + }) } return { @@ -73,7 +95,7 @@ export function createStagedDiffSource(): DiffSource { async fetchFile(file: string): Promise { const dir = root() - if (!dir || !file) return null + if (!dir || !file || !resolveInside(dir, file)) return null // Resolve the entry for this single file so we know its status. Reading // both refs blindly would still work, but knowing the status lets us @@ -81,10 +103,23 @@ export function createStagedDiffSource(): DiffSource { const entry = await fileEntry(git, dir, file, log) if (!entry) return null - if (entry.binary) return summarize(entry) - + const mime = imageMime(file) + if (entry.binary && !mime) 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 (mime) { + const image = await loadImage( + file, + entry.status === "added" + ? undefined + : { bytes: beforeBytes, read: () => showBlobBytes(git, dir, "HEAD", file) }, + entry.status === "deleted" + ? undefined + : { bytes: afterBytes, read: () => showBlobBytes(git, dir, INDEX_REF, file) }, + ) + return { ...summarize(entry), summarized: false, image } + } + if (beforeBytes > MAX_DETAIL_BYTES || afterBytes > MAX_DETAIL_BYTES) { log("Staged detail skipped: file too large", { file, beforeBytes, afterBytes, cap: MAX_DETAIL_BYTES }) return summarize(entry) @@ -110,7 +145,7 @@ export function createStagedDiffSource(): DiffSource { tracked: true, generatedLike: generatedLike(file), summarized, - stamp: `${entry.status}:${entry.additions}:${entry.deletions}`, + stamp: entry.stamp ?? `${entry.status}:${entry.additions}:${entry.deletions}`, } }, @@ -144,7 +179,7 @@ async function fileEntry( dir, ) const stats = parseNumstat(counts.code === 0 ? counts.stdout : "") - return { + const entry = { file: item.file, status: item.status, additions: stats.get(item.file)?.additions ?? 0, @@ -152,4 +187,10 @@ async function fileEntry( tracked: true, binary: stats.get(item.file)?.binary ?? false, } + if (!imageMime(item.file)) return entry + const [before, after] = await Promise.all([ + item.status === "added" ? "missing" : blobOid(git, dir, "HEAD", item.file), + item.status === "deleted" ? "missing" : blobOid(git, dir, INDEX_REF, item.file), + ]) + return stamp(entry, before, after) } diff --git a/packages/kilo-vscode/src/diff/sources/unstaged.ts b/packages/kilo-vscode/src/diff/sources/unstaged.ts index 3aeb5db1704..b4d0c996161 100644 --- a/packages/kilo-vscode/src/diff/sources/unstaged.ts +++ b/packages/kilo-vscode/src/diff/sources/unstaged.ts @@ -4,18 +4,24 @@ 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 { imageMime, loadImage } from "../shared/image" +import { resolveInside } from "../shared/path" import type { DiffFile } from "../types" import type { DiffSource, DiffSourceDescriptor, DiffSourceFetch } from "./types" import { + blobOid, blobSize, + diskStamp, fileSize, INDEX_REF, MAX_DETAIL_BYTES, parseNameStatus, parseNumstat, + parseRawOids, readDisk, - resolveInside, + readDiskBytes, showBlob, + showBlobBytes, summarize, type FileEntry, } from "./git-status" @@ -29,6 +35,11 @@ export const UNSTAGED_DESCRIPTOR: DiffSourceDescriptor = { capabilities: { revert: false, comments: true }, } +function stamp(entry: FileEntry, before: string, after: string): FileEntry { + if (!imageMime(entry.file)) return entry + return { ...entry, stamp: `${entry.status}:${before}:${after}` } +} + /** * Diff between the working tree and the index — what `git diff` shows for * tracked files, plus untracked files (treated as fully-added). Read-only; @@ -42,23 +53,33 @@ export function createUnstagedDiffSource(): DiffSource { const root = (): string | undefined => getWorkspaceRoot() const listTracked = async (dir: string): Promise => { - const [nameStatus, numstat] = await Promise.all([ + const [nameStatus, numstat, raw] = await Promise.all([ git.execGit(["-c", "core.quotepath=false", "diff", "--name-status", "--no-renames"], dir), git.execGit(["-c", "core.quotepath=false", "diff", "--numstat", "--no-renames"], dir), + git.execGit(["-c", "core.quotepath=false", "diff", "--raw", "--abbrev=64", "--no-renames"], dir), ]) if (nameStatus.code !== 0) { log("git diff --name-status failed", { code: nameStatus.code, stderr: nameStatus.stderr.trim() }) return [] } const counts = parseNumstat(numstat.code === 0 ? numstat.stdout : "") - return parseNameStatus(nameStatus.stdout).map((item) => ({ - file: item.file, - status: item.status, - additions: counts.get(item.file)?.additions ?? 0, - deletions: counts.get(item.file)?.deletions ?? 0, - tracked: true, - binary: counts.get(item.file)?.binary ?? false, - })) + const refs = parseRawOids(raw.code === 0 ? raw.stdout : "") + return Promise.all( + parseNameStatus(nameStatus.stdout).map(async (item) => { + const entry = { + file: item.file, + status: item.status, + additions: counts.get(item.file)?.additions ?? 0, + deletions: counts.get(item.file)?.deletions ?? 0, + tracked: true, + binary: counts.get(item.file)?.binary ?? false, + } + if (!imageMime(item.file)) return entry + const before = item.status === "added" ? "missing" : (refs.get(item.file)?.before ?? "missing") + const after = item.status === "deleted" ? "missing" : await diskStamp(dir, item.file) + return stamp(entry, before, after) + }), + ) } const listUntracked = async (dir: string): Promise => { @@ -116,17 +137,24 @@ export function createUnstagedDiffSource(): DiffSource { async fetchFile(file: string): Promise { const dir = root() - if (!dir || !file) return null + if (!dir || !file || !resolveInside(dir, file)) return null const entry = await fileEntry(git, dir, file, log) if (!entry) return null + const mime = imageMime(file) + if (entry.binary && !mime) return summarize(entry) - if (entry.binary) return summarize(entry) + const bytes = await detailBytes(git, dir, file, entry) + const image = await imageDetail(git, dir, file, entry, bytes.before, bytes.after) + if (image) return image - 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) { - log("Unstaged detail skipped: file too large", { file, beforeBytes, afterBytes, cap: MAX_DETAIL_BYTES }) + if (bytes.before > MAX_DETAIL_BYTES || bytes.after > MAX_DETAIL_BYTES) { + log("Unstaged detail skipped: file too large", { + file, + beforeBytes: bytes.before, + afterBytes: bytes.after, + cap: MAX_DETAIL_BYTES, + }) return summarize(entry) } @@ -170,6 +198,31 @@ export function createUnstagedDiffSource(): DiffSource { } } +async function detailBytes(git: GitOps, dir: string, file: string, entry: FileEntry) { + const before = !entry.tracked || entry.status === "added" ? 0 : await blobSize(git, dir, INDEX_REF, file) + const after = entry.status === "deleted" ? 0 : await fileSize(dir, file) + return { before, after } +} + +async function imageDetail( + git: GitOps, + dir: string, + file: string, + entry: FileEntry, + beforeBytes: number, + afterBytes: number, +): Promise { + if (!imageMime(file)) return undefined + const image = await loadImage( + file, + !entry.tracked || entry.status === "added" + ? undefined + : { bytes: beforeBytes, read: () => showBlobBytes(git, dir, INDEX_REF, file) }, + entry.status === "deleted" ? undefined : { bytes: afterBytes, read: () => readDiskBytes(dir, file) }, + ) + return { ...summarize(entry), summarized: false, image } +} + async function fileEntry( git: GitOps, dir: string, @@ -190,7 +243,7 @@ async function fileEntry( dir, ) const stats = parseNumstat(counts.code === 0 ? counts.stdout : "") - return { + const entry = { file: item.file, status: item.status, additions: stats.get(item.file)?.additions ?? 0, @@ -198,6 +251,12 @@ async function fileEntry( tracked: true, binary: stats.get(item.file)?.binary ?? false, } + if (!imageMime(item.file)) return entry + const [before, after] = await Promise.all([ + item.status === "added" ? "missing" : blobOid(git, dir, INDEX_REF, item.file), + item.status === "deleted" ? "missing" : diskStamp(dir, item.file), + ]) + return stamp(entry, before, after) } } @@ -210,6 +269,8 @@ async function fileEntry( log("Unstaged file rejected: outside workspace", { file }) return undefined } + const untracked = await git.execGit(["ls-files", "--others", "--exclude-standard", "--", file], dir) + if (untracked.code !== 0 || !untracked.stdout.split("\n").includes(file)) return undefined const stat = await fs.lstat(full).catch(() => undefined) if (!stat) { log("Unstaged file not found", { file }) diff --git a/packages/kilo-vscode/src/diff/sources/worktree.ts b/packages/kilo-vscode/src/diff/sources/worktree.ts index 36a436fce3a..1f3d8ee25a4 100644 --- a/packages/kilo-vscode/src/diff/sources/worktree.ts +++ b/packages/kilo-vscode/src/diff/sources/worktree.ts @@ -156,5 +156,7 @@ function toDiffFile(entry: WorktreeDiffEntry): DiffFile { generatedLike: entry.generatedLike, summarized: entry.summarized, stamp: entry.stamp, + kind: entry.kind, + image: entry.image, } } diff --git a/packages/kilo-vscode/src/diff/types.ts b/packages/kilo-vscode/src/diff/types.ts index a6d9def400f..913d4560a31 100644 --- a/packages/kilo-vscode/src/diff/types.ts +++ b/packages/kilo-vscode/src/diff/types.ts @@ -12,6 +12,20 @@ export interface PanelContext { baseBranchOverride?: string } +export type DiffImageError = "too-large" | "unreadable" + +export interface DiffImageSide { + mime: string + bytes: number + data?: string + error?: DiffImageError +} + +export interface DiffImage { + before?: DiffImageSide + after?: DiffImageSide +} + /** Mirrors `WorktreeFileDiff` in webview-ui/src/types/messages/agent-manager.ts. */ export interface DiffFile { file: string @@ -26,4 +40,6 @@ export interface DiffFile { generatedLike?: boolean summarized?: boolean stamp?: string + kind?: "image" + image?: DiffImage } diff --git a/packages/kilo-vscode/src/review-utils.ts b/packages/kilo-vscode/src/review-utils.ts index 42dac76b284..c0d7eccbeda 100644 --- a/packages/kilo-vscode/src/review-utils.ts +++ b/packages/kilo-vscode/src/review-utils.ts @@ -23,15 +23,17 @@ export function openFileInEditor( prefix = "Kilo", ): void { const uri = vscode.Uri.file(filePath) - const target = Math.max(1, Math.floor(line ?? 1)) - const col = column !== undefined && column > 0 ? column - 1 : 0 - const pos = new vscode.Position(target - 1, col) - const selection = new vscode.Range(pos, pos) + const options: vscode.TextDocumentShowOptions = { viewColumn, preview: true } + if (line !== undefined && line > 0) { + const target = Math.max(1, Math.floor(line)) + const col = column !== undefined && column > 0 ? column - 1 : 0 + const pos = new vscode.Position(target - 1, col) + options.selection = new vscode.Range(pos, pos) + } - vscode.workspace.openTextDocument(uri).then( - (doc) => vscode.window.showTextDocument(doc, { viewColumn, preview: true, selection }), - (err) => console.error(`[Kilo New] ${prefix}: Failed to open file:`, uri.fsPath, err), - ) + void vscode.commands + .executeCommand("vscode.open", uri, options) + .then(undefined, (err) => console.error(`[Kilo New] ${prefix}: Failed to open file:`, uri.fsPath, err)) } export function openWorkspaceRelativeFile(relativePath: string, line?: number, column?: number): void { diff --git a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts index d45c3873e2e..78599763053 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts @@ -24,6 +24,7 @@ const TSX_FILES = [ path.join(ROOT, "webview-ui/agent-manager/sortable-tab.tsx"), path.join(ROOT, "webview-ui/agent-manager/DiffPanel.tsx"), path.join(ROOT, "webview-ui/diff-viewer/FullScreenDiffView.tsx"), + path.join(ROOT, "webview-ui/diff-viewer/ImageDiffView.tsx"), path.join(ROOT, "webview-ui/diff-viewer/MarkdownDiffView.tsx"), path.join(ROOT, "webview-ui/diff-viewer/MarkdownAnnotationLayer.tsx"), path.join(ROOT, "webview-ui/diff-viewer/markdown-comment-ranges.ts"), 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 009b459df7e..381ac518a38 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 @@ -41,6 +41,20 @@ describe("agent manager diff state", () => { expect(result.stale.size).toBe(0) }) + it("preserves loaded image data when summary metadata is unchanged", () => { + const image = { + before: { mime: "image/png", bytes: 3, data: "b2xk" }, + after: { mime: "image/png", bytes: 3, data: "bmV3" }, + } + const prev = [diff({ file: "asset.png", kind: "image", summarized: false, image })] + const next = [diff({ file: "asset.png", kind: "image", summarized: true })] + const result = mergeWorktreeDiffs(prev, next) + + expect(result.diffs[0]).toBe(prev[0]) + expect(result.diffs[0]?.image).toBe(image) + expect(result.stale.size).toBe(0) + }) + it("replaces detailed content when patch anchors change", () => { const prev = [diff({ summarized: false, before: "old\n", after: "new\n", patch: "@@ -1 +1 @@\n-old\n+new\n" })] const next = [diff({ summarized: false, before: "old\n", after: "new\n", patch: "@@ -100 +100 @@\n-old\n+new\n" })] @@ -74,6 +88,7 @@ describe("agent manager diff state", () => { 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: "assets/banner.png", kind: "image", summarized: true, additions: 0 }), diff({ file: "src/huge.ts", additions: EXTREME_DIFF_CHANGED_LINES + 1 }), ]), ).toEqual(["src/app.ts", "node_modules/pkg/index.js", "src/huge.ts"]) @@ -121,13 +136,18 @@ describe("agent manager diff state", () => { expect(toggleOpenFiles(diffs, files)).toEqual([]) }) - it("prevents non-text diffs from entering open state", () => { + it("opens images while preventing other non-text diffs from entering open state", () => { const audio = diff({ file: "audio/alert.wav", summarized: false, additions: 0 }) + const image = diff({ file: "assets/banner.png", kind: "image", summarized: true, additions: 0 }) const text = diff({ file: "src/app.ts" }) expect(isDiffExpandable(audio)).toBe(false) + expect(isDiffExpandable(image)).toBe(true) expect(isDiffExpandable(text)).toBe(true) - expect(sanitizeOpenFiles([audio, text], [audio.file, text.file])).toEqual([text.file]) + expect(sanitizeOpenFiles([audio, image, text], [audio.file, image.file, text.file])).toEqual([ + image.file, + text.file, + ]) }) }) diff --git a/packages/kilo-vscode/tests/unit/diff-image.test.ts b/packages/kilo-vscode/tests/unit/diff-image.test.ts new file mode 100644 index 00000000000..6bb7e4df969 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/diff-image.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from "bun:test" +import * as fs from "fs/promises" +import * as os from "os" +import * as path from "path" +import { imageMime, loadImage, MAX_IMAGE_BYTES, readImageFile } from "../../src/diff/shared/image" +import { parseRawOids } from "../../src/diff/sources/git-status" + +describe("diff images", () => { + it("recognizes every image format handled by the VS Code image preview", () => { + const files = [ + "photo.jpg", + "photo.jpe", + "photo.jpeg", + "image.png", + "image.bmp", + "animation.gif", + "favicon.ico", + "image.webp", + "image.avif", + "vector.svg", + ] + + for (const file of files) expect(imageMime(file)).toStartWith("image/") + expect(imageMime("ASSETS/LOGO.PNG")).toBe("image/png") + expect(imageMime("archive.zip")).toBeUndefined() + expect(imageMime("photo.tiff")).toBeUndefined() + }) + + it("parses image blob identities from a single raw diff", () => { + const before = "1".repeat(40) + const after = "2".repeat(40) + const refs = parseRawOids(`:100644 100644 ${before} ${after} M\tassets/banner.png\n`) + + expect(refs.get("assets/banner.png")).toEqual({ before, after }) + }) + + it("encodes image sides without converting bytes to text", async () => { + const before = Buffer.from([0x00, 0xff, 0x10, 0x80]) + const after = Buffer.from([0x89, 0x50, 0x4e, 0x47]) + const image = await loadImage( + "asset.png", + { bytes: before.byteLength, read: async () => before }, + { bytes: after.byteLength, read: async () => after }, + ) + + expect(image?.before).toEqual({ mime: "image/png", bytes: 4, data: before.toString("base64") }) + expect(image?.after).toEqual({ mime: "image/png", bytes: 4, data: after.toString("base64") }) + }) + + it("bounds mutable image reads to the payload cap", async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "diff-image-test-")) + const file = path.join(dir, "large.png") + try { + await fs.writeFile(file, Buffer.alloc(MAX_IMAGE_BYTES + 100)) + expect((await readImageFile(file))?.byteLength).toBe(MAX_IMAGE_BYTES + 1) + } finally { + await fs.rm(dir, { recursive: true, force: true }) + } + }) + + it("does not read image sides over the webview payload cap", async () => { + let reads = 0 + const image = await loadImage("asset.webp", { + bytes: MAX_IMAGE_BYTES + 1, + read: async () => { + reads++ + return Buffer.from("unused") + }, + }) + + expect(reads).toBe(0) + expect(image?.before).toEqual({ + mime: "image/webp", + bytes: MAX_IMAGE_BYTES + 1, + error: "too-large", + }) + }) +}) 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 4db1b322db7..ce870f0abbe 100644 --- a/packages/kilo-vscode/tests/unit/diff-session-source.test.ts +++ b/packages/kilo-vscode/tests/unit/diff-session-source.test.ts @@ -91,6 +91,55 @@ describe("createSessionDiffSource.fetch", () => { expect(result.diffs[2]?.summarized).toBe(true) }) + it("rebuilds text-backed SVG snapshot sides without sending them to Pierre", async () => { + const before = '' + const after = '' + const patch = [ + "diff --git a/assets/banner.svg b/assets/banner.svg", + "--- a/assets/banner.svg", + "+++ b/assets/banner.svg", + "@@ -1 +1 @@", + `-${before}`, + `+${after}`, + "", + ].join("\n") + const { fetch } = recording([{ file: "assets/banner.svg", patch, additions: 1, deletions: 1, status: "modified" }]) + const result = await createSessionDiffSource("s-image", fetch, "/repo").fetch() + + expect(result.diffs[0]).toMatchObject({ + file: "assets/banner.svg", + before: "", + after: "", + patch: "", + kind: "image", + summarized: false, + image: { + before: { mime: "image/svg+xml", data: Buffer.from(`${before}\n`).toString("base64") }, + after: { mime: "image/svg+xml", data: Buffer.from(`${after}\n`).toString("base64") }, + }, + }) + }) + + it("reuses converted image details while the snapshot is unchanged", async () => { + const { fetch } = recording([ + { file: "assets/banner.svg", patch: modifiedPatch, additions: 1, deletions: 1, status: "modified" }, + ]) + const source = createSessionDiffSource("s-image-cache", fetch, "/repo") + const first = await source.fetch() + const second = await source.fetch() + + expect(second.diffs).toBe(first.diffs) + }) + + it("marks binary snapshot images unavailable when their sides were not retained", async () => { + const { fetch } = recording([ + { file: "assets/banner.png", patch: "", additions: 0, deletions: 0, status: "modified" }, + ]) + const result = await createSessionDiffSource("s-image", fetch, "/repo").fetch() + + expect(result.diffs[0]?.image).toEqual({}) + }) + it("propagates errors from the underlying fetch", async () => { const { fetch } = recording(new Error("network down")) const source = createSessionDiffSource("s3", fetch) diff --git a/packages/kilo-vscode/tests/unit/local-diff.test.ts b/packages/kilo-vscode/tests/unit/local-diff.test.ts index 67467db1e60..1e00c06470d 100644 --- a/packages/kilo-vscode/tests/unit/local-diff.test.ts +++ b/packages/kilo-vscode/tests/unit/local-diff.test.ts @@ -237,6 +237,28 @@ describe("diffSummary", () => { }) }) + it("loads binary-safe before and after data for image diffs", async () => { + await withRepo(async (dir, base) => { + const before = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0x01]) + const after = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0xff]) + await fs.writeFile(path.join(dir, "banner.png"), before) + runSync(dir, ["add", "banner.png"]) + runSync(dir, ["commit", "-m", "add banner"]) + runSync(dir, ["branch", "-f", base]) + await fs.writeFile(path.join(dir, "banner.png"), after) + + const local = createLocalDiff(git()) + const summary = (await local.summary(dir, base)).find((entry) => entry.file === "banner.png") + const detail = await local.file(dir, base, "banner.png") + + expect(summary?.kind).toBe("image") + expect(summary?.summarized).toBe(true) + expect(detail?.summarized).toBe(false) + expect(detail?.image?.before?.data).toBe(before.toString("base64")) + expect(detail?.image?.after?.data).toBe(after.toString("base64")) + }) + }) + it("marks generated-like files via generatedLike flag", async () => { await withRepo(async (dir, base) => { await fs.mkdir(path.join(dir, "dist"), { recursive: true }) @@ -266,6 +288,20 @@ describe("diffFile", () => { }) }) + it("rejects image detail paths outside the repository", async () => { + await withRepo(async (dir, base) => { + const name = `${path.basename(dir)}-secret.png` + const secret = path.join(path.dirname(dir), name) + await fs.writeFile(secret, Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00])) + try { + expect(await diffFile(git(), dir, base, `../${name}`)).toBeNull() + expect(await diffFile(git(), dir, base, secret)).toBeNull() + } finally { + await fs.rm(secret, { force: true }) + } + }) + }) + it("returns before/after/patch for a modified tracked file", async () => { await withRepo(async (dir, base) => { await fs.writeFile(path.join(dir, "seed.txt"), "seed\nmore\n") diff --git a/packages/kilo-vscode/tests/unit/review-utils.test.ts b/packages/kilo-vscode/tests/unit/review-utils.test.ts new file mode 100644 index 00000000000..67793473010 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/review-utils.test.ts @@ -0,0 +1,29 @@ +import { afterEach, describe, expect, it, spyOn } from "bun:test" +import * as vscode from "vscode" +import { openFileInEditor } from "../../src/review-utils" + +const execute = spyOn(vscode.commands, "executeCommand") + +afterEach(() => { + execute.mockClear() +}) + +describe("openFileInEditor", () => { + it("delegates to vscode.open so custom image editors can handle the file", () => { + openFileInEditor("/repo/assets/banner.png", undefined, undefined, vscode.ViewColumn.One) + + expect(execute).toHaveBeenCalledTimes(1) + const [command, uri, options] = execute.mock.calls[0]! + expect(command).toBe("vscode.open") + expect((uri as vscode.Uri).fsPath).toBe("/repo/assets/banner.png") + expect(options).toEqual({ viewColumn: vscode.ViewColumn.One, preview: true }) + }) + + it("preserves line and column navigation for text editors", () => { + openFileInEditor("/repo/src/app.ts", 7, 3, vscode.ViewColumn.One) + + const options = execute.mock.calls[0]?.[2] as vscode.TextDocumentShowOptions + expect(options.selection?.start.line).toBe(6) + expect(options.selection?.start.character).toBe(2) + }) +}) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx index ea9b09a4e3b..279265b660e 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx @@ -57,6 +57,7 @@ import { DiffEndMarker } from "../diff-viewer/DiffEndMarker" import { VirtualDiffList } from "../diff-viewer/VirtualDiffList" import { treeOrder } from "../diff-viewer/file-tree-utils" import { isMarkdownFile, MarkdownDiffView } from "../diff-viewer/MarkdownDiffView" +import { ImageDiffView } from "../diff-viewer/ImageDiffView" import { createDiffRows, diffToken } from "../diff-viewer/diff-state" // --- Data model --- @@ -247,6 +248,15 @@ export const DiffPanel: Component = (props) => { ), ) + const request = (diff: WorktreeFileDiff) => { + if (!props.onRequestDiff || props.loadingFiles?.has(diff.file)) return + if (!isDiffExpandable(diff) || diff.summarized !== true) return + const value = diffToken(diff) + if (requested.get(diff.file) === value) return + requested.set(diff.file, value) + props.onRequestDiff(diff.file) + } + createEffect( on( () => [open(), props.diffs] as const, @@ -255,15 +265,10 @@ export const DiffPanel: Component = (props) => { for (const file of requested.keys()) { if (!files.has(file)) requested.delete(file) } - if (!props.onRequestDiff) return for (const file of next) { - if (props.loadingFiles?.has(file)) continue const diff = props.diffs.find((item) => item.file === file) - if (!diff || !isDiffExpandable(diff) || diff.summarized !== true) continue - const value = diffToken(diff) - if (requested.get(file) === value) continue - requested.set(file, value) - props.onRequestDiff(file) + if (!diff || diff.kind === "image") continue + request(diff) } }, { defer: true }, @@ -559,6 +564,10 @@ export const DiffPanel: Component = (props) => { const isLoadingDetail = () => props.loadingFiles?.has(diff.file) ?? false const fileCommentCount = () => (commentsByFile().get(diff.file) ?? []).length + createEffect(() => { + if (diff.kind === "image" && open().includes(diff.file)) request(diff) + }) + return ( = (props) => { + + {t("agentManager.review.image")} + {t("agentManager.review.largeFileCollapsed")} @@ -681,36 +693,43 @@ export const DiffPanel: Component = (props) => { } > - before={{ name: diff.file, contents: diff.before }} - after={{ name: diff.file, contents: diff.after }} - patch={diff.patch} - diffStyle={props.diffStyle ?? "unified"} - virtualized={shouldVirtualizeDiff(diff)} - annotations={annotationsForFile(diff.file)} - renderAnnotation={buildAnnotation} - enableGutterUtility={true} - onGutterUtilityClick={(result) => handleGutterClick(diff.file, result)} - onLineNumberClick={(event) => { - if (event.annotationSide === "deletions") return - props.onOpenFile?.(diff.file, event.lineNumber) - }} - /> + + before={{ name: diff.file, contents: diff.before }} + after={{ name: diff.file, contents: diff.after }} + patch={diff.patch} + diffStyle={props.diffStyle ?? "unified"} + virtualized={shouldVirtualizeDiff(diff)} + annotations={annotationsForFile(diff.file)} + renderAnnotation={buildAnnotation} + enableGutterUtility={true} + onGutterUtilityClick={(result) => handleGutterClick(diff.file, result)} + onLineNumberClick={(event) => { + if (event.annotationSide === "deletions") return + props.onOpenFile?.(diff.file, event.lineNumber) + }} + /> + } + > + handleGutterClick(diff.file, result)} + onLineNumberClick={(event) => { + if (event.annotationSide === "deletions") return + props.onOpenFile?.(diff.file, event.lineNumber) + }} + /> + } > - handleGutterClick(diff.file, result)} - onLineNumberClick={(event) => { - if (event.annotationSide === "deletions") return - props.onOpenFile?.(diff.file, event.lineNumber) - }} - /> + diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index 0ef4932198c..fc9d3af1df9 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -1954,6 +1954,78 @@ body.am-wt-dragging-active * { gap: 8px; } +.am-image-diff { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr)); + gap: 1px; + border-top: 1px solid var(--border-weak-base); + background: var(--border-weak-base); + cursor: auto; +} + +.am-image-diff-pane { + min-width: 0; + background: var(--vscode-editor-background, var(--surface-base)); +} + +.am-image-diff-label { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + min-height: 32px; + padding: 0 12px; + border-bottom: 1px solid var(--border-weak-base); + color: var(--text-weak); + font-size: var(--font-size-small); + font-weight: 600; +} + +.am-image-diff-canvas { + display: flex; + align-items: center; + justify-content: center; + min-height: 180px; + padding: 20px; + background-color: var(--vscode-editor-background, var(--surface-base)); + background-image: conic-gradient( + from 90deg at 1px 1px, + color-mix(in oklab, var(--text-base) 5%, transparent) 25%, + transparent 0 + ); + background-size: 16px 16px; + overflow: auto; +} + +.am-image-diff-canvas img { + display: block; + max-width: 100%; + max-height: 440px; + object-fit: contain; + box-shadow: 0 8px 28px color-mix(in oklab, black 20%, transparent); +} + +.am-diff-panel .am-image-diff-canvas img { + max-height: 320px; +} + +.am-image-diff-message, +.am-image-diff-unavailable { + color: var(--text-weak); + font-size: var(--font-size-small); + text-align: center; +} + +.am-image-diff-unavailable { + display: flex; + align-items: center; + justify-content: center; + min-height: 120px; + padding: 20px; + border-top: 1px solid var(--border-weak-base); + background: var(--vscode-editor-background, var(--surface-base)); +} + .am-markdown-diff { display: grid; grid-template-columns: minmax(0, 1fr); diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts index cc94798638a..f2522ece3d8 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts @@ -187,6 +187,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} collapsed", "agentManager.review.collapsedWithLarge": "{{collapsed}} collapsed, {{large}} large", "agentManager.review.largeFileCollapsed": "Large file (collapsed)", + "agentManager.review.image": "image", + "agentManager.review.imageBefore": "Before", + "agentManager.review.imageAfter": "After", + "agentManager.review.imageTooLarge": "Image is too large to preview ({{size}}).", + "agentManager.review.imageUnreadable": "This image could not be rendered.", + "agentManager.review.imageUnavailable": "Image preview is unavailable for this session snapshot.", "agentManager.review.endOfLongDiff": "You made it to the end!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx index 5922c9080c3..34a3f7a31c1 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx +++ b/packages/kilo-vscode/webview-ui/diff-viewer/FullScreenDiffView.tsx @@ -58,6 +58,7 @@ import { import { DiffEndMarker } from "./DiffEndMarker" import { VirtualDiffList } from "./VirtualDiffList" import { isMarkdownFile, MarkdownDiffView } from "./MarkdownDiffView" +import { ImageDiffView } from "./ImageDiffView" import { createDiffRows, diffToken } from "./diff-state" type DiffStyle = "unified" | "split" @@ -253,6 +254,15 @@ export const FullScreenDiffView: Component = (props) => ), ) + const request = (diff: WorktreeFileDiff) => { + if (!props.onRequestDiff || props.loadingFiles?.has(diff.file)) return + if (!isDiffExpandable(diff) || diff.summarized !== true) return + const value = diffToken(diff) + if (requested.get(diff.file) === value) return + requested.set(diff.file, value) + props.onRequestDiff(diff.file) + } + createEffect( on( () => [open(), props.diffs] as const, @@ -261,15 +271,10 @@ export const FullScreenDiffView: Component = (props) => for (const file of requested.keys()) { if (!files.has(file)) requested.delete(file) } - if (!props.onRequestDiff) return for (const file of next) { - if (props.loadingFiles?.has(file)) continue const diff = props.diffs.find((item) => item.file === file) - if (!diff || !isDiffExpandable(diff) || diff.summarized !== true) continue - const value = diffToken(diff) - if (requested.get(file) === value) continue - requested.set(file, value) - props.onRequestDiff(file) + if (!diff || diff.kind === "image") continue + request(diff) } }, { defer: true }, @@ -634,6 +639,10 @@ export const FullScreenDiffView: Component = (props) => const isLoadingDetail = () => props.loadingFiles?.has(diff.file) ?? false const fileCommentCount = () => (commentsByFile().get(diff.file) ?? []).length + createEffect(() => { + if (diff.kind === "image" && open().includes(diff.file)) request(diff) + }) + return ( @@ -671,6 +680,9 @@ export const FullScreenDiffView: Component = (props) => + + {t("agentManager.review.image")} + {t("agentManager.review.largeFileCollapsed")} @@ -752,36 +764,43 @@ export const FullScreenDiffView: Component = (props) => } > - before={{ name: diff.file, contents: diff.before }} - after={{ name: diff.file, contents: diff.after }} - patch={diff.patch} - diffStyle={props.diffStyle} - virtualized={shouldVirtualizeDiff(diff)} - annotations={annotationsForFile(diff.file)} - renderAnnotation={buildAnnotation} - enableGutterUtility={props.canComment !== false} - onGutterUtilityClick={(result) => handleGutterClick(diff.file, result)} - onLineNumberClick={(event) => { - if (event.annotationSide === "deletions") return - props.onOpenFile?.(diff.file, event.lineNumber) - }} - /> + + before={{ name: diff.file, contents: diff.before }} + after={{ name: diff.file, contents: diff.after }} + patch={diff.patch} + diffStyle={props.diffStyle} + virtualized={shouldVirtualizeDiff(diff)} + annotations={annotationsForFile(diff.file)} + renderAnnotation={buildAnnotation} + enableGutterUtility={props.canComment !== false} + onGutterUtilityClick={(result) => handleGutterClick(diff.file, result)} + onLineNumberClick={(event) => { + if (event.annotationSide === "deletions") return + props.onOpenFile?.(diff.file, event.lineNumber) + }} + /> + } + > + handleGutterClick(diff.file, result)} + onLineNumberClick={(event) => { + if (event.annotationSide === "deletions") return + props.onOpenFile?.(diff.file, event.lineNumber) + }} + /> + } > - handleGutterClick(diff.file, result)} - onLineNumberClick={(event) => { - if (event.annotationSide === "deletions") return - props.onOpenFile?.(diff.file, event.lineNumber) - }} - /> + diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/ImageDiffView.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/ImageDiffView.tsx new file mode 100644 index 00000000000..4d4ae85a2cb --- /dev/null +++ b/packages/kilo-vscode/webview-ui/diff-viewer/ImageDiffView.tsx @@ -0,0 +1,80 @@ +import { type Component, createEffect, createSignal, Show } from "solid-js" +import type { DiffImageSide, WorktreeFileDiff } from "../src/types/messages" +import { useLanguage } from "../src/context/language" + +interface ImageDiffViewProps { + diff: WorktreeFileDiff +} + +interface ImagePaneProps { + file: string + label: string + image: DiffImageSide +} + +function size(bytes: number): string { + if (bytes < 1_000) return `${bytes} B` + if (bytes < 1_000_000) return `${(bytes / 1_000).toFixed(1)} KB` + return `${(bytes / 1_000_000).toFixed(1)} MB` +} + +const ImagePane: Component = (props) => { + const { t } = useLanguage() + const [failed, setFailed] = createSignal(false) + + createEffect(() => { + props.image.data + setFailed(false) + }) + + const message = () => { + if (props.image.error === "too-large") { + return t("agentManager.review.imageTooLarge", { size: size(props.image.bytes) }) + } + if (props.image.error === "unreadable" || failed()) return t("agentManager.review.imageUnreadable") + return undefined + } + + return ( +
+
+ {props.label} + {size(props.image.bytes)} +
+
+ {message()}}> + {`${props.file} setFailed(true)} + /> + +
+
+ ) +} + +export const ImageDiffView: Component = (props) => { + const { t } = useLanguage() + const image = () => props.diff.image + const available = () => Boolean(image()?.before || image()?.after) + + return ( + {t("agentManager.review.imageUnavailable")}} + > +
+ + {(side) => } + + + {(side) => } + +
+
+ ) +} 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 c59a22c78db..a0513512057 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 @@ -14,7 +14,7 @@ export function shouldVirtualizeDiff(diff: WorktreeFileDiff): boolean { } export function isDiffExpandable(diff: WorktreeFileDiff): boolean { - return diff.summarized === true || Boolean(diff.patch || diff.before || diff.after) + return diff.kind === "image" || diff.summarized === true || Boolean(diff.patch || diff.before || diff.after) } export function sanitizeOpenFiles(diffs: WorktreeFileDiff[], open: string[]): string[] { @@ -27,7 +27,7 @@ export function expandableOpenFiles(diffs: WorktreeFileDiff[]): string[] { } export function initialOpenFiles(diffs: WorktreeFileDiff[]): string[] { - return expandableOpenFiles(diffs) + return diffs.filter((diff) => diff.kind !== "image" && isDiffExpandable(diff)).map((diff) => diff.file) } export function allOpenFiles(diffs: WorktreeFileDiff[], open: string[]): boolean { diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/diff-state.ts b/packages/kilo-vscode/webview-ui/diff-viewer/diff-state.ts index 4d977e13ce0..32ebe6db77a 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/diff-state.ts +++ b/packages/kilo-vscode/webview-ui/diff-viewer/diff-state.ts @@ -10,7 +10,8 @@ export function sameDiffMeta(left: WorktreeFileDiff, right: WorktreeFileDiff) { left.tracked === right.tracked && left.generatedLike === right.generatedLike && left.summarized === right.summarized && - left.stamp === right.stamp + left.stamp === right.stamp && + left.kind === right.kind ) } @@ -79,6 +80,7 @@ export function mergeWorktreeDiffs(prev: WorktreeFileDiff[], next: WorktreeFileD existing.before === diff.before && existing.after === diff.after && existing.patch === diff.patch && + existing.image === diff.image && sameDiffMeta(existing, diff) ) return existing @@ -91,12 +93,14 @@ export function mergeWorktreeDiffs(prev: WorktreeFileDiff[], next: WorktreeFileD before: existing.before, after: existing.after, patch: existing.patch, + image: existing.image, summarized: false, } if ( existing.before === merged.before && existing.after === merged.after && existing.patch === merged.patch && + existing.image === merged.image && sameDiffMeta(existing, merged) ) return existing diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts b/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts index 0a28d60db91..553997c59ec 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/agent-manager.ts @@ -123,6 +123,20 @@ export interface ExternalWorktreeInfo { branch: string } +export type DiffImageError = "too-large" | "unreadable" + +export interface DiffImageSide { + mime: string + bytes: number + data?: string + error?: DiffImageError +} + +export interface DiffImage { + before?: DiffImageSide + after?: DiffImageSide +} + // Shared FileDiff shape (matches Snapshot.FileDiff from CLI backend) export interface WorktreeFileDiff { file: string @@ -137,6 +151,8 @@ export interface WorktreeFileDiff { generatedLike?: boolean summarized?: boolean stamp?: string + kind?: "image" + image?: DiffImage } export type AgentManagerApplyWorktreeDiffStatus = "checking" | "applying" | "success" | "conflict" | "error" From 148b324c9494949bd92234de836416a7809668ec Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Mon, 15 Jun 2026 16:08:50 +0200 Subject: [PATCH 2/3] fix(vscode): translate image diff labels --- packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts | 6 ++++++ packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts | 6 ++++++ packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts | 6 ++++++ packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts | 6 ++++++ packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts | 6 ++++++ packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts | 6 ++++++ packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts | 6 ++++++ packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts | 6 ++++++ packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts | 6 ++++++ packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts | 6 ++++++ packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts | 6 ++++++ packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts | 6 ++++++ packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts | 6 ++++++ packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts | 6 ++++++ packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts | 6 ++++++ packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts | 6 ++++++ packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts | 6 ++++++ packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts | 6 ++++++ packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts | 6 ++++++ 19 files changed, 114 insertions(+) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts index 22b81da3b39..82f7c1df3ae 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts @@ -180,6 +180,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} مطوي", "agentManager.review.collapsedWithLarge": "{{collapsed}} مطوي، {{large}} كبير", "agentManager.review.largeFileCollapsed": "ملف كبير (مطوي)", + "agentManager.review.image": "صورة", + "agentManager.review.imageBefore": "قبل", + "agentManager.review.imageAfter": "بعد", + "agentManager.review.imageTooLarge": "الصورة كبيرة جدًا بحيث يتعذر معاينتها ({{size}}).", + "agentManager.review.imageUnreadable": "تعذر عرض هذه الصورة.", + "agentManager.review.imageUnavailable": "معاينة الصورة غير متاحة في لقطة الجلسة هذه.", "agentManager.review.endOfLongDiff": "لقد وصلت إلى النهاية!", "agentManager.import.pullRequest": "طلب سحب", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts index aec59b1bcf0..0fc6e14e066 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts @@ -182,6 +182,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} recolhidos", "agentManager.review.collapsedWithLarge": "{{collapsed}} recolhidos, {{large}} grandes", "agentManager.review.largeFileCollapsed": "Arquivo grande (recolhido)", + "agentManager.review.image": "Imagem", + "agentManager.review.imageBefore": "Antes", + "agentManager.review.imageAfter": "Depois", + "agentManager.review.imageTooLarge": "A imagem é muito grande para ser visualizada ({{size}}).", + "agentManager.review.imageUnreadable": "Não foi possível renderizar esta imagem.", + "agentManager.review.imageUnavailable": "A visualização da imagem não está disponível para este instantâneo da sessão.", "agentManager.review.endOfLongDiff": "Você chegou ao fim!", "agentManager.import.pullRequest": "Solicitação de extração", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts index d3b3ca7953b..ddb68c76991 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts @@ -183,6 +183,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} sažeto", "agentManager.review.collapsedWithLarge": "{{collapsed}} sažeto, {{large}} velikih", "agentManager.review.largeFileCollapsed": "Velika datoteka (sažeto)", + "agentManager.review.image": "Slika", + "agentManager.review.imageBefore": "Prije", + "agentManager.review.imageAfter": "Poslije", + "agentManager.review.imageTooLarge": "Slika je prevelika za pregled ({{size}}).", + "agentManager.review.imageUnreadable": "Ova slika se nije mogla prikazati.", + "agentManager.review.imageUnavailable": "Pregled slike nije dostupan za ovaj snimak sesije.", "agentManager.review.endOfLongDiff": "Došli ste do kraja!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts index 194ad09be4a..5089b29a5c2 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts @@ -182,6 +182,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} foldet sammen", "agentManager.review.collapsedWithLarge": "{{collapsed}} foldet sammen, {{large}} store", "agentManager.review.largeFileCollapsed": "Stor fil (sammenklappet)", + "agentManager.review.image": "Billede", + "agentManager.review.imageBefore": "Før", + "agentManager.review.imageAfter": "Efter", + "agentManager.review.imageTooLarge": "Billedet er for stort til at kunne forhåndsvises ({{size}}).", + "agentManager.review.imageUnreadable": "Dette billede kunne ikke gengives.", + "agentManager.review.imageUnavailable": "Forhåndsvisning af billedet er ikke tilgængelig for dette snapshot af sessionen.", "agentManager.review.endOfLongDiff": "Du nåede slutningen!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts index f6643237ba9..bf022edb96c 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts @@ -183,6 +183,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} eingeklappt", "agentManager.review.collapsedWithLarge": "{{collapsed}} eingeklappt, {{large}} groß", "agentManager.review.largeFileCollapsed": "Große Datei (eingeklappt)", + "agentManager.review.image": "Bild", + "agentManager.review.imageBefore": "Vorher", + "agentManager.review.imageAfter": "Nachher", + "agentManager.review.imageTooLarge": "Das Bild ist zu groß für eine Vorschau ({{size}}).", + "agentManager.review.imageUnreadable": "Dieses Bild konnte nicht dargestellt werden.", + "agentManager.review.imageUnavailable": "Für diesen Sitzungssnapshot ist keine Bildvorschau verfügbar.", "agentManager.review.endOfLongDiff": "Du hast das Ende erreicht!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts index bfddc7036fd..dc07e16d7bb 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts @@ -182,6 +182,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} contraídos", "agentManager.review.collapsedWithLarge": "{{collapsed}} contraídos, {{large}} grandes", "agentManager.review.largeFileCollapsed": "Archivo grande (contraído)", + "agentManager.review.image": "Imagen", + "agentManager.review.imageBefore": "Antes", + "agentManager.review.imageAfter": "Después", + "agentManager.review.imageTooLarge": "La imagen es demasiado grande para previsualizarla ({{size}}).", + "agentManager.review.imageUnreadable": "No se pudo renderizar esta imagen.", + "agentManager.review.imageUnavailable": "La vista previa de la imagen no está disponible para esta instantánea de la sesión.", "agentManager.review.endOfLongDiff": "¡Llegaste al final!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts index 435308590a4..8052baa2194 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts @@ -183,6 +183,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} repliés", "agentManager.review.collapsedWithLarge": "{{collapsed}} repliés, {{large}} volumineux", "agentManager.review.largeFileCollapsed": "Fichier volumineux (replié)", + "agentManager.review.image": "Image", + "agentManager.review.imageBefore": "Avant", + "agentManager.review.imageAfter": "Après", + "agentManager.review.imageTooLarge": "L’image est trop volumineuse pour être prévisualisée ({{size}}).", + "agentManager.review.imageUnreadable": "Cette image n’a pas pu être affichée.", + "agentManager.review.imageUnavailable": "L’aperçu de l’image n’est pas disponible pour cet instantané de session.", "agentManager.review.endOfLongDiff": "Vous êtes arrivé à la fin !", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts index 24e5636bb09..e877f1d560d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts @@ -188,6 +188,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} compressi", "agentManager.review.collapsedWithLarge": "{{collapsed}} compressi, {{large}} grandi", "agentManager.review.largeFileCollapsed": "File grande (compresso)", + "agentManager.review.image": "Immagine", + "agentManager.review.imageBefore": "Prima", + "agentManager.review.imageAfter": "Dopo", + "agentManager.review.imageTooLarge": "L'immagine è troppo grande per visualizzare l'anteprima ({{size}}).", + "agentManager.review.imageUnreadable": "Non è stato possibile visualizzare questa immagine.", + "agentManager.review.imageUnavailable": "L'anteprima dell'immagine non è disponibile per questa istantanea della sessione.", "agentManager.review.endOfLongDiff": "Sei arrivato alla fine!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts index 4d37c64e2e3..2935cc9b979 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts @@ -182,6 +182,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} 件折りたたみ", "agentManager.review.collapsedWithLarge": "{{collapsed}} 件折りたたみ、{{large}} 件がサイズ大", "agentManager.review.largeFileCollapsed": "大きなファイル(折りたたみ)", + "agentManager.review.image": "画像", + "agentManager.review.imageBefore": "変更前", + "agentManager.review.imageAfter": "変更後", + "agentManager.review.imageTooLarge": "画像が大きすぎるためプレビューできません({{size}})。", + "agentManager.review.imageUnreadable": "この画像を表示できませんでした。", + "agentManager.review.imageUnavailable": "このセッションスナップショットでは画像プレビューを利用できません。", "agentManager.review.endOfLongDiff": "最後まで到達しました!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts index f7d7e9a2d92..a2764c6f16d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts @@ -182,6 +182,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}}개 접힘", "agentManager.review.collapsedWithLarge": "{{collapsed}}개 접힘, {{large}}개 대용량", "agentManager.review.largeFileCollapsed": "큰 파일(접힘)", + "agentManager.review.image": "이미지", + "agentManager.review.imageBefore": "변경 전", + "agentManager.review.imageAfter": "변경 후", + "agentManager.review.imageTooLarge": "이미지가 너무 커서 미리 볼 수 없습니다({{size}}).", + "agentManager.review.imageUnreadable": "이 이미지를 렌더링할 수 없습니다.", + "agentManager.review.imageUnavailable": "이 세션 스냅샷에서는 이미지 미리보기를 사용할 수 없습니다.", "agentManager.review.endOfLongDiff": "끝까지 도달했습니다!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts index 713c7349005..9dffdf4d70a 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts @@ -188,6 +188,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} ingeklapt", "agentManager.review.collapsedWithLarge": "{{collapsed}} ingeklapt, {{large}} groot", "agentManager.review.largeFileCollapsed": "Groot bestand (ingeklapt)", + "agentManager.review.image": "Afbeelding", + "agentManager.review.imageBefore": "Voor", + "agentManager.review.imageAfter": "Na", + "agentManager.review.imageTooLarge": "Afbeelding is te groot voor een voorbeeldweergave ({{size}}).", + "agentManager.review.imageUnreadable": "Deze afbeelding kon niet worden weergegeven.", + "agentManager.review.imageUnavailable": "Afbeeldingsvoorbeeld is niet beschikbaar voor deze sessiesnapshot.", "agentManager.review.endOfLongDiff": "Je hebt het einde bereikt!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts index a412c141caa..be07a49aa25 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts @@ -182,6 +182,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} kollapset", "agentManager.review.collapsedWithLarge": "{{collapsed}} kollapset, {{large}} store", "agentManager.review.largeFileCollapsed": "Stor fil (sammenfoldet)", + "agentManager.review.image": "Bilde", + "agentManager.review.imageBefore": "Før", + "agentManager.review.imageAfter": "Etter", + "agentManager.review.imageTooLarge": "Bildet er for stort til å forhåndsvises ({{size}}).", + "agentManager.review.imageUnreadable": "Dette bildet kunne ikke gjengis.", + "agentManager.review.imageUnavailable": "Forhåndsvisning av bildet er ikke tilgjengelig for dette øyeblikksbildet av økten.", "agentManager.review.endOfLongDiff": "Du nådde slutten!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts index f985f44198b..2eb0f92c84d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts @@ -181,6 +181,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} zwiniętych", "agentManager.review.collapsedWithLarge": "{{collapsed}} zwiniętych, {{large}} dużych", "agentManager.review.largeFileCollapsed": "Duży plik (zwinięty)", + "agentManager.review.image": "Obraz", + "agentManager.review.imageBefore": "Przed", + "agentManager.review.imageAfter": "Po", + "agentManager.review.imageTooLarge": "Obraz jest zbyt duży, aby wyświetlić podgląd ({{size}}).", + "agentManager.review.imageUnreadable": "Nie można wyświetlić tego obrazu.", + "agentManager.review.imageUnavailable": "Podgląd obrazu jest niedostępny dla tej migawki sesji.", "agentManager.review.endOfLongDiff": "Dotarłeś do końca!", "agentManager.import.pullRequest": "Prośba o scalenie", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts index 4b5a8c9ee37..b3d604a8c7f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts @@ -182,6 +182,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} свернуто", "agentManager.review.collapsedWithLarge": "{{collapsed}} свернуто, {{large}} больших", "agentManager.review.largeFileCollapsed": "Большой файл (свернут)", + "agentManager.review.image": "Изображение", + "agentManager.review.imageBefore": "До", + "agentManager.review.imageAfter": "После", + "agentManager.review.imageTooLarge": "Изображение слишком большое для предварительного просмотра ({{size}}).", + "agentManager.review.imageUnreadable": "Не удалось отобразить это изображение.", + "agentManager.review.imageUnavailable": "Предварительный просмотр изображения недоступен для этого снимка сеанса.", "agentManager.review.endOfLongDiff": "Вы дошли до конца!", "agentManager.import.pullRequest": "Запрос на слияние", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts index 26e20065593..a00249118a6 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts @@ -178,6 +178,12 @@ export const dict = { "agentManager.review.collapsedOnly": "ยุบ {{count}} รายการ", "agentManager.review.collapsedWithLarge": "ยุบ {{collapsed}} รายการ, ขนาดใหญ่ {{large}} รายการ", "agentManager.review.largeFileCollapsed": "ไฟล์ขนาดใหญ่ (พับอยู่)", + "agentManager.review.image": "รูปภาพ", + "agentManager.review.imageBefore": "ก่อน", + "agentManager.review.imageAfter": "หลัง", + "agentManager.review.imageTooLarge": "รูปภาพมีขนาดใหญ่เกินกว่าจะแสดงตัวอย่าง ({{size}})", + "agentManager.review.imageUnreadable": "ไม่สามารถแสดงผลรูปภาพนี้ได้", + "agentManager.review.imageUnavailable": "ไม่สามารถแสดงตัวอย่างรูปภาพสำหรับสแนปช็อตของเซสชันนี้ได้", "agentManager.review.endOfLongDiff": "คุณมาถึงท้ายสุดแล้ว!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts index 4ac533f5f4b..1d4951aa42b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts @@ -189,6 +189,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} daraltıldı", "agentManager.review.collapsedWithLarge": "{{collapsed}} daraltıldı, {{large}} büyük", "agentManager.review.largeFileCollapsed": "Büyük dosya (daraltıldı)", + "agentManager.review.image": "Görsel", + "agentManager.review.imageBefore": "Önce", + "agentManager.review.imageAfter": "Sonra", + "agentManager.review.imageTooLarge": "Görsel önizleme için çok büyük ({{size}}).", + "agentManager.review.imageUnreadable": "Bu görsel görüntülenemedi.", + "agentManager.review.imageUnavailable": "Bu oturum anlık görüntüsü için görsel önizlemesi kullanılamıyor.", "agentManager.review.endOfLongDiff": "Sonuna geldiniz!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts index f34fcafe5ce..96cb6a5866f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts @@ -190,6 +190,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} згорнуто", "agentManager.review.collapsedWithLarge": "{{collapsed}} згорнуто, {{large}} великих", "agentManager.review.largeFileCollapsed": "Великий файл (згорнуто)", + "agentManager.review.image": "Зображення", + "agentManager.review.imageBefore": "До", + "agentManager.review.imageAfter": "Після", + "agentManager.review.imageTooLarge": "Зображення завелике для попереднього перегляду ({{size}}).", + "agentManager.review.imageUnreadable": "Не вдалося відобразити це зображення.", + "agentManager.review.imageUnavailable": "Попередній перегляд зображення недоступний для цього знімка сеансу.", "agentManager.review.endOfLongDiff": "Ви дійшли до кінця!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts index fc4460778d5..d64ffdf2c33 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts @@ -177,6 +177,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} 个已折叠", "agentManager.review.collapsedWithLarge": "{{collapsed}} 个已折叠,{{large}} 个过大", "agentManager.review.largeFileCollapsed": "大文件(已折叠)", + "agentManager.review.image": "图片", + "agentManager.review.imageBefore": "之前", + "agentManager.review.imageAfter": "之后", + "agentManager.review.imageTooLarge": "图片过大,无法预览({{size}})。", + "agentManager.review.imageUnreadable": "无法渲染此图片。", + "agentManager.review.imageUnavailable": "此会话快照无法提供图片预览。", "agentManager.review.endOfLongDiff": "你已经到末尾了!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts index 96e306580ae..3d3170591d3 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts @@ -177,6 +177,12 @@ export const dict = { "agentManager.review.collapsedOnly": "{{count}} 個已摺疊", "agentManager.review.collapsedWithLarge": "{{collapsed}} 個已摺疊,{{large}} 個過大", "agentManager.review.largeFileCollapsed": "大型檔案(已摺疊)", + "agentManager.review.image": "圖片", + "agentManager.review.imageBefore": "變更前", + "agentManager.review.imageAfter": "變更後", + "agentManager.review.imageTooLarge": "圖片太大,無法預覽({{size}})。", + "agentManager.review.imageUnreadable": "無法呈現此圖片。", + "agentManager.review.imageUnavailable": "此工作階段快照無法預覽圖片。", "agentManager.review.endOfLongDiff": "你已經到最後了!", "agentManager.import.pullRequest": "Pull Request", From a081fe37f928f624e91e806a8ce7ab622711a8b7 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Mon, 15 Jun 2026 16:20:03 +0200 Subject: [PATCH 3/3] style(vscode): format image diff translations --- packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts | 3 ++- packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts | 3 ++- packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts | 3 ++- packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts | 3 ++- packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts | 3 ++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts index 0fc6e14e066..b74ac0eab4a 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts @@ -187,7 +187,8 @@ export const dict = { "agentManager.review.imageAfter": "Depois", "agentManager.review.imageTooLarge": "A imagem é muito grande para ser visualizada ({{size}}).", "agentManager.review.imageUnreadable": "Não foi possível renderizar esta imagem.", - "agentManager.review.imageUnavailable": "A visualização da imagem não está disponível para este instantâneo da sessão.", + "agentManager.review.imageUnavailable": + "A visualização da imagem não está disponível para este instantâneo da sessão.", "agentManager.review.endOfLongDiff": "Você chegou ao fim!", "agentManager.import.pullRequest": "Solicitação de extração", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts index 5089b29a5c2..aeae9335d86 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts @@ -187,7 +187,8 @@ export const dict = { "agentManager.review.imageAfter": "Efter", "agentManager.review.imageTooLarge": "Billedet er for stort til at kunne forhåndsvises ({{size}}).", "agentManager.review.imageUnreadable": "Dette billede kunne ikke gengives.", - "agentManager.review.imageUnavailable": "Forhåndsvisning af billedet er ikke tilgængelig for dette snapshot af sessionen.", + "agentManager.review.imageUnavailable": + "Forhåndsvisning af billedet er ikke tilgængelig for dette snapshot af sessionen.", "agentManager.review.endOfLongDiff": "Du nåede slutningen!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts index dc07e16d7bb..d9ef8188815 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts @@ -187,7 +187,8 @@ export const dict = { "agentManager.review.imageAfter": "Después", "agentManager.review.imageTooLarge": "La imagen es demasiado grande para previsualizarla ({{size}}).", "agentManager.review.imageUnreadable": "No se pudo renderizar esta imagen.", - "agentManager.review.imageUnavailable": "La vista previa de la imagen no está disponible para esta instantánea de la sesión.", + "agentManager.review.imageUnavailable": + "La vista previa de la imagen no está disponible para esta instantánea de la sesión.", "agentManager.review.endOfLongDiff": "¡Llegaste al final!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts index e877f1d560d..d1162022595 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/it.ts @@ -193,7 +193,8 @@ export const dict = { "agentManager.review.imageAfter": "Dopo", "agentManager.review.imageTooLarge": "L'immagine è troppo grande per visualizzare l'anteprima ({{size}}).", "agentManager.review.imageUnreadable": "Non è stato possibile visualizzare questa immagine.", - "agentManager.review.imageUnavailable": "L'anteprima dell'immagine non è disponibile per questa istantanea della sessione.", + "agentManager.review.imageUnavailable": + "L'anteprima dell'immagine non è disponibile per questa istantanea della sessione.", "agentManager.review.endOfLongDiff": "Sei arrivato alla fine!", "agentManager.import.pullRequest": "Pull Request", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts index be07a49aa25..f9a81430a4b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts @@ -187,7 +187,8 @@ export const dict = { "agentManager.review.imageAfter": "Etter", "agentManager.review.imageTooLarge": "Bildet er for stort til å forhåndsvises ({{size}}).", "agentManager.review.imageUnreadable": "Dette bildet kunne ikke gjengis.", - "agentManager.review.imageUnavailable": "Forhåndsvisning av bildet er ikke tilgjengelig for dette øyeblikksbildet av økten.", + "agentManager.review.imageUnavailable": + "Forhåndsvisning av bildet er ikke tilgjengelig for dette øyeblikksbildet av økten.", "agentManager.review.endOfLongDiff": "Du nådde slutten!", "agentManager.import.pullRequest": "Pull Request",