From 346387e276944df04662945c34cf6780a39e05ff Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Mon, 16 Mar 2026 10:51:53 +0200 Subject: [PATCH 01/74] feat: add process tree utility for memory diagnostics Add ProcessInfo module that collects PID, command, RSS memory, and V8 heap stats for the current process and all descendant child processes. Uses ps and pgrep to walk the process tree on macOS/Linux. --- .../opencode/src/kilocode/process-info.ts | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 packages/opencode/src/kilocode/process-info.ts diff --git a/packages/opencode/src/kilocode/process-info.ts b/packages/opencode/src/kilocode/process-info.ts new file mode 100644 index 00000000000..63a6a275b56 --- /dev/null +++ b/packages/opencode/src/kilocode/process-info.ts @@ -0,0 +1,135 @@ +// kilocode_change - new file +import z from "zod" +import { Log } from "../util/log" + +export namespace ProcessInfo { + const log = Log.create({ service: "process-info" }) + + export const Info = z + .object({ + pid: z.number(), + ppid: z.number(), + rss: z.number(), + command: z.string(), + args: z.string(), + }) + .meta({ + ref: "ProcessInfo", + }) + export type Info = z.infer + + export const Status = z + .object({ + self: Info, + heap: z.object({ + used: z.number(), + total: z.number(), + external: z.number(), + buffers: z.number(), + }), + children: z.array(Info), + }) + .meta({ + ref: "ProcessStatus", + }) + export type Status = z.infer + + function format(bytes: number): string { + if (bytes >= 1024 * 1024 * 1024) return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB" + if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + " MB" + if (bytes >= 1024) return (bytes / 1024).toFixed(0) + " KB" + return bytes + " B" + } + + export function pretty(info: Info): string { + return `PID ${info.pid} | ${format(info.rss)} | ${info.command} ${info.args}`.trim() + } + + export function formatBytes(bytes: number): string { + return format(bytes) + } + + async function descendants(pid: number): Promise { + if (process.platform === "win32") return [] + const pids: number[] = [] + const queue = [pid] + while (queue.length > 0) { + const current = queue.shift()! + const proc = Bun.spawn(["pgrep", "-P", String(current)], { stdout: "pipe", stderr: "pipe" }) + const [code, out] = await Promise.all([proc.exited, new Response(proc.stdout).text()]).catch( + () => [-1, ""] as const, + ) + if (code !== 0) continue + for (const tok of out.trim().split(/\s+/)) { + const cpid = parseInt(tok, 10) + if (!isNaN(cpid) && pids.indexOf(cpid) === -1) { + pids.push(cpid) + queue.push(cpid) + } + } + } + return pids + } + + async function info(pid: number): Promise { + if (process.platform === "win32") return undefined + const proc = Bun.spawn(["ps", "-p", String(pid), "-o", "pid=,ppid=,rss=,comm=,args="], { + stdout: "pipe", + stderr: "pipe", + }) + const [code, out] = await Promise.all([proc.exited, new Response(proc.stdout).text()]).catch( + () => [-1, ""] as const, + ) + if (code !== 0) return undefined + const line = out.trim() + if (!line) return undefined + // ps output: PID PPID RSS COMM ARGS + // RSS is in KB from ps + const parts = line.trim().split(/\s+/) + if (parts.length < 4) return undefined + const parsed = { + pid: parseInt(parts[0], 10), + ppid: parseInt(parts[1], 10), + rss: parseInt(parts[2], 10) * 1024, // convert KB to bytes + command: parts[3], + args: parts.slice(4).join(" "), + } + if (isNaN(parsed.pid)) return undefined + return parsed + } + + export async function status(): Promise { + const pid = process.pid + const mem = process.memoryUsage() + + const self = (await info(pid)) ?? { + pid, + ppid: process.ppid, + rss: mem.rss, + command: process.argv0, + args: process.argv.slice(1).join(" "), + } + + const cpids = await descendants(pid) + const results = await Promise.all(cpids.map((p) => info(p).catch(() => undefined))) + const children = results.filter((x): x is Info => x !== undefined) + + log.info("process status", { + self: self.pid, + rss: format(self.rss), + heap: format(mem.heapUsed), + children: children.length, + }) + + return { + self, + heap: { + used: mem.heapUsed, + total: mem.heapTotal, + external: mem.external, + buffers: mem.arrayBuffers, + }, + children, + } + } +} From dff17ba39d6a32ff09e3a9a257e6254ffeab6af4 Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Mon, 16 Mar 2026 10:51:58 +0200 Subject: [PATCH 02/74] feat: add GET /process server route for process status Expose process tree and memory info via a new API endpoint that returns the current process stats, V8 heap breakdown, and all child processes with their RSS memory usage. --- packages/opencode/src/server/server.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index d37112823a1..63a9df32072 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -11,6 +11,7 @@ import z from "zod" import { Provider } from "../provider/provider" import { NamedError } from "@opencode-ai/util/error" import { LSP } from "../lsp" +import { ProcessInfo } from "../kilocode/process-info" // kilocode_change import { Format } from "../format" import { TuiRoutes } from "./routes/tui" import { Instance } from "../project/instance" @@ -521,6 +522,29 @@ export namespace Server { return c.json(await LSP.status()) }, ) + // kilocode_change start - process diagnostics route + .get( + "/process", + describeRoute({ + summary: "Get process status", + description: "Get process tree with memory usage for all related processes", + operationId: "process.status", + responses: { + 200: { + description: "Process status", + content: { + "application/json": { + schema: resolver(ProcessInfo.Status), + }, + }, + }, + }, + }), + async (c) => { + return c.json(await ProcessInfo.status()) + }, + ) + // kilocode_change end .get( "/formatter", describeRoute({ From 5de611e354297006d363714b47ba7ad5ecfe6f9b Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Mon, 16 Mar 2026 10:52:03 +0200 Subject: [PATCH 03/74] chore(sdk): regenerate SDK with ProcessInfo and ProcessStatus types Auto-generated from the new GET /process endpoint. Adds ProcessInfo, ProcessStatus types and sdk.client.process.status() method. --- packages/sdk/js/src/v2/gen/sdk.gen.ts | 38 ++++++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 38 ++++++++++ packages/sdk/openapi.json | 95 +++++++++++++++++++++++++ 3 files changed, 171 insertions(+) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 9104c813c2e..c791ad7b5ea 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -96,6 +96,7 @@ import type { PermissionRuleset, PermissionSavePatternRulesErrors, PermissionSavePatternRulesResponses, + ProcessStatusResponses, ProjectCurrentResponses, ProjectListResponses, ProjectUpdateErrors, @@ -4393,6 +4394,38 @@ export class Lsp extends HeyApiClient { } } +export class Process extends HeyApiClient { + /** + * Get process status + * + * Get process tree with memory usage for all related processes + */ + public status( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/process", + ...options, + ...params, + }) + } +} + export class Formatter extends HeyApiClient { /** * Get formatter status @@ -4600,6 +4633,11 @@ export class KiloClient extends HeyApiClient { return (this._lsp ??= new Lsp({ client: this.client })) } + private _process?: Process + get process(): Process { + return (this._process ??= new Process({ client: this.client })) + } + private _formatter?: Formatter get formatter(): Formatter { return (this._formatter ??= new Formatter({ client: this.client })) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 5c724d3621f..01ea044c806 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1947,6 +1947,25 @@ export type LspStatus = { status: "connected" | "error" } +export type ProcessInfo = { + pid: number + ppid: number + rss: number + command: string + args: string +} + +export type ProcessStatus = { + self: ProcessInfo + heap: { + used: number + total: number + external: number + buffers: number + } + children: Array +} + export type FormatterStatus = { name: string extensions: Array @@ -5526,6 +5545,25 @@ export type LspStatusResponses = { export type LspStatusResponse = LspStatusResponses[keyof LspStatusResponses] +export type ProcessStatusData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/process" +} + +export type ProcessStatusResponses = { + /** + * Process status + */ + 200: ProcessStatus +} + +export type ProcessStatusResponse = ProcessStatusResponses[keyof ProcessStatusResponses] + export type FormatterStatusData = { body?: never path?: never diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 45dd0752210..96e355f319c 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -7981,6 +7981,47 @@ ] } }, + "/process": { + "get": { + "operationId": "process.status", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "workspace", + "schema": { + "type": "string" + } + } + ], + "summary": "Get process status", + "description": "Get process tree with memory usage for all related processes", + "responses": { + "200": { + "description": "Process status", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProcessStatus" + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\n\nconst client = createKiloClient()\nawait client.process.status({\n ...\n})" + } + ] + } + }, "/formatter": { "get": { "operationId": "formatter.status", @@ -13176,6 +13217,60 @@ }, "required": ["id", "name", "root", "status"] }, + "ProcessInfo": { + "type": "object", + "properties": { + "pid": { + "type": "number" + }, + "ppid": { + "type": "number" + }, + "rss": { + "type": "number" + }, + "command": { + "type": "string" + }, + "args": { + "type": "string" + } + }, + "required": ["pid", "ppid", "rss", "command", "args"] + }, + "ProcessStatus": { + "type": "object", + "properties": { + "self": { + "$ref": "#/components/schemas/ProcessInfo" + }, + "heap": { + "type": "object", + "properties": { + "used": { + "type": "number" + }, + "total": { + "type": "number" + }, + "external": { + "type": "number" + }, + "buffers": { + "type": "number" + } + }, + "required": ["used", "total", "external", "buffers"] + }, + "children": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProcessInfo" + } + } + }, + "required": ["self", "heap", "children"] + }, "FormatterStatus": { "type": "object", "properties": { From bde1f0cadf35afe44ca7bf7c68d7aedccf31d571 Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Mon, 16 Mar 2026 10:52:11 +0200 Subject: [PATCH 04/74] feat: display process tree with memory usage in TUI status dialog Show all related processes (self + children) with PID, command name, and RSS memory in the /status dialog. Highlights processes exceeding 512 MB in warning color. Auto-refreshes every 3 seconds while the dialog is open to track memory changes in real time. --- .../cli/cmd/tui/component/dialog-status.tsx | 82 ++++++++++++++++++- .../opencode/src/cli/cmd/tui/context/sync.tsx | 4 + 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx index 6c67536598f..2e3a6e0afab 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx @@ -3,15 +3,51 @@ import { fileURLToPath } from "bun" import { useTheme } from "../context/theme" import { useDialog } from "@tui/ui/dialog" import { useSync } from "@tui/context/sync" -import { For, Match, Switch, Show, createMemo } from "solid-js" +import { useSDK } from "@tui/context/sdk" // kilocode_change +import { For, Match, Switch, Show, createMemo, onMount, onCleanup } from "solid-js" import { Installation } from "../../../../installation" +import type { ProcessInfo } from "@kilocode/sdk/v2" // kilocode_change export type DialogStatusProps = {} +// kilocode_change start - format bytes for process memory display +function formatBytes(bytes: number): string { + if (bytes >= 1024 * 1024 * 1024) return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB" + if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + " MB" + if (bytes >= 1024) return (bytes / 1024).toFixed(0) + " KB" + return bytes + " B" +} + +function shortCommand(info: ProcessInfo): string { + const cmd = info.command.split("/").pop() ?? info.command + const args = info.args + if (!args || args === info.command) return cmd + // Show first meaningful arg (e.g. "typescript-language-server" from "bun x typescript-language-server --stdio") + const parts = args.split(/\s+/) + const interesting = parts.find( + (p) => !p.startsWith("-") && !p.startsWith("/") && p !== cmd && p !== "x" && p !== "run", + ) + return interesting ? `${cmd} → ${interesting}` : cmd +} +// kilocode_change end + export function DialogStatus() { const sync = useSync() const { theme } = useTheme() const dialog = useDialog() + // kilocode_change start - refresh process data while dialog is open + const sdk = useSDK() + const refresh = () => { + sdk.client.process.status().then((x) => { + if (x.data) sync.set("processes", x.data) + }) + } + onMount(() => { + refresh() + const timer = setInterval(refresh, 3000) + onCleanup(() => clearInterval(timer)) + }) + // kilocode_change end const enabledFormatters = createMemo(() => sync.data.formatter.filter((f) => f.enabled)) @@ -53,6 +89,50 @@ export function DialogStatus() { {/* kilocode_change start */} Kilo v{Installation.VERSION} {/* kilocode_change end */} + {/* kilocode_change start - process memory diagnostics */} + + {(procs) => { + const total = createMemo(() => { + const all = [procs().self, ...procs().children] + return all.reduce((sum, p) => sum + p.rss, 0) + }) + return ( + + + Processes ({1 + procs().children.length}) — {formatBytes(total())} total RSS + + + + • + + + self{" "} + + PID {procs().self.pid} | {formatBytes(procs().self.rss)} RSS | heap {formatBytes(procs().heap.used)} + /{formatBytes(procs().heap.total)} + + + + + {(child) => ( + + 512 * 1024 * 1024 ? theme.warning : theme.success }}> + • + + + {shortCommand(child)}{" "} + + PID {child.pid} | {formatBytes(child.rss)} + + + + )} + + + ) + }} + + {/* kilocode_change end */} 0} fallback={No MCP Servers}> {Object.keys(sync.data.mcp).length} MCP Servers diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 87cf87719c6..fe478ad4360 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -17,6 +17,7 @@ import type { ProviderListResponse, ProviderAuthMethod, VcsInfo, + ProcessStatus, // kilocode_change } from "@kilocode/sdk/v2" import { createStore, produce, reconcile } from "solid-js/store" import { useSDK } from "@tui/context/sdk" @@ -71,6 +72,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ [key: string]: McpResource } formatter: FormatterStatus[] + processes: ProcessStatus | undefined // kilocode_change vcs: VcsInfo | undefined path: Path }>({ @@ -98,6 +100,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ mcp: {}, mcp_resource: {}, formatter: [], + processes: undefined, // kilocode_change vcs: undefined, path: { state: "", config: "", worktree: "", directory: "" }, }) @@ -407,6 +410,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ sdk.client.mcp.status().then((x) => setStore("mcp", reconcile(x.data!))), sdk.client.experimental.resource.list().then((x) => setStore("mcp_resource", reconcile(x.data ?? {}))), sdk.client.formatter.status().then((x) => setStore("formatter", reconcile(x.data!))), + sdk.client.process.status().then((x) => setStore("processes", reconcile(x.data!))), // kilocode_change sdk.client.session.status().then((x) => { setStore("session_status", reconcile(x.data!)) }), From 03afafbdc33de71d4834f12542754be81a2cce41 Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Mon, 16 Mar 2026 11:02:44 +0200 Subject: [PATCH 05/74] fix: use ucomm for reliable ps parsing and extract script names from args MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On macOS, ps -o comm= is an alias for command (full path + args), which broke whitespace-based column parsing, producing garbled labels like '.nvm → en'. Switch to ucomm which returns just the executable basename. Also improve shortCommand to extract .js/.ts script basenames from path arguments, so node processes show useful labels like 'node → tsserver' instead of just 'node'. --- .../cli/cmd/tui/component/dialog-status.tsx | 30 +++++++++++++++++-- .../opencode/src/kilocode/process-info.ts | 8 +++-- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx index 2e3a6e0afab..d77a91f8076 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx @@ -22,12 +22,38 @@ function shortCommand(info: ProcessInfo): string { const cmd = info.command.split("/").pop() ?? info.command const args = info.args if (!args || args === info.command) return cmd - // Show first meaningful arg (e.g. "typescript-language-server" from "bun x typescript-language-server --stdio") const parts = args.split(/\s+/) + + // For node/bun: find the script being run (.js/.ts/.mjs/.cjs file) + if (cmd === "node" || cmd === "bun") { + const script = parts.find((p) => /\.(js|ts|mjs|cjs)(['"]?)$/.test(p)) + if (script) { + const name = script + .split("/") + .pop()! + .replace(/\.(js|ts|mjs|cjs)$/, "") + return `${cmd} → ${name}` + } + } + + // Find first non-flag, non-path, non-trivial arg const interesting = parts.find( (p) => !p.startsWith("-") && !p.startsWith("/") && p !== cmd && p !== "x" && p !== "run", ) - return interesting ? `${cmd} → ${interesting}` : cmd + if (interesting) return `${cmd} → ${interesting}` + + // Last resort: extract basename from first path arg that isn't the executable itself + const exe = parts[0] + const path = parts.find((p) => p.startsWith("/") && p !== exe) + if (path) { + const base = path + .split("/") + .pop()! + .replace(/\.(js|ts|mjs|cjs)$/, "") + return `${cmd} → ${base}` + } + + return cmd } // kilocode_change end diff --git a/packages/opencode/src/kilocode/process-info.ts b/packages/opencode/src/kilocode/process-info.ts index 63a6a275b56..96ba5712843 100644 --- a/packages/opencode/src/kilocode/process-info.ts +++ b/packages/opencode/src/kilocode/process-info.ts @@ -73,7 +73,9 @@ export namespace ProcessInfo { async function info(pid: number): Promise { if (process.platform === "win32") return undefined - const proc = Bun.spawn(["ps", "-p", String(pid), "-o", "pid=,ppid=,rss=,comm=,args="], { + // ucomm gives the short executable name (e.g. "node", "bun") + // comm on macOS is an alias for command (full path + args) which breaks whitespace parsing + const proc = Bun.spawn(["ps", "-p", String(pid), "-o", "pid=,ppid=,rss=,ucomm=,args="], { stdout: "pipe", stderr: "pipe", }) @@ -83,8 +85,8 @@ export namespace ProcessInfo { if (code !== 0) return undefined const line = out.trim() if (!line) return undefined - // ps output: PID PPID RSS COMM ARGS - // RSS is in KB from ps + // ps output: PID PPID RSS UCOMM ARGS... + // RSS is in KB from ps, ucomm is a single word (executable basename) const parts = line.trim().split(/\s+/) if (parts.length < 4) return undefined const parsed = { From 7888b6f83d392323865fe5a5ee497cad21bd9711 Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Mon, 16 Mar 2026 11:09:46 +0200 Subject: [PATCH 06/74] fix: cap tsserver memory at 1.5 GB and disable automatic typing acquisition Pass maxTsServerMemory (1536 MB) and disableAutomaticTypingAcquisition via typescript-language-server initialization options to prevent tsserver from consuming 2.5+ GB on large monorepos. This reduces peak memory from ~2.8 GB to ~1.5 GB for the entire TypeScript LSP subsystem. --- packages/opencode/src/lsp/server.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index dbd4c59acf1..11ffd8ccd55 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -119,6 +119,10 @@ export namespace LSPServer { return { process: proc, initialization: { + // kilocode_change start - cap tsserver memory to prevent multi-GB usage on large monorepos + maxTsServerMemory: 1536, + disableAutomaticTypingAcquisition: true, + // kilocode_change end tsserver: { path: tsserver, }, From 2191d094c533557929a45ef50c94e8b236bdf70d Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Mon, 16 Mar 2026 11:51:30 +0200 Subject: [PATCH 07/74] feat: add tsgo/tsc diagnostic runner for lightweight TypeScript checking Shells out to tsgo --noEmit (with tsc fallback) and parses the file(line,col): error TSxxxx: message output into Diagnostic[]. Uses ~50MB peak memory and 0 idle, replacing the need for a persistent typescript-language-server process. --- packages/opencode/src/kilocode/ts-check.ts | 91 ++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 packages/opencode/src/kilocode/ts-check.ts diff --git a/packages/opencode/src/kilocode/ts-check.ts b/packages/opencode/src/kilocode/ts-check.ts new file mode 100644 index 00000000000..8d410aae25d --- /dev/null +++ b/packages/opencode/src/kilocode/ts-check.ts @@ -0,0 +1,91 @@ +// kilocode_change - new file +import type { Diagnostic } from "vscode-languageserver-types" +import { Log } from "../util/log" +import { Filesystem } from "../util/filesystem" +import path from "path" + +export namespace TsCheck { + const log = Log.create({ service: "ts-check" }) + + // Match: file(line,col): error TSxxxx: message + const DIAG_RE = /^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+(TS\d+):\s+(.*)$/ + + export async function run(root: string): Promise> { + const result = new Map() + const bin = await resolve(root) + if (!bin) { + log.info("no typescript checker found", { root }) + return result + } + + log.info("running ts check", { bin: bin.cmd, root }) + const start = Date.now() + + const proc = Bun.spawn([bin.cmd, "--noEmit", "--pretty", "false"], { + cwd: root, + stdout: "pipe", + stderr: "pipe", + env: { ...process.env }, + }) + + const stdout = await new Response(proc.stdout).text() + const stderr = await new Response(proc.stderr).text() + const code = await proc.exited + + log.info("ts check done", { + exit: code, + elapsed: Date.now() - start, + lines: stdout.split("\n").length, + }) + + if (stderr.trim()) { + log.info("ts check stderr", { stderr: stderr.slice(0, 500) }) + } + + for (const line of stdout.split("\n")) { + const m = DIAG_RE.exec(line) + if (!m) continue + + const [, file, row, col, severity, code, msg] = m + const abs = path.isAbsolute(file) ? file : path.resolve(root, file) + const normalized = Filesystem.normalizePath(abs) + + const diag: Diagnostic = { + range: { + start: { line: parseInt(row) - 1, character: parseInt(col) - 1 }, + end: { line: parseInt(row) - 1, character: parseInt(col) - 1 }, + }, + severity: severity === "error" ? 1 : 2, + message: msg, + source: "ts", + code, + } + + const arr = result.get(normalized) ?? [] + arr.push(diag) + result.set(normalized, arr) + } + + return result + } + + async function resolve(root: string): Promise<{ cmd: string } | undefined> { + // 1. Try local tsgo from node_modules + const local = path.join(root, "node_modules", ".bin", "tsgo") + if (await Filesystem.exists(local)) return { cmd: local } + + // 2. Try global tsgo + const global = Bun.which("tsgo") + if (global) return { cmd: global } + + // 3. Try local tsc + const tsc = path.join(root, "node_modules", ".bin", "tsc") + if (await Filesystem.exists(tsc)) return { cmd: tsc } + + // 4. Try global tsc + const gtsc = Bun.which("tsc") + if (gtsc) return { cmd: gtsc } + + return undefined + } +} From f8ed8ee844ac8521f3a9748ebc0c233f2c7656e3 Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Mon, 16 Mar 2026 11:51:34 +0200 Subject: [PATCH 08/74] feat: add lightweight LSPClient adapter backed by tsgo diagnostics Duck-types LSPClient.Info without spawning a persistent process. Runs TsCheck on notify.open(), coalesces rapid calls, and provides a stub connection for unsupported LSP operations (hover, definition). --- packages/opencode/src/kilocode/ts-client.ts | 87 +++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 packages/opencode/src/kilocode/ts-client.ts diff --git a/packages/opencode/src/kilocode/ts-client.ts b/packages/opencode/src/kilocode/ts-client.ts new file mode 100644 index 00000000000..a4349b871ac --- /dev/null +++ b/packages/opencode/src/kilocode/ts-client.ts @@ -0,0 +1,87 @@ +// kilocode_change - new file +// Lightweight TypeScript diagnostic client that shells out to tsgo/tsc +// instead of spawning a persistent typescript-language-server process. +// This drops memory from ~500MB persistent to ~50MB peak (0 idle). + +import type { LSPClient } from "../lsp/client" +import { TsCheck } from "./ts-check" +import { Log } from "../util/log" +import { Filesystem } from "../util/filesystem" +import path from "path" +import { Instance } from "../project/instance" + +export namespace TsClient { + const log = Log.create({ service: "ts-client" }) + + export function create(input: { root: string }): LSPClient.Info { + const diagnostics = new Map() + // Track files we've seen to replicate the first-publish suppression + // from client.ts:60 — skip the first Bus publish for unseen files. + // Since we compute diagnostics synchronously on notify.open, we don't + // use Bus at all; instead waitForDiagnostics resolves immediately. + let pending: Promise | undefined + + const client: LSPClient.Info = { + root: input.root, + get serverID() { + return "typescript" + }, + get connection(): any { + // LSP namespace methods (hover, definition, etc.) call + // connection.sendRequest() directly. Provide a stub that + // rejects so those code paths surface a clear error instead + // of crashing with "cannot read property sendRequest of undefined". + return { + sendRequest() { + return Promise.reject( + new Error("TypeScript LSP operations are not supported in lightweight diagnostic mode"), + ) + }, + sendNotification() { + return Promise.resolve() + }, + } + }, + notify: { + async open(input: { path: string }) { + const abs = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path) + log.info("notify.open (queuing tsgo)", { path: abs }) + + // Run a full project check. Multiple rapid calls are coalesced: + // we store the promise and reuse it until it settles. + if (!pending) { + pending = TsCheck.run(client.root) + .then((result) => { + // Replace the entire diagnostics map with fresh results + diagnostics.clear() + for (const [file, diags] of result) { + diagnostics.set(file, diags) + } + pending = undefined + }) + .catch((err) => { + log.error("ts check failed", { error: err }) + pending = undefined + }) + } + await pending + }, + }, + get diagnostics() { + return diagnostics + }, + async waitForDiagnostics(_input: { path: string }) { + // Diagnostics are computed synchronously during notify.open. + // If there's a pending run, wait for it; otherwise resolve immediately. + if (pending) await pending + }, + async shutdown() { + log.info("shutting down ts-client") + diagnostics.clear() + }, + } + + log.info("created lightweight ts client", { root: input.root }) + return client + } +} From 722c5852ec49d1e16a1d54ac9f13d41e4715a252 Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Mon, 16 Mar 2026 11:51:39 +0200 Subject: [PATCH 09/74] feat: replace typescript-language-server with lightweight tsgo client Short-circuit getClients() to create a TsClient for TypeScript files instead of spawning typescript-language-server. The server's spawn() now returns undefined; root() and extensions are preserved for routing. Drops ~500MB persistent memory to ~50MB peak / 0 idle per session. --- packages/opencode/src/lsp/index.ts | 11 ++++++++++ packages/opencode/src/lsp/server.ts | 33 +++++++++-------------------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts index 8c621212fe3..078dd51c1d6 100644 --- a/packages/opencode/src/lsp/index.ts +++ b/packages/opencode/src/lsp/index.ts @@ -10,6 +10,7 @@ import { Config } from "../config/config" import { spawn } from "child_process" import { Instance } from "../project/instance" import { Flag } from "@/flag/flag" +import { TsClient } from "../kilocode/ts-client" // kilocode_change export namespace LSP { const log = Log.create({ service: "lsp" }) @@ -235,6 +236,16 @@ export namespace LSP { continue } + // kilocode_change start - use lightweight tsgo-based client instead of spawning typescript-language-server + if (server.id === "typescript") { + const client = TsClient.create({ root }) + s.clients.push(client) + result.push(client) + Bus.publish(Event.Updated, {}) + continue + } + // kilocode_change end + const inflight = s.spawning.get(root + server.id) if (inflight) { const client = await inflight diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 11ffd8ccd55..06c84da192f 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -98,6 +98,13 @@ export namespace LSPServer { }, } + // kilocode_change start - replaced typescript-language-server with lightweight tsgo/tsc diagnostic client + // The spawn() returns undefined so no node process is started. The actual + // TypeScript diagnostics are handled by TsClient (src/kilocode/ts-client.ts) + // which shells out to tsgo --noEmit on demand, using ~50MB peak / 0 idle + // instead of the ~500MB persistent typescript-language-server. + // The root() and extensions are preserved because getClients() still uses + // them for routing files to the correct project root. export const Typescript: Info = { id: "typescript", root: NearestRoot( @@ -105,31 +112,11 @@ export namespace LSPServer { ["deno.json", "deno.jsonc"], ), extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"], - async spawn(root) { - const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch(() => {}) - log.info("typescript server", { tsserver }) - if (!tsserver) return - const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], { - cwd: root, - env: { - ...process.env, - BUN_BE_BUN: "1", - }, - }) - return { - process: proc, - initialization: { - // kilocode_change start - cap tsserver memory to prevent multi-GB usage on large monorepos - maxTsServerMemory: 1536, - disableAutomaticTypingAcquisition: true, - // kilocode_change end - tsserver: { - path: tsserver, - }, - }, - } + async spawn() { + return undefined }, } + // kilocode_change end export const Vue: Info = { id: "vue", From f9ea76843c2534134433711d82b377e28f2f9467 Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Mon, 16 Mar 2026 12:05:20 +0200 Subject: [PATCH 10/74] fix: resolve native tsgo binary directly, skip node.js wrapper The node_modules/.bin/tsgo script is a node.js wrapper that spawns a node process just to execFileSync the native binary. This caused a 3+ GB node process visible in the process tree. Now resolve the platform-specific native binary directly from @typescript/native-preview-{platform}-{arch}, walking up the directory tree including bun's hoisted .bun/ layout. --- packages/opencode/src/kilocode/ts-check.ts | 95 ++++++++++++++++------ 1 file changed, 70 insertions(+), 25 deletions(-) diff --git a/packages/opencode/src/kilocode/ts-check.ts b/packages/opencode/src/kilocode/ts-check.ts index 8d410aae25d..6fbec9e18d9 100644 --- a/packages/opencode/src/kilocode/ts-check.ts +++ b/packages/opencode/src/kilocode/ts-check.ts @@ -3,6 +3,7 @@ import type { Diagnostic } from "vscode-languageserver-types" import { Log } from "../util/log" import { Filesystem } from "../util/filesystem" import path from "path" +import fs from "fs/promises" export namespace TsCheck { const log = Log.create({ service: "ts-check" }) @@ -18,10 +19,10 @@ export namespace TsCheck { return result } - log.info("running ts check", { bin: bin.cmd, root }) + log.info("running ts check", { bin, root }) const start = Date.now() - const proc = Bun.spawn([bin.cmd, "--noEmit", "--pretty", "false"], { + const proc = Bun.spawn([bin, "--noEmit", "--pretty", "false"], { cwd: root, stdout: "pipe", stderr: "pipe", @@ -30,10 +31,9 @@ export namespace TsCheck { const stdout = await new Response(proc.stdout).text() const stderr = await new Response(proc.stderr).text() - const code = await proc.exited + await proc.exited log.info("ts check done", { - exit: code, elapsed: Date.now() - start, lines: stdout.split("\n").length, }) @@ -45,20 +45,24 @@ export namespace TsCheck { for (const line of stdout.split("\n")) { const m = DIAG_RE.exec(line) if (!m) continue + if (m.length < 7) continue - const [, file, row, col, severity, code, msg] = m + const file = m[1]! + const row = parseInt(m[2]!, 10) - 1 + const col = parseInt(m[3]!, 10) - 1 + const sev = m[4]! const abs = path.isAbsolute(file) ? file : path.resolve(root, file) const normalized = Filesystem.normalizePath(abs) const diag: Diagnostic = { range: { - start: { line: parseInt(row) - 1, character: parseInt(col) - 1 }, - end: { line: parseInt(row) - 1, character: parseInt(col) - 1 }, + start: { line: row, character: col }, + end: { line: row, character: col }, }, - severity: severity === "error" ? 1 : 2, - message: msg, + severity: sev === "error" ? 1 : 2, + message: m[6]!, source: "ts", - code, + code: m[5]!, } const arr = result.get(normalized) ?? [] @@ -69,23 +73,64 @@ export namespace TsCheck { return result } - async function resolve(root: string): Promise<{ cmd: string } | undefined> { - // 1. Try local tsgo from node_modules - const local = path.join(root, "node_modules", ".bin", "tsgo") - if (await Filesystem.exists(local)) return { cmd: local } + // Resolve the native tsgo binary directly, avoiding the node.js wrapper + // (node_modules/.bin/tsgo is a #!/usr/bin/env node script that spawns a + // node process just to execFileSync the native binary — adding ~200MB overhead). + async function resolve(root: string): Promise { + // 1. Try resolving the native tsgo binary from the platform-specific package + const native = await native_tsgo(root) + if (native) return native - // 2. Try global tsgo - const global = Bun.which("tsgo") - if (global) return { cmd: global } - - // 3. Try local tsc - const tsc = path.join(root, "node_modules", ".bin", "tsc") - if (await Filesystem.exists(tsc)) return { cmd: tsc } - - // 4. Try global tsc - const gtsc = Bun.which("tsc") - if (gtsc) return { cmd: gtsc } + // 2. Try global tsc (fallback — this is a node script so still spawns node, + // but it's the last resort) + const tsc = Bun.which("tsc") + if (tsc) return tsc return undefined } + + // Resolve the native tsgo binary by finding the platform-specific package. + // The @typescript/native-preview npm package includes platform-specific + // optional dependencies like @typescript/native-preview-darwin-arm64 that + // contain the actual native binary at lib/tsgo. + async function native_tsgo(root: string): Promise { + const pkg = `@typescript/native-preview-${process.platform}-${process.arch}` + + // Walk up from root looking in node_modules (including .bun hoisted paths) + let dir = root + while (true) { + // Standard node_modules layout + const standard = path.join(dir, "node_modules", pkg, "lib", "tsgo") + if (await exists(standard)) return standard + + // Bun hoisted layout: node_modules/.bun/@/node_modules//lib/tsgo + const bun = path.join(dir, "node_modules", ".bun") + if (await exists(bun)) { + const match = await scan_bun(bun, pkg) + if (match) return match + } + + const parent = path.dirname(dir) + if (parent === dir) break + dir = parent + } + + return undefined + } + + // Scan .bun hoisted directory for the platform package + async function scan_bun(dir: string, pkg: string): Promise { + const prefix = pkg.replace("/", "+") + const entries = await fs.readdir(dir).catch(() => [] as string[]) + for (const entry of entries) { + if (!entry.startsWith(prefix + "@")) continue + const bin = path.join(dir, entry, "node_modules", pkg, "lib", "tsgo") + if (await exists(bin)) return bin + } + return undefined + } + + async function exists(p: string): Promise { + return Filesystem.exists(p) + } } From 08675183b5ae581d2eef9e4062181ca9fbea9a90 Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Mon, 16 Mar 2026 12:05:25 +0200 Subject: [PATCH 11/74] fix: defer tsgo to waitForDiagnostics, skip warm-up calls notify.open() is now a no-op. Warm-up calls from read.ts (touchFile(path, false)) no longer spawn tsgo processes. Only write/edit/apply_patch trigger a check via waitForDiagnostics(). --- packages/opencode/src/kilocode/ts-client.ts | 58 ++++++++++----------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/packages/opencode/src/kilocode/ts-client.ts b/packages/opencode/src/kilocode/ts-client.ts index a4349b871ac..b88bbb52e66 100644 --- a/packages/opencode/src/kilocode/ts-client.ts +++ b/packages/opencode/src/kilocode/ts-client.ts @@ -6,7 +6,6 @@ import type { LSPClient } from "../lsp/client" import { TsCheck } from "./ts-check" import { Log } from "../util/log" -import { Filesystem } from "../util/filesystem" import path from "path" import { Instance } from "../project/instance" @@ -15,12 +14,29 @@ export namespace TsClient { export function create(input: { root: string }): LSPClient.Info { const diagnostics = new Map() - // Track files we've seen to replicate the first-publish suppression - // from client.ts:60 — skip the first Bus publish for unseen files. - // Since we compute diagnostics synchronously on notify.open, we don't - // use Bus at all; instead waitForDiagnostics resolves immediately. + // Pending tsgo run promise — used for coalescing rapid calls. + // Only set when waitForDiagnostics() triggers a run, NOT on + // notify.open() (which is a warm-up call from read.ts). let pending: Promise | undefined + function check(): Promise { + if (pending) return pending + pending = TsCheck.run(client.root) + .then((result) => { + diagnostics.clear() + for (const [file, diags] of result) { + diagnostics.set(file, diags) + } + }) + .catch((err) => { + log.error("ts check failed", { error: err }) + }) + .finally(() => { + pending = undefined + }) + return pending + } + const client: LSPClient.Info = { root: input.root, get serverID() { @@ -43,37 +59,19 @@ export namespace TsClient { } }, notify: { - async open(input: { path: string }) { - const abs = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path) - log.info("notify.open (queuing tsgo)", { path: abs }) - - // Run a full project check. Multiple rapid calls are coalesced: - // we store the promise and reuse it until it settles. - if (!pending) { - pending = TsCheck.run(client.root) - .then((result) => { - // Replace the entire diagnostics map with fresh results - diagnostics.clear() - for (const [file, diags] of result) { - diagnostics.set(file, diags) - } - pending = undefined - }) - .catch((err) => { - log.error("ts check failed", { error: err }) - pending = undefined - }) - } - await pending + async open(_input: { path: string }) { + // No-op. Warm-up calls from read.ts (touchFile(path, false)) + // trigger notify.open() but should NOT spawn tsgo. The actual + // check is deferred to waitForDiagnostics() which is only + // called when tools need diagnostics (write, edit, apply_patch). }, }, get diagnostics() { return diagnostics }, async waitForDiagnostics(_input: { path: string }) { - // Diagnostics are computed synchronously during notify.open. - // If there's a pending run, wait for it; otherwise resolve immediately. - if (pending) await pending + // Run tsgo --noEmit and wait for results. Coalesces concurrent calls. + await check() }, async shutdown() { log.info("shutting down ts-client") From 7078246d7dd102a59723953b714b2486ce5766dc Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Mon, 16 Mar 2026 14:15:05 +0200 Subject: [PATCH 12/74] perf: enable --incremental for warm tsgo runs (~200-400ms after first check) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tsgo writes a .tsbuildinfo cache so subsequent runs skip re-checking unchanged files. First edit is still ~1.3s cold, but every edit after that reuses the cache and drops to ~200-400ms — matching the old language server latency with zero persistent memory. --- packages/opencode/src/kilocode/ts-check.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/kilocode/ts-check.ts b/packages/opencode/src/kilocode/ts-check.ts index 6fbec9e18d9..3eacade23bb 100644 --- a/packages/opencode/src/kilocode/ts-check.ts +++ b/packages/opencode/src/kilocode/ts-check.ts @@ -22,7 +22,10 @@ export namespace TsCheck { log.info("running ts check", { bin, root }) const start = Date.now() - const proc = Bun.spawn([bin, "--noEmit", "--pretty", "false"], { + // --incremental writes a .tsbuildinfo cache so subsequent runs only + // re-check changed files. First run is cold (~1.3s), warm runs + // reuse the cache and typically finish in ~200-400ms. + const proc = Bun.spawn([bin, "--noEmit", "--pretty", "false", "--incremental"], { cwd: root, stdout: "pipe", stderr: "pipe", From e3cbc1b3ecb0f99d988c5876d552ed1d29f1a7b8 Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Mon, 16 Mar 2026 15:33:26 +0200 Subject: [PATCH 13/74] refactor: remove process tree status dialog feature Remove the /process server route, ProcessInfo module, and all process-related UI in the status dialog (formatBytes, shortCommand, refresh timer, process JSX). This simplifies the status dialog and removes ~460 lines of code that added process tree diagnostics. --- .../cli/cmd/tui/component/dialog-status.tsx | 108 +------------- .../opencode/src/cli/cmd/tui/context/sync.tsx | 4 - .../opencode/src/kilocode/process-info.ts | 137 ------------------ packages/opencode/src/server/server.ts | 25 +--- packages/sdk/js/src/v2/gen/sdk.gen.ts | 38 ----- packages/sdk/js/src/v2/gen/types.gen.ts | 38 ----- packages/sdk/openapi.json | 95 ------------ 7 files changed, 2 insertions(+), 443 deletions(-) delete mode 100644 packages/opencode/src/kilocode/process-info.ts diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx index d77a91f8076..6c67536598f 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx @@ -3,77 +3,15 @@ import { fileURLToPath } from "bun" import { useTheme } from "../context/theme" import { useDialog } from "@tui/ui/dialog" import { useSync } from "@tui/context/sync" -import { useSDK } from "@tui/context/sdk" // kilocode_change -import { For, Match, Switch, Show, createMemo, onMount, onCleanup } from "solid-js" +import { For, Match, Switch, Show, createMemo } from "solid-js" import { Installation } from "../../../../installation" -import type { ProcessInfo } from "@kilocode/sdk/v2" // kilocode_change export type DialogStatusProps = {} -// kilocode_change start - format bytes for process memory display -function formatBytes(bytes: number): string { - if (bytes >= 1024 * 1024 * 1024) return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB" - if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + " MB" - if (bytes >= 1024) return (bytes / 1024).toFixed(0) + " KB" - return bytes + " B" -} - -function shortCommand(info: ProcessInfo): string { - const cmd = info.command.split("/").pop() ?? info.command - const args = info.args - if (!args || args === info.command) return cmd - const parts = args.split(/\s+/) - - // For node/bun: find the script being run (.js/.ts/.mjs/.cjs file) - if (cmd === "node" || cmd === "bun") { - const script = parts.find((p) => /\.(js|ts|mjs|cjs)(['"]?)$/.test(p)) - if (script) { - const name = script - .split("/") - .pop()! - .replace(/\.(js|ts|mjs|cjs)$/, "") - return `${cmd} → ${name}` - } - } - - // Find first non-flag, non-path, non-trivial arg - const interesting = parts.find( - (p) => !p.startsWith("-") && !p.startsWith("/") && p !== cmd && p !== "x" && p !== "run", - ) - if (interesting) return `${cmd} → ${interesting}` - - // Last resort: extract basename from first path arg that isn't the executable itself - const exe = parts[0] - const path = parts.find((p) => p.startsWith("/") && p !== exe) - if (path) { - const base = path - .split("/") - .pop()! - .replace(/\.(js|ts|mjs|cjs)$/, "") - return `${cmd} → ${base}` - } - - return cmd -} -// kilocode_change end - export function DialogStatus() { const sync = useSync() const { theme } = useTheme() const dialog = useDialog() - // kilocode_change start - refresh process data while dialog is open - const sdk = useSDK() - const refresh = () => { - sdk.client.process.status().then((x) => { - if (x.data) sync.set("processes", x.data) - }) - } - onMount(() => { - refresh() - const timer = setInterval(refresh, 3000) - onCleanup(() => clearInterval(timer)) - }) - // kilocode_change end const enabledFormatters = createMemo(() => sync.data.formatter.filter((f) => f.enabled)) @@ -115,50 +53,6 @@ export function DialogStatus() { {/* kilocode_change start */} Kilo v{Installation.VERSION} {/* kilocode_change end */} - {/* kilocode_change start - process memory diagnostics */} - - {(procs) => { - const total = createMemo(() => { - const all = [procs().self, ...procs().children] - return all.reduce((sum, p) => sum + p.rss, 0) - }) - return ( - - - Processes ({1 + procs().children.length}) — {formatBytes(total())} total RSS - - - - • - - - self{" "} - - PID {procs().self.pid} | {formatBytes(procs().self.rss)} RSS | heap {formatBytes(procs().heap.used)} - /{formatBytes(procs().heap.total)} - - - - - {(child) => ( - - 512 * 1024 * 1024 ? theme.warning : theme.success }}> - • - - - {shortCommand(child)}{" "} - - PID {child.pid} | {formatBytes(child.rss)} - - - - )} - - - ) - }} - - {/* kilocode_change end */} 0} fallback={No MCP Servers}> {Object.keys(sync.data.mcp).length} MCP Servers diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index fe478ad4360..87cf87719c6 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -17,7 +17,6 @@ import type { ProviderListResponse, ProviderAuthMethod, VcsInfo, - ProcessStatus, // kilocode_change } from "@kilocode/sdk/v2" import { createStore, produce, reconcile } from "solid-js/store" import { useSDK } from "@tui/context/sdk" @@ -72,7 +71,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ [key: string]: McpResource } formatter: FormatterStatus[] - processes: ProcessStatus | undefined // kilocode_change vcs: VcsInfo | undefined path: Path }>({ @@ -100,7 +98,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ mcp: {}, mcp_resource: {}, formatter: [], - processes: undefined, // kilocode_change vcs: undefined, path: { state: "", config: "", worktree: "", directory: "" }, }) @@ -410,7 +407,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ sdk.client.mcp.status().then((x) => setStore("mcp", reconcile(x.data!))), sdk.client.experimental.resource.list().then((x) => setStore("mcp_resource", reconcile(x.data ?? {}))), sdk.client.formatter.status().then((x) => setStore("formatter", reconcile(x.data!))), - sdk.client.process.status().then((x) => setStore("processes", reconcile(x.data!))), // kilocode_change sdk.client.session.status().then((x) => { setStore("session_status", reconcile(x.data!)) }), diff --git a/packages/opencode/src/kilocode/process-info.ts b/packages/opencode/src/kilocode/process-info.ts deleted file mode 100644 index 96ba5712843..00000000000 --- a/packages/opencode/src/kilocode/process-info.ts +++ /dev/null @@ -1,137 +0,0 @@ -// kilocode_change - new file -import z from "zod" -import { Log } from "../util/log" - -export namespace ProcessInfo { - const log = Log.create({ service: "process-info" }) - - export const Info = z - .object({ - pid: z.number(), - ppid: z.number(), - rss: z.number(), - command: z.string(), - args: z.string(), - }) - .meta({ - ref: "ProcessInfo", - }) - export type Info = z.infer - - export const Status = z - .object({ - self: Info, - heap: z.object({ - used: z.number(), - total: z.number(), - external: z.number(), - buffers: z.number(), - }), - children: z.array(Info), - }) - .meta({ - ref: "ProcessStatus", - }) - export type Status = z.infer - - function format(bytes: number): string { - if (bytes >= 1024 * 1024 * 1024) return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB" - if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + " MB" - if (bytes >= 1024) return (bytes / 1024).toFixed(0) + " KB" - return bytes + " B" - } - - export function pretty(info: Info): string { - return `PID ${info.pid} | ${format(info.rss)} | ${info.command} ${info.args}`.trim() - } - - export function formatBytes(bytes: number): string { - return format(bytes) - } - - async function descendants(pid: number): Promise { - if (process.platform === "win32") return [] - const pids: number[] = [] - const queue = [pid] - while (queue.length > 0) { - const current = queue.shift()! - const proc = Bun.spawn(["pgrep", "-P", String(current)], { stdout: "pipe", stderr: "pipe" }) - const [code, out] = await Promise.all([proc.exited, new Response(proc.stdout).text()]).catch( - () => [-1, ""] as const, - ) - if (code !== 0) continue - for (const tok of out.trim().split(/\s+/)) { - const cpid = parseInt(tok, 10) - if (!isNaN(cpid) && pids.indexOf(cpid) === -1) { - pids.push(cpid) - queue.push(cpid) - } - } - } - return pids - } - - async function info(pid: number): Promise { - if (process.platform === "win32") return undefined - // ucomm gives the short executable name (e.g. "node", "bun") - // comm on macOS is an alias for command (full path + args) which breaks whitespace parsing - const proc = Bun.spawn(["ps", "-p", String(pid), "-o", "pid=,ppid=,rss=,ucomm=,args="], { - stdout: "pipe", - stderr: "pipe", - }) - const [code, out] = await Promise.all([proc.exited, new Response(proc.stdout).text()]).catch( - () => [-1, ""] as const, - ) - if (code !== 0) return undefined - const line = out.trim() - if (!line) return undefined - // ps output: PID PPID RSS UCOMM ARGS... - // RSS is in KB from ps, ucomm is a single word (executable basename) - const parts = line.trim().split(/\s+/) - if (parts.length < 4) return undefined - const parsed = { - pid: parseInt(parts[0], 10), - ppid: parseInt(parts[1], 10), - rss: parseInt(parts[2], 10) * 1024, // convert KB to bytes - command: parts[3], - args: parts.slice(4).join(" "), - } - if (isNaN(parsed.pid)) return undefined - return parsed - } - - export async function status(): Promise { - const pid = process.pid - const mem = process.memoryUsage() - - const self = (await info(pid)) ?? { - pid, - ppid: process.ppid, - rss: mem.rss, - command: process.argv0, - args: process.argv.slice(1).join(" "), - } - - const cpids = await descendants(pid) - const results = await Promise.all(cpids.map((p) => info(p).catch(() => undefined))) - const children = results.filter((x): x is Info => x !== undefined) - - log.info("process status", { - self: self.pid, - rss: format(self.rss), - heap: format(mem.heapUsed), - children: children.length, - }) - - return { - self, - heap: { - used: mem.heapUsed, - total: mem.heapTotal, - external: mem.external, - buffers: mem.arrayBuffers, - }, - children, - } - } -} diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 504d620202c..3c5b0be3881 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -11,7 +11,6 @@ import z from "zod" import { Provider } from "../provider/provider" import { NamedError } from "@opencode-ai/util/error" import { LSP } from "../lsp" -import { ProcessInfo } from "../kilocode/process-info" // kilocode_change import { Format } from "../format" import { TuiRoutes } from "./routes/tui" import { Instance } from "../project/instance" @@ -524,29 +523,7 @@ export namespace Server { return c.json(await LSP.status()) }, ) - // kilocode_change start - process diagnostics route - .get( - "/process", - describeRoute({ - summary: "Get process status", - description: "Get process tree with memory usage for all related processes", - operationId: "process.status", - responses: { - 200: { - description: "Process status", - content: { - "application/json": { - schema: resolver(ProcessInfo.Status), - }, - }, - }, - }, - }), - async (c) => { - return c.json(await ProcessInfo.status()) - }, - ) - // kilocode_change end + .get( "/formatter", describeRoute({ diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index edae97612ec..9fbc4210568 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -98,7 +98,6 @@ import type { PermissionRuleset, PermissionSavePatternRulesErrors, PermissionSavePatternRulesResponses, - ProcessStatusResponses, ProjectCurrentResponses, ProjectListResponses, ProjectUpdateErrors, @@ -4437,38 +4436,6 @@ export class Lsp extends HeyApiClient { } } -export class Process extends HeyApiClient { - /** - * Get process status - * - * Get process tree with memory usage for all related processes - */ - public status( - parameters?: { - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/process", - ...options, - ...params, - }) - } -} - export class Formatter extends HeyApiClient { /** * Get formatter status @@ -4681,11 +4648,6 @@ export class KiloClient extends HeyApiClient { return (this._lsp ??= new Lsp({ client: this.client })) } - private _process?: Process - get process(): Process { - return (this._process ??= new Process({ client: this.client })) - } - private _formatter?: Formatter get formatter(): Formatter { return (this._formatter ??= new Formatter({ client: this.client })) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index e2e271c9b36..fea79d1ecfa 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1947,25 +1947,6 @@ export type LspStatus = { status: "connected" | "error" } -export type ProcessInfo = { - pid: number - ppid: number - rss: number - command: string - args: string -} - -export type ProcessStatus = { - self: ProcessInfo - heap: { - used: number - total: number - external: number - buffers: number - } - children: Array -} - export type FormatterStatus = { name: string extensions: Array @@ -5575,25 +5556,6 @@ export type LspStatusResponses = { export type LspStatusResponse = LspStatusResponses[keyof LspStatusResponses] -export type ProcessStatusData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/process" -} - -export type ProcessStatusResponses = { - /** - * Process status - */ - 200: ProcessStatus -} - -export type ProcessStatusResponse = ProcessStatusResponses[keyof ProcessStatusResponses] - export type FormatterStatusData = { body?: never path?: never diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 2f3c618bb85..5c04569c5f1 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -8047,47 +8047,6 @@ ] } }, - "/process": { - "get": { - "operationId": "process.status", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "workspace", - "schema": { - "type": "string" - } - } - ], - "summary": "Get process status", - "description": "Get process tree with memory usage for all related processes", - "responses": { - "200": { - "description": "Process status", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProcessStatus" - } - } - } - } - }, - "x-codeSamples": [ - { - "lang": "js", - "source": "import { createKiloClient } from \"@kilocode/sdk\n\nconst client = createKiloClient()\nawait client.process.status({\n ...\n})" - } - ] - } - }, "/formatter": { "get": { "operationId": "formatter.status", @@ -13283,60 +13242,6 @@ }, "required": ["id", "name", "root", "status"] }, - "ProcessInfo": { - "type": "object", - "properties": { - "pid": { - "type": "number" - }, - "ppid": { - "type": "number" - }, - "rss": { - "type": "number" - }, - "command": { - "type": "string" - }, - "args": { - "type": "string" - } - }, - "required": ["pid", "ppid", "rss", "command", "args"] - }, - "ProcessStatus": { - "type": "object", - "properties": { - "self": { - "$ref": "#/components/schemas/ProcessInfo" - }, - "heap": { - "type": "object", - "properties": { - "used": { - "type": "number" - }, - "total": { - "type": "number" - }, - "external": { - "type": "number" - }, - "buffers": { - "type": "number" - } - }, - "required": ["used", "total", "external", "buffers"] - }, - "children": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProcessInfo" - } - } - }, - "required": ["self", "heap", "children"] - }, "FormatterStatus": { "type": "object", "properties": { From 215770fa9e5d95f5e3ab375317ce7d9257d8b1a4 Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Mon, 16 Mar 2026 15:35:02 +0200 Subject: [PATCH 14/74] feat: use persistent tsgo --lsp server when KILO_EXPERIMENTAL_LSP_TOOL is enabled When the flag is set, Typescript.spawn() resolves the native tsgo binary and starts it with --lsp --stdio as a persistent LSP server, providing full diagnostics, hover, go-to-definition, etc. The TsClient lightweight fallback (tsgo --noEmit on demand) remains the default when the flag is not set. - Export native_tsgo() from ts-check.ts for reuse by the LSP server spawn - Restore Typescript.spawn() body gated on KILO_EXPERIMENTAL_LSP_TOOL - Gate TsClient short-circuit in index.ts on !Flag.KILO_EXPERIMENTAL_LSP_TOOL --- packages/opencode/src/kilocode/ts-check.ts | 3 ++- packages/opencode/src/lsp/index.ts | 4 ++-- packages/opencode/src/lsp/server.ts | 26 ++++++++++++++-------- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/opencode/src/kilocode/ts-check.ts b/packages/opencode/src/kilocode/ts-check.ts index 3eacade23bb..71fa0cba64b 100644 --- a/packages/opencode/src/kilocode/ts-check.ts +++ b/packages/opencode/src/kilocode/ts-check.ts @@ -96,7 +96,8 @@ export namespace TsCheck { // The @typescript/native-preview npm package includes platform-specific // optional dependencies like @typescript/native-preview-darwin-arm64 that // contain the actual native binary at lib/tsgo. - async function native_tsgo(root: string): Promise { + // Exported for use by the LSP server spawn (tsgo --lsp --stdio). + export async function native_tsgo(root: string): Promise { const pkg = `@typescript/native-preview-${process.platform}-${process.arch}` // Walk up from root looking in node_modules (including .bun hoisted paths) diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts index 078dd51c1d6..77039deb0f7 100644 --- a/packages/opencode/src/lsp/index.ts +++ b/packages/opencode/src/lsp/index.ts @@ -236,8 +236,8 @@ export namespace LSP { continue } - // kilocode_change start - use lightweight tsgo-based client instead of spawning typescript-language-server - if (server.id === "typescript") { + // kilocode_change start - use lightweight tsgo-based client when persistent LSP is not enabled + if (server.id === "typescript" && !Flag.KILO_EXPERIMENTAL_LSP_TOOL) { const client = TsClient.create({ root }) s.clients.push(client) result.push(client) diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index 06c84da192f..7afcdc97b25 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -10,6 +10,7 @@ import fs from "fs/promises" import { Filesystem } from "../util/filesystem" import { Instance } from "../project/instance" import { Flag } from "../flag/flag" +import { TsCheck } from "../kilocode/ts-check" // kilocode_change import { Archive } from "../util/archive" import { Process } from "../util/process" @@ -98,13 +99,11 @@ export namespace LSPServer { }, } - // kilocode_change start - replaced typescript-language-server with lightweight tsgo/tsc diagnostic client - // The spawn() returns undefined so no node process is started. The actual - // TypeScript diagnostics are handled by TsClient (src/kilocode/ts-client.ts) - // which shells out to tsgo --noEmit on demand, using ~50MB peak / 0 idle - // instead of the ~500MB persistent typescript-language-server. - // The root() and extensions are preserved because getClients() still uses - // them for routing files to the correct project root. + // kilocode_change start - tsgo native LSP or lightweight diagnostic client + // When KILO_EXPERIMENTAL_LSP_TOOL is enabled, spawn tsgo --lsp --stdio as a + // persistent LSP server (full diagnostics, hover, go-to-definition, etc.). + // Otherwise spawn() returns undefined and getClients() in index.ts falls + // through to the lightweight TsClient that shells out to tsgo --noEmit on demand. export const Typescript: Info = { id: "typescript", root: NearestRoot( @@ -112,8 +111,17 @@ export namespace LSPServer { ["deno.json", "deno.jsonc"], ), extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"], - async spawn() { - return undefined + async spawn(root) { + if (!Flag.KILO_EXPERIMENTAL_LSP_TOOL) return undefined + const bin = await TsCheck.native_tsgo(root) + if (!bin) { + log.info("tsgo native binary not found, falling back to lightweight client") + return undefined + } + log.info("spawning tsgo --lsp", { bin, root }) + return { + process: spawn(bin, ["--lsp", "--stdio"], { cwd: root }), + } }, } // kilocode_change end From 8ba7232cb42f5cb699325eabf6f2c03f76569468 Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Mon, 16 Mar 2026 15:58:49 +0200 Subject: [PATCH 15/74] chore: update @typescript/native-preview to 7.0.0-dev.20260316.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump tsgo from 20251207 to 20260316 (~3 months of nightly releases). The new version is stricter about type resolution — it no longer auto-resolves @types/* packages without explicit 'types' in tsconfig. Add 'types' field to sdk, plugin, kilo-telemetry, and app tsconfigs. --- bun.lock | 18 +++++++++--------- package.json | 2 +- packages/app/tsconfig.json | 1 + packages/kilo-telemetry/tsconfig.json | 1 + packages/plugin/tsconfig.json | 3 ++- packages/sdk/js/tsconfig.json | 1 + 6 files changed, 15 insertions(+), 11 deletions(-) diff --git a/bun.lock b/bun.lock index a3a18be5fa2..8b0c1a2a23d 100644 --- a/bun.lock +++ b/bun.lock @@ -573,7 +573,7 @@ "@types/luxon": "3.7.1", "@types/node": "22.13.9", "@types/semver": "7.7.1", - "@typescript/native-preview": "7.0.0-dev.20251207.1", + "@typescript/native-preview": "7.0.0-dev.20260316.1", "ai": "5.0.124", "diff": "8.0.2", "dompurify": "3.3.1", @@ -2050,21 +2050,21 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg=="], - "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20251207.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251207.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20251207.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20251207.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20251207.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20251207.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20251207.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20251207.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-4QcRnzB0pi9rS0AOvg8kWbmuwHv5X7B2EXHbgcms9+56hsZ8SZrZjNgBJb2rUIodJ4kU5mrkj/xlTTT4r9VcpQ=="], + "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260316.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260316.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260316.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260316.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260316.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260316.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260316.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260316.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-s+QGNx+3zxTZBuZw3oNOFlHqpbmg0cTgBd/b6SRZ5mo3vFChkhflYqRW2IvTvU9a3PPX3bQAkQ/gWbDZCmNC3Q=="], - "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20251207.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-waWJnuuvkXh4WdpbTjYf7pyahJzx0ycesV2BylyHrE9OxU9FSKcD/cRLQYvbq3YcBSdF7sZwRLDBer7qTeLsYA=="], + "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260316.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TjeMEMabLsc5VNYy8WVlu1oHBVqibwSbkIRSyqANFxyD6iWnCFquDvliwErVo8TFIu0c8C+C+tgFSvYkhVZMMw=="], - "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20251207.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-3bkD9QuIjxETtp6J1l5X2oKgudJ8z+8fwUq0izCjK1JrIs2vW1aQnbzxhynErSyHWH7URGhHHzcsXHbikckAsg=="], + "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260316.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Lv/JmtMfNbMJiIEZlByQ5zSR1t9WoE8rFuZxU0vpiyfUEjSbuBMG8pt+Ryqj6uiylR3XThlV3EaVYsJ7Um6n8w=="], - "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20251207.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OjrZBq8XJkB7uCQvT1AZ1FPsp+lT0cHxY5SisE+ZTAU6V0IHAZMwJ7J/mnwlGsBcCKRLBT+lX3hgEuOTSwHr9w=="], + "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260316.1", "", { "os": "linux", "cpu": "arm" }, "sha512-vItkqjOuVY9OfqdovSyEjnAbNMM+QGM9AqzGRknX1nZjGlWXsUTL3IPuv5by69SOqw5TLi8ddx82cyu6F3ZRVQ=="], - "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20251207.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qhp06OObkwy5B+PlAhAmq+Ls3GVt4LHAovrTRcpLB3Mk3yJ0h9DnIQwPQiayp16TdvTsGHI3jdIX4MGm5L/ghA=="], + "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260316.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-xA4DekkAesjnWyp8p0iF79Rf0q2NVszxedd9M2Ztb0WBSDQFiECVYJSQMFd4+FKNiSq9DnadPy68Dly+B1r17A=="], - "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20251207.1", "", { "os": "linux", "cpu": "x64" }, "sha512-fPRw0zfTBeVmrkgi5Le+sSwoeAz6pIdvcsa1OYZcrspueS9hn3qSC5bLEc5yX4NJP1vItadBqyGLUQ7u8FJjow=="], + "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260316.1", "", { "os": "linux", "cpu": "x64" }, "sha512-osY+4HCIpi9Bu4jNz49k8BVOB9A04BG6mWF7WltmAQWBIAeosa4n/qtKokfAZDTD5/moHSn20p7hZAlGI8JWjw=="], - "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20251207.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-KxY1i+HxeSFfzZ+HVsKwMGBM79laTRZv1ibFqHu22CEsfSPDt4yiV1QFis8Nw7OBXswNqJG/UGqY47VP8FeTvw=="], + "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260316.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-DcWceiTXClIakJhk0+8KjQ+pBp435HaA6uw9EtDTo75uWUEPVf9D489KKbylRChci/paYX8uPKlROo9+6N8M9g=="], - "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20251207.1", "", { "os": "win32", "cpu": "x64" }, "sha512-5l51HlXjX7lXwo65DEl1IaCFLjmkMtL6K3NrSEamPNeNTtTQwZRa3pQ9V65dCglnnCQ0M3+VF1RqzC7FU0iDKg=="], + "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260316.1", "", { "os": "win32", "cpu": "x64" }, "sha512-LvpV1hyQS0U9yMLHgWexhC7oSeBpcNbIJtYC6Iyvu63Mb6J/cP0k2fQmnAVB2yesMMQFtuY6v2YIx17vE0Ymfw=="], "@typescript/vfs": ["@typescript/vfs@1.6.4", "", { "dependencies": { "debug": "^4.4.3" }, "peerDependencies": { "typescript": "*" } }, "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ=="], diff --git a/package.json b/package.json index e350051098c..2a51f7d7afc 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "marked-shiki": "1.2.1", "@playwright/test": "1.51.0", "typescript": "5.8.2", - "@typescript/native-preview": "7.0.0-dev.20251207.1", + "@typescript/native-preview": "7.0.0-dev.20260316.1", "zod": "4.1.8", "remeda": "2.26.0", "shiki": "3.20.0", diff --git a/packages/app/tsconfig.json b/packages/app/tsconfig.json index bfd45ff85e4..3a3659485c1 100644 --- a/packages/app/tsconfig.json +++ b/packages/app/tsconfig.json @@ -17,6 +17,7 @@ "emitDeclarationOnly": true, "outDir": "node_modules/.ts-dist", "isolatedModules": true, + "types": ["bun", "node"], "paths": { "@/*": ["./src/*"] } diff --git a/packages/kilo-telemetry/tsconfig.json b/packages/kilo-telemetry/tsconfig.json index dd08759fbbc..d7c900fa255 100644 --- a/packages/kilo-telemetry/tsconfig.json +++ b/packages/kilo-telemetry/tsconfig.json @@ -4,6 +4,7 @@ "compilerOptions": { "outDir": "./dist", "rootDir": "./src", + "types": ["bun"], "skipLibCheck": true, "noUncheckedIndexedAccess": false }, diff --git a/packages/plugin/tsconfig.json b/packages/plugin/tsconfig.json index 58072c81c96..4d6d87dec9a 100644 --- a/packages/plugin/tsconfig.json +++ b/packages/plugin/tsconfig.json @@ -6,7 +6,8 @@ "module": "preserve", "declaration": true, "moduleResolution": "bundler", - "lib": ["es2022", "dom", "dom.iterable"] + "lib": ["es2022", "dom", "dom.iterable"], + "types": ["node"] }, "include": ["src"] } diff --git a/packages/sdk/js/tsconfig.json b/packages/sdk/js/tsconfig.json index 3ab5fcb7689..54e6b62d0b2 100644 --- a/packages/sdk/js/tsconfig.json +++ b/packages/sdk/js/tsconfig.json @@ -7,6 +7,7 @@ "declaration": true, "moduleResolution": "nodenext", "lib": ["es2022", "dom", "dom.iterable"], + "types": ["node"], "composite": true, "rootDir": "src" }, From 6c3a662bb98ec75e6fa8048da91b1c22d131c7ff Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Mon, 16 Mar 2026 16:14:05 +0200 Subject: [PATCH 16/74] fix: fallback to TsClient when tsgo LSP spawn fails in experimental mode When KILO_EXPERIMENTAL_LSP_TOOL is on but tsgo binary is missing, spawn() returns undefined and schedule() marks typescript as broken. Now falls back to the lightweight TsClient instead of losing TS diagnostics entirely. --- packages/opencode/src/lsp/index.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts index 77039deb0f7..c5a13b37d27 100644 --- a/packages/opencode/src/lsp/index.ts +++ b/packages/opencode/src/lsp/index.ts @@ -264,6 +264,16 @@ export namespace LSP { }) const client = await task + // kilocode_change start - fallback to lightweight client when tsgo LSP spawn fails + if (!client && server.id === "typescript") { + s.broken.delete(root + server.id) + const fallback = TsClient.create({ root }) + s.clients.push(fallback) + result.push(fallback) + Bus.publish(Event.Updated, {}) + continue + } + // kilocode_change end if (!client) continue result.push(client) From 2602c6701da189c417574e3e67cda2977e04ed3b Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Mon, 16 Mar 2026 16:14:10 +0200 Subject: [PATCH 17/74] fix: resolve workspace-local tsc from node_modules Bun.which('tsc') only finds a globally installed tsc. Most projects have typescript in node_modules without a global install. Walk up from root looking for node_modules/typescript/bin/tsc before falling back to the global tsc. --- packages/opencode/src/kilocode/ts-check.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/kilocode/ts-check.ts b/packages/opencode/src/kilocode/ts-check.ts index 71fa0cba64b..14e26dded3a 100644 --- a/packages/opencode/src/kilocode/ts-check.ts +++ b/packages/opencode/src/kilocode/ts-check.ts @@ -84,14 +84,30 @@ export namespace TsCheck { const native = await native_tsgo(root) if (native) return native - // 2. Try global tsc (fallback — this is a node script so still spawns node, - // but it's the last resort) + // 2. Try workspace-local tsc from node_modules + const local = await local_tsc(root) + if (local) return local + + // 3. Try global tsc (fallback) const tsc = Bun.which("tsc") if (tsc) return tsc return undefined } + // Walk up from root looking for node_modules/typescript/bin/tsc + async function local_tsc(root: string): Promise { + let dir = root + while (true) { + const bin = path.join(dir, "node_modules", "typescript", "bin", "tsc") + if (await exists(bin)) return bin + const parent = path.dirname(dir) + if (parent === dir) break + dir = parent + } + return undefined + } + // Resolve the native tsgo binary by finding the platform-specific package. // The @typescript/native-preview npm package includes platform-specific // optional dependencies like @typescript/native-preview-darwin-arm64 that From be5709066f34479d62d5ea3da2b89ca7a74ac905 Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Tue, 17 Mar 2026 11:10:54 +0200 Subject: [PATCH 18/74] kill stuck tsgo process after timeout --- packages/opencode/src/kilocode/ts-check.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/kilocode/ts-check.ts b/packages/opencode/src/kilocode/ts-check.ts index 14e26dded3a..d01c6b7e8e3 100644 --- a/packages/opencode/src/kilocode/ts-check.ts +++ b/packages/opencode/src/kilocode/ts-check.ts @@ -32,9 +32,17 @@ export namespace TsCheck { env: { ...process.env }, }) + const TIMEOUT = 30_000 const stdout = await new Response(proc.stdout).text() const stderr = await new Response(proc.stderr).text() - await proc.exited + const exited = await Promise.race([ + proc.exited.then(() => true), + new Promise((r) => setTimeout(() => r(false), TIMEOUT)), + ]) + if (!exited) { + log.warn("ts check timed out, killing process", { elapsed: Date.now() - start }) + proc.kill() + } log.info("ts check done", { elapsed: Date.now() - start, From bf5b827f2f48b7eea56a9154873a3005ca9544da Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Tue, 17 Mar 2026 11:11:58 +0200 Subject: [PATCH 19/74] add timeout to TsClient diagnostic wait --- packages/opencode/src/kilocode/ts-client.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/kilocode/ts-client.ts b/packages/opencode/src/kilocode/ts-client.ts index b88bbb52e66..dd4ebc1bf3f 100644 --- a/packages/opencode/src/kilocode/ts-client.ts +++ b/packages/opencode/src/kilocode/ts-client.ts @@ -6,6 +6,7 @@ import type { LSPClient } from "../lsp/client" import { TsCheck } from "./ts-check" import { Log } from "../util/log" +import { withTimeout } from "../util/timeout" import path from "path" import { Instance } from "../project/instance" @@ -71,7 +72,9 @@ export namespace TsClient { }, async waitForDiagnostics(_input: { path: string }) { // Run tsgo --noEmit and wait for results. Coalesces concurrent calls. - await check() + // 30s cap matches the process timeout in TsCheck.run(). Silent catch + // matches the real LSPClient's .catch(() => {}) on its 3s timeout. + await withTimeout(check(), 30_000).catch(() => {}) }, async shutdown() { log.info("shutting down ts-client") From 573680446effb6812babc3e47bb36384e2ecac57 Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Tue, 17 Mar 2026 11:12:30 +0200 Subject: [PATCH 20/74] publish bus event after TsClient diagnostics --- packages/opencode/src/kilocode/ts-client.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/kilocode/ts-client.ts b/packages/opencode/src/kilocode/ts-client.ts index dd4ebc1bf3f..d1b21d08dad 100644 --- a/packages/opencode/src/kilocode/ts-client.ts +++ b/packages/opencode/src/kilocode/ts-client.ts @@ -3,7 +3,8 @@ // instead of spawning a persistent typescript-language-server process. // This drops memory from ~500MB persistent to ~50MB peak (0 idle). -import type { LSPClient } from "../lsp/client" +import { LSPClient } from "../lsp/client" +import { Bus } from "../bus" import { TsCheck } from "./ts-check" import { Log } from "../util/log" import { withTimeout } from "../util/timeout" @@ -28,6 +29,12 @@ export namespace TsClient { for (const [file, diags] of result) { diagnostics.set(file, diags) } + for (const file of result.keys()) { + Bus.publish(LSPClient.Event.Diagnostics, { + path: file, + serverID: client.serverID, + }) + } }) .catch((err) => { log.error("ts check failed", { error: err }) From 207fc8b02244ac2d61c27a65e1e86a60e272ca77 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:06:10 +0000 Subject: [PATCH 21/74] test(vscode): add CI check for missing translation keys Add i18n-keys.test.ts that validates all string-literal t() keys used in the webview, cli-backend, and autocomplete code exist in the corresponding English dictionaries. Runs as part of bun run test:unit in CI. Also adds missing 'session.status.retry' key to the sidebar English dictionary that was caught by the new test. --- .../kilo-vscode/tests/unit/i18n-keys.test.ts | 189 ++++++++++++++++++ .../kilo-vscode/webview-ui/src/i18n/en.ts | 1 + 2 files changed, 190 insertions(+) create mode 100644 packages/kilo-vscode/tests/unit/i18n-keys.test.ts diff --git a/packages/kilo-vscode/tests/unit/i18n-keys.test.ts b/packages/kilo-vscode/tests/unit/i18n-keys.test.ts new file mode 100644 index 00000000000..e4ea2dab5e3 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/i18n-keys.test.ts @@ -0,0 +1,189 @@ +/** + * Translation key validation + * + * Ensures every string-literal translation key passed to a t() function + * actually exists in the corresponding English dictionary. + * + * Three independent key pools are checked: + * 1. Webview (sidebar + agent manager): merged from app, ui, kilo-i18n, agent-manager dicts + * 2. CLI backend (extension-side server-manager): cli-backend/i18n dict + * 3. Autocomplete (extension-side): autocomplete/i18n dict + * + * Dynamic keys (template literals, variables) are intentionally skipped — + * only string literals are validated. + */ + +import { describe, it, expect } from "bun:test" +import { Glob } from "bun" +import path from "node:path" + +// ── Dictionaries ──────────────────────────────────────────────────────────── + +// Webview layer 1: app-local (sidebar) +import { dict as appEn } from "../../webview-ui/src/i18n/en" +// Webview layer 2: upstream UI (@opencode-ai/ui re-exported via @kilocode/kilo-ui) +import { dict as uiEn } from "../../../ui/src/i18n/en" +// Webview layer 3: kilo-i18n overrides +import { dict as kiloEn } from "../../../kilo-i18n/src/en" +// Webview layer 4: agent manager +import { dict as amEn } from "../../webview-ui/agent-manager/i18n/en" + +// Extension-side: CLI backend +import { dict as cliEn } from "../../src/services/cli-backend/i18n/en" +// Extension-side: autocomplete +import { dict as acEn } from "../../src/services/autocomplete/i18n/en" + +const ROOT = path.resolve(import.meta.dir, "../..") + +// Merge webview dictionaries in the same priority order as language.tsx +const webviewKeys = new Set(Object.keys({ ...appEn, ...uiEn, ...kiloEn, ...amEn })) +const cliKeys = new Set(Object.keys(cliEn)) +const acKeys = new Set(Object.keys(acEn)) + +// ── File scanning ─────────────────────────────────────────────────────────── + +interface Missing { + file: string + line: number + key: string +} + +/** + * Regex to match t("key") / t('key') calls. + * + * Captures the string literal argument to any of these patterns: + * t("key") language.t("key") i18n.t("key") + * + * Skips template literals (backticks) and variable arguments. + */ +const T_CALL = /(?:^|[^a-zA-Z_$])t\(\s*(?:["'])([^"']+)["']/g + +function extractKeys(content: string): Array<{ line: number; key: string }> { + const results: Array<{ line: number; key: string }> = [] + const lines = content.split("\n") + for (let i = 0; i < lines.length; i++) { + const text = lines[i] + let match: RegExpExecArray | null + T_CALL.lastIndex = 0 + while ((match = T_CALL.exec(text)) !== null) { + const key = match[1] + if (key) results.push({ line: i + 1, key }) + } + } + return results +} + +async function collectFiles(glob: Glob, dir: string): Promise { + const files: string[] = [] + for await (const entry of glob.scan({ cwd: dir, absolute: true })) { + files.push(entry) + } + return files +} + +// ── Webview files ─────────────────────────────────────────────────────────── + +async function findWebviewMissing(): Promise { + const glob = new Glob("**/*.{ts,tsx}") + const dirs = [path.join(ROOT, "webview-ui/src"), path.join(ROOT, "webview-ui/agent-manager")] + + const files: string[] = [] + for (const dir of dirs) { + files.push(...(await collectFiles(glob, dir))) + } + + // Exclude i18n dictionary files themselves and storybook files + const filtered = files.filter((f) => !f.includes("/i18n/") && !f.includes(".stories.") && !f.includes("/stories/")) + + const missing: Missing[] = [] + for (const file of filtered) { + const content = await Bun.file(file).text() + for (const { line, key } of extractKeys(content)) { + if (!webviewKeys.has(key)) { + missing.push({ file: path.relative(ROOT, file), line, key }) + } + } + } + return missing +} + +// ── CLI backend files ─────────────────────────────────────────────────────── + +async function findCliBackendMissing(): Promise { + const glob = new Glob("**/*.ts") + const dir = path.join(ROOT, "src/services/cli-backend") + + const files = (await collectFiles(glob, dir)).filter((f) => !f.includes("/i18n/")) + + const missing: Missing[] = [] + for (const file of files) { + const content = await Bun.file(file).text() + for (const { line, key } of extractKeys(content)) { + if (!cliKeys.has(key)) { + missing.push({ file: path.relative(ROOT, file), line, key }) + } + } + } + return missing +} + +// ── Autocomplete files ────────────────────────────────────────────────────── + +async function findAutocompleteMissing(): Promise { + const glob = new Glob("**/*.ts") + const dir = path.join(ROOT, "src/services/autocomplete") + + const files = (await collectFiles(glob, dir)).filter((f) => !f.includes("/i18n/") && !f.includes("/shims/")) + + const missing: Missing[] = [] + for (const file of files) { + const content = await Bun.file(file).text() + for (const { line, key } of extractKeys(content)) { + if (!acKeys.has(key)) { + missing.push({ file: path.relative(ROOT, file), line, key }) + } + } + } + return missing +} + +// ── Tests ─────────────────────────────────────────────────────────────────── + +function formatReport(missing: Missing[]): string { + return missing.map((m) => ` ${m.file}:${m.line} — "${m.key}"`).join("\n") +} + +describe("i18n key validation — no missing translation keys", () => { + it("webview: all t() string literal keys exist in merged dictionaries", async () => { + const missing = await findWebviewMissing() + if (missing.length > 0) { + expect( + missing, + `Found ${missing.length} translation key(s) not present in any dictionary:\n${formatReport(missing)}`, + ).toEqual([]) + } + expect(missing).toEqual([]) + }) + + it("cli-backend: all t() string literal keys exist in cli-backend dictionary", async () => { + const missing = await findCliBackendMissing() + if (missing.length > 0) { + expect( + missing, + `Found ${missing.length} translation key(s) not present in cli-backend dictionary:\n${formatReport(missing)}`, + ).toEqual([]) + } + expect(missing).toEqual([]) + }) + + it("autocomplete: all t() string literal keys exist in autocomplete dictionary", async () => { + const missing = await findAutocompleteMissing() + if (missing.length > 0) { + expect( + missing, + `Found ${missing.length} translation key(s) not present in autocomplete dictionary:\n${formatReport(missing)}`, + ).toEqual([]) + } + expect(missing).toEqual([]) + }) +}) diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index 23eef2801c6..72872fb9ae2 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -838,6 +838,7 @@ export const dict = { "Kilo Code is an AI coding assistant. Ask it to build features, fix bugs, or explain your codebase.", "session.messages.scrollToBottom": "Scroll to bottom", "session.status.writingResponse": "Writing response...", + "session.status.retry": "Retrying…", "session.status.retrying": "Retrying (attempt {{ attempt }})… {{ message }}", "session.status.working": "Working...", From b2a59fe5f677ef84da8d6388ee49b0598fb9d8ae Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:34:20 +0000 Subject: [PATCH 22/74] fix(vscode): reload autocomplete when CLI backend connection state changes AutocompleteServiceManager only called model.reload() once at startup, before the CLI backend had finished connecting. Since it never subscribed to connection state changes, hasValidCredentials() permanently returned false, causing the 'No autocomplete model configured' error even when the user was logged into Kilo Gateway with credits. Subscribe to connectionService.onStateChange() so the autocomplete model and status bar re-evaluate whenever the backend connects or disconnects. --- .../autocomplete/AutocompleteServiceManager.ts | 11 +++++++++++ .../__tests__/AutocompleteServiceManager.spec.ts | 4 +++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts b/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts index 4aaad29e7c6..6fb130edcdd 100644 --- a/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts +++ b/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts @@ -59,6 +59,7 @@ export class AutocompleteServiceManager { public readonly codeActionProvider: AutocompleteCodeActionProvider public readonly inlineCompletionProvider: AutocompleteInlineCompletionProvider private inlineCompletionProviderDisposable: vscode.Disposable | null = null + private unsubscribeState: (() => void) | null = null constructor(context: vscode.ExtensionContext, connectionService: KiloConnectionService) { if (AutocompleteServiceManager._instance) { @@ -86,6 +87,12 @@ export class AutocompleteServiceManager { new AutocompleteTelemetry(), ) + // Reload when CLI backend connection state changes so autocomplete + // picks up the connected state even if it wasn't ready at startup. + this.unsubscribeState = connectionService.onStateChange(() => { + void this.load() + }) + void this.load() } @@ -360,6 +367,10 @@ export class AutocompleteServiceManager { this.snoozeTimer = null } + // Unsubscribe from connection state changes + this.unsubscribeState?.() + this.unsubscribeState = null + // Dispose inline completion provider registration if (this.inlineCompletionProviderDisposable) { this.inlineCompletionProviderDisposable.dispose() diff --git a/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteServiceManager.spec.ts b/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteServiceManager.spec.ts index 456eaa5fbcd..11fb9ff0042 100644 --- a/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteServiceManager.spec.ts +++ b/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteServiceManager.spec.ts @@ -169,7 +169,9 @@ async function createManager(): Promise { postStateToWebview: vi.fn().mockResolvedValue(undefined), } - const manager = new AutocompleteServiceManager(context, cline as any) + const connection = { onStateChange: vi.fn().mockReturnValue(() => {}), ...cline } + + const manager = new AutocompleteServiceManager(context, connection as any) await manager.load() From b74fe9baffe7795e16bc479e3fdfb651df5c5a3a Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:36:56 +0000 Subject: [PATCH 23/74] test(vscode): add locale completeness checks and fix missing translations Extend i18n-keys.test.ts with locale completeness validation that ensures every English key has a corresponding translation in all locale files. Covers sidebar app, kilo-i18n, and cli-backend dictionaries. Add missing keys to all 15 non-English locale files: - session.status.retry - prompt.placeholder.error - settings.providers.modeModels - settings.providers.modeModels.description - error.details.show (Norwegian only) --- .../kilo-vscode/tests/unit/i18n-keys.test.ts | 192 ++++++++++++++++-- .../kilo-vscode/webview-ui/src/i18n/ar.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/br.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/bs.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/da.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/de.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/es.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/fr.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/ja.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/ko.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/no.ts | 6 + .../kilo-vscode/webview-ui/src/i18n/pl.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/ru.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/th.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/zh.ts | 5 + .../kilo-vscode/webview-ui/src/i18n/zht.ts | 5 + 16 files changed, 256 insertions(+), 12 deletions(-) diff --git a/packages/kilo-vscode/tests/unit/i18n-keys.test.ts b/packages/kilo-vscode/tests/unit/i18n-keys.test.ts index e4ea2dab5e3..fb40b104d9c 100644 --- a/packages/kilo-vscode/tests/unit/i18n-keys.test.ts +++ b/packages/kilo-vscode/tests/unit/i18n-keys.test.ts @@ -1,13 +1,14 @@ /** * Translation key validation * - * Ensures every string-literal translation key passed to a t() function - * actually exists in the corresponding English dictionary. + * 1. Ensures every string-literal translation key passed to a t() function + * actually exists in the corresponding English dictionary. + * 2. Ensures every English key has a translation in all other locale files. * * Three independent key pools are checked: - * 1. Webview (sidebar + agent manager): merged from app, ui, kilo-i18n, agent-manager dicts - * 2. CLI backend (extension-side server-manager): cli-backend/i18n dict - * 3. Autocomplete (extension-side): autocomplete/i18n dict + * - Webview (sidebar + agent manager): merged from app, ui, kilo-i18n, agent-manager dicts + * - CLI backend (extension-side server-manager): cli-backend/i18n dict + * - Autocomplete (extension-side): autocomplete/i18n dict * * Dynamic keys (template literals, variables) are intentionally skipped — * only string literals are validated. @@ -17,24 +18,132 @@ import { describe, it, expect } from "bun:test" import { Glob } from "bun" import path from "node:path" -// ── Dictionaries ──────────────────────────────────────────────────────────── +// ── Webview dictionaries ──────────────────────────────────────────────────── -// Webview layer 1: app-local (sidebar) +// Layer 1: app-local (sidebar) import { dict as appEn } from "../../webview-ui/src/i18n/en" -// Webview layer 2: upstream UI (@opencode-ai/ui re-exported via @kilocode/kilo-ui) +import { dict as appZh } from "../../webview-ui/src/i18n/zh" +import { dict as appZht } from "../../webview-ui/src/i18n/zht" +import { dict as appKo } from "../../webview-ui/src/i18n/ko" +import { dict as appDe } from "../../webview-ui/src/i18n/de" +import { dict as appEs } from "../../webview-ui/src/i18n/es" +import { dict as appFr } from "../../webview-ui/src/i18n/fr" +import { dict as appDa } from "../../webview-ui/src/i18n/da" +import { dict as appJa } from "../../webview-ui/src/i18n/ja" +import { dict as appPl } from "../../webview-ui/src/i18n/pl" +import { dict as appRu } from "../../webview-ui/src/i18n/ru" +import { dict as appAr } from "../../webview-ui/src/i18n/ar" +import { dict as appNo } from "../../webview-ui/src/i18n/no" +import { dict as appBr } from "../../webview-ui/src/i18n/br" +import { dict as appTh } from "../../webview-ui/src/i18n/th" +import { dict as appBs } from "../../webview-ui/src/i18n/bs" + +// Layer 2: upstream UI (@opencode-ai/ui re-exported via @kilocode/kilo-ui) import { dict as uiEn } from "../../../ui/src/i18n/en" -// Webview layer 3: kilo-i18n overrides + +// Layer 3: kilo-i18n overrides import { dict as kiloEn } from "../../../kilo-i18n/src/en" -// Webview layer 4: agent manager +import { dict as kiloZh } from "../../../kilo-i18n/src/zh" +import { dict as kiloZht } from "../../../kilo-i18n/src/zht" +import { dict as kiloKo } from "../../../kilo-i18n/src/ko" +import { dict as kiloDe } from "../../../kilo-i18n/src/de" +import { dict as kiloEs } from "../../../kilo-i18n/src/es" +import { dict as kiloFr } from "../../../kilo-i18n/src/fr" +import { dict as kiloDa } from "../../../kilo-i18n/src/da" +import { dict as kiloJa } from "../../../kilo-i18n/src/ja" +import { dict as kiloPl } from "../../../kilo-i18n/src/pl" +import { dict as kiloRu } from "../../../kilo-i18n/src/ru" +import { dict as kiloAr } from "../../../kilo-i18n/src/ar" +import { dict as kiloNo } from "../../../kilo-i18n/src/no" +import { dict as kiloBr } from "../../../kilo-i18n/src/br" +import { dict as kiloTh } from "../../../kilo-i18n/src/th" +import { dict as kiloBs } from "../../../kilo-i18n/src/bs" + +// Layer 4: agent manager (locale alignment already tested in agent-manager-i18n-split.test.ts) import { dict as amEn } from "../../webview-ui/agent-manager/i18n/en" -// Extension-side: CLI backend +// ── Extension-side dictionaries ───────────────────────────────────────────── + import { dict as cliEn } from "../../src/services/cli-backend/i18n/en" -// Extension-side: autocomplete +import { dict as cliZh } from "../../src/services/cli-backend/i18n/zh" +import { dict as cliZht } from "../../src/services/cli-backend/i18n/zht" +import { dict as cliDe } from "../../src/services/cli-backend/i18n/de" +import { dict as cliEs } from "../../src/services/cli-backend/i18n/es" +import { dict as cliFr } from "../../src/services/cli-backend/i18n/fr" +import { dict as cliDa } from "../../src/services/cli-backend/i18n/da" +import { dict as cliJa } from "../../src/services/cli-backend/i18n/ja" +import { dict as cliKo } from "../../src/services/cli-backend/i18n/ko" +import { dict as cliPl } from "../../src/services/cli-backend/i18n/pl" +import { dict as cliRu } from "../../src/services/cli-backend/i18n/ru" +import { dict as cliAr } from "../../src/services/cli-backend/i18n/ar" +import { dict as cliNo } from "../../src/services/cli-backend/i18n/no" +import { dict as cliBr } from "../../src/services/cli-backend/i18n/br" +import { dict as cliTh } from "../../src/services/cli-backend/i18n/th" +import { dict as cliBs } from "../../src/services/cli-backend/i18n/bs" + import { dict as acEn } from "../../src/services/autocomplete/i18n/en" +// ── Locale maps ───────────────────────────────────────────────────────────── + const ROOT = path.resolve(import.meta.dir, "../..") +const appLocales: Record> = { + en: appEn, + zh: appZh, + zht: appZht, + ko: appKo, + de: appDe, + es: appEs, + fr: appFr, + da: appDa, + ja: appJa, + pl: appPl, + ru: appRu, + ar: appAr, + no: appNo, + br: appBr, + th: appTh, + bs: appBs, +} + +const kiloLocales: Record> = { + en: kiloEn, + zh: kiloZh, + zht: kiloZht, + ko: kiloKo, + de: kiloDe, + es: kiloEs, + fr: kiloFr, + da: kiloDa, + ja: kiloJa, + pl: kiloPl, + ru: kiloRu, + ar: kiloAr, + no: kiloNo, + br: kiloBr, + th: kiloTh, + bs: kiloBs, +} + +const cliLocales: Record> = { + en: cliEn, + zh: cliZh, + zht: cliZht, + de: cliDe, + es: cliEs, + fr: cliFr, + da: cliDa, + ja: cliJa, + ko: cliKo, + pl: cliPl, + ru: cliRu, + ar: cliAr, + no: cliNo, + br: cliBr, + th: cliTh, + bs: cliBs, +} + // Merge webview dictionaries in the same priority order as language.tsx const webviewKeys = new Set(Object.keys({ ...appEn, ...uiEn, ...kiloEn, ...amEn })) const cliKeys = new Set(Object.keys(cliEn)) @@ -147,6 +256,30 @@ async function findAutocompleteMissing(): Promise { return missing } +// ── Locale completeness helpers ───────────────────────────────────────────── + +function findMissingLocaleKeys( + en: Record, + locales: Record>, +): Array<{ locale: string; key: string }> { + const base = Object.keys(en) + const results: Array<{ locale: string; key: string }> = [] + for (const [locale, dict] of Object.entries(locales)) { + if (locale === "en") continue + const keys = new Set(Object.keys(dict)) + for (const key of base) { + if (!keys.has(key)) { + results.push({ locale, key }) + } + } + } + return results +} + +function formatLocaleReport(items: Array<{ locale: string; key: string }>): string { + return items.map((m) => ` [${m.locale}] "${m.key}"`).join("\n") +} + // ── Tests ─────────────────────────────────────────────────────────────────── function formatReport(missing: Missing[]): string { @@ -187,3 +320,38 @@ describe("i18n key validation — no missing translation keys", () => { expect(missing).toEqual([]) }) }) + +describe("i18n locale completeness — every English key exists in all locales", () => { + it("sidebar app: every English key has a translation in all locales", () => { + const missing = findMissingLocaleKeys(appEn, appLocales) + if (missing.length > 0) { + expect( + missing, + `Found ${missing.length} missing sidebar translation(s):\n${formatLocaleReport(missing)}`, + ).toEqual([]) + } + expect(missing).toEqual([]) + }) + + it("kilo-i18n: every English key has a translation in all locales", () => { + const missing = findMissingLocaleKeys(kiloEn, kiloLocales) + if (missing.length > 0) { + expect( + missing, + `Found ${missing.length} missing kilo-i18n translation(s):\n${formatLocaleReport(missing)}`, + ).toEqual([]) + } + expect(missing).toEqual([]) + }) + + it("cli-backend: every English key has a translation in all locales", () => { + const missing = findMissingLocaleKeys(cliEn, cliLocales) + if (missing.length > 0) { + expect( + missing, + `Found ${missing.length} missing cli-backend translation(s):\n${formatLocaleReport(missing)}`, + ).toEqual([]) + } + expect(missing).toEqual([]) + }) +}) diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index c4fa3f25acb..f0decff554e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -680,6 +680,9 @@ export const dict = { "settings.providers.tag.config": "التكوين", "settings.providers.tag.custom": "مخصص", "settings.providers.tag.other": "أخرى", + "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels.description": + "Override the default model for specific modes. If not set, the global default model is used.", "settings.models.title": "النماذج", "settings.models.description": "ستكون إعدادات النموذج قابلة للتكوين هنا.", "settings.agents.title": "الوكلاء", @@ -827,12 +830,14 @@ export const dict = { "Kilo Code هو مساعد ترميز بالذكاء الاصطناعي. اطلب منه بناء ميزات أو إصلاح أخطاء أو شرح قاعدة الشيفرة.", "session.messages.scrollToBottom": "التمرير إلى الأسفل", "session.status.writingResponse": "...جارٍ كتابة الرد", + "session.status.retry": "Retrying…", "session.status.retrying": "...إعادة المحاولة (المحاولة {{ attempt }})… {{ message }}", "session.status.working": "...جارٍ العمل", "dialog.model.noProviders": "لا يوجد موفرون", "prompt.placeholder.connecting": "جارٍ الاتصال بالخادم...", + "prompt.placeholder.error": "", "prompt.placeholder.default": "اكتب رسالة... (Enter للإرسال، Shift+Enter لسطر جديد)", "context.usage.sessionCost": "تكلفة الجلسة", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index e8a16504af0..79a171678e8 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -687,6 +687,9 @@ export const dict = { "settings.providers.tag.config": "Configuração", "settings.providers.tag.custom": "Personalizado", "settings.providers.tag.other": "Outro", + "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels.description": + "Override the default model for specific modes. If not set, the global default model is used.", "settings.models.title": "Modelos", "settings.models.description": "Configurações de modelos estarão disponíveis aqui.", "settings.agents.title": "Agentes", @@ -835,12 +838,14 @@ export const dict = { "Kilo Code é um assistente de programação com IA. Peça para criar funcionalidades, corrigir bugs ou explicar sua base de código.", "session.messages.scrollToBottom": "Rolar para o final", "session.status.writingResponse": "Escrevendo resposta…", + "session.status.retry": "Retrying…", "session.status.retrying": "Tentando novamente (tentativa {{ attempt }})… {{ message }}", "session.status.working": "Trabalhando…", "dialog.model.noProviders": "Nenhum provedor", "prompt.placeholder.connecting": "Conectando ao servidor...", + "prompt.placeholder.error": "", "prompt.placeholder.default": "Digite uma mensagem... (Enter para enviar, Shift+Enter para nova linha)", "context.usage.sessionCost": "Custo da sessão", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index 5097fd6a307..9c1bef45bac 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -691,6 +691,9 @@ export const dict = { "settings.providers.tag.config": "Konfiguracija", "settings.providers.tag.custom": "Prilagođeno", "settings.providers.tag.other": "Ostalo", + "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels.description": + "Override the default model for specific modes. If not set, the global default model is used.", "settings.models.title": "Modeli", "settings.models.description": "Postavke modela će se ovdje moći podešavati.", "settings.agents.title": "Agenti", @@ -838,12 +841,14 @@ export const dict = { "Kilo Code je AI asistent za programiranje. Zatražite da gradi funkcionalnosti, ispravlja greške ili objasni vašu bazu koda.", "session.messages.scrollToBottom": "Pomakni se na dno", "session.status.writingResponse": "Pisanje odgovora…", + "session.status.retry": "Retrying…", "session.status.retrying": "Ponovni pokušaj (pokušaj {{ attempt }})… {{ message }}", "session.status.working": "Radim…", "dialog.model.noProviders": "Nema pružatelja", "prompt.placeholder.connecting": "Povezivanje na server...", + "prompt.placeholder.error": "", "prompt.placeholder.default": "Unesite poruku... (Enter za slanje, Shift+Enter za novi red)", "context.usage.sessionCost": "Cijena sesije", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index 1e979d37779..e7de1399ffc 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -686,6 +686,9 @@ export const dict = { "settings.providers.tag.config": "Konfiguration", "settings.providers.tag.custom": "Brugerdefineret", "settings.providers.tag.other": "Andet", + "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels.description": + "Override the default model for specific modes. If not set, the global default model is used.", "settings.models.title": "Modeller", "settings.models.description": "Modelindstillinger vil kunne konfigureres her.", "settings.agents.title": "Agenter", @@ -833,12 +836,14 @@ export const dict = { "Kilo Code er en AI-kodningsassistent. Bed den om at bygge funktioner, rette fejl eller forklare din kodebase.", "session.messages.scrollToBottom": "Rul til bunden", "session.status.writingResponse": "Skriver svar…", + "session.status.retry": "Retrying…", "session.status.retrying": "Prøver igen (forsøg {{ attempt }})… {{ message }}", "session.status.working": "Arbejder…", "dialog.model.noProviders": "Ingen udbydere", "prompt.placeholder.connecting": "Opretter forbindelse til server...", + "prompt.placeholder.error": "", "prompt.placeholder.default": "Skriv en besked... (Enter for at sende, Shift+Enter for ny linje)", "context.usage.sessionCost": "Sessionsomkostning", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 99f0f585e0a..c9b2a01c7a1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -696,6 +696,9 @@ export const dict = { "settings.providers.tag.config": "Konfiguration", "settings.providers.tag.custom": "Benutzerdefiniert", "settings.providers.tag.other": "Andere", + "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels.description": + "Override the default model for specific modes. If not set, the global default model is used.", "settings.models.title": "Modelle", "settings.models.description": "Modelleinstellungen können hier konfiguriert werden.", "settings.agents.title": "Agenten", @@ -843,12 +846,14 @@ export const dict = { "Kilo Code ist ein KI-Programmierassistent. Bitten Sie ihn, Funktionen zu erstellen, Fehler zu beheben oder Ihre Codebasis zu erklären.", "session.messages.scrollToBottom": "Nach unten scrollen", "session.status.writingResponse": "Antwort wird geschrieben…", + "session.status.retry": "Retrying…", "session.status.retrying": "Erneuter Versuch ({{ attempt }})… {{ message }}", "session.status.working": "Wird bearbeitet…", "dialog.model.noProviders": "Keine Anbieter", "prompt.placeholder.connecting": "Verbindung zum Server wird hergestellt...", + "prompt.placeholder.error": "", "prompt.placeholder.default": "Nachricht eingeben... (Enter zum Senden, Shift+Enter für neue Zeile)", "context.usage.sessionCost": "Sitzungskosten", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index 47d663824f3..d49ca0d1e04 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -692,6 +692,9 @@ export const dict = { "settings.providers.tag.config": "Configuración", "settings.providers.tag.custom": "Personalizado", "settings.providers.tag.other": "Otro", + "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels.description": + "Override the default model for specific modes. If not set, the global default model is used.", "settings.models.title": "Modelos", "settings.models.description": "La configuración de modelos estará disponible aquí.", "settings.agents.title": "Agentes", @@ -840,12 +843,14 @@ export const dict = { "Kilo Code es un asistente de programación con IA. Pídele que cree funcionalidades, corrija errores o explique tu código.", "session.messages.scrollToBottom": "Desplazar al final", "session.status.writingResponse": "Escribiendo respuesta…", + "session.status.retry": "Retrying…", "session.status.retrying": "Reintentando (intento {{ attempt }})… {{ message }}", "session.status.working": "Trabajando…", "dialog.model.noProviders": "Sin proveedores", "prompt.placeholder.connecting": "Conectando al servidor...", + "prompt.placeholder.error": "", "prompt.placeholder.default": "Escribe un mensaje... (Enter para enviar, Shift+Enter para nueva línea)", "context.usage.sessionCost": "Coste de la sesión", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index da0135db55d..35f02b3a486 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -696,6 +696,9 @@ export const dict = { "settings.providers.tag.config": "Configuration", "settings.providers.tag.custom": "Personnalisé", "settings.providers.tag.other": "Autre", + "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels.description": + "Override the default model for specific modes. If not set, the global default model is used.", "settings.models.title": "Modèles", "settings.models.description": "Les paramètres des modèles seront configurables ici.", "settings.agents.title": "Agents", @@ -845,12 +848,14 @@ export const dict = { "Kilo Code est un assistant de programmation IA. Demandez-lui de créer des fonctionnalités, corriger des bugs ou expliquer votre code.", "session.messages.scrollToBottom": "Défiler vers le bas", "session.status.writingResponse": "Rédaction de la réponse…", + "session.status.retry": "Retrying…", "session.status.retrying": "Nouvelle tentative (essai {{ attempt }})… {{ message }}", "session.status.working": "En cours…", "dialog.model.noProviders": "Aucun fournisseur", "prompt.placeholder.connecting": "Connexion au serveur...", + "prompt.placeholder.error": "", "prompt.placeholder.default": "Tapez un message... (Entrée pour envoyer, Maj+Entrée pour un saut de ligne)", "context.usage.sessionCost": "Coût de la session", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 7c7e68c0ced..5d91287959f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -685,6 +685,9 @@ export const dict = { "settings.providers.tag.config": "設定", "settings.providers.tag.custom": "カスタム", "settings.providers.tag.other": "その他", + "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels.description": + "Override the default model for specific modes. If not set, the global default model is used.", "settings.models.title": "モデル", "settings.models.description": "モデル設定はここで構成できます。", "settings.agents.title": "エージェント", @@ -832,12 +835,14 @@ export const dict = { "Kilo CodeはAIコーディングアシスタントです。機能の構築、バグの修正、コードベースの説明を依頼できます。", "session.messages.scrollToBottom": "下にスクロール", "session.status.writingResponse": "応答を作成中…", + "session.status.retry": "Retrying…", "session.status.retrying": "再試行中({{ attempt }}回目)… {{ message }}", "session.status.working": "作業中…", "dialog.model.noProviders": "プロバイダーなし", "prompt.placeholder.connecting": "サーバーに接続中...", + "prompt.placeholder.error": "", "prompt.placeholder.default": "メッセージを入力... (Enterで送信、Shift+Enterで改行)", "context.usage.sessionCost": "セッションコスト", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index db088d27173..a8464a71f20 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -686,6 +686,9 @@ export const dict = { "settings.providers.tag.config": "구성", "settings.providers.tag.custom": "사용자 지정", "settings.providers.tag.other": "기타", + "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels.description": + "Override the default model for specific modes. If not set, the global default model is used.", "settings.models.title": "모델", "settings.models.description": "모델 설정은 여기서 구성할 수 있습니다.", "settings.agents.title": "에이전트", @@ -832,12 +835,14 @@ export const dict = { "Kilo Code는 AI 코딩 어시스턴트입니다. 기능 구축, 버그 수정 또는 코드베이스 설명을 요청하세요.", "session.messages.scrollToBottom": "하단으로 스크롤", "session.status.writingResponse": "응답 작성 중...", + "session.status.retry": "Retrying…", "session.status.retrying": "재시도 중 ({{ attempt }}번째 시도)… {{ message }}", "session.status.working": "작업 중...", "dialog.model.noProviders": "공급자 없음", "prompt.placeholder.connecting": "서버에 연결 중...", + "prompt.placeholder.error": "", "prompt.placeholder.default": "메시지를 입력하세요... (Enter로 전송, Shift+Enter로 줄 바꿈)", "context.usage.sessionCost": "세션 비용", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 5a5205d4532..e5c707a83ae 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -398,6 +398,7 @@ export const dict = { "error.page.title": "Noe gikk galt", "error.page.description": "Det oppstod en feil under lasting av applikasjonen.", "error.page.details.label": "Feildetaljer", + "error.details.show": "Details", "error.page.action.restart": "Start på nytt", "error.page.action.checking": "Sjekker...", "error.page.action.checkUpdates": "Se etter oppdateringer", @@ -689,6 +690,9 @@ export const dict = { "settings.providers.tag.config": "Konfigurasjon", "settings.providers.tag.custom": "Tilpasset", "settings.providers.tag.other": "Annet", + "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels.description": + "Override the default model for specific modes. If not set, the global default model is used.", "settings.models.title": "Modeller", "settings.models.description": "Modellinnstillinger vil kunne konfigureres her.", "settings.agents.title": "Agenter", @@ -836,12 +840,14 @@ export const dict = { "Kilo Code er en AI-kodingsassistent. Be den om å bygge funksjoner, fikse feil eller forklare kodebasen din.", "session.messages.scrollToBottom": "Rull til bunnen", "session.status.writingResponse": "Skriver svar…", + "session.status.retry": "Retrying…", "session.status.retrying": "Prøver på nytt (forsøk {{ attempt }})… {{ message }}", "session.status.working": "Arbeider…", "dialog.model.noProviders": "Ingen leverandører", "prompt.placeholder.connecting": "Kobler til server...", + "prompt.placeholder.error": "", "prompt.placeholder.default": "Skriv en melding... (Enter for å sende, Shift+Enter for ny linje)", "context.usage.sessionCost": "Sesjonskostnad", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 97923dc9774..0d6dba9c3dc 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -689,6 +689,9 @@ export const dict = { "settings.providers.tag.config": "Konfiguracja", "settings.providers.tag.custom": "Niestandardowe", "settings.providers.tag.other": "Inne", + "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels.description": + "Override the default model for specific modes. If not set, the global default model is used.", "settings.models.title": "Modele", "settings.models.description": "Ustawienia modeli będą tutaj konfigurowalne.", "settings.agents.title": "Agenci", @@ -835,12 +838,14 @@ export const dict = { "Kilo Code to asystent programowania AI. Poproś go o tworzenie funkcji, naprawę błędów lub wyjaśnienie bazy kodu.", "session.messages.scrollToBottom": "Przewiń na dół", "session.status.writingResponse": "Pisanie odpowiedzi…", + "session.status.retry": "Retrying…", "session.status.retrying": "Ponawiam próbę ({{ attempt }})… {{ message }}", "session.status.working": "Pracuję…", "dialog.model.noProviders": "Brak dostawców", "prompt.placeholder.connecting": "Łączenie z serwerem...", + "prompt.placeholder.error": "", "prompt.placeholder.default": "Wpisz wiadomość... (Enter, aby wysłać, Shift+Enter dla nowej linii)", "context.usage.sessionCost": "Koszt sesji", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 08100091c9e..5192306a677 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -692,6 +692,9 @@ export const dict = { "settings.providers.tag.config": "Конфигурация", "settings.providers.tag.custom": "Пользовательский", "settings.providers.tag.other": "Другое", + "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels.description": + "Override the default model for specific modes. If not set, the global default model is used.", "settings.models.title": "Модели", "settings.models.description": "Настройки моделей будут доступны здесь.", "settings.agents.title": "Агенты", @@ -839,12 +842,14 @@ export const dict = { "Kilo Code — это AI-помощник для программирования. Попросите его создать функции, исправить ошибки или объяснить вашу кодовую базу.", "session.messages.scrollToBottom": "Прокрутить вниз", "session.status.writingResponse": "Пишу ответ…", + "session.status.retry": "Retrying…", "session.status.retrying": "Повторная попытка ({{ attempt }})… {{ message }}", "session.status.working": "Работаю…", "dialog.model.noProviders": "Нет провайдеров", "prompt.placeholder.connecting": "Подключение к серверу...", + "prompt.placeholder.error": "", "prompt.placeholder.default": "Введите сообщение... (Enter для отправки, Shift+Enter для новой строки)", "context.usage.sessionCost": "Стоимость сессии", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index cdfd4b6bf06..0241f742947 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -683,6 +683,9 @@ export const dict = { "settings.providers.tag.config": "กำหนดค่า", "settings.providers.tag.custom": "กำหนดเอง", "settings.providers.tag.other": "อื่น ๆ", + "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels.description": + "Override the default model for specific modes. If not set, the global default model is used.", "settings.models.title": "โมเดล", "settings.models.description": "การตั้งค่าโมเดลจะสามารถกำหนดค่าได้ที่นี่", "settings.agents.title": "เอเจนต์", @@ -828,12 +831,14 @@ export const dict = { "session.messages.welcome": "Kilo Code เป็นผู้ช่วยเขียนโค้ด AI ขอให้สร้างฟีเจอร์ แก้ไขบัก หรืออธิบายโค้ดเบสของคุณ", "session.messages.scrollToBottom": "เลื่อนไปด้านล่าง", "session.status.writingResponse": "กำลังเขียนคำตอบ...", + "session.status.retry": "Retrying…", "session.status.retrying": "กำลังลองใหม่ (ครั้งที่ {{ attempt }})… {{ message }}", "session.status.working": "กำลังทำงาน...", "dialog.model.noProviders": "ไม่มีผู้ให้บริการ", "prompt.placeholder.connecting": "กำลังเชื่อมต่อกับเซิร์ฟเวอร์...", + "prompt.placeholder.error": "", "prompt.placeholder.default": "พิมพ์ข้อความ... (Enter เพื่อส่ง, Shift+Enter เพื่อขึ้นบรรทัดใหม่)", "context.usage.sessionCost": "ค่าใช้จ่ายเซสชัน", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index ab749eb7c67..03ed924a7de 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -676,6 +676,9 @@ export const dict = { "settings.providers.tag.config": "配置", "settings.providers.tag.custom": "自定义", "settings.providers.tag.other": "其他", + "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels.description": + "Override the default model for specific modes. If not set, the global default model is used.", "settings.models.title": "模型", "settings.models.description": "模型设置将在此处可配置。", "settings.agents.title": "智能体", @@ -821,12 +824,14 @@ export const dict = { "session.messages.welcome": "Kilo Code 是一个 AI 编程助手。让它帮你构建功能、修复 bug 或解释代码库。", "session.messages.scrollToBottom": "滚动到底部", "session.status.writingResponse": "正在撰写回复…", + "session.status.retry": "Retrying…", "session.status.retrying": "正在重试(第 {{ attempt }} 次)… {{ message }}", "session.status.working": "处理中…", "dialog.model.noProviders": "无供应商", "prompt.placeholder.connecting": "正在连接服务器...", + "prompt.placeholder.error": "", "prompt.placeholder.default": "输入消息... (Enter 发送,Shift+Enter 换行)", "context.usage.sessionCost": "会话费用", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index 07ae7f89231..54aa5214ba6 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -678,6 +678,9 @@ export const dict = { "settings.providers.tag.config": "設定", "settings.providers.tag.custom": "自訂", "settings.providers.tag.other": "其他", + "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels.description": + "Override the default model for specific modes. If not set, the global default model is used.", "settings.models.title": "模型", "settings.models.description": "可在此調整模型設定。", "settings.agents.title": "Agents", @@ -823,12 +826,14 @@ export const dict = { "session.messages.welcome": "Kilo Code 是一個 AI 程式設計助手。讓它幫你建構功能、修復 bug 或解釋程式碼庫。", "session.messages.scrollToBottom": "捲動至底部", "session.status.writingResponse": "正在撰寫回覆…", + "session.status.retry": "Retrying…", "session.status.retrying": "正在重試(第 {{ attempt }} 次)… {{ message }}", "session.status.working": "處理中…", "dialog.model.noProviders": "沒有供應商", "prompt.placeholder.connecting": "正在連線至伺服器...", + "prompt.placeholder.error": "", "prompt.placeholder.default": "輸入訊息... (Enter 送出,Shift+Enter 換行)", "context.usage.sessionCost": "工作階段費用", From ee3a6d61431b8692333816be9777c6e5f86b7c0a Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:50:14 +0000 Subject: [PATCH 24/74] fix(vscode): address i18n test review feedback - Fix multiline t() detection: run regex on full file content with dotAll flag instead of line-by-line, so t(\n "key"\n) calls are captured - Add shared UI (packages/ui/src/i18n) locale completeness validation to close the coverage gap for the merged webview dictionaries --- .../kilo-vscode/tests/unit/i18n-keys.test.ts | 80 +++++++++++++++---- 1 file changed, 65 insertions(+), 15 deletions(-) diff --git a/packages/kilo-vscode/tests/unit/i18n-keys.test.ts b/packages/kilo-vscode/tests/unit/i18n-keys.test.ts index fb40b104d9c..3410f5a2ad3 100644 --- a/packages/kilo-vscode/tests/unit/i18n-keys.test.ts +++ b/packages/kilo-vscode/tests/unit/i18n-keys.test.ts @@ -40,6 +40,21 @@ import { dict as appBs } from "../../webview-ui/src/i18n/bs" // Layer 2: upstream UI (@opencode-ai/ui re-exported via @kilocode/kilo-ui) import { dict as uiEn } from "../../../ui/src/i18n/en" +import { dict as uiZh } from "../../../ui/src/i18n/zh" +import { dict as uiZht } from "../../../ui/src/i18n/zht" +import { dict as uiKo } from "../../../ui/src/i18n/ko" +import { dict as uiDe } from "../../../ui/src/i18n/de" +import { dict as uiEs } from "../../../ui/src/i18n/es" +import { dict as uiFr } from "../../../ui/src/i18n/fr" +import { dict as uiDa } from "../../../ui/src/i18n/da" +import { dict as uiJa } from "../../../ui/src/i18n/ja" +import { dict as uiPl } from "../../../ui/src/i18n/pl" +import { dict as uiRu } from "../../../ui/src/i18n/ru" +import { dict as uiAr } from "../../../ui/src/i18n/ar" +import { dict as uiNo } from "../../../ui/src/i18n/no" +import { dict as uiBr } from "../../../ui/src/i18n/br" +import { dict as uiTh } from "../../../ui/src/i18n/th" +import { dict as uiBs } from "../../../ui/src/i18n/bs" // Layer 3: kilo-i18n overrides import { dict as kiloEn } from "../../../kilo-i18n/src/en" @@ -125,6 +140,25 @@ const kiloLocales: Record> = { bs: kiloBs, } +const uiLocales: Record> = { + en: uiEn, + zh: uiZh, + zht: uiZht, + ko: uiKo, + de: uiDe, + es: uiEs, + fr: uiFr, + da: uiDa, + ja: uiJa, + pl: uiPl, + ru: uiRu, + ar: uiAr, + no: uiNo, + br: uiBr, + th: uiTh, + bs: uiBs, +} + const cliLocales: Record> = { en: cliEn, zh: cliZh, @@ -158,26 +192,31 @@ interface Missing { } /** - * Regex to match t("key") / t('key') calls. + * Regex to match t("key") / t('key') calls, including multiline. * - * Captures the string literal argument to any of these patterns: - * t("key") language.t("key") i18n.t("key") - * - * Skips template literals (backticks) and variable arguments. + * The `s` (dotAll) flag lets `\s*` span newlines so that calls like: + * t( + * "key" + * ) + * are captured. Runs against the entire file content, not line-by-line. */ -const T_CALL = /(?:^|[^a-zA-Z_$])t\(\s*(?:["'])([^"']+)["']/g +const T_CALL = /(?:^|[^a-zA-Z_$])t\(\s*(?:["'])([^"']+)["']/gs + +function offsetToLine(content: string, offset: number): number { + let line = 1 + for (let i = 0; i < offset && i < content.length; i++) { + if (content[i] === "\n") line++ + } + return line +} function extractKeys(content: string): Array<{ line: number; key: string }> { const results: Array<{ line: number; key: string }> = [] - const lines = content.split("\n") - for (let i = 0; i < lines.length; i++) { - const text = lines[i] - let match: RegExpExecArray | null - T_CALL.lastIndex = 0 - while ((match = T_CALL.exec(text)) !== null) { - const key = match[1] - if (key) results.push({ line: i + 1, key }) - } + let match: RegExpExecArray | null + T_CALL.lastIndex = 0 + while ((match = T_CALL.exec(content)) !== null) { + const key = match[1] + if (key) results.push({ line: offsetToLine(content, match.index), key }) } return results } @@ -322,6 +361,17 @@ describe("i18n key validation — no missing translation keys", () => { }) describe("i18n locale completeness — every English key exists in all locales", () => { + it("shared UI: every English key has a translation in all locales", () => { + const missing = findMissingLocaleKeys(uiEn, uiLocales) + if (missing.length > 0) { + expect( + missing, + `Found ${missing.length} missing shared UI translation(s):\n${formatLocaleReport(missing)}`, + ).toEqual([]) + } + expect(missing).toEqual([]) + }) + it("sidebar app: every English key has a translation in all locales", () => { const missing = findMissingLocaleKeys(appEn, appLocales) if (missing.length > 0) { From 13ff40c583fc67379cf6c914c5a755f29a039126 Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 17 Mar 2026 12:14:34 +0100 Subject: [PATCH 25/74] fix: don't dispose all instances on global config update Config.updateGlobal() was calling Instance.disposeAll() on every config change, which destroyed all session state, MCP connections, and in-flight operations across every project. This meant that any config change (e.g. removing a mode in VS Code) would effectively kill all running CLI instances. The global.reset() call already clears the config cache so consumers will pick up the new config on their next read. The disposeAll() was unnecessary overkill. --- packages/opencode/src/config/config.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 540e9976db1..b86631ad398 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1553,17 +1553,19 @@ export namespace Config { global.reset() - void Instance.disposeAll() - .catch(() => undefined) - .finally(() => { - GlobalBus.emit("event", { - directory: "global", - payload: { - type: Event.Disposed.type, - properties: {}, - }, - }) - }) + // kilocode_change start - only reset config cache, don't dispose all instances. + // Instance.disposeAll() was destroying all session state, MCP connections, and + // in-flight operations across every project whenever any global config changed + // (e.g. removing a mode). The cache reset above is sufficient — consumers will + // pick up the new config on their next read. + GlobalBus.emit("event", { + directory: "global", + payload: { + type: Event.Disposed.type, + properties: {}, + }, + }) + // kilocode_change end return next } From 2dcbc7fb1d6a0fe6064044c1eda19ae928c6788f Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 17 Mar 2026 12:24:59 +0100 Subject: [PATCH 26/74] refactor(vscode): remove dead hasKilocodeProfileWithNoBalance code and stale tests The hasKilocodeProfileWithNoBalance property was never set to true in the current CLI-backend architecture, making the no-credits status bar state unreachable. Also removes AutocompleteModel.spec.ts which tested an old profile-based architecture that no longer exists. --- .../autocomplete/AutocompleteModel.ts | 1 - .../AutocompleteServiceManager.ts | 4 +- .../autocomplete/AutocompleteStatusBar.ts | 10 - .../__tests__/AutocompleteModel.spec.ts | 732 ------------------ .../src/services/autocomplete/i18n/ar.ts | 2 - .../src/services/autocomplete/i18n/ca.ts | 2 - .../src/services/autocomplete/i18n/cs.ts | 2 - .../src/services/autocomplete/i18n/de.ts | 2 - .../src/services/autocomplete/i18n/en.ts | 2 - .../src/services/autocomplete/i18n/es.ts | 2 - .../src/services/autocomplete/i18n/fr.ts | 2 - .../src/services/autocomplete/i18n/hi.ts | 2 - .../src/services/autocomplete/i18n/id.ts | 2 - .../src/services/autocomplete/i18n/it.ts | 2 - .../src/services/autocomplete/i18n/ja.ts | 2 - .../src/services/autocomplete/i18n/ko.ts | 2 - .../src/services/autocomplete/i18n/nl.ts | 2 - .../src/services/autocomplete/i18n/pl.ts | 2 - .../src/services/autocomplete/i18n/pt-BR.ts | 2 - .../src/services/autocomplete/i18n/ru.ts | 2 - .../src/services/autocomplete/i18n/sk.ts | 2 - .../src/services/autocomplete/i18n/th.ts | 2 - .../src/services/autocomplete/i18n/tr.ts | 2 - .../src/services/autocomplete/i18n/uk.ts | 2 - .../src/services/autocomplete/i18n/vi.ts | 2 - .../src/services/autocomplete/i18n/zh-CN.ts | 2 - .../src/services/autocomplete/i18n/zh-TW.ts | 2 - .../src/services/autocomplete/types.ts | 1 - .../components/settings/AgentBehaviourTab.tsx | 2 + packages/sdk/js/openapi.json | 0 30 files changed, 3 insertions(+), 793 deletions(-) delete mode 100644 packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteModel.spec.ts create mode 100644 packages/sdk/js/openapi.json diff --git a/packages/kilo-vscode/src/services/autocomplete/AutocompleteModel.ts b/packages/kilo-vscode/src/services/autocomplete/AutocompleteModel.ts index 71adb447a9b..58c60988650 100644 --- a/packages/kilo-vscode/src/services/autocomplete/AutocompleteModel.ts +++ b/packages/kilo-vscode/src/services/autocomplete/AutocompleteModel.ts @@ -21,7 +21,6 @@ export class AutocompleteModel { public profileName: string | null = null public profileType: string | null = null public loaded = false - public hasKilocodeProfileWithNoBalance = false constructor(connectionService?: KiloConnectionService) { if (connectionService) { diff --git a/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts b/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts index 6fb130edcdd..eaad9936311 100644 --- a/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts +++ b/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts @@ -18,7 +18,6 @@ export interface AutocompleteServiceSettings { provider?: string model?: string snoozeUntil?: number - hasKilocodeProfileWithNoBalance?: boolean } function readSettings(): AutocompleteServiceSettings { @@ -315,7 +314,7 @@ export class AutocompleteServiceManager { } private hasNoUsableProvider(): boolean { - return this.model.loaded && !this.model.hasValidCredentials() && !this.model.hasKilocodeProfileWithNoBalance + return this.model.loaded && !this.model.hasValidCredentials() } private updateCostTracking(cost: number, _inputTokens: number, _outputTokens: number): void { @@ -335,7 +334,6 @@ export class AutocompleteServiceManager { model: this.getCurrentModelName(), provider: this.getCurrentProviderName(), profileName: this.model.profileName, - hasKilocodeProfileWithNoBalance: this.model.hasKilocodeProfileWithNoBalance, hasNoUsableProvider: this.hasNoUsableProvider(), totalSessionCost: this.sessionCost, completionCount: this.completionCount, diff --git a/packages/kilo-vscode/src/services/autocomplete/AutocompleteStatusBar.ts b/packages/kilo-vscode/src/services/autocomplete/AutocompleteStatusBar.ts index 2719d8d96ea..867a2f67717 100644 --- a/packages/kilo-vscode/src/services/autocomplete/AutocompleteStatusBar.ts +++ b/packages/kilo-vscode/src/services/autocomplete/AutocompleteStatusBar.ts @@ -86,22 +86,12 @@ export class AutocompleteStatusBar { } public render() { - if (this.props.hasKilocodeProfileWithNoBalance) { - return this.renderNoCreditsError() - } if (this.props.hasNoUsableProvider) { return this.renderNoUsableProviderError() } return this.renderDefault() } - private renderNoCreditsError() { - this.statusBar.text = t("kilocode:autocomplete.statusBar.warning") - this.statusBar.tooltip = this.createMarkdownTooltip( - t("kilocode:autocomplete.statusBar.tooltip.noCredits", { command: SETTINGS_COMMAND }), - ) - } - private renderNoUsableProviderError() { this.statusBar.text = t("kilocode:autocomplete.statusBar.warning") this.statusBar.tooltip = this.createMarkdownTooltip( diff --git a/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteModel.spec.ts b/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteModel.spec.ts deleted file mode 100644 index 20f91a1c2c2..00000000000 --- a/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteModel.spec.ts +++ /dev/null @@ -1,732 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" -import { AutocompleteModel } from "../AutocompleteModel" -import { ProviderSettingsManager } from "../../../core/config/ProviderSettingsManager" -import { AUTOCOMPLETE_PROVIDER_MODELS } from "../utils/kilocode-utils" -import * as apiIndex from "../../../api" -import { TelemetryService } from "../../../../packages/telemetry/src/TelemetryService" - -describe("AutocompleteModel", () => { - let mockProviderSettingsManager: ProviderSettingsManager - - beforeEach(() => { - mockProviderSettingsManager = { - listConfig: vi.fn(), - getProfile: vi.fn(), - } as any - }) - - describe("reload", () => { - it("sorts profiles by supportedProviders index order", async () => { - const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()] - const profiles = [ - { id: "3", name: "profile3", apiProvider: supportedProviders[2] }, - { id: "1", name: "profile1", apiProvider: supportedProviders[0] }, - { id: "2", name: "profile2", apiProvider: supportedProviders[1] }, - ] as any - - vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles) - vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({ - id: "1", - name: "profile1", - apiProvider: supportedProviders[0], - mistralApiKey: "test-key", - } as any) - - const model = new AutocompleteModel() - await model.reload(mockProviderSettingsManager) - - expect(mockProviderSettingsManager.getProfile).toHaveBeenCalledWith({ id: "1" }) - }) - - it("filters out profiles without apiProvider", async () => { - const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()] - const profiles = [ - { id: "1", name: "profile1", apiProvider: undefined }, - { id: "2", name: "profile2", apiProvider: supportedProviders[0] }, - ] as any - - vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles) - vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({ - id: "2", - name: "profile2", - apiProvider: supportedProviders[0], - mistralApiKey: "test-key", - } as any) - - const model = new AutocompleteModel() - await model.reload(mockProviderSettingsManager) - - expect(mockProviderSettingsManager.getProfile).toHaveBeenCalledWith({ id: "2" }) - }) - - it("filters out profiles with unsupported apiProvider", async () => { - const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()] - const profiles = [ - { id: "1", name: "profile1", apiProvider: "unsupported" }, - { id: "2", name: "profile2", apiProvider: supportedProviders[0] }, - ] as any - - vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles) - vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({ - id: "2", - name: "profile2", - apiProvider: supportedProviders[0], - mistralApiKey: "test-key", - } as any) - - const model = new AutocompleteModel() - await model.reload(mockProviderSettingsManager) - - expect(mockProviderSettingsManager.getProfile).toHaveBeenCalledWith({ id: "2" }) - }) - - it("handles empty profile list", async () => { - vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue([]) - - const model = new AutocompleteModel() - const result = await model.reload(mockProviderSettingsManager) - - expect(mockProviderSettingsManager.getProfile).not.toHaveBeenCalled() - expect(model.hasValidCredentials()).toBe(false) - expect(result).toBe(false) - }) - - it("returns true when profile found", async () => { - const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()] - const profiles = [{ id: "1", name: "mistral-profile", apiProvider: "mistral" }] as any - - vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles) - vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({ - id: "1", - name: "mistral-profile", - apiProvider: "mistral", - mistralApiKey: "test-key", - } as any) - - const model = new AutocompleteModel() - const result = await model.reload(mockProviderSettingsManager) - - expect(result).toBe(true) - expect(model.loaded).toBe(true) - }) - }) - - describe("provider usability", () => { - beforeEach(() => { - // Mock fetch globally for these tests - vi.stubGlobal("fetch", vi.fn()) - - // Mock TelemetryService - const mockTelemetryService = { - captureEvent: vi.fn(), - captureException: vi.fn(), - isTelemetryEnabled: vi.fn().mockReturnValue(false), - shutdown: vi.fn(), - } - vi.spyOn(TelemetryService, "hasInstance").mockReturnValue(true) - vi.spyOn(TelemetryService, "instance", "get").mockReturnValue(mockTelemetryService as any) - }) - - afterEach(() => { - // Restore fetch - vi.unstubAllGlobals() - - // Restore TelemetryService mocks - vi.restoreAllMocks() - }) - - it("should skip kilocode provider when balance is zero and use openrouter instead", async () => { - const profiles = [ - { id: "1", name: "kilocode-profile", apiProvider: "kilocode" }, - { id: "2", name: "openrouter-profile", apiProvider: "openrouter" }, - ] as any - - vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles) - - // Mock profiles with tokens - vi.mocked(mockProviderSettingsManager.getProfile).mockImplementation(async (args: any) => { - if (args.id === "1") { - return { - id: "1", - name: "kilocode-profile", - apiProvider: "kilocode", - kilocodeToken: "test-token", - } as any - } else if (args.id === "2") { - return { - id: "2", - name: "openrouter-profile", - apiProvider: "openrouter", - openRouterApiKey: "test-key", - } as any - } - return null as any - }) - - // Mock fetch to return zero balance for kilocode - ;(global.fetch as any).mockImplementation(async (url: string) => { - if (url.includes("/api/profile/balance")) { - return { - ok: true, - json: async () => ({ data: { balance: 0 } }), - } as any - } - // For OpenRouter models endpoint - if (url.includes("/models")) { - return { - ok: true, - json: async () => ({ data: [] }), - } as any - } - // For other URLs, return a basic response - return { - ok: true, - json: async () => ({}), - } as any - }) - - const model = new AutocompleteModel() - const result = await model.reload(mockProviderSettingsManager) - - // Should have tried both providers but used openrouter (since kilocode balance is 0) - expect(result).toBe(true) - expect(model.loaded).toBe(true) - }) - - it("should use kilocode provider when balance is greater than zero", async () => { - const profiles = [ - { id: "1", name: "kilocode-profile", apiProvider: "kilocode" }, - { id: "2", name: "openrouter-profile", apiProvider: "openrouter" }, - ] as any - - vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles) - - // Mock profiles with tokens - vi.mocked(mockProviderSettingsManager.getProfile).mockImplementation(async (args: any) => { - if (args.id === "1") { - return { - id: "1", - name: "kilocode-profile", - apiProvider: "kilocode", - kilocodeToken: "test-token", - } as any - } else if (args.id === "2") { - return { - id: "2", - name: "openrouter-profile", - apiProvider: "openrouter", - openRouterApiKey: "test-key", - } as any - } - return null as any - }) - - // Mock fetch to return positive balance for kilocode - ;(global.fetch as any).mockImplementation(async (url: string) => { - if (url.includes("/api/profile/balance")) { - return { - ok: true, - json: async () => ({ data: { balance: 10.5 } }), - } as any - } - // For OpenRouter models endpoint - if (url.includes("/models")) { - return { - ok: true, - json: async () => ({ data: [] }), - } as any - } - // For other URLs, return a basic response - return { - ok: true, - json: async () => ({}), - } as any - }) - - const model = new AutocompleteModel() - const result = await model.reload(mockProviderSettingsManager) - - // Should have used kilocode provider (first one with positive balance) - expect(result).toBe(true) - expect(model.loaded).toBe(true) - }) - - it("should handle kilocode provider with no token", async () => { - const profiles = [ - { id: "1", name: "kilocode-profile", apiProvider: "kilocode" }, - { id: "2", name: "openrouter-profile", apiProvider: "openrouter" }, - ] as any - - vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles) - - // Mock profiles - kilocode without token - vi.mocked(mockProviderSettingsManager.getProfile).mockImplementation(async (args: any) => { - if (args.id === "1") { - return { - id: "1", - name: "kilocode-profile", - apiProvider: "kilocode", - kilocodeToken: "", // No token - } as any - } else if (args.id === "2") { - return { - id: "2", - name: "openrouter-profile", - apiProvider: "openrouter", - openRouterApiKey: "test-key", - } as any - } - return null as any - }) - - // Mock fetch to handle the no-token case - ;(global.fetch as any).mockImplementation(async (url: string) => { - if (url.includes("/api/profile/balance")) { - // This should not be called since there's no token - return { - ok: false, - status: 401, - } as any - } - // For OpenRouter models endpoint - if (url.includes("/models")) { - return { - ok: true, - json: async () => ({ data: [] }), - } as any - } - // For other URLs, return a basic response - return { - ok: true, - json: async () => ({}), - } as any - }) - - const model = new AutocompleteModel() - const result = await model.reload(mockProviderSettingsManager) - - // Should skip kilocode (no token) and use openrouter - expect(result).toBe(true) - expect(model.loaded).toBe(true) - }) - - it("should set hasKilocodeProfileWithNoBalance when kilocode profile exists but has no balance", async () => { - const profiles = [{ id: "1", name: "kilocode-profile", apiProvider: "kilocode" }] as any - - vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles) - - // Mock profile with token - vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({ - id: "1", - name: "kilocode-profile", - apiProvider: "kilocode", - kilocodeToken: "test-token", - } as any) - - // Mock fetch to return zero balance for kilocode - ;(global.fetch as any).mockImplementation(async (url: string) => { - if (url.includes("/api/profile/balance")) { - return { - ok: true, - json: async () => ({ balance: 0 }), - } as any - } - return { - ok: true, - json: async () => ({}), - } as any - }) - - const model = new AutocompleteModel() - const result = await model.reload(mockProviderSettingsManager) - - // Should not find a usable provider - expect(result).toBe(false) - expect(model.loaded).toBe(true) - // Should have set the flag indicating kilocode profile exists but has no balance - expect(model.hasKilocodeProfileWithNoBalance).toBe(true) - }) - - it("should not set hasKilocodeProfileWithNoBalance when kilocode profile has balance", async () => { - const profiles = [{ id: "1", name: "kilocode-profile", apiProvider: "kilocode" }] as any - - vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles) - - // Mock profile with token - vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({ - id: "1", - name: "kilocode-profile", - apiProvider: "kilocode", - kilocodeToken: "test-token", - } as any) - - // Mock fetch to return positive balance for kilocode and valid models response - ;(global.fetch as any).mockImplementation(async (url: string) => { - if (url.includes("/api/profile/balance")) { - return { - ok: true, - json: async () => ({ balance: 10.5 }), - } as any - } - // For OpenRouter/Kilocode models endpoint - if (url.includes("/models")) { - return { - ok: true, - json: async () => ({ - data: [ - { - id: "mistralai/codestral-2508", - name: "Codestral", - context_length: 32000, - pricing: { prompt: "0.0001", completion: "0.0003" }, - }, - ], - }), - } as any - } - return { - ok: true, - json: async () => ({}), - } as any - }) - - const model = new AutocompleteModel() - const result = await model.reload(mockProviderSettingsManager) - - // Should find a usable provider - expect(result).toBe(true) - expect(model.loaded).toBe(true) - // Should not have set the flag - expect(model.hasKilocodeProfileWithNoBalance).toBe(false) - }) - - it("should clear hasKilocodeProfileWithNoBalance on reload", async () => { - const profiles = [{ id: "1", name: "kilocode-profile", apiProvider: "kilocode" }] as any - - vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles) - - // Mock profile with token - vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({ - id: "1", - name: "kilocode-profile", - apiProvider: "kilocode", - kilocodeToken: "test-token", - } as any) - - // First reload: zero balance - ;(global.fetch as any).mockImplementation(async (url: string) => { - if (url.includes("/api/profile/balance")) { - return { - ok: true, - json: async () => ({ balance: 0 }), - } as any - } - return { - ok: true, - json: async () => ({}), - } as any - }) - - const model = new AutocompleteModel() - await model.reload(mockProviderSettingsManager) - expect(model.hasKilocodeProfileWithNoBalance).toBe(true) - - // Second reload: positive balance with valid models response - ;(global.fetch as any).mockImplementation(async (url: string) => { - if (url.includes("/api/profile/balance")) { - return { - ok: true, - json: async () => ({ balance: 10.5 }), - } as any - } - // For OpenRouter/Kilocode models endpoint - if (url.includes("/models")) { - return { - ok: true, - json: async () => ({ - data: [ - { - id: "mistralai/codestral-2508", - name: "Codestral", - context_length: 32000, - pricing: { prompt: "0.0001", completion: "0.0003" }, - }, - ], - }), - } as any - } - return { - ok: true, - json: async () => ({}), - } as any - }) - - await model.reload(mockProviderSettingsManager) - // Flag should be cleared after reload with positive balance - expect(model.hasKilocodeProfileWithNoBalance).toBe(false) - }) - }) - - describe("getProviderDisplayName", () => { - it("returns undefined when no provider is loaded", () => { - const model = new AutocompleteModel() - expect(model.getProviderDisplayName()).toBeUndefined() - }) - - it("returns provider name from API handler when provider is loaded", async () => { - const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()] - const profiles = [{ id: "1", name: "profile1", apiProvider: supportedProviders[0] }] as any - - vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles) - vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({ - id: "1", - name: "profile1", - apiProvider: supportedProviders[0], - mistralApiKey: "test-key", - } as any) - - // Mock buildApiHandler to return a handler with providerName - const mockApiHandler = { - providerName: "Mistral", - getModel: vi.fn().mockReturnValue({ id: "mistral-model", info: {} }), - createMessage: vi.fn(), - countTokens: vi.fn(), - } - vi.spyOn(apiIndex, "buildApiHandler").mockReturnValue(mockApiHandler as any) - - const model = new AutocompleteModel() - await model.reload(mockProviderSettingsManager) - - const providerName = model.getProviderDisplayName() - expect(providerName).toBeTruthy() - expect(typeof providerName).toBe("string") - expect(providerName).toBe("Mistral") - - // Restore the spy - vi.restoreAllMocks() - }) - - describe("profile information", () => { - it("returns null for profile name when no profile is loaded", () => { - const model = new AutocompleteModel() - expect(model.profileName).toBeNull() - }) - - it("returns null for profile type when no profile is loaded", () => { - const model = new AutocompleteModel() - expect(model.profileType).toBeNull() - }) - - it("stores and returns profile name after loading", async () => { - const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()] - const profiles = [ - { - id: "1", - name: "My Autocomplete Profile", - apiProvider: supportedProviders[0], - profileType: "autocomplete", - }, - ] as any - - vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles) - vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({ - id: "1", - name: "My Autocomplete Profile", - apiProvider: supportedProviders[0], - profileType: "autocomplete", - mistralApiKey: "test-key", - } as any) - - const model = new AutocompleteModel() - await model.reload(mockProviderSettingsManager) - - expect(model.profileName).toBe("My Autocomplete Profile") - }) - - it("stores and returns profile type after loading", async () => { - const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()] - const profiles = [ - { id: "1", name: "My Profile", apiProvider: supportedProviders[0], profileType: "autocomplete" }, - ] as any - - vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles) - vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({ - id: "1", - name: "My Profile", - apiProvider: supportedProviders[0], - profileType: "autocomplete", - mistralApiKey: "test-key", - } as any) - - const model = new AutocompleteModel() - await model.reload(mockProviderSettingsManager) - - expect(model.profileType).toBe("autocomplete") - }) - - it("clears profile information on cleanup", async () => { - const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()] - const profiles = [ - { id: "1", name: "My Profile", apiProvider: supportedProviders[0], profileType: "autocomplete" }, - ] as any - - vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles) - vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({ - id: "1", - name: "My Profile", - apiProvider: supportedProviders[0], - profileType: "autocomplete", - mistralApiKey: "test-key", - } as any) - - const model = new AutocompleteModel() - await model.reload(mockProviderSettingsManager) - - expect(model.profileName).toBe("My Profile") - expect(model.profileType).toBe("autocomplete") - - // Reload with empty profiles to trigger cleanup - vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue([]) - await model.reload(mockProviderSettingsManager) - - expect(model.profileName).toBeNull() - expect(model.profileType).toBeNull() - }) - }) - }) - - describe("reload model override behavior", () => { - beforeEach(() => { - // Mock buildApiHandler to return a handler with getModel - const mockApiHandler = { - getModel: vi.fn().mockReturnValue({ id: "test-model", info: {} }), - createMessage: vi.fn(), - countTokens: vi.fn(), - } - vi.spyOn(apiIndex, "buildApiHandler").mockReturnValue(mockApiHandler as any) - }) - - afterEach(() => { - vi.restoreAllMocks() - }) - - it("should use custom model for explicit autocomplete profiles", async () => { - const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()] - const provider = supportedProviders[0] - const customModelId = "custom-autocomplete-model" - - const profiles = [ - { - id: "1", - name: "My Autocomplete Profile", - apiProvider: provider, - profileType: "autocomplete", - }, - ] as any - - vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles) - vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({ - id: "1", - name: "My Autocomplete Profile", - apiProvider: provider, - profileType: "autocomplete", - mistralApiKey: "test-key", - apiModelId: customModelId, // Custom model set by user - } as any) - - // Mock buildApiHandler to return the custom model - const mockApiHandler = { - getModel: vi.fn().mockReturnValue({ id: customModelId, info: {} }), - createMessage: vi.fn(), - countTokens: vi.fn(), - } - vi.spyOn(apiIndex, "buildApiHandler").mockReturnValue(mockApiHandler as any) - - const model = new AutocompleteModel() - await model.reload(mockProviderSettingsManager) - - // The model should use the custom model from the profile - const modelName = model.getModelName() - expect(modelName).toBe(customModelId) - }) - - it("should override model for non-autocomplete profiles", async () => { - const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()] - const provider = supportedProviders[0] - const defaultAutocompleteModel = AUTOCOMPLETE_PROVIDER_MODELS.get(provider) - - const profiles = [ - { - id: "1", - name: "My Chat Profile", - apiProvider: provider, - profileType: "chat", // Not an autocomplete profile - }, - ] as any - - vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles) - vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({ - id: "1", - name: "My Chat Profile", - apiProvider: provider, - profileType: "chat", - mistralApiKey: "test-key", - apiModelId: "custom-chat-model", // This should be overridden - } as any) - - // Mock buildApiHandler to return the overridden model - const mockApiHandler = { - getModel: vi.fn().mockReturnValue({ id: defaultAutocompleteModel, info: {} }), - createMessage: vi.fn(), - countTokens: vi.fn(), - } - vi.spyOn(apiIndex, "buildApiHandler").mockReturnValue(mockApiHandler as any) - - const model = new AutocompleteModel() - await model.reload(mockProviderSettingsManager) - - // The model should be overridden with the default autocomplete model - const modelName = model.getModelName() - expect(modelName).toBe(defaultAutocompleteModel) - }) - - it("should override model for profiles without profileType", async () => { - const supportedProviders = [...AUTOCOMPLETE_PROVIDER_MODELS.keys()] - const provider = supportedProviders[0] - const defaultAutocompleteModel = AUTOCOMPLETE_PROVIDER_MODELS.get(provider) - - const profiles = [ - { - id: "1", - name: "My Generic Profile", - apiProvider: provider, - // No profileType specified - }, - ] as any - - vi.mocked(mockProviderSettingsManager.listConfig).mockResolvedValue(profiles) - vi.mocked(mockProviderSettingsManager.getProfile).mockResolvedValue({ - id: "1", - name: "My Generic Profile", - apiProvider: provider, - mistralApiKey: "test-key", - apiModelId: "custom-model", // This should be overridden - } as any) - - // Mock buildApiHandler to return the overridden model - const mockApiHandler = { - getModel: vi.fn().mockReturnValue({ id: defaultAutocompleteModel, info: {} }), - createMessage: vi.fn(), - countTokens: vi.fn(), - } - vi.spyOn(apiIndex, "buildApiHandler").mockReturnValue(mockApiHandler as any) - - const model = new AutocompleteModel() - await model.reload(mockProviderSettingsManager) - - // The model should be overridden with the default autocomplete model - const modelName = model.getModelName() - expect(modelName).toBe(defaultAutocompleteModel) - }) - }) -}) diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/ar.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/ar.ts index c85054d374d..c2df6d81c72 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/ar.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/ar.ts @@ -6,8 +6,6 @@ export const dict = { "kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete", "kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete", "kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (معطل)", - "kilocode:autocomplete.statusBar.tooltip.noCredits": - "**لا يوجد رصيد في حسابك**\n\nحساب Kilo Code الخاص بك لا يحتوي على رصيد. لاستخدام الإكمال التلقائي، يرجى إضافة رصيد إلى حسابك.\n\n[فتح الإعدادات]({{command}})", "kilocode:autocomplete.statusBar.tooltip.noUsableProvider": "**لم يتم تكوين نموذج الإكمال التلقائي**\n\nلتمكين الإكمال التلقائي، أضف ملفًا شخصيًا مع أحد هذه المزودين المدعومين: {{providers}}.\n\n[فتح الإعدادات]({{command}})", "kilocode:autocomplete.statusBar.tooltip.sessionTotal": "إجمالي تكلفة الجلسة:", diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/ca.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/ca.ts index df4b70cff80..92a7802c492 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/ca.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/ca.ts @@ -6,8 +6,6 @@ export const dict = { "kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete", "kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete", "kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (desactivat)", - "kilocode:autocomplete.statusBar.tooltip.noCredits": - "**No tens crèdits al teu compte**\n\nEl teu compte de Kilo Code no té crèdits. Per utilitzar l'autocompletat, si us plau afegeix crèdits al teu compte.\n\n[Obrir Configuració]({{command}})", "kilocode:autocomplete.statusBar.tooltip.noUsableProvider": "**No s'ha configurat cap model d'autocompletat**\n\nPer habilitar l'autocompletat, afegeix un perfil amb un d'aquests proveïdors compatibles: {{providers}}.\n\n[Obrir Configuració]({{command}})", "kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Cost total de la sessió:", diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/cs.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/cs.ts index 7b22314cd5f..a12a46c8a0c 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/cs.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/cs.ts @@ -6,8 +6,6 @@ export const dict = { "kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete", "kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete", "kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (zakázáno)", - "kilocode:autocomplete.statusBar.tooltip.noCredits": - "**Na tvém účtu nejsou žádné kredity**\n\nTvůj účet Kilo Code nemá žádné kredity. Pro použití automatického doplňování prosím přidej kredity na svůj účet.\n\n[Otevřít Nastavení]({{command}})", "kilocode:autocomplete.statusBar.tooltip.noUsableProvider": "**Není nakonfigurován žádný model automatického doplňování**\n\nPro povolení automatického doplňování přidej profil s jedním z těchto podporovaných poskytovatelů: {{providers}}.\n\n[Otevřít Nastavení]({{command}})", "kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Celkové náklady relace:", diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/de.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/de.ts index 507187a4e50..2525a491c04 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/de.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/de.ts @@ -6,8 +6,6 @@ export const dict = { "kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete", "kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete", "kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (deaktiviert)", - "kilocode:autocomplete.statusBar.tooltip.noCredits": - "**Kein Guthaben auf deinem Konto**\n\nDein Kilo Code Konto hat kein Guthaben. Um Autocomplete zu nutzen, füge bitte Guthaben zu deinem Konto hinzu.\n\n[Einstellungen öffnen]({{command}})", "kilocode:autocomplete.statusBar.tooltip.noUsableProvider": "**Kein Autocomplete-Modell konfiguriert**\n\nUm Autocomplete zu aktivieren, füge ein Profil mit einem dieser unterstützten Anbieter hinzu: {{providers}}.\n\n[Einstellungen öffnen]({{command}})", "kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Sitzungsgesamtkosten:", diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/en.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/en.ts index a0d5d2f02fe..bd536d23c07 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/en.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/en.ts @@ -7,8 +7,6 @@ export const dict = { "kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete", "kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete", "kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (disabled)", - "kilocode:autocomplete.statusBar.tooltip.noCredits": - "**No credits in your account**\n\nYour Kilo Code account has no credits. To use autocomplete, please add credits to your account.\n\n[Open Settings]({{command}})", "kilocode:autocomplete.statusBar.tooltip.noUsableProvider": "**No autocomplete model configured**\n\nTo enable autocomplete, add a profile with one of these supported providers: {{providers}}.\n\n[Open Settings]({{command}})", "kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Session total cost:", diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/es.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/es.ts index adf5e07a993..2348ca32e9a 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/es.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/es.ts @@ -6,8 +6,6 @@ export const dict = { "kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete", "kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete", "kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (deshabilitado)", - "kilocode:autocomplete.statusBar.tooltip.noCredits": - "**No hay créditos en tu cuenta**\n\nTu cuenta de Kilo Code no tiene créditos. Para usar el autocompletado, por favor añade créditos a tu cuenta.\n\n[Abrir Configuración]({{command}})", "kilocode:autocomplete.statusBar.tooltip.noUsableProvider": "**No hay modelo de autocompletado configurado**\n\nPara habilitar el autocompletado, añade un perfil con uno de estos proveedores compatibles: {{providers}}.\n\n[Abrir Configuración]({{command}})", "kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Costo total de la sesión:", diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/fr.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/fr.ts index 65f9d13a86f..56dd0f139aa 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/fr.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/fr.ts @@ -6,8 +6,6 @@ export const dict = { "kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete", "kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete", "kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (désactivé)", - "kilocode:autocomplete.statusBar.tooltip.noCredits": - "**Pas de crédits sur ton compte**\n\nTon compte Kilo Code n'a pas de crédits. Pour utiliser l'autocomplétion, ajoute des crédits à ton compte.\n\n[Ouvrir les Paramètres]({{command}})", "kilocode:autocomplete.statusBar.tooltip.noUsableProvider": "**Aucun modèle d'autocomplétion configuré**\n\nPour activer l'autocomplétion, ajoute un profil avec l'un de ces fournisseurs pris en charge : {{providers}}.\n\n[Ouvrir les Paramètres]({{command}})", "kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Coût total de la session :", diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/hi.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/hi.ts index 7cb24ae396c..b23e34b5bb4 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/hi.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/hi.ts @@ -6,8 +6,6 @@ export const dict = { "kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete", "kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete", "kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (अक्षम)", - "kilocode:autocomplete.statusBar.tooltip.noCredits": - "**आपके खाते में कोई क्रेडिट नहीं है**\n\nआपके Kilo Code खाते में कोई क्रेडिट नहीं है। ऑटोकम्प्लीट का उपयोग करने के लिए, कृपया अपने खाते में क्रेडिट जोड़ें।\n\n[सेटिंग्स खोलें]({{command}})", "kilocode:autocomplete.statusBar.tooltip.noUsableProvider": "**कोई ऑटोकम्प्लीट मॉडल कॉन्फ़िगर नहीं किया गया**\n\nऑटोकम्प्लीट सक्षम करने के लिए, इन समर्थित प्रदाताओं में से एक के साथ एक प्रोफ़ाइल जोड़ें: {{providers}}।\n\n[सेटिंग्स खोलें]({{command}})", "kilocode:autocomplete.statusBar.tooltip.sessionTotal": "सत्र की कुल लागत:", diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/id.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/id.ts index 062ebf6f835..4f7384d404d 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/id.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/id.ts @@ -6,8 +6,6 @@ export const dict = { "kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete", "kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete", "kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (dinonaktifkan)", - "kilocode:autocomplete.statusBar.tooltip.noCredits": - "**Tidak ada kredit di akun kamu**\n\nAkun Kilo Code kamu tidak memiliki kredit. Untuk menggunakan autocomplete, silakan tambahkan kredit ke akun kamu.\n\n[Buka Pengaturan]({{command}})", "kilocode:autocomplete.statusBar.tooltip.noUsableProvider": "**Tidak ada model autocomplete yang dikonfigurasi**\n\nUntuk mengaktifkan autocomplete, tambahkan profil dengan salah satu penyedia yang didukung ini: {{providers}}.\n\n[Buka Pengaturan]({{command}})", "kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Total biaya sesi:", diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/it.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/it.ts index bdc75654b60..5bd075d981b 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/it.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/it.ts @@ -6,8 +6,6 @@ export const dict = { "kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete", "kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete", "kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (disabilitato)", - "kilocode:autocomplete.statusBar.tooltip.noCredits": - "**Nessun credito nel tuo account**\n\nIl tuo account Kilo Code non ha crediti. Per usare l'autocompletamento, aggiungi crediti al tuo account.\n\n[Apri Impostazioni]({{command}})", "kilocode:autocomplete.statusBar.tooltip.noUsableProvider": "**Nessun modello di autocompletamento configurato**\n\nPer abilitare l'autocompletamento, aggiungi un profilo con uno di questi provider supportati: {{providers}}.\n\n[Apri Impostazioni]({{command}})", "kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Costo totale della sessione:", diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/ja.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/ja.ts index b4d3a9f2772..ad59af271d7 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/ja.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/ja.ts @@ -6,8 +6,6 @@ export const dict = { "kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete", "kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete", "kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete(無効)", - "kilocode:autocomplete.statusBar.tooltip.noCredits": - "**アカウントにクレジットがありません**\n\nKilo Code アカウントにクレジットがありません。オートコンプリートを使用するには、アカウントにクレジットを追加してください。\n\n[設定を開く]({{command}})", "kilocode:autocomplete.statusBar.tooltip.noUsableProvider": "**オートコンプリートモデルが設定されていません**\n\nオートコンプリートを有効にするには、これらのサポートされているプロバイダーのいずれかでプロファイルを追加してください: {{providers}}。\n\n[設定を開く]({{command}})", "kilocode:autocomplete.statusBar.tooltip.sessionTotal": "セッション合計コスト:", diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/ko.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/ko.ts index cc55702d80b..cc4eab6dfb8 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/ko.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/ko.ts @@ -6,8 +6,6 @@ export const dict = { "kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete", "kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete", "kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (비활성화됨)", - "kilocode:autocomplete.statusBar.tooltip.noCredits": - "**계정에 크레딧이 없습니다**\n\nKilo Code 계정에 크레딧이 없습니다. 자동완성을 사용하려면 계정에 크레딧을 추가해 주세요.\n\n[설정 열기]({{command}})", "kilocode:autocomplete.statusBar.tooltip.noUsableProvider": "**자동완성 모델이 구성되지 않았습니다**\n\n자동완성을 활성화하려면 다음 지원되는 제공업체 중 하나로 프로필을 추가하세요: {{providers}}.\n\n[설정 열기]({{command}})", "kilocode:autocomplete.statusBar.tooltip.sessionTotal": "세션 총 비용:", diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/nl.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/nl.ts index 8ca573dab41..b30b2d2834d 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/nl.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/nl.ts @@ -6,8 +6,6 @@ export const dict = { "kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete", "kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete", "kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (uitgeschakeld)", - "kilocode:autocomplete.statusBar.tooltip.noCredits": - "**Geen tegoed op je account**\n\nJe Kilo Code account heeft geen tegoed. Om autocomplete te gebruiken, voeg tegoed toe aan je account.\n\n[Instellingen openen]({{command}})", "kilocode:autocomplete.statusBar.tooltip.noUsableProvider": "**Geen autocomplete model geconfigureerd**\n\nOm autocomplete in te schakelen, voeg een profiel toe met een van deze ondersteunde providers: {{providers}}.\n\n[Instellingen openen]({{command}})", "kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Totale sessiekosten:", diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/pl.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/pl.ts index 7bb800c8d6c..64251cd2df7 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/pl.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/pl.ts @@ -6,8 +6,6 @@ export const dict = { "kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete", "kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete", "kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (wyłączone)", - "kilocode:autocomplete.statusBar.tooltip.noCredits": - "**Brak środków na koncie**\n\nTwoje konto Kilo Code nie ma środków. Aby korzystać z autouzupełniania, dodaj środki do swojego konta.\n\n[Otwórz Ustawienia]({{command}})", "kilocode:autocomplete.statusBar.tooltip.noUsableProvider": "**Nie skonfigurowano modelu autouzupełniania**\n\nAby włączyć autouzupełnianie, dodaj profil z jednym z tych obsługiwanych dostawców: {{providers}}.\n\n[Otwórz Ustawienia]({{command}})", "kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Całkowity koszt sesji:", diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/pt-BR.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/pt-BR.ts index a4794f2cf73..8c879217de2 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/pt-BR.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/pt-BR.ts @@ -6,8 +6,6 @@ export const dict = { "kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete", "kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete", "kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (desabilitado)", - "kilocode:autocomplete.statusBar.tooltip.noCredits": - "**Sem créditos na sua conta**\n\nSua conta Kilo Code não tem créditos. Para usar o autocompletar, adicione créditos à sua conta.\n\n[Abrir Configurações]({{command}})", "kilocode:autocomplete.statusBar.tooltip.noUsableProvider": "**Nenhum modelo de autocompletar configurado**\n\nPara habilitar o autocompletar, adicione um perfil com um destes provedores suportados: {{providers}}.\n\n[Abrir Configurações]({{command}})", "kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Custo total da sessão:", diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/ru.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/ru.ts index 4e1afb0d3f0..bb572c5f3e4 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/ru.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/ru.ts @@ -6,8 +6,6 @@ export const dict = { "kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete", "kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete", "kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (отключено)", - "kilocode:autocomplete.statusBar.tooltip.noCredits": - "**На вашем счёте нет кредитов**\n\nНа вашем счёте Kilo Code нет кредитов. Чтобы использовать автодополнение, пожалуйста, пополните счёт.\n\n[Открыть Настройки]({{command}})", "kilocode:autocomplete.statusBar.tooltip.noUsableProvider": "**Модель автодополнения не настроена**\n\nЧтобы включить автодополнение, добавьте профиль с одним из поддерживаемых провайдеров: {{providers}}.\n\n[Открыть Настройки]({{command}})", "kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Общая стоимость сессии:", diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/sk.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/sk.ts index 4e2959c3b98..8ff2f3c8e15 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/sk.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/sk.ts @@ -6,8 +6,6 @@ export const dict = { "kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete", "kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete", "kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (zakázané)", - "kilocode:autocomplete.statusBar.tooltip.noCredits": - "**Na tvojom účte nie sú žiadne kredity**\n\nTvoj účet Kilo Code nemá žiadne kredity. Pre použitie automatického doplňovania prosím pridaj kredity na svoj účet.\n\n[Otvoriť Nastavenia]({{command}})", "kilocode:autocomplete.statusBar.tooltip.noUsableProvider": "**Nie je nakonfigurovaný žiadny model automatického doplňovania**\n\nPre povolenie automatického doplňovania pridaj profil s jedným z týchto podporovaných poskytovateľov: {{providers}}.\n\n[Otvoriť Nastavenia]({{command}})", "kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Celkové náklady relácie:", diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/th.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/th.ts index 3f3b18aa595..3ceeca319f3 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/th.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/th.ts @@ -6,8 +6,6 @@ export const dict = { "kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete", "kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete", "kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (ปิดใช้งาน)", - "kilocode:autocomplete.statusBar.tooltip.noCredits": - "**ไม่มีเครดิตในบัญชีของคุณ**\n\nบัญชี Kilo Code ของคุณไม่มีเครดิต หากต้องการใช้การเติมข้อความอัตโนมัติ กรุณาเพิ่มเครดิตในบัญชีของคุณ\n\n[เปิดการตั้งค่า]({{command}})", "kilocode:autocomplete.statusBar.tooltip.noUsableProvider": "**ไม่ได้กำหนดค่าโมเดลการเติมข้อความอัตโนมัติ**\n\nหากต้องการเปิดใช้งานการเติมข้อความอัตโนมัติ ให้เพิ่มโปรไฟล์กับผู้ให้บริการที่รองรับเหล่านี้: {{providers}}\n\n[เปิดการตั้งค่า]({{command}})", "kilocode:autocomplete.statusBar.tooltip.sessionTotal": "ค่าใช้จ่ายรวมของเซสชัน:", diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/tr.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/tr.ts index d7c9586926b..417ba03ee1c 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/tr.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/tr.ts @@ -6,8 +6,6 @@ export const dict = { "kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete", "kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete", "kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (devre dışı)", - "kilocode:autocomplete.statusBar.tooltip.noCredits": - "**Hesabınızda kredi yok**\n\nKilo Code hesabınızda kredi bulunmuyor. Otomatik tamamlamayı kullanmak için lütfen hesabınıza kredi ekleyin.\n\n[Ayarları Aç]({{command}})", "kilocode:autocomplete.statusBar.tooltip.noUsableProvider": "**Otomatik tamamlama modeli yapılandırılmadı**\n\nOtomatik tamamlamayı etkinleştirmek için desteklenen sağlayıcılardan biriyle bir profil ekleyin: {{providers}}.\n\n[Ayarları Aç]({{command}})", "kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Oturum toplam maliyeti:", diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/uk.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/uk.ts index 57d742b661b..3dc1b1b32a4 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/uk.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/uk.ts @@ -6,8 +6,6 @@ export const dict = { "kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete", "kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete", "kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (вимкнено)", - "kilocode:autocomplete.statusBar.tooltip.noCredits": - "**На вашому рахунку немає кредитів**\n\nНа вашому рахунку Kilo Code немає кредитів. Щоб використовувати автодоповнення, будь ласка, поповніть рахунок.\n\n[Відкрити Налаштування]({{command}})", "kilocode:autocomplete.statusBar.tooltip.noUsableProvider": "**Модель автодоповнення не налаштована**\n\nЩоб увімкнути автодоповнення, додайте профіль з одним із підтримуваних провайдерів: {{providers}}.\n\n[Відкрити Налаштування]({{command}})", "kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Загальна вартість сесії:", diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/vi.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/vi.ts index c1f9f90775a..f645e3e2fe0 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/vi.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/vi.ts @@ -6,8 +6,6 @@ export const dict = { "kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete", "kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete", "kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete (đã tắt)", - "kilocode:autocomplete.statusBar.tooltip.noCredits": - "**Không có tín dụng trong tài khoản của bạn**\n\nTài khoản Kilo Code của bạn không có tín dụng. Để sử dụng tự động hoàn thành, vui lòng thêm tín dụng vào tài khoản của bạn.\n\n[Mở Cài đặt]({{command}})", "kilocode:autocomplete.statusBar.tooltip.noUsableProvider": "**Chưa cấu hình mô hình tự động hoàn thành**\n\nĐể bật tự động hoàn thành, hãy thêm hồ sơ với một trong các nhà cung cấp được hỗ trợ sau: {{providers}}.\n\n[Mở Cài đặt]({{command}})", "kilocode:autocomplete.statusBar.tooltip.sessionTotal": "Tổng chi phí phiên:", diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/zh-CN.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/zh-CN.ts index 4efa687064d..da8cf67adbf 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/zh-CN.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/zh-CN.ts @@ -6,8 +6,6 @@ export const dict = { "kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete", "kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete", "kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete(已禁用)", - "kilocode:autocomplete.statusBar.tooltip.noCredits": - "**账户余额不足**\n\n你的 Kilo Code 账户没有余额。要使用自动补全功能,请为账户充值。\n\n[打开设置]({{command}})", "kilocode:autocomplete.statusBar.tooltip.noUsableProvider": "**未配置自动补全模型**\n\n要启用自动补全,请添加一个使用以下支持的提供商的配置文件:{{providers}}。\n\n[打开设置]({{command}})", "kilocode:autocomplete.statusBar.tooltip.sessionTotal": "会话总费用:", diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/zh-TW.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/zh-TW.ts index a6bb2fd388b..7bce63c2476 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/zh-TW.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/zh-TW.ts @@ -6,8 +6,6 @@ export const dict = { "kilocode:autocomplete.statusBar.warning": "$(warning) Autocomplete", "kilocode:autocomplete.statusBar.tooltip.basic": "Kilo Code Autocomplete", "kilocode:autocomplete.statusBar.tooltip.disabled": "Kilo Code Autocomplete(已停用)", - "kilocode:autocomplete.statusBar.tooltip.noCredits": - "**帳戶中沒有額度**\n\nKilo Code 帳戶沒有額度。若要使用 Autocomplete,請為帳戶儲值。\n\n[開啟設定]({{command}})", "kilocode:autocomplete.statusBar.tooltip.noUsableProvider": "**未設定 Autocomplete 模型**\n\n若要啟用 Autocomplete,請新增一個使用以下支援供應商的設定檔:{{providers}}。\n\n[開啟設定]({{command}})", "kilocode:autocomplete.statusBar.tooltip.sessionTotal": "工作階段總費用:", diff --git a/packages/kilo-vscode/src/services/autocomplete/types.ts b/packages/kilo-vscode/src/services/autocomplete/types.ts index d32531f55f7..cce2f84cdc7 100644 --- a/packages/kilo-vscode/src/services/autocomplete/types.ts +++ b/packages/kilo-vscode/src/services/autocomplete/types.ts @@ -137,7 +137,6 @@ export interface AutocompleteStatusBarStateProps { model?: string provider?: string profileName?: string | null - hasKilocodeProfileWithNoBalance?: boolean hasNoUsableProvider?: boolean totalSessionCost: number completionCount: number diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx index ada39f81c6e..16f3c8cd273 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx @@ -358,6 +358,8 @@ const AgentBehaviourTab: Component = () => { /> +
Date: Tue, 17 Mar 2026 12:33:02 +0100 Subject: [PATCH 27/74] fix(vscode): revert stray test div and empty openapi.json from previous commit --- .../webview-ui/src/components/settings/AgentBehaviourTab.tsx | 2 -- packages/sdk/js/openapi.json | 0 2 files changed, 2 deletions(-) delete mode 100644 packages/sdk/js/openapi.json diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx index 16f3c8cd273..ada39f81c6e 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx @@ -358,8 +358,6 @@ const AgentBehaviourTab: Component = () => { /> -
Date: Tue, 17 Mar 2026 12:35:14 +0100 Subject: [PATCH 28/74] test(vscode): rewrite AutocompleteModel tests for current CLI-backend architecture --- .../__tests__/AutocompleteModel.spec.ts | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteModel.spec.ts diff --git a/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteModel.spec.ts b/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteModel.spec.ts new file mode 100644 index 00000000000..dd17da63618 --- /dev/null +++ b/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteModel.spec.ts @@ -0,0 +1,206 @@ +import { describe, it, expect, vi } from "vitest" +import { AutocompleteModel } from "../AutocompleteModel" +import type { KiloConnectionService } from "../../cli-backend" + +function createMockConnectionService(state: "connecting" | "connected" | "disconnected" | "error" = "connected") { + return { + getConnectionState: vi.fn().mockReturnValue(state), + getClient: vi.fn().mockReturnValue({ + kilo: { + fim: vi.fn(), + }, + }), + onStateChange: vi.fn().mockReturnValue(() => {}), + } as unknown as KiloConnectionService +} + +describe("AutocompleteModel", () => { + describe("constructor", () => { + it("sets loaded to true when connection service is provided", () => { + const connection = createMockConnectionService() + const model = new AutocompleteModel(connection) + expect(model.loaded).toBe(true) + }) + + it("defaults loaded to false without connection service", () => { + const model = new AutocompleteModel() + expect(model.loaded).toBe(false) + }) + + it("defaults profileName and profileType to null", () => { + const model = new AutocompleteModel() + expect(model.profileName).toBeNull() + expect(model.profileType).toBeNull() + }) + }) + + describe("setConnectionService", () => { + it("sets the connection service after construction", () => { + const model = new AutocompleteModel() + expect(model.hasValidCredentials()).toBe(false) + + const connection = createMockConnectionService("connected") + model.setConnectionService(connection) + expect(model.hasValidCredentials()).toBe(true) + }) + }) + + describe("reload", () => { + it("sets loaded to true", async () => { + const model = new AutocompleteModel() + expect(model.loaded).toBe(false) + await model.reload() + expect(model.loaded).toBe(true) + }) + + it("returns true when connected", async () => { + const connection = createMockConnectionService("connected") + const model = new AutocompleteModel(connection) + expect(await model.reload()).toBe(true) + }) + + it("returns false when disconnected", async () => { + const connection = createMockConnectionService("disconnected") + const model = new AutocompleteModel(connection) + expect(await model.reload()).toBe(false) + }) + + it("returns false when connecting", async () => { + const connection = createMockConnectionService("connecting") + const model = new AutocompleteModel(connection) + expect(await model.reload()).toBe(false) + }) + + it("returns false when in error state", async () => { + const connection = createMockConnectionService("error") + const model = new AutocompleteModel(connection) + expect(await model.reload()).toBe(false) + }) + + it("returns false without connection service", async () => { + const model = new AutocompleteModel() + expect(await model.reload()).toBe(false) + }) + }) + + describe("hasValidCredentials", () => { + it("returns true when connected", () => { + const connection = createMockConnectionService("connected") + const model = new AutocompleteModel(connection) + expect(model.hasValidCredentials()).toBe(true) + }) + + it("returns false when disconnected", () => { + const connection = createMockConnectionService("disconnected") + const model = new AutocompleteModel(connection) + expect(model.hasValidCredentials()).toBe(false) + }) + + it("returns false without connection service", () => { + const model = new AutocompleteModel() + expect(model.hasValidCredentials()).toBe(false) + }) + }) + + describe("supportsFim", () => { + it("always returns true", () => { + const model = new AutocompleteModel() + expect(model.supportsFim()).toBe(true) + }) + }) + + describe("getModelName", () => { + it("returns the default model", () => { + const model = new AutocompleteModel() + expect(model.getModelName()).toBe("mistralai/codestral-2508") + }) + }) + + describe("getProviderDisplayName", () => { + it("returns Kilo Gateway", () => { + const model = new AutocompleteModel() + expect(model.getProviderDisplayName()).toBe("Kilo Gateway") + }) + }) + + describe("generateFimResponse", () => { + it("throws when connection service is not available", async () => { + const model = new AutocompleteModel() + await expect(model.generateFimResponse("prefix", "suffix", vi.fn())).rejects.toThrow( + "Connection service is not available", + ) + }) + + it("throws when not connected", async () => { + const connection = createMockConnectionService("disconnected") + const model = new AutocompleteModel(connection) + await expect(model.generateFimResponse("prefix", "suffix", vi.fn())).rejects.toThrow( + "CLI backend is not connected (state: disconnected)", + ) + }) + + it("streams chunks and returns metadata", async () => { + const chunks = [ + { choices: [{ delta: { content: "hello" } }] }, + { + choices: [{ delta: { content: " world" } }], + usage: { prompt_tokens: 10, completion_tokens: 5 }, + cost: 0.001, + }, + ] + + const connection = createMockConnectionService("connected") + const client = (connection as any).getClient() + client.kilo.fim.mockResolvedValue({ + stream: (async function* () { + for (const chunk of chunks) yield chunk + })(), + }) + + const model = new AutocompleteModel(connection) + const received: string[] = [] + const result = await model.generateFimResponse("prefix", "suffix", (text) => received.push(text)) + + expect(received).toEqual(["hello", " world"]) + expect(result).toEqual({ + cost: 0.001, + inputTokens: 10, + outputTokens: 5, + cacheWriteTokens: 0, + cacheReadTokens: 0, + }) + }) + + it("passes model parameters to fim call", async () => { + const connection = createMockConnectionService("connected") + const client = (connection as any).getClient() + client.kilo.fim.mockResolvedValue({ + stream: (async function* () {})(), + }) + + const model = new AutocompleteModel(connection) + const signal = new AbortController().signal + await model.generateFimResponse("pre", "suf", vi.fn(), signal) + + expect(client.kilo.fim).toHaveBeenCalledWith( + { + prefix: "pre", + suffix: "suf", + model: "mistralai/codestral-2508", + maxTokens: 256, + temperature: 0.2, + }, + { signal }, + ) + }) + }) + + describe("generateResponse", () => { + it("throws because FIM is the primary strategy", async () => { + const model = new AutocompleteModel() + await expect(model.generateResponse("system", "user", vi.fn())).rejects.toThrow( + "Chat-based completions are not supported via CLI backend", + ) + }) + }) +}) From c1b5b23dca5263a7a81437681eeb27728ff2d19e Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 17 Mar 2026 12:48:55 +0100 Subject: [PATCH 29/74] fix(vscode): guard Select onSelect handlers against no-op config updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kobalte's Select fires onChange when the options list changes (even if the selected value is the same), which triggers updateConfig → PATCH /global/config → global.disposed → configLoaded → new options array → onChange again, creating an infinite loop that floods SSE with events and kills the server. Add guards to all three settings Select components to skip updateConfig when the new value equals the current config value. --- .../src/components/settings/AgentBehaviourTab.tsx | 7 ++++++- .../webview-ui/src/components/settings/DisplayTab.tsx | 7 ++++++- .../webview-ui/src/components/settings/ExperimentalTab.tsx | 7 ++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx index ada39f81c6e..8d555b0cff9 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx @@ -239,7 +239,12 @@ const AgentBehaviourTab: Component = () => { current={defaultAgentOptions().find((o) => o.value === (config().default_agent ?? ""))} value={(o) => o.value} label={(o) => o.label} - onSelect={(o) => o && updateConfig({ default_agent: o.value || undefined })} + onSelect={(o) => { + if (!o) return + const next = o.value || undefined + if (next === (config().default_agent ?? undefined)) return + updateConfig({ default_agent: next }) + }} variant="secondary" size="small" triggerVariant="settings" diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/DisplayTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/DisplayTab.tsx index 9ba75d67b3c..812209e7c91 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/DisplayTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/DisplayTab.tsx @@ -46,7 +46,12 @@ const DisplayTab: Component = () => { current={LAYOUT_OPTIONS.find((o) => o.value === (config().layout ?? "auto"))} value={(o) => o.value} label={(o) => language.t(o.labelKey)} - onSelect={(o) => o && updateConfig({ layout: o.value as "auto" | "stretch" })} + onSelect={(o) => { + if (!o) return + const next = o.value as "auto" | "stretch" + if (next === (config().layout ?? "auto")) return + updateConfig({ layout: next }) + }} variant="secondary" size="small" triggerVariant="settings" diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx index 94d0ec10b18..4f060d2e0df 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/ExperimentalTab.tsx @@ -43,7 +43,12 @@ const ExperimentalTab: Component = () => { current={SHARE_OPTIONS.find((o) => o.value === (config().share ?? "manual"))} value={(o) => o.value} label={(o) => language.t(o.labelKey)} - onSelect={(o) => o && updateConfig({ share: o.value as "manual" | "auto" | "disabled" })} + onSelect={(o) => { + if (!o) return + const next = o.value as "manual" | "auto" | "disabled" + if (next === (config().share ?? "manual")) return + updateConfig({ share: next }) + }} variant="secondary" size="small" triggerVariant="settings" From 1043d9dd52ad2cb51eca41ef9af95cb583d9d7b9 Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 17 Mar 2026 13:02:05 +0100 Subject: [PATCH 30/74] refactor(vscode): remove unused loaded/reload from AutocompleteModel The loaded flag was always true after construction (set unconditionally) and reload() did nothing beyond that. Connection state is already checked live via hasValidCredentials(). Removes the dead indirection from AutocompleteModel, AutocompleteServiceManager, and ChatTextAreaAutocomplete. --- .../autocomplete/AutocompleteModel.ts | 17 ------ .../AutocompleteServiceManager.ts | 18 +----- .../__tests__/AutocompleteModel.spec.ts | 61 ++++--------------- .../AutocompleteServiceManager.spec.ts | 5 -- .../ChatTextAreaAutocomplete.ts | 11 ---- .../ChatTextAreaAutocomplete.spec.ts | 1 - 6 files changed, 15 insertions(+), 98 deletions(-) diff --git a/packages/kilo-vscode/src/services/autocomplete/AutocompleteModel.ts b/packages/kilo-vscode/src/services/autocomplete/AutocompleteModel.ts index 58c60988650..bfadd50204b 100644 --- a/packages/kilo-vscode/src/services/autocomplete/AutocompleteModel.ts +++ b/packages/kilo-vscode/src/services/autocomplete/AutocompleteModel.ts @@ -20,12 +20,10 @@ export class AutocompleteModel { private connectionService: KiloConnectionService | null = null public profileName: string | null = null public profileType: string | null = null - public loaded = false constructor(connectionService?: KiloConnectionService) { if (connectionService) { this.connectionService = connectionService - this.loaded = true } } @@ -36,21 +34,6 @@ export class AutocompleteModel { this.connectionService = service } - /** - * Load model configuration. - * Returns true if the connection service is available. - */ - public async reload(): Promise { - this.loaded = true - - if (this.connectionService) { - const state = this.connectionService.getConnectionState() - return state === "connected" - } - - return false - } - public supportsFim(): boolean { return true } diff --git a/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts b/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts index eaad9936311..09dd95e0c66 100644 --- a/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts +++ b/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts @@ -103,7 +103,6 @@ export class AutocompleteServiceManager { } public async load() { - await this.model.reload() this.settings = readSettings() await this.updateGlobalContext() @@ -243,11 +242,6 @@ export class AutocompleteServiceManager { const document = editor.document - // Ensure model is loaded - if (!this.model.loaded) { - await this.load() - } - // Call the inline completion provider directly with manual trigger context const position = editor.selection.active const context: vscode.InlineCompletionContext = { @@ -299,22 +293,16 @@ export class AutocompleteServiceManager { }) } - private getCurrentModelName(): string | undefined { - if (!this.model.loaded) { - return - } + private getCurrentModelName(): string { return this.model.getModelName() } - private getCurrentProviderName(): string | undefined { - if (!this.model.loaded) { - return - } + private getCurrentProviderName(): string { return this.model.getProviderDisplayName() } private hasNoUsableProvider(): boolean { - return this.model.loaded && !this.model.hasValidCredentials() + return !this.model.hasValidCredentials() } private updateCostTracking(cost: number, _inputTokens: number, _outputTokens: number): void { diff --git a/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteModel.spec.ts b/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteModel.spec.ts index dd17da63618..d94bcd0612e 100644 --- a/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteModel.spec.ts +++ b/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteModel.spec.ts @@ -16,17 +16,6 @@ function createMockConnectionService(state: "connecting" | "connected" | "discon describe("AutocompleteModel", () => { describe("constructor", () => { - it("sets loaded to true when connection service is provided", () => { - const connection = createMockConnectionService() - const model = new AutocompleteModel(connection) - expect(model.loaded).toBe(true) - }) - - it("defaults loaded to false without connection service", () => { - const model = new AutocompleteModel() - expect(model.loaded).toBe(false) - }) - it("defaults profileName and profileType to null", () => { const model = new AutocompleteModel() expect(model.profileName).toBeNull() @@ -45,44 +34,6 @@ describe("AutocompleteModel", () => { }) }) - describe("reload", () => { - it("sets loaded to true", async () => { - const model = new AutocompleteModel() - expect(model.loaded).toBe(false) - await model.reload() - expect(model.loaded).toBe(true) - }) - - it("returns true when connected", async () => { - const connection = createMockConnectionService("connected") - const model = new AutocompleteModel(connection) - expect(await model.reload()).toBe(true) - }) - - it("returns false when disconnected", async () => { - const connection = createMockConnectionService("disconnected") - const model = new AutocompleteModel(connection) - expect(await model.reload()).toBe(false) - }) - - it("returns false when connecting", async () => { - const connection = createMockConnectionService("connecting") - const model = new AutocompleteModel(connection) - expect(await model.reload()).toBe(false) - }) - - it("returns false when in error state", async () => { - const connection = createMockConnectionService("error") - const model = new AutocompleteModel(connection) - expect(await model.reload()).toBe(false) - }) - - it("returns false without connection service", async () => { - const model = new AutocompleteModel() - expect(await model.reload()).toBe(false) - }) - }) - describe("hasValidCredentials", () => { it("returns true when connected", () => { const connection = createMockConnectionService("connected") @@ -96,6 +47,18 @@ describe("AutocompleteModel", () => { expect(model.hasValidCredentials()).toBe(false) }) + it("returns false when connecting", () => { + const connection = createMockConnectionService("connecting") + const model = new AutocompleteModel(connection) + expect(model.hasValidCredentials()).toBe(false) + }) + + it("returns false when in error state", () => { + const connection = createMockConnectionService("error") + const model = new AutocompleteModel(connection) + expect(model.hasValidCredentials()).toBe(false) + }) + it("returns false without connection service", () => { const model = new AutocompleteModel() expect(model.hasValidCredentials()).toBe(false) diff --git a/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteServiceManager.spec.ts b/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteServiceManager.spec.ts index 11fb9ff0042..5d4c68a34cf 100644 --- a/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteServiceManager.spec.ts +++ b/packages/kilo-vscode/src/services/autocomplete/__tests__/AutocompleteServiceManager.spec.ts @@ -63,13 +63,8 @@ vi.mock("vscode", () => { vi.mock("../AutocompleteModel", () => { class AutocompleteModel { - public loaded = false public profileName = "test-profile" - public async reload(): Promise { - this.loaded = true - } - public getModelName(): string { return "test-model" } diff --git a/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete.ts b/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete.ts index 113247c89e7..6dc2bb97c9b 100644 --- a/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete.ts +++ b/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/ChatTextAreaAutocomplete.ts @@ -15,10 +15,6 @@ export class ChatTextAreaAutocomplete { this.telemetry = new AutocompleteTelemetry("chat-textarea") } - async initialize(): Promise { - return this.model.reload() - } - async getCompletion(userText: string, visibleCodeContext?: VisibleCodeContext): Promise<{ suggestion: string }> { const startTime = Date.now() @@ -29,13 +25,6 @@ export class ChatTextAreaAutocomplete { provider: this.model.getProviderDisplayName(), } - if (!this.model.loaded) { - const loaded = await this.initialize() - if (!loaded) { - return { suggestion: "" } - } - } - // Check if model has valid credentials (but don't require FIM) if (!this.model.hasValidCredentials()) { return { suggestion: "" } diff --git a/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/__tests__/ChatTextAreaAutocomplete.spec.ts b/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/__tests__/ChatTextAreaAutocomplete.spec.ts index dc43ff1b076..6fb284743d5 100644 --- a/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/__tests__/ChatTextAreaAutocomplete.spec.ts +++ b/packages/kilo-vscode/src/services/autocomplete/chat-autocomplete/__tests__/ChatTextAreaAutocomplete.spec.ts @@ -16,7 +16,6 @@ describe("ChatTextAreaAutocomplete", () => { it("should work with non-FIM models using chat-based completion", async () => { // Setup: Model without FIM support (like Mistral) const mockModel = new AutocompleteModel() - mockModel.loaded = true vi.spyOn(mockModel, "hasValidCredentials").mockReturnValue(true) vi.spyOn(mockModel, "supportsFim").mockReturnValue(false) From d50fbb21adab9e6d6f95c2be0524b1eb75499e57 Mon Sep 17 00:00:00 2001 From: Marius Date: Tue, 17 Mar 2026 13:26:19 +0100 Subject: [PATCH 31/74] test(vscode): add architecture test for package.json command sync (#7173) * test(vscode): add architecture test for package.json command sync * fix: match registerCommand() calls instead of any string occurrence Addresses review feedback: the test now extracts command IDs specifically from registerCommand() call sites, so an executeCommand() reference won't satisfy the check. --- .../tests/unit/extension-arch.test.ts | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 packages/kilo-vscode/tests/unit/extension-arch.test.ts diff --git a/packages/kilo-vscode/tests/unit/extension-arch.test.ts b/packages/kilo-vscode/tests/unit/extension-arch.test.ts new file mode 100644 index 00000000000..8a935d450d2 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/extension-arch.test.ts @@ -0,0 +1,86 @@ +/** + * Architecture test: package.json ↔ source command sync + * + * Every command declared in package.json contributes.commands must have a + * matching registerCommand() call somewhere in src/. A declaration without + * an implementation causes a silent "command not found" error at runtime + * that is hard to diagnose — VS Code shows no warning at activation time. + */ + +import { describe, it, expect } from "bun:test" +import fs from "node:fs" +import path from "node:path" + +const ROOT = path.resolve(import.meta.dir, "../..") +const PKG_JSON_FILE = path.join(ROOT, "package.json") +const SRC_DIR = path.join(ROOT, "src") + +function readSrcFiles(dir: string): string { + const parts: string[] = [] + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name) + if (entry.isDirectory()) { + parts.push(readSrcFiles(full)) + } else if (entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts") && !entry.name.endsWith(".spec.ts")) { + parts.push(fs.readFileSync(full, "utf-8")) + } + } + return parts.join("\n") +} + +describe("Extension — package.json command sync", () => { + const pkg = JSON.parse(fs.readFileSync(PKG_JSON_FILE, "utf-8")) + const declared: string[] = pkg.contributes?.commands?.map((c: { command: string }) => c.command) ?? [] + const source = readSrcFiles(SRC_DIR) + + // Extract command IDs that appear in registerCommand() calls specifically. + // This avoids false positives from executeCommand() or other string references. + const registered = new Set([...source.matchAll(/registerCommand\s*\(\s*["']([^"']+)["']/g)].map((m) => m[1])) + + /** + * Every command declared in package.json must be registered via registerCommand() + * somewhere in src/. A bare string match would accept executeCommand() references, + * which don't actually register a handler. + * + * Commands registered via template literals (e.g. jumpTo${i}) are detected by + * checking the dynamic registerCommand pattern in source instead. + */ + it("every contributes.commands entry has a registerCommand() call", () => { + // Commands generated via template literals can't be extracted by regex, + // so verify the dynamic registration pattern exists in source instead. + const dynamic: Record = { + "kilo-code.new.agentManager.jumpTo": "registerCommand(`kilo-code.new.agentManager.jumpTo${", + } + + const missing: string[] = [] + for (const cmd of declared) { + const entry = Object.entries(dynamic).find(([prefix]) => cmd.startsWith(prefix)) + if (entry) { + const [, pattern] = entry + if (!source.includes(pattern)) missing.push(`${cmd} (dynamic pattern not found)`) + continue + } + if (!registered.has(cmd)) missing.push(cmd) + } + + expect( + missing, + `Commands declared in package.json but not registered via registerCommand().\n` + + `Add registerCommand("...", ...) or remove the declaration:\n` + + missing.map((m) => ` - ${m}`).join("\n"), + ).toEqual([]) + }) + + /** + * All declared commands must use the kilo-code.new. prefix. + * The legacy kilo-code.* namespace (without .new.) belongs to the old + * extension and must not be reintroduced. + */ + it("all declared commands use the kilo-code.new. prefix", () => { + const bad = declared.filter((cmd) => !cmd.startsWith("kilo-code.new.")) + expect( + bad, + `Commands without "kilo-code.new." prefix — use the namespaced form:\n` + bad.map((b) => ` - ${b}`).join("\n"), + ).toEqual([]) + }) +}) From 8df8fc85205f1d753ea3fd69a52a56e971a0e02a Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 17 Mar 2026 13:32:59 +0100 Subject: [PATCH 32/74] fix(i18n): add prompt.placeholder.error translations for all 16 locales --- packages/kilo-vscode/webview-ui/src/i18n/ar.ts | 2 +- packages/kilo-vscode/webview-ui/src/i18n/br.ts | 2 +- packages/kilo-vscode/webview-ui/src/i18n/bs.ts | 2 +- packages/kilo-vscode/webview-ui/src/i18n/da.ts | 2 +- packages/kilo-vscode/webview-ui/src/i18n/de.ts | 3 ++- packages/kilo-vscode/webview-ui/src/i18n/en.ts | 2 +- packages/kilo-vscode/webview-ui/src/i18n/es.ts | 2 +- packages/kilo-vscode/webview-ui/src/i18n/fr.ts | 2 +- packages/kilo-vscode/webview-ui/src/i18n/ja.ts | 2 +- packages/kilo-vscode/webview-ui/src/i18n/ko.ts | 2 +- packages/kilo-vscode/webview-ui/src/i18n/no.ts | 2 +- packages/kilo-vscode/webview-ui/src/i18n/pl.ts | 2 +- packages/kilo-vscode/webview-ui/src/i18n/ru.ts | 2 +- packages/kilo-vscode/webview-ui/src/i18n/th.ts | 2 +- packages/kilo-vscode/webview-ui/src/i18n/zh.ts | 2 +- packages/kilo-vscode/webview-ui/src/i18n/zht.ts | 2 +- 16 files changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index f0decff554e..da05e2cb574 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -837,7 +837,7 @@ export const dict = { "dialog.model.noProviders": "لا يوجد موفرون", "prompt.placeholder.connecting": "جارٍ الاتصال بالخادم...", - "prompt.placeholder.error": "", + "prompt.placeholder.error": "فشل الاتصال. تحقق من لوحة الإخراج أو أعد تشغيل الإضافة.", "prompt.placeholder.default": "اكتب رسالة... (Enter للإرسال، Shift+Enter لسطر جديد)", "context.usage.sessionCost": "تكلفة الجلسة", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 79a171678e8..8e1d0266692 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -845,7 +845,7 @@ export const dict = { "dialog.model.noProviders": "Nenhum provedor", "prompt.placeholder.connecting": "Conectando ao servidor...", - "prompt.placeholder.error": "", + "prompt.placeholder.error": "Conexão falhou. Verifique o painel de saída ou reinicie a extensão.", "prompt.placeholder.default": "Digite uma mensagem... (Enter para enviar, Shift+Enter para nova linha)", "context.usage.sessionCost": "Custo da sessão", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index 9c1bef45bac..8d7d00617bd 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -848,7 +848,7 @@ export const dict = { "dialog.model.noProviders": "Nema pružatelja", "prompt.placeholder.connecting": "Povezivanje na server...", - "prompt.placeholder.error": "", + "prompt.placeholder.error": "Povezivanje nije uspjelo. Provjerite panel za izlaz ili ponovo pokrenite ekstenziju.", "prompt.placeholder.default": "Unesite poruku... (Enter za slanje, Shift+Enter za novi red)", "context.usage.sessionCost": "Cijena sesije", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index e7de1399ffc..b23ed88ff3f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -843,7 +843,7 @@ export const dict = { "dialog.model.noProviders": "Ingen udbydere", "prompt.placeholder.connecting": "Opretter forbindelse til server...", - "prompt.placeholder.error": "", + "prompt.placeholder.error": "Forbindelse mislykkedes. Tjek outputpanelet eller genstart udvidelsen.", "prompt.placeholder.default": "Skriv en besked... (Enter for at sende, Shift+Enter for ny linje)", "context.usage.sessionCost": "Sessionsomkostning", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index c9b2a01c7a1..2464ba322f4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -853,7 +853,8 @@ export const dict = { "dialog.model.noProviders": "Keine Anbieter", "prompt.placeholder.connecting": "Verbindung zum Server wird hergestellt...", - "prompt.placeholder.error": "", + "prompt.placeholder.error": + "Verbindung fehlgeschlagen. Überprüfen Sie das Ausgabepanel oder starten Sie die Erweiterung neu.", "prompt.placeholder.default": "Nachricht eingeben... (Enter zum Senden, Shift+Enter für neue Zeile)", "context.usage.sessionCost": "Sitzungskosten", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index 72872fb9ae2..57ecac511c5 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -846,7 +846,7 @@ export const dict = { "prompt.placeholder.connecting": "Connecting to server...", "prompt.placeholder.default": "Type a message... (Enter to send, Shift+Enter for new line)", - "prompt.placeholder.error": "", + "prompt.placeholder.error": "Connection failed. Check the output panel or restart the extension.", "context.usage.sessionCost": "Session cost", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index d49ca0d1e04..858f0e4ae06 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -850,7 +850,7 @@ export const dict = { "dialog.model.noProviders": "Sin proveedores", "prompt.placeholder.connecting": "Conectando al servidor...", - "prompt.placeholder.error": "", + "prompt.placeholder.error": "Conexión fallida. Revisa el panel de salida o reinicia la extensión.", "prompt.placeholder.default": "Escribe un mensaje... (Enter para enviar, Shift+Enter para nueva línea)", "context.usage.sessionCost": "Coste de la sesión", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index 35f02b3a486..f221576c3b4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -855,7 +855,7 @@ export const dict = { "dialog.model.noProviders": "Aucun fournisseur", "prompt.placeholder.connecting": "Connexion au serveur...", - "prompt.placeholder.error": "", + "prompt.placeholder.error": "Échec de la connexion. Vérifiez le panneau de sortie ou redémarrez l'extension.", "prompt.placeholder.default": "Tapez un message... (Entrée pour envoyer, Maj+Entrée pour un saut de ligne)", "context.usage.sessionCost": "Coût de la session", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 5d91287959f..37683e27b74 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -842,7 +842,7 @@ export const dict = { "dialog.model.noProviders": "プロバイダーなし", "prompt.placeholder.connecting": "サーバーに接続中...", - "prompt.placeholder.error": "", + "prompt.placeholder.error": "接続に失敗しました。出力パネルを確認するか、拡張機能を再起動してください。", "prompt.placeholder.default": "メッセージを入力... (Enterで送信、Shift+Enterで改行)", "context.usage.sessionCost": "セッションコスト", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index a8464a71f20..42e58e00f14 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -842,7 +842,7 @@ export const dict = { "dialog.model.noProviders": "공급자 없음", "prompt.placeholder.connecting": "서버에 연결 중...", - "prompt.placeholder.error": "", + "prompt.placeholder.error": "연결에 실패했습니다. 출력 패널을 확인하거나 확장 프로그램을 다시 시작하세요.", "prompt.placeholder.default": "메시지를 입력하세요... (Enter로 전송, Shift+Enter로 줄 바꿈)", "context.usage.sessionCost": "세션 비용", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index e5c707a83ae..86c1ed107a6 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -847,7 +847,7 @@ export const dict = { "dialog.model.noProviders": "Ingen leverandører", "prompt.placeholder.connecting": "Kobler til server...", - "prompt.placeholder.error": "", + "prompt.placeholder.error": "Tilkobling mislyktes. Sjekk utdatapanelet eller start utvidelsen på nytt.", "prompt.placeholder.default": "Skriv en melding... (Enter for å sende, Shift+Enter for ny linje)", "context.usage.sessionCost": "Sesjonskostnad", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 0d6dba9c3dc..f2eb77a7b83 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -845,7 +845,7 @@ export const dict = { "dialog.model.noProviders": "Brak dostawców", "prompt.placeholder.connecting": "Łączenie z serwerem...", - "prompt.placeholder.error": "", + "prompt.placeholder.error": "Połączenie nie powiodło się. Sprawdź panel wyjściowy lub uruchom ponownie rozszerzenie.", "prompt.placeholder.default": "Wpisz wiadomość... (Enter, aby wysłać, Shift+Enter dla nowej linii)", "context.usage.sessionCost": "Koszt sesji", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 5192306a677..8ecee17b951 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -849,7 +849,7 @@ export const dict = { "dialog.model.noProviders": "Нет провайдеров", "prompt.placeholder.connecting": "Подключение к серверу...", - "prompt.placeholder.error": "", + "prompt.placeholder.error": "Не удалось подключиться. Проверьте панель вывода или перезапустите расширение.", "prompt.placeholder.default": "Введите сообщение... (Enter для отправки, Shift+Enter для новой строки)", "context.usage.sessionCost": "Стоимость сессии", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index 0241f742947..8441da2a53c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -838,7 +838,7 @@ export const dict = { "dialog.model.noProviders": "ไม่มีผู้ให้บริการ", "prompt.placeholder.connecting": "กำลังเชื่อมต่อกับเซิร์ฟเวอร์...", - "prompt.placeholder.error": "", + "prompt.placeholder.error": "การเชื่อมต่อล้มเหลว ตรวจสอบแผงเอาต์พุตหรือรีสตาร์ทส่วนขยาย", "prompt.placeholder.default": "พิมพ์ข้อความ... (Enter เพื่อส่ง, Shift+Enter เพื่อขึ้นบรรทัดใหม่)", "context.usage.sessionCost": "ค่าใช้จ่ายเซสชัน", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 03ed924a7de..5c415474e2a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -831,7 +831,7 @@ export const dict = { "dialog.model.noProviders": "无供应商", "prompt.placeholder.connecting": "正在连接服务器...", - "prompt.placeholder.error": "", + "prompt.placeholder.error": "连接失败。请检查输出面板或重启扩展。", "prompt.placeholder.default": "输入消息... (Enter 发送,Shift+Enter 换行)", "context.usage.sessionCost": "会话费用", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index 54aa5214ba6..1950c444017 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -833,7 +833,7 @@ export const dict = { "dialog.model.noProviders": "沒有供應商", "prompt.placeholder.connecting": "正在連線至伺服器...", - "prompt.placeholder.error": "", + "prompt.placeholder.error": "連線失敗。請檢查輸出面板或重新啟動擴充功能。", "prompt.placeholder.default": "輸入訊息... (Enter 送出,Shift+Enter 換行)", "context.usage.sessionCost": "工作階段費用", From b70ebd76c1304adc92dc09ddecd09e10eae7ca91 Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 17 Mar 2026 13:41:24 +0100 Subject: [PATCH 33/74] fix(i18n): translate error.details.show to Norwegian --- packages/kilo-vscode/webview-ui/src/i18n/no.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 86c1ed107a6..31a443719f7 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -398,7 +398,7 @@ export const dict = { "error.page.title": "Noe gikk galt", "error.page.description": "Det oppstod en feil under lasting av applikasjonen.", "error.page.details.label": "Feildetaljer", - "error.details.show": "Details", + "error.details.show": "Detaljer", "error.page.action.restart": "Start på nytt", "error.page.action.checking": "Sjekker...", "error.page.action.checkUpdates": "Se etter oppdateringer", From 201a62b505eefaaad463fd44c07143d32f29179d Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 17 Mar 2026 13:54:07 +0100 Subject: [PATCH 34/74] fix(vscode): stop pushing provider disposable into context.subscriptions on reconnect The inline completion provider registration was pushed into context.subscriptions on every reconnect, accumulating dead disposables. Track it only via the instance field which is properly disposed and replaced on re-registration. --- .../src/services/autocomplete/AutocompleteServiceManager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts b/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts index 09dd95e0c66..b69bbaa158a 100644 --- a/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts +++ b/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts @@ -124,12 +124,12 @@ export class AutocompleteServiceManager { return } - // Register classic provider + // Register classic provider (tracked via this.inlineCompletionProviderDisposable, + // not context.subscriptions, so re-registration on reconnect doesn't leak) this.inlineCompletionProviderDisposable = vscode.languages.registerInlineCompletionItemProvider( { scheme: "file" }, this.inlineCompletionProvider, ) - this.context.subscriptions.push(this.inlineCompletionProviderDisposable) } public async disable() { From d7aaee5e889d443f2ece9e24afe220c655fa30b8 Mon Sep 17 00:00:00 2001 From: Marius Date: Tue, 17 Mar 2026 13:55:57 +0100 Subject: [PATCH 35/74] fix(ui): unify tool card arrow direction and header consistency (#7177) * fix(ui): unify tool card arrow direction and header consistency * chore: update visual regression baselines --------- Co-authored-by: github-actions[bot] --- packages/kilo-ui/src/components/basic-tool.css | 9 +++++++++ packages/kilo-ui/src/components/collapsible.css | 11 +++++++++++ packages/kilo-ui/src/components/message-part.css | 6 +++++- packages/kilo-ui/src/components/message-part.tsx | 2 +- .../with-reasoning-collapsed-chromium-linux.png | 4 ++-- .../with-reasoning-expanded-chromium-linux.png | 4 ++-- 6 files changed, 30 insertions(+), 6 deletions(-) diff --git a/packages/kilo-ui/src/components/basic-tool.css b/packages/kilo-ui/src/components/basic-tool.css index 84fa512d83c..50f086d83e8 100644 --- a/packages/kilo-ui/src/components/basic-tool.css +++ b/packages/kilo-ui/src/components/basic-tool.css @@ -73,12 +73,21 @@ html[data-theme="kilo-vscode"] [data-component="tool-part-wrapper"][data-part-ty padding: 0 8px; height: 36px; background-color: var(--surface-inset-base, var(--vscode-sideBar-background)); + + &:hover { + background-color: var(--surface-inset-base-hover, var(--vscode-list-hoverBackground)); + } } [data-slot="collapsible-trigger"][aria-expanded="true"] { border-bottom: 1px solid var(--border-weak-base, var(--vscode-panel-border)); } + /* Arrow always visible (no fade-on-hover) */ + [data-slot="collapsible-arrow"] { + opacity: 1; + } + /* Arrow: DOWN when collapsed, UP when expanded (matches question-dock toggle) */ [data-slot="collapsible-arrow-icon"] { transform: translateZ(0) rotate(0deg); diff --git a/packages/kilo-ui/src/components/collapsible.css b/packages/kilo-ui/src/components/collapsible.css index 84190941936..b6ddd4a009d 100644 --- a/packages/kilo-ui/src/components/collapsible.css +++ b/packages/kilo-ui/src/components/collapsible.css @@ -5,3 +5,14 @@ cursor: pointer; } } + +/* VS Code theme: tool-collapsible arrows point DOWN (collapsed) and UP (expanded), never right */ +html[data-theme="kilo-vscode"] [data-component="collapsible"].tool-collapsible { + [data-slot="collapsible-arrow-icon"] { + transform: translateZ(0) rotate(0deg); + } + + [data-slot="collapsible-trigger"][aria-expanded="true"] [data-slot="collapsible-arrow-icon"] { + transform: translateZ(0) rotate(180deg); + } +} diff --git a/packages/kilo-ui/src/components/message-part.css b/packages/kilo-ui/src/components/message-part.css index bd2db869f91..189e11f4454 100644 --- a/packages/kilo-ui/src/components/message-part.css +++ b/packages/kilo-ui/src/components/message-part.css @@ -139,12 +139,16 @@ html[data-theme="kilo-vscode"] [data-component="bash-output"] { } [data-slot="collapsible-trigger"] { - height: 28px; + height: 36px; padding: 0 8px; gap: 6px; background: var(--surface-inset-base); border-radius: 0; cursor: pointer; + + &:hover { + background-color: var(--surface-inset-base-hover, var(--vscode-list-hoverBackground)); + } } [data-slot="reasoning-header"] { diff --git a/packages/kilo-ui/src/components/message-part.tsx b/packages/kilo-ui/src/components/message-part.tsx index 157e015f9e8..a9ef66d2b01 100644 --- a/packages/kilo-ui/src/components/message-part.tsx +++ b/packages/kilo-ui/src/components/message-part.tsx @@ -21,7 +21,7 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props: MessagePartProp return (
- +
diff --git a/packages/kilo-ui/tests/visual-regression.spec.ts-snapshots/components-messagepart/with-reasoning-collapsed-chromium-linux.png b/packages/kilo-ui/tests/visual-regression.spec.ts-snapshots/components-messagepart/with-reasoning-collapsed-chromium-linux.png index fdf49245b20..36ab5c857cb 100644 --- a/packages/kilo-ui/tests/visual-regression.spec.ts-snapshots/components-messagepart/with-reasoning-collapsed-chromium-linux.png +++ b/packages/kilo-ui/tests/visual-regression.spec.ts-snapshots/components-messagepart/with-reasoning-collapsed-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf93ca79d15deab7dc52473f68305b521c62d10e968a9258e09609ad9c04fde6 -size 15837 +oid sha256:67cb95242834a2ac3633b70a68a8ff9a9f343938092f06926a44639ba5c01121 +size 15968 diff --git a/packages/kilo-ui/tests/visual-regression.spec.ts-snapshots/components-messagepart/with-reasoning-expanded-chromium-linux.png b/packages/kilo-ui/tests/visual-regression.spec.ts-snapshots/components-messagepart/with-reasoning-expanded-chromium-linux.png index 3da58463454..a72714105d9 100644 --- a/packages/kilo-ui/tests/visual-regression.spec.ts-snapshots/components-messagepart/with-reasoning-expanded-chromium-linux.png +++ b/packages/kilo-ui/tests/visual-regression.spec.ts-snapshots/components-messagepart/with-reasoning-expanded-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:14bf991adcbc6fcbf53f4cff2275dcec255943b5545c624231aeb73529ca2d6e -size 32478 +oid sha256:6d93bf6465c33895aa54bc2479315ea2fba38d06488f3d7cdf868b60caaefd9e +size 32623 From 01e931cad366852b1c1bb1ff10c704d5d6c0b936 Mon Sep 17 00:00:00 2001 From: Marius Date: Tue, 17 Mar 2026 14:05:58 +0100 Subject: [PATCH 36/74] feat(vscode): add keyboard navigation to QuestionDock (#7175) * feat(vscode): add keyboard navigation to QuestionDock * fix(vscode): guard Cmd+Enter submit against empty answers Match the footer submit button's disabled condition so Cmd+Enter cannot send partial data when no answer is selected. --- .../src/components/chat/QuestionDock.tsx | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/QuestionDock.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/QuestionDock.tsx index 9eb79072ada..cdf4bce13dd 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/QuestionDock.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/QuestionDock.tsx @@ -29,6 +29,8 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => collapsed: false, }) + let root!: HTMLDivElement + // Reset sending state when an error occurs for this question createEffect(() => { if (session.questionErrors().has(props.request.id)) { @@ -55,16 +57,20 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => return language.t("question.summary", { n, total: total() }) }) + const focusPrompt = () => requestAnimationFrame(() => window.dispatchEvent(new Event("focusPrompt"))) + const reply = (answers: string[][]) => { if (store.sending) return setStore("sending", true) session.replyToQuestion(props.request.id, answers) + focusPrompt() } const reject = () => { if (store.sending) return setStore("sending", true) session.rejectQuestion(props.request.id) + focusPrompt() } const submit = () => { @@ -122,6 +128,27 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => pick(opt.label) } + const onKey = (e: KeyboardEvent) => { + if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return + if ((e.target as HTMLElement).tagName === "INPUT") return + e.preventDefault() + const el = e.currentTarget as HTMLElement + const items = Array.from( + el.querySelectorAll("button[data-slot='question-option']:not(:disabled)"), + ) + if (!items.length) return + const idx = items.findIndex((b) => b === document.activeElement) + const next = + e.key === "ArrowDown" + ? idx === -1 + ? 0 + : (idx + 1) % items.length + : idx === -1 + ? items.length - 1 + : (idx - 1 + items.length) % items.length + items[next]?.focus() + } + const handleCustomSubmit = (e: Event) => { e.preventDefault() if (store.sending) return @@ -150,11 +177,44 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => const toggleCollapse = () => setStore("collapsed", !store.collapsed) + const onRoot = (e: KeyboardEvent) => { + if (e.key === "Escape") { + e.preventDefault() + e.stopPropagation() + if (store.editing) { + setStore("editing", false) + return + } + reject() + return + } + if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { + e.preventDefault() + e.stopPropagation() + if (store.sending) return + if (!confirm() && (store.answers[store.tab]?.length ?? 0) === 0) return + submit() + return + } + } + + // Auto-focus first option when dock appears, tab changes, or editing ends + createEffect(() => { + void store.tab + if (store.collapsed || store.editing || confirm()) return + requestAnimationFrame(() => { + const btn = root?.querySelector("button[data-slot='question-option']:not(:disabled)") + btn?.focus() + }) + }) + return (
e.stopPropagation()} + onKeyDown={onRoot} > {/* Single unified header row — always visible */}
@@ -208,7 +268,7 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => {language.t("ui.question.singleHint")}
}>
{language.t("ui.question.multiHint")}
-
+
{(opt, i) => { const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false From d744762d5ebb2a38eca8821b34150fc9b6b5c3de Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 17 Mar 2026 14:06:53 +0100 Subject: [PATCH 37/74] fix(i18n): translate remaining English keys (modeModels, retry) for all locales --- packages/kilo-vscode/webview-ui/src/i18n/ar.ts | 6 +++--- packages/kilo-vscode/webview-ui/src/i18n/br.ts | 6 +++--- packages/kilo-vscode/webview-ui/src/i18n/bs.ts | 6 +++--- packages/kilo-vscode/webview-ui/src/i18n/da.ts | 6 +++--- packages/kilo-vscode/webview-ui/src/i18n/de.ts | 6 +++--- packages/kilo-vscode/webview-ui/src/i18n/es.ts | 6 +++--- packages/kilo-vscode/webview-ui/src/i18n/fr.ts | 6 +++--- packages/kilo-vscode/webview-ui/src/i18n/ja.ts | 6 +++--- packages/kilo-vscode/webview-ui/src/i18n/ko.ts | 4 ++-- packages/kilo-vscode/webview-ui/src/i18n/no.ts | 4 ++-- packages/kilo-vscode/webview-ui/src/i18n/pl.ts | 4 ++-- packages/kilo-vscode/webview-ui/src/i18n/ru.ts | 4 ++-- packages/kilo-vscode/webview-ui/src/i18n/th.ts | 4 ++-- packages/kilo-vscode/webview-ui/src/i18n/zh.ts | 5 ++--- packages/kilo-vscode/webview-ui/src/i18n/zht.ts | 5 ++--- 15 files changed, 38 insertions(+), 40 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index da05e2cb574..1e07ba7075f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -680,9 +680,9 @@ export const dict = { "settings.providers.tag.config": "التكوين", "settings.providers.tag.custom": "مخصص", "settings.providers.tag.other": "أخرى", - "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels": "نموذج لكل وضع", "settings.providers.modeModels.description": - "Override the default model for specific modes. If not set, the global default model is used.", + "تجاوز النموذج الافتراضي لأوضاع محددة. إذا لم يتم التعيين، يتم استخدام النموذج الافتراضي العام.", "settings.models.title": "النماذج", "settings.models.description": "ستكون إعدادات النموذج قابلة للتكوين هنا.", "settings.agents.title": "الوكلاء", @@ -830,7 +830,7 @@ export const dict = { "Kilo Code هو مساعد ترميز بالذكاء الاصطناعي. اطلب منه بناء ميزات أو إصلاح أخطاء أو شرح قاعدة الشيفرة.", "session.messages.scrollToBottom": "التمرير إلى الأسفل", "session.status.writingResponse": "...جارٍ كتابة الرد", - "session.status.retry": "Retrying…", + "session.status.retry": "جارٍ إعادة المحاولة…", "session.status.retrying": "...إعادة المحاولة (المحاولة {{ attempt }})… {{ message }}", "session.status.working": "...جارٍ العمل", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 8e1d0266692..4620236d32b 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -687,9 +687,9 @@ export const dict = { "settings.providers.tag.config": "Configuração", "settings.providers.tag.custom": "Personalizado", "settings.providers.tag.other": "Outro", - "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels": "Modelo por Modo", "settings.providers.modeModels.description": - "Override the default model for specific modes. If not set, the global default model is used.", + "Substitua o modelo padrão para modos específicos. Se não definido, o modelo padrão global é usado.", "settings.models.title": "Modelos", "settings.models.description": "Configurações de modelos estarão disponíveis aqui.", "settings.agents.title": "Agentes", @@ -838,7 +838,7 @@ export const dict = { "Kilo Code é um assistente de programação com IA. Peça para criar funcionalidades, corrigir bugs ou explicar sua base de código.", "session.messages.scrollToBottom": "Rolar para o final", "session.status.writingResponse": "Escrevendo resposta…", - "session.status.retry": "Retrying…", + "session.status.retry": "Tentando novamente…", "session.status.retrying": "Tentando novamente (tentativa {{ attempt }})… {{ message }}", "session.status.working": "Trabalhando…", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index 8d7d00617bd..0fa2ac754fd 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -691,9 +691,9 @@ export const dict = { "settings.providers.tag.config": "Konfiguracija", "settings.providers.tag.custom": "Prilagođeno", "settings.providers.tag.other": "Ostalo", - "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels": "Model po režimu", "settings.providers.modeModels.description": - "Override the default model for specific modes. If not set, the global default model is used.", + "Zamijenite podrazumijevani model za određene režime. Ako nije postavljeno, koristi se globalni podrazumijevani model.", "settings.models.title": "Modeli", "settings.models.description": "Postavke modela će se ovdje moći podešavati.", "settings.agents.title": "Agenti", @@ -841,7 +841,7 @@ export const dict = { "Kilo Code je AI asistent za programiranje. Zatražite da gradi funkcionalnosti, ispravlja greške ili objasni vašu bazu koda.", "session.messages.scrollToBottom": "Pomakni se na dno", "session.status.writingResponse": "Pisanje odgovora…", - "session.status.retry": "Retrying…", + "session.status.retry": "Ponovni pokušaj…", "session.status.retrying": "Ponovni pokušaj (pokušaj {{ attempt }})… {{ message }}", "session.status.working": "Radim…", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index b23ed88ff3f..70aaa315c00 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -686,9 +686,9 @@ export const dict = { "settings.providers.tag.config": "Konfiguration", "settings.providers.tag.custom": "Brugerdefineret", "settings.providers.tag.other": "Andet", - "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels": "Model pr. tilstand", "settings.providers.modeModels.description": - "Override the default model for specific modes. If not set, the global default model is used.", + "Tilsidesæt standardmodellen for bestemte tilstande. Hvis ikke angivet, bruges den globale standardmodel.", "settings.models.title": "Modeller", "settings.models.description": "Modelindstillinger vil kunne konfigureres her.", "settings.agents.title": "Agenter", @@ -836,7 +836,7 @@ export const dict = { "Kilo Code er en AI-kodningsassistent. Bed den om at bygge funktioner, rette fejl eller forklare din kodebase.", "session.messages.scrollToBottom": "Rul til bunden", "session.status.writingResponse": "Skriver svar…", - "session.status.retry": "Retrying…", + "session.status.retry": "Prøver igen…", "session.status.retrying": "Prøver igen (forsøg {{ attempt }})… {{ message }}", "session.status.working": "Arbejder…", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 2464ba322f4..ade7ea97d77 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -696,9 +696,9 @@ export const dict = { "settings.providers.tag.config": "Konfiguration", "settings.providers.tag.custom": "Benutzerdefiniert", "settings.providers.tag.other": "Andere", - "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels": "Modell pro Modus", "settings.providers.modeModels.description": - "Override the default model for specific modes. If not set, the global default model is used.", + "Überschreiben Sie das Standardmodell für bestimmte Modi. Wenn nicht festgelegt, wird das globale Standardmodell verwendet.", "settings.models.title": "Modelle", "settings.models.description": "Modelleinstellungen können hier konfiguriert werden.", "settings.agents.title": "Agenten", @@ -846,7 +846,7 @@ export const dict = { "Kilo Code ist ein KI-Programmierassistent. Bitten Sie ihn, Funktionen zu erstellen, Fehler zu beheben oder Ihre Codebasis zu erklären.", "session.messages.scrollToBottom": "Nach unten scrollen", "session.status.writingResponse": "Antwort wird geschrieben…", - "session.status.retry": "Retrying…", + "session.status.retry": "Erneuter Versuch…", "session.status.retrying": "Erneuter Versuch ({{ attempt }})… {{ message }}", "session.status.working": "Wird bearbeitet…", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index 858f0e4ae06..eb14645a5d3 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -692,9 +692,9 @@ export const dict = { "settings.providers.tag.config": "Configuración", "settings.providers.tag.custom": "Personalizado", "settings.providers.tag.other": "Otro", - "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels": "Modelo por modo", "settings.providers.modeModels.description": - "Override the default model for specific modes. If not set, the global default model is used.", + "Anula el modelo predeterminado para modos específicos. Si no se establece, se usa el modelo predeterminado global.", "settings.models.title": "Modelos", "settings.models.description": "La configuración de modelos estará disponible aquí.", "settings.agents.title": "Agentes", @@ -843,7 +843,7 @@ export const dict = { "Kilo Code es un asistente de programación con IA. Pídele que cree funcionalidades, corrija errores o explique tu código.", "session.messages.scrollToBottom": "Desplazar al final", "session.status.writingResponse": "Escribiendo respuesta…", - "session.status.retry": "Retrying…", + "session.status.retry": "Reintentando…", "session.status.retrying": "Reintentando (intento {{ attempt }})… {{ message }}", "session.status.working": "Trabajando…", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index f221576c3b4..80650e504a6 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -696,9 +696,9 @@ export const dict = { "settings.providers.tag.config": "Configuration", "settings.providers.tag.custom": "Personnalisé", "settings.providers.tag.other": "Autre", - "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels": "Modèle par mode", "settings.providers.modeModels.description": - "Override the default model for specific modes. If not set, the global default model is used.", + "Remplacez le modèle par défaut pour des modes spécifiques. Si non défini, le modèle par défaut global est utilisé.", "settings.models.title": "Modèles", "settings.models.description": "Les paramètres des modèles seront configurables ici.", "settings.agents.title": "Agents", @@ -848,7 +848,7 @@ export const dict = { "Kilo Code est un assistant de programmation IA. Demandez-lui de créer des fonctionnalités, corriger des bugs ou expliquer votre code.", "session.messages.scrollToBottom": "Défiler vers le bas", "session.status.writingResponse": "Rédaction de la réponse…", - "session.status.retry": "Retrying…", + "session.status.retry": "Nouvelle tentative…", "session.status.retrying": "Nouvelle tentative (essai {{ attempt }})… {{ message }}", "session.status.working": "En cours…", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 37683e27b74..94441ab90de 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -685,9 +685,9 @@ export const dict = { "settings.providers.tag.config": "設定", "settings.providers.tag.custom": "カスタム", "settings.providers.tag.other": "その他", - "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels": "モードごとのモデル", "settings.providers.modeModels.description": - "Override the default model for specific modes. If not set, the global default model is used.", + "特定のモードのデフォルトモデルを上書きします。設定されていない場合、グローバルデフォルトモデルが使用されます。", "settings.models.title": "モデル", "settings.models.description": "モデル設定はここで構成できます。", "settings.agents.title": "エージェント", @@ -835,7 +835,7 @@ export const dict = { "Kilo CodeはAIコーディングアシスタントです。機能の構築、バグの修正、コードベースの説明を依頼できます。", "session.messages.scrollToBottom": "下にスクロール", "session.status.writingResponse": "応答を作成中…", - "session.status.retry": "Retrying…", + "session.status.retry": "再試行中…", "session.status.retrying": "再試行中({{ attempt }}回目)… {{ message }}", "session.status.working": "作業中…", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 42e58e00f14..8e25b72f687 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -686,9 +686,9 @@ export const dict = { "settings.providers.tag.config": "구성", "settings.providers.tag.custom": "사용자 지정", "settings.providers.tag.other": "기타", - "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels": "모드별 모델", "settings.providers.modeModels.description": - "Override the default model for specific modes. If not set, the global default model is used.", + "특정 모드의 기본 모델을 재정의합니다. 설정하지 않으면 전역 기본 모델이 사용됩니다.", "settings.models.title": "모델", "settings.models.description": "모델 설정은 여기서 구성할 수 있습니다.", "settings.agents.title": "에이전트", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 31a443719f7..c498c64b351 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -690,9 +690,9 @@ export const dict = { "settings.providers.tag.config": "Konfigurasjon", "settings.providers.tag.custom": "Tilpasset", "settings.providers.tag.other": "Annet", - "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels": "Modell per modus", "settings.providers.modeModels.description": - "Override the default model for specific modes. If not set, the global default model is used.", + "Overstyr standardmodellen for bestemte moduser. Hvis ikke angitt, brukes den globale standardmodellen.", "settings.models.title": "Modeller", "settings.models.description": "Modellinnstillinger vil kunne konfigureres her.", "settings.agents.title": "Agenter", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index f2eb77a7b83..1c2f2ef0b26 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -689,9 +689,9 @@ export const dict = { "settings.providers.tag.config": "Konfiguracja", "settings.providers.tag.custom": "Niestandardowe", "settings.providers.tag.other": "Inne", - "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels": "Model na tryb", "settings.providers.modeModels.description": - "Override the default model for specific modes. If not set, the global default model is used.", + "Zastąp domyślny model dla określonych trybów. Jeśli nie ustawiono, używany jest globalny domyślny model.", "settings.models.title": "Modele", "settings.models.description": "Ustawienia modeli będą tutaj konfigurowalne.", "settings.agents.title": "Agenci", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 8ecee17b951..8879bb7cda8 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -692,9 +692,9 @@ export const dict = { "settings.providers.tag.config": "Конфигурация", "settings.providers.tag.custom": "Пользовательский", "settings.providers.tag.other": "Другое", - "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels": "Модель для режима", "settings.providers.modeModels.description": - "Override the default model for specific modes. If not set, the global default model is used.", + "Переопределите модель по умолчанию для определённых режимов. Если не задано, используется глобальная модель по умолчанию.", "settings.models.title": "Модели", "settings.models.description": "Настройки моделей будут доступны здесь.", "settings.agents.title": "Агенты", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index 8441da2a53c..485dd0cbe04 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -683,9 +683,9 @@ export const dict = { "settings.providers.tag.config": "กำหนดค่า", "settings.providers.tag.custom": "กำหนดเอง", "settings.providers.tag.other": "อื่น ๆ", - "settings.providers.modeModels": "Model per Mode", + "settings.providers.modeModels": "โมเดลต่อโหมด", "settings.providers.modeModels.description": - "Override the default model for specific modes. If not set, the global default model is used.", + "แทนที่โมเดลเริ่มต้นสำหรับโหมดที่กำหนด หากไม่ได้ตั้งค่า จะใช้โมเดลเริ่มต้นทั่วไป", "settings.models.title": "โมเดล", "settings.models.description": "การตั้งค่าโมเดลจะสามารถกำหนดค่าได้ที่นี่", "settings.agents.title": "เอเจนต์", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 5c415474e2a..1fe39bff6d3 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -676,9 +676,8 @@ export const dict = { "settings.providers.tag.config": "配置", "settings.providers.tag.custom": "自定义", "settings.providers.tag.other": "其他", - "settings.providers.modeModels": "Model per Mode", - "settings.providers.modeModels.description": - "Override the default model for specific modes. If not set, the global default model is used.", + "settings.providers.modeModels": "按模式选择模型", + "settings.providers.modeModels.description": "为特定模式覆盖默认模型。如果未设置,将使用全局默认模型。", "settings.models.title": "模型", "settings.models.description": "模型设置将在此处可配置。", "settings.agents.title": "智能体", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index 1950c444017..dc90c91951f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -678,9 +678,8 @@ export const dict = { "settings.providers.tag.config": "設定", "settings.providers.tag.custom": "自訂", "settings.providers.tag.other": "其他", - "settings.providers.modeModels": "Model per Mode", - "settings.providers.modeModels.description": - "Override the default model for specific modes. If not set, the global default model is used.", + "settings.providers.modeModels": "按模式選擇模型", + "settings.providers.modeModels.description": "為特定模式覆寫預設模型。如果未設定,將使用全域預設模型。", "settings.models.title": "模型", "settings.models.description": "可在此調整模型設定。", "settings.agents.title": "Agents", From f91b3c55c2015ced58d8fc5d8c28e5ef3581a32c Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 17 Mar 2026 14:10:37 +0100 Subject: [PATCH 38/74] fix(i18n): translate remaining English keys (retry, unseen session) for all locales --- packages/kilo-vscode/webview-ui/src/i18n/ar.ts | 4 ++-- packages/kilo-vscode/webview-ui/src/i18n/br.ts | 4 ++-- packages/kilo-vscode/webview-ui/src/i18n/da.ts | 4 ++-- packages/kilo-vscode/webview-ui/src/i18n/de.ts | 4 ++-- packages/kilo-vscode/webview-ui/src/i18n/es.ts | 4 ++-- packages/kilo-vscode/webview-ui/src/i18n/fr.ts | 4 ++-- packages/kilo-vscode/webview-ui/src/i18n/ja.ts | 4 ++-- packages/kilo-vscode/webview-ui/src/i18n/ko.ts | 6 +++--- packages/kilo-vscode/webview-ui/src/i18n/no.ts | 6 +++--- packages/kilo-vscode/webview-ui/src/i18n/pl.ts | 6 +++--- packages/kilo-vscode/webview-ui/src/i18n/ru.ts | 6 +++--- packages/kilo-vscode/webview-ui/src/i18n/th.ts | 6 +++--- packages/kilo-vscode/webview-ui/src/i18n/zh.ts | 6 +++--- packages/kilo-vscode/webview-ui/src/i18n/zht.ts | 2 +- 14 files changed, 33 insertions(+), 33 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 1e07ba7075f..ae1331da7c4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -28,8 +28,8 @@ export const dict = { "command.settings.open": "فتح الإعدادات", "command.session.previous": "الجلسة السابقة", "command.session.next": "الجلسة التالية", - "command.session.previous.unseen": "Previous unread session", - "command.session.next.unseen": "Next unread session", + "command.session.previous.unseen": "الجلسة غير المقروءة السابقة", + "command.session.next.unseen": "الجلسة غير المقروءة التالية", "command.session.archive": "أرشفة الجلسة", "command.palette": "لوحة الأوامر", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 4620236d32b..18b78e86894 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -28,8 +28,8 @@ export const dict = { "command.settings.open": "Abrir configurações", "command.session.previous": "Sessão anterior", "command.session.next": "Próxima sessão", - "command.session.previous.unseen": "Previous unread session", - "command.session.next.unseen": "Next unread session", + "command.session.previous.unseen": "Sessão não lida anterior", + "command.session.next.unseen": "Próxima sessão não lida", "command.session.archive": "Arquivar sessão", "command.palette": "Paleta de comandos", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index 70aaa315c00..e1bbb6db85e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -28,8 +28,8 @@ export const dict = { "command.settings.open": "Åbn indstillinger", "command.session.previous": "Forrige session", "command.session.next": "Næste session", - "command.session.previous.unseen": "Previous unread session", - "command.session.next.unseen": "Next unread session", + "command.session.previous.unseen": "Forrige ulæste session", + "command.session.next.unseen": "Næste ulæste session", "command.session.archive": "Arkivér session", "command.palette": "Kommandopalette", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index ade7ea97d77..dcc2d4a0647 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -32,8 +32,8 @@ export const dict = { "command.settings.open": "Einstellungen öffnen", "command.session.previous": "Vorherige Sitzung", "command.session.next": "Nächste Sitzung", - "command.session.previous.unseen": "Previous unread session", - "command.session.next.unseen": "Next unread session", + "command.session.previous.unseen": "Vorherige ungelesene Sitzung", + "command.session.next.unseen": "Nächste ungelesene Sitzung", "command.session.archive": "Sitzung archivieren", "command.palette": "Befehlspalette", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index eb14645a5d3..6efb8e724b5 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -28,8 +28,8 @@ export const dict = { "command.settings.open": "Abrir ajustes", "command.session.previous": "Sesión anterior", "command.session.next": "Siguiente sesión", - "command.session.previous.unseen": "Previous unread session", - "command.session.next.unseen": "Next unread session", + "command.session.previous.unseen": "Sesión no leída anterior", + "command.session.next.unseen": "Siguiente sesión no leída", "command.session.archive": "Archivar sesión", "command.palette": "Paleta de comandos", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index 80650e504a6..106bfd293b6 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -28,8 +28,8 @@ export const dict = { "command.settings.open": "Ouvrir les paramètres", "command.session.previous": "Session précédente", "command.session.next": "Session suivante", - "command.session.previous.unseen": "Previous unread session", - "command.session.next.unseen": "Next unread session", + "command.session.previous.unseen": "Session non lue précédente", + "command.session.next.unseen": "Session non lue suivante", "command.session.archive": "Archiver la session", "command.palette": "Palette de commandes", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 94441ab90de..cdea158c527 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -28,8 +28,8 @@ export const dict = { "command.settings.open": "設定を開く", "command.session.previous": "前のセッション", "command.session.next": "次のセッション", - "command.session.previous.unseen": "Previous unread session", - "command.session.next.unseen": "Next unread session", + "command.session.previous.unseen": "前の未読セッション", + "command.session.next.unseen": "次の未読セッション", "command.session.archive": "セッションをアーカイブ", "command.palette": "コマンドパレット", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 8e25b72f687..7ccb5048ec3 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -32,8 +32,8 @@ export const dict = { "command.settings.open": "설정 열기", "command.session.previous": "이전 세션", "command.session.next": "다음 세션", - "command.session.previous.unseen": "Previous unread session", - "command.session.next.unseen": "Next unread session", + "command.session.previous.unseen": "이전 읽지 않은 세션", + "command.session.next.unseen": "다음 읽지 않은 세션", "command.session.archive": "세션 보관", "command.palette": "명령 팔레트", @@ -835,7 +835,7 @@ export const dict = { "Kilo Code는 AI 코딩 어시스턴트입니다. 기능 구축, 버그 수정 또는 코드베이스 설명을 요청하세요.", "session.messages.scrollToBottom": "하단으로 스크롤", "session.status.writingResponse": "응답 작성 중...", - "session.status.retry": "Retrying…", + "session.status.retry": "재시도 중…", "session.status.retrying": "재시도 중 ({{ attempt }}번째 시도)… {{ message }}", "session.status.working": "작업 중...", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index c498c64b351..d40e28c181a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -31,8 +31,8 @@ export const dict = { "command.settings.open": "Åpne innstillinger", "command.session.previous": "Forrige sesjon", "command.session.next": "Neste sesjon", - "command.session.previous.unseen": "Previous unread session", - "command.session.next.unseen": "Next unread session", + "command.session.previous.unseen": "Forrige uleste økt", + "command.session.next.unseen": "Neste uleste økt", "command.session.archive": "Arkiver sesjon", "command.palette": "Kommandopalett", @@ -840,7 +840,7 @@ export const dict = { "Kilo Code er en AI-kodingsassistent. Be den om å bygge funksjoner, fikse feil eller forklare kodebasen din.", "session.messages.scrollToBottom": "Rull til bunnen", "session.status.writingResponse": "Skriver svar…", - "session.status.retry": "Retrying…", + "session.status.retry": "Prøver på nytt…", "session.status.retrying": "Prøver på nytt (forsøk {{ attempt }})… {{ message }}", "session.status.working": "Arbeider…", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 1c2f2ef0b26..ea2b87c1569 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -28,8 +28,8 @@ export const dict = { "command.settings.open": "Otwórz ustawienia", "command.session.previous": "Poprzednia sesja", "command.session.next": "Następna sesja", - "command.session.previous.unseen": "Previous unread session", - "command.session.next.unseen": "Next unread session", + "command.session.previous.unseen": "Poprzednia nieprzeczytana sesja", + "command.session.next.unseen": "Następna nieprzeczytana sesja", "command.session.archive": "Zarchiwizuj sesję", "command.palette": "Paleta poleceń", @@ -838,7 +838,7 @@ export const dict = { "Kilo Code to asystent programowania AI. Poproś go o tworzenie funkcji, naprawę błędów lub wyjaśnienie bazy kodu.", "session.messages.scrollToBottom": "Przewiń na dół", "session.status.writingResponse": "Pisanie odpowiedzi…", - "session.status.retry": "Retrying…", + "session.status.retry": "Ponawianie…", "session.status.retrying": "Ponawiam próbę ({{ attempt }})… {{ message }}", "session.status.working": "Pracuję…", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 8879bb7cda8..9636b2caf17 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -28,8 +28,8 @@ export const dict = { "command.settings.open": "Открыть настройки", "command.session.previous": "Предыдущая сессия", "command.session.next": "Следующая сессия", - "command.session.previous.unseen": "Previous unread session", - "command.session.next.unseen": "Next unread session", + "command.session.previous.unseen": "Предыдущая непрочитанная сессия", + "command.session.next.unseen": "Следующая непрочитанная сессия", "command.session.archive": "Архивировать сессию", "command.palette": "Палитра команд", @@ -842,7 +842,7 @@ export const dict = { "Kilo Code — это AI-помощник для программирования. Попросите его создать функции, исправить ошибки или объяснить вашу кодовую базу.", "session.messages.scrollToBottom": "Прокрутить вниз", "session.status.writingResponse": "Пишу ответ…", - "session.status.retry": "Retrying…", + "session.status.retry": "Повторная попытка…", "session.status.retrying": "Повторная попытка ({{ attempt }})… {{ message }}", "session.status.working": "Работаю…", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index 485dd0cbe04..492a3b78cab 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -28,8 +28,8 @@ export const dict = { "command.settings.open": "เปิดการตั้งค่า", "command.session.previous": "เซสชันก่อนหน้า", "command.session.next": "เซสชันถัดไป", - "command.session.previous.unseen": "Previous unread session", - "command.session.next.unseen": "Next unread session", + "command.session.previous.unseen": "เซสชันที่ยังไม่ได้อ่านก่อนหน้า", + "command.session.next.unseen": "เซสชันที่ยังไม่ได้อ่านถัดไป", "command.session.archive": "จัดเก็บเซสชัน", "command.palette": "คำสั่งค้นหา", @@ -831,7 +831,7 @@ export const dict = { "session.messages.welcome": "Kilo Code เป็นผู้ช่วยเขียนโค้ด AI ขอให้สร้างฟีเจอร์ แก้ไขบัก หรืออธิบายโค้ดเบสของคุณ", "session.messages.scrollToBottom": "เลื่อนไปด้านล่าง", "session.status.writingResponse": "กำลังเขียนคำตอบ...", - "session.status.retry": "Retrying…", + "session.status.retry": "กำลังลองใหม่…", "session.status.retrying": "กำลังลองใหม่ (ครั้งที่ {{ attempt }})… {{ message }}", "session.status.working": "กำลังทำงาน...", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 1fe39bff6d3..eeb6304366e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -32,8 +32,8 @@ export const dict = { "command.settings.open": "打开设置", "command.session.previous": "上一个会话", "command.session.next": "下一个会话", - "command.session.previous.unseen": "Previous unread session", - "command.session.next.unseen": "Next unread session", + "command.session.previous.unseen": "上一个未读会话", + "command.session.next.unseen": "下一个未读会话", "command.session.archive": "归档会话", "command.palette": "命令面板", @@ -823,7 +823,7 @@ export const dict = { "session.messages.welcome": "Kilo Code 是一个 AI 编程助手。让它帮你构建功能、修复 bug 或解释代码库。", "session.messages.scrollToBottom": "滚动到底部", "session.status.writingResponse": "正在撰写回复…", - "session.status.retry": "Retrying…", + "session.status.retry": "正在重试…", "session.status.retrying": "正在重试(第 {{ attempt }} 次)… {{ message }}", "session.status.working": "处理中…", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index dc90c91951f..449bde5c80a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -825,7 +825,7 @@ export const dict = { "session.messages.welcome": "Kilo Code 是一個 AI 程式設計助手。讓它幫你建構功能、修復 bug 或解釋程式碼庫。", "session.messages.scrollToBottom": "捲動至底部", "session.status.writingResponse": "正在撰寫回覆…", - "session.status.retry": "Retrying…", + "session.status.retry": "正在重試…", "session.status.retrying": "正在重試(第 {{ attempt }} 次)… {{ message }}", "session.status.working": "處理中…", From 0c92a892aececcdfe7515e8cc2de1ae383d479ba Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Mon, 16 Mar 2026 19:27:51 +0100 Subject: [PATCH 39/74] feat(marketplace): add MCP servers and modes tabs Implements the full marketplace with all three tabs functional: - MCP Servers: browse, search, filter by status/tags, install with scope selection, method picker, and parameter form - Modes: browse, search, filter by status/tags, install/remove - Skills: unchanged from previous PR Rewrites marketplace components to use kilo-ui Dialog/Card components, useDialog() for modal management, and vscode.onMessage for result subscriptions. Adds MarketplaceListView shared component for MCP and Modes tabs. Full installer with MCP config writing (local/remote), mode YAML parsing, and template parameter substitution. Adds 18 new i18n keys per locale (16 languages) for MCP/mode specific strings (badges, filters, install method, parameters, type labels). --- packages/kilo-i18n/src/ar.ts | 18 ++ packages/kilo-i18n/src/br.ts | 18 ++ packages/kilo-i18n/src/bs.ts | 18 ++ packages/kilo-i18n/src/da.ts | 18 ++ packages/kilo-i18n/src/de.ts | 18 ++ packages/kilo-i18n/src/en.ts | 18 ++ packages/kilo-i18n/src/es.ts | 18 ++ packages/kilo-i18n/src/fr.ts | 18 ++ packages/kilo-i18n/src/ja.ts | 18 ++ packages/kilo-i18n/src/ko.ts | 18 ++ packages/kilo-i18n/src/no.ts | 18 ++ packages/kilo-i18n/src/pl.ts | 18 ++ packages/kilo-i18n/src/ru.ts | 18 ++ packages/kilo-i18n/src/th.ts | 18 ++ packages/kilo-i18n/src/zh.ts | 18 ++ packages/kilo-i18n/src/zht.ts | 18 ++ packages/kilo-vscode/src/KiloProvider.ts | 33 ++- .../src/services/marketplace/installer.ts | 154 ++++++++++- .../components/marketplace/InstallModal.tsx | 181 +++++++++---- .../src/components/marketplace/ItemCard.tsx | 43 ++-- .../marketplace/MarketplaceListView.tsx | 136 ++++++++++ .../marketplace/MarketplaceView.tsx | 240 ++++++++++-------- .../components/marketplace/RemoveDialog.tsx | 35 +-- .../marketplace/SkillsMarketplace.tsx | 8 +- .../src/components/marketplace/index.ts | 2 +- .../components/marketplace/marketplace.css | 222 +++++++++------- .../src/components/marketplace/utils.ts | 12 +- .../webview-ui/src/types/marketplace.ts | 20 +- 28 files changed, 1067 insertions(+), 307 deletions(-) create mode 100644 packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceListView.tsx diff --git a/packages/kilo-i18n/src/ar.ts b/packages/kilo-i18n/src/ar.ts index 4abab3098f2..6bd89a624c6 100644 --- a/packages/kilo-i18n/src/ar.ts +++ b/packages/kilo-i18n/src/ar.ts @@ -43,4 +43,22 @@ export const dict = { "هل أنت متأكد أنك تريد إزالة هذا الـ {{type}}؟ سيؤدي هذا إلى إزالته من تكوين {{scope}} الخاص بك.", "marketplace.remove.cancel": "إلغاء", "marketplace.remove.confirm.button": "إزالة", + "marketplace.tab.mcp": "MCP", + "marketplace.search": "بحث...", + "marketplace.filter.all": "جميع العناصر", + "marketplace.filter.notInstalled": "غير مثبت", + "marketplace.empty": "لم يتم العثور على عناصر", + "marketplace.badge.mcpServer": "خادم MCP", + "marketplace.badge.mode": "وضع", + "marketplace.card.by": "بواسطة {{author}}", + "marketplace.install.method": "طريقة التثبيت", + "marketplace.install.parameters": "المعلمات", + "marketplace.install.optional": "(اختياري)", + "marketplace.install.required": "{{name}} مطلوب", + "marketplace.scope.project": "مشروع", + "marketplace.scope.global": "عام", + "marketplace.remove.type.mcp": "خادم MCP", + "marketplace.remove.type.skill": "مهارة", + "marketplace.remove.type.mode": "وضع", + "marketplace.remove.failed": "فشلت إزالة {{name}}", } diff --git a/packages/kilo-i18n/src/br.ts b/packages/kilo-i18n/src/br.ts index cfa23e214d6..6acda4e7942 100644 --- a/packages/kilo-i18n/src/br.ts +++ b/packages/kilo-i18n/src/br.ts @@ -43,4 +43,22 @@ export const dict = { "Tem certeza que deseja remover este {{type}}? Isso o removerá da sua configuração {{scope}}.", "marketplace.remove.cancel": "Cancelar", "marketplace.remove.confirm.button": "Remover", + "marketplace.tab.mcp": "MCP", + "marketplace.search": "Pesquisar...", + "marketplace.filter.all": "Todos os Itens", + "marketplace.filter.notInstalled": "Não Instalado", + "marketplace.empty": "Nenhum item encontrado", + "marketplace.badge.mcpServer": "Servidor MCP", + "marketplace.badge.mode": "Modo", + "marketplace.card.by": "por {{author}}", + "marketplace.install.method": "Método de Instalação", + "marketplace.install.parameters": "Parâmetros", + "marketplace.install.optional": "(opcional)", + "marketplace.install.required": "{{name}} é obrigatório", + "marketplace.scope.project": "projeto", + "marketplace.scope.global": "global", + "marketplace.remove.type.mcp": "servidor MCP", + "marketplace.remove.type.skill": "habilidade", + "marketplace.remove.type.mode": "modo", + "marketplace.remove.failed": "Falha ao remover {{name}}", } diff --git a/packages/kilo-i18n/src/bs.ts b/packages/kilo-i18n/src/bs.ts index aecc7423546..a2a8be0ed1f 100644 --- a/packages/kilo-i18n/src/bs.ts +++ b/packages/kilo-i18n/src/bs.ts @@ -48,4 +48,22 @@ export const dict = { "Jeste li sigurni da želite ukloniti ovaj {{type}}? Ovo će ga ukloniti iz vaše {{scope}} konfiguracije.", "marketplace.remove.cancel": "Otkaži", "marketplace.remove.confirm.button": "Ukloni", + "marketplace.tab.mcp": "MCP", + "marketplace.search": "Pretraži...", + "marketplace.filter.all": "Sve stavke", + "marketplace.filter.notInstalled": "Nije instalirano", + "marketplace.empty": "Nema pronađenih stavki", + "marketplace.badge.mcpServer": "MCP Server", + "marketplace.badge.mode": "Režim", + "marketplace.card.by": "od {{author}}", + "marketplace.install.method": "Metoda instalacije", + "marketplace.install.parameters": "Parametri", + "marketplace.install.optional": "(opciono)", + "marketplace.install.required": "{{name}} je obavezno", + "marketplace.scope.project": "projekat", + "marketplace.scope.global": "globalno", + "marketplace.remove.type.mcp": "MCP server", + "marketplace.remove.type.skill": "vještina", + "marketplace.remove.type.mode": "režim", + "marketplace.remove.failed": "Uklanjanje {{name}} nije uspjelo", } diff --git a/packages/kilo-i18n/src/da.ts b/packages/kilo-i18n/src/da.ts index fb5541bdaeb..8c5ff923725 100644 --- a/packages/kilo-i18n/src/da.ts +++ b/packages/kilo-i18n/src/da.ts @@ -43,4 +43,22 @@ export const dict = { "Er du sikker på, at du vil fjerne denne {{type}}? Dette vil fjerne den fra din {{scope}} konfiguration.", "marketplace.remove.cancel": "Annuller", "marketplace.remove.confirm.button": "Fjern", + "marketplace.tab.mcp": "MCP", + "marketplace.search": "Søg...", + "marketplace.filter.all": "Alle elementer", + "marketplace.filter.notInstalled": "Ikke installeret", + "marketplace.empty": "Ingen elementer fundet", + "marketplace.badge.mcpServer": "MCP-server", + "marketplace.badge.mode": "Tilstand", + "marketplace.card.by": "af {{author}}", + "marketplace.install.method": "Installationsmetode", + "marketplace.install.parameters": "Parametre", + "marketplace.install.optional": "(valgfrit)", + "marketplace.install.required": "{{name}} er påkrævet", + "marketplace.scope.project": "projekt", + "marketplace.scope.global": "global", + "marketplace.remove.type.mcp": "MCP-server", + "marketplace.remove.type.skill": "færdighed", + "marketplace.remove.type.mode": "tilstand", + "marketplace.remove.failed": "Kunne ikke fjerne {{name}}", } diff --git a/packages/kilo-i18n/src/de.ts b/packages/kilo-i18n/src/de.ts index bd34499ade6..0ee1c8bd0c7 100644 --- a/packages/kilo-i18n/src/de.ts +++ b/packages/kilo-i18n/src/de.ts @@ -43,4 +43,22 @@ export const dict = { "Sind Sie sicher, dass Sie diese {{type}} entfernen möchten? Dadurch wird sie aus Ihrer {{scope}}-Konfiguration entfernt.", "marketplace.remove.cancel": "Abbrechen", "marketplace.remove.confirm.button": "Entfernen", + "marketplace.tab.mcp": "MCP", + "marketplace.search": "Suchen...", + "marketplace.filter.all": "Alle Elemente", + "marketplace.filter.notInstalled": "Nicht installiert", + "marketplace.empty": "Keine Elemente gefunden", + "marketplace.badge.mcpServer": "MCP-Server", + "marketplace.badge.mode": "Modus", + "marketplace.card.by": "von {{author}}", + "marketplace.install.method": "Installationsmethode", + "marketplace.install.parameters": "Parameter", + "marketplace.install.optional": "(optional)", + "marketplace.install.required": "{{name}} ist erforderlich", + "marketplace.scope.project": "Projekt", + "marketplace.scope.global": "Global", + "marketplace.remove.type.mcp": "MCP-Server", + "marketplace.remove.type.skill": "Skill", + "marketplace.remove.type.mode": "Modus", + "marketplace.remove.failed": "Fehler beim Entfernen von {{name}}", } diff --git a/packages/kilo-i18n/src/en.ts b/packages/kilo-i18n/src/en.ts index 5cccd33c5c2..d8ae2dee47d 100644 --- a/packages/kilo-i18n/src/en.ts +++ b/packages/kilo-i18n/src/en.ts @@ -45,4 +45,22 @@ export const dict = { "Are you sure you want to remove this {{type}}? This will remove it from your {{scope}} configuration.", "marketplace.remove.cancel": "Cancel", "marketplace.remove.confirm.button": "Remove", + "marketplace.tab.mcp": "MCP", + "marketplace.search": "Search...", + "marketplace.filter.all": "All Items", + "marketplace.filter.notInstalled": "Not Installed", + "marketplace.empty": "No items found", + "marketplace.badge.mcpServer": "MCP Server", + "marketplace.badge.mode": "Mode", + "marketplace.card.by": "by {{author}}", + "marketplace.install.method": "Installation Method", + "marketplace.install.parameters": "Parameters", + "marketplace.install.optional": "(optional)", + "marketplace.install.required": "{{name}} is required", + "marketplace.scope.project": "project", + "marketplace.scope.global": "global", + "marketplace.remove.type.mcp": "MCP server", + "marketplace.remove.type.skill": "skill", + "marketplace.remove.type.mode": "mode", + "marketplace.remove.failed": "Failed to remove {{name}}", } diff --git a/packages/kilo-i18n/src/es.ts b/packages/kilo-i18n/src/es.ts index cdedb7864b8..570e93f2672 100644 --- a/packages/kilo-i18n/src/es.ts +++ b/packages/kilo-i18n/src/es.ts @@ -43,4 +43,22 @@ export const dict = { "¿Estás seguro de que deseas eliminar este {{type}}? Esto lo eliminará de tu configuración {{scope}}.", "marketplace.remove.cancel": "Cancelar", "marketplace.remove.confirm.button": "Eliminar", + "marketplace.tab.mcp": "MCP", + "marketplace.search": "Buscar...", + "marketplace.filter.all": "Todos los elementos", + "marketplace.filter.notInstalled": "No instalado", + "marketplace.empty": "No se encontraron elementos", + "marketplace.badge.mcpServer": "Servidor MCP", + "marketplace.badge.mode": "Modo", + "marketplace.card.by": "por {{author}}", + "marketplace.install.method": "Método de instalación", + "marketplace.install.parameters": "Parámetros", + "marketplace.install.optional": "(opcional)", + "marketplace.install.required": "{{name}} es obligatorio", + "marketplace.scope.project": "proyecto", + "marketplace.scope.global": "global", + "marketplace.remove.type.mcp": "servidor MCP", + "marketplace.remove.type.skill": "habilidad", + "marketplace.remove.type.mode": "modo", + "marketplace.remove.failed": "Error al eliminar {{name}}", } diff --git a/packages/kilo-i18n/src/fr.ts b/packages/kilo-i18n/src/fr.ts index e741db9adc7..545f324f9e8 100644 --- a/packages/kilo-i18n/src/fr.ts +++ b/packages/kilo-i18n/src/fr.ts @@ -43,4 +43,22 @@ export const dict = { "Êtes-vous sûr de vouloir supprimer ce {{type}} ? Cela le supprimera de votre configuration {{scope}}.", "marketplace.remove.cancel": "Annuler", "marketplace.remove.confirm.button": "Supprimer", + "marketplace.tab.mcp": "MCP", + "marketplace.search": "Rechercher...", + "marketplace.filter.all": "Tous les éléments", + "marketplace.filter.notInstalled": "Non installé", + "marketplace.empty": "Aucun élément trouvé", + "marketplace.badge.mcpServer": "Serveur MCP", + "marketplace.badge.mode": "Mode", + "marketplace.card.by": "par {{author}}", + "marketplace.install.method": "Méthode d'installation", + "marketplace.install.parameters": "Paramètres", + "marketplace.install.optional": "(facultatif)", + "marketplace.install.required": "{{name}} est requis", + "marketplace.scope.project": "projet", + "marketplace.scope.global": "global", + "marketplace.remove.type.mcp": "serveur MCP", + "marketplace.remove.type.skill": "compétence", + "marketplace.remove.type.mode": "mode", + "marketplace.remove.failed": "Échec de la suppression de {{name}}", } diff --git a/packages/kilo-i18n/src/ja.ts b/packages/kilo-i18n/src/ja.ts index 460697114ab..c368f713a5e 100644 --- a/packages/kilo-i18n/src/ja.ts +++ b/packages/kilo-i18n/src/ja.ts @@ -41,4 +41,22 @@ export const dict = { "marketplace.remove.confirm": "この{{type}}を削除してもよろしいですか? これにより、{{scope}}設定から削除されます。", "marketplace.remove.cancel": "キャンセル", "marketplace.remove.confirm.button": "削除", + "marketplace.tab.mcp": "MCP", + "marketplace.search": "検索...", + "marketplace.filter.all": "すべてのアイテム", + "marketplace.filter.notInstalled": "未インストール", + "marketplace.empty": "アイテムが見つかりません", + "marketplace.badge.mcpServer": "MCPサーバー", + "marketplace.badge.mode": "モード", + "marketplace.card.by": "作成者: {{author}}", + "marketplace.install.method": "インストール方法", + "marketplace.install.parameters": "パラメーター", + "marketplace.install.optional": "(任意)", + "marketplace.install.required": "{{name}} は必須です", + "marketplace.scope.project": "プロジェクト", + "marketplace.scope.global": "グローバル", + "marketplace.remove.type.mcp": "MCPサーバー", + "marketplace.remove.type.skill": "スキル", + "marketplace.remove.type.mode": "モード", + "marketplace.remove.failed": "{{name}} の削除に失敗しました", } diff --git a/packages/kilo-i18n/src/ko.ts b/packages/kilo-i18n/src/ko.ts index 1d2d11e4443..857dd478ea6 100644 --- a/packages/kilo-i18n/src/ko.ts +++ b/packages/kilo-i18n/src/ko.ts @@ -41,4 +41,22 @@ export const dict = { "marketplace.remove.confirm": "이 {{type}}을(를) 제거하시겠습니까? {{scope}} 구성에서 제거됩니다.", "marketplace.remove.cancel": "취소", "marketplace.remove.confirm.button": "제거", + "marketplace.tab.mcp": "MCP", + "marketplace.search": "검색...", + "marketplace.filter.all": "모든 항목", + "marketplace.filter.notInstalled": "설치되지 않음", + "marketplace.empty": "항목을 찾을 수 없음", + "marketplace.badge.mcpServer": "MCP 서버", + "marketplace.badge.mode": "모드", + "marketplace.card.by": "제작: {{author}}", + "marketplace.install.method": "설치 방법", + "marketplace.install.parameters": "매개변수", + "marketplace.install.optional": "(선택 사항)", + "marketplace.install.required": "{{name}}이(가) 필요합니다", + "marketplace.scope.project": "프로젝트", + "marketplace.scope.global": "글로벌", + "marketplace.remove.type.mcp": "MCP 서버", + "marketplace.remove.type.skill": "스킬", + "marketplace.remove.type.mode": "모드", + "marketplace.remove.failed": "{{name}} 제거 실패", } diff --git a/packages/kilo-i18n/src/no.ts b/packages/kilo-i18n/src/no.ts index 70e5f3a3e0c..6878d64b0c3 100644 --- a/packages/kilo-i18n/src/no.ts +++ b/packages/kilo-i18n/src/no.ts @@ -43,4 +43,22 @@ export const dict = { "Er du sikker på at du vil fjerne denne {{type}}? Dette vil fjerne den fra din {{scope}}-konfigurasjon.", "marketplace.remove.cancel": "Avbryt", "marketplace.remove.confirm.button": "Fjern", + "marketplace.tab.mcp": "MCP", + "marketplace.search": "Søk...", + "marketplace.filter.all": "Alle elementer", + "marketplace.filter.notInstalled": "Ikke installert", + "marketplace.empty": "Ingen elementer funnet", + "marketplace.badge.mcpServer": "MCP-server", + "marketplace.badge.mode": "Modus", + "marketplace.card.by": "av {{author}}", + "marketplace.install.method": "Installasjonsmetode", + "marketplace.install.parameters": "Parametere", + "marketplace.install.optional": "(valgfritt)", + "marketplace.install.required": "{{name}} er påkrevd", + "marketplace.scope.project": "prosjekt", + "marketplace.scope.global": "global", + "marketplace.remove.type.mcp": "MCP-server", + "marketplace.remove.type.skill": "ferdighet", + "marketplace.remove.type.mode": "modus", + "marketplace.remove.failed": "Kunne ikke fjerne {{name}}", } diff --git a/packages/kilo-i18n/src/pl.ts b/packages/kilo-i18n/src/pl.ts index b4b7f0685a1..1cc4c5f420d 100644 --- a/packages/kilo-i18n/src/pl.ts +++ b/packages/kilo-i18n/src/pl.ts @@ -43,4 +43,22 @@ export const dict = { "Czy na pewno chcesz usunąć ten {{type}}? Spowoduje to usunięcie z konfiguracji: {{scope}}.", "marketplace.remove.cancel": "Anuluj", "marketplace.remove.confirm.button": "Usuń", + "marketplace.tab.mcp": "MCP", + "marketplace.search": "Szukaj...", + "marketplace.filter.all": "Wszystkie elementy", + "marketplace.filter.notInstalled": "Nie zainstalowano", + "marketplace.empty": "Nie znaleziono elementów", + "marketplace.badge.mcpServer": "Serwer MCP", + "marketplace.badge.mode": "Tryb", + "marketplace.card.by": "autor: {{author}}", + "marketplace.install.method": "Metoda instalacji", + "marketplace.install.parameters": "Parametry", + "marketplace.install.optional": "(opcjonalne)", + "marketplace.install.required": "{{name}} jest wymagane", + "marketplace.scope.project": "projekt", + "marketplace.scope.global": "globalny", + "marketplace.remove.type.mcp": "serwer MCP", + "marketplace.remove.type.skill": "umiejętność", + "marketplace.remove.type.mode": "tryb", + "marketplace.remove.failed": "Nie udało się usunąć {{name}}", } diff --git a/packages/kilo-i18n/src/ru.ts b/packages/kilo-i18n/src/ru.ts index 581b6925967..cf8a06e54a0 100644 --- a/packages/kilo-i18n/src/ru.ts +++ b/packages/kilo-i18n/src/ru.ts @@ -43,4 +43,22 @@ export const dict = { "Вы уверены, что хотите удалить этот {{type}}? Это удалит его из вашей конфигурации ({{scope}}).", "marketplace.remove.cancel": "Отмена", "marketplace.remove.confirm.button": "Удалить", + "marketplace.tab.mcp": "MCP", + "marketplace.search": "Поиск...", + "marketplace.filter.all": "Все элементы", + "marketplace.filter.notInstalled": "Не установлено", + "marketplace.empty": "Элементы не найдены", + "marketplace.badge.mcpServer": "MCP-сервер", + "marketplace.badge.mode": "Режим", + "marketplace.card.by": "от {{author}}", + "marketplace.install.method": "Способ установки", + "marketplace.install.parameters": "Параметры", + "marketplace.install.optional": "(необязательно)", + "marketplace.install.required": "Требуется {{name}}", + "marketplace.scope.project": "проект", + "marketplace.scope.global": "глобально", + "marketplace.remove.type.mcp": "MCP-сервер", + "marketplace.remove.type.skill": "навык", + "marketplace.remove.type.mode": "режим", + "marketplace.remove.failed": "Не удалось удалить {{name}}", } diff --git a/packages/kilo-i18n/src/th.ts b/packages/kilo-i18n/src/th.ts index 5fe5514aab0..196fbbecca9 100644 --- a/packages/kilo-i18n/src/th.ts +++ b/packages/kilo-i18n/src/th.ts @@ -43,4 +43,22 @@ export const dict = { "คุณแน่ใจหรือไม่ว่าต้องการลบ {{type}} นี้? สิ่งนี้จะลบออกจากการกำหนดค่า {{scope}} ของคุณ", "marketplace.remove.cancel": "ยกเลิก", "marketplace.remove.confirm.button": "ลบ", + "marketplace.tab.mcp": "MCP", + "marketplace.search": "ค้นหา...", + "marketplace.filter.all": "รายการทั้งหมด", + "marketplace.filter.notInstalled": "ยังไม่ได้ติดตั้ง", + "marketplace.empty": "ไม่พบรายการ", + "marketplace.badge.mcpServer": "เซิร์ฟเวอร์ MCP", + "marketplace.badge.mode": "โหมด", + "marketplace.card.by": "โดย {{author}}", + "marketplace.install.method": "วิธีการติดตั้ง", + "marketplace.install.parameters": "พารามิเตอร์", + "marketplace.install.optional": "(ไม่บังคับ)", + "marketplace.install.required": "จำเป็นต้องใช้ {{name}}", + "marketplace.scope.project": "โปรเจ็กต์", + "marketplace.scope.global": "โกลบอล", + "marketplace.remove.type.mcp": "เซิร์ฟเวอร์ MCP", + "marketplace.remove.type.skill": "ทักษะ", + "marketplace.remove.type.mode": "โหมด", + "marketplace.remove.failed": "ไม่สามารถลบ {{name}} ได้", } diff --git a/packages/kilo-i18n/src/zh.ts b/packages/kilo-i18n/src/zh.ts index a5f9d8870d3..3ddf2b8c988 100644 --- a/packages/kilo-i18n/src/zh.ts +++ b/packages/kilo-i18n/src/zh.ts @@ -40,4 +40,22 @@ export const dict = { "marketplace.remove.confirm": "确定要移除此 {{type}} 吗?这会将其从你的 {{scope}} 配置中移除。", "marketplace.remove.cancel": "取消", "marketplace.remove.confirm.button": "移除", + "marketplace.tab.mcp": "MCP", + "marketplace.search": "搜索...", + "marketplace.filter.all": "所有项目", + "marketplace.filter.notInstalled": "未安装", + "marketplace.empty": "未找到项目", + "marketplace.badge.mcpServer": "MCP 服务器", + "marketplace.badge.mode": "模式", + "marketplace.card.by": "作者:{{author}}", + "marketplace.install.method": "安装方法", + "marketplace.install.parameters": "参数", + "marketplace.install.optional": "(可选)", + "marketplace.install.required": "{{name}} 是必填项", + "marketplace.scope.project": "项目", + "marketplace.scope.global": "全局", + "marketplace.remove.type.mcp": "MCP 服务器", + "marketplace.remove.type.skill": "技能", + "marketplace.remove.type.mode": "模式", + "marketplace.remove.failed": "移除 {{name}} 失败", } diff --git a/packages/kilo-i18n/src/zht.ts b/packages/kilo-i18n/src/zht.ts index 219ed9d7f95..ff74dab6e2f 100644 --- a/packages/kilo-i18n/src/zht.ts +++ b/packages/kilo-i18n/src/zht.ts @@ -40,4 +40,22 @@ export const dict = { "marketplace.remove.confirm": "確定要移除此 {{type}} 嗎?這會將其從你的 {{scope}} 設定中移除。", "marketplace.remove.cancel": "取消", "marketplace.remove.confirm.button": "移除", + "marketplace.tab.mcp": "MCP", + "marketplace.search": "搜尋...", + "marketplace.filter.all": "所有項目", + "marketplace.filter.notInstalled": "未安裝", + "marketplace.empty": "找不到項目", + "marketplace.badge.mcpServer": "MCP 伺服器", + "marketplace.badge.mode": "模式", + "marketplace.card.by": "作者:{{author}}", + "marketplace.install.method": "安裝方法", + "marketplace.install.parameters": "參數", + "marketplace.install.optional": "(選填)", + "marketplace.install.required": "{{name}} 是必填項", + "marketplace.scope.project": "專案", + "marketplace.scope.global": "全域", + "marketplace.remove.type.mcp": "MCP 伺服器", + "marketplace.remove.type.skill": "技能", + "marketplace.remove.type.mode": "模式", + "marketplace.remove.failed": "移除 {{name}} 失敗", } diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 09e98a3cda2..f8273f964a2 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -669,8 +669,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper case "removeInstalledMarketplaceItem": { const workspace = this.getProjectDirectory(this.currentSession?.id) const scope = message.mpInstallOptions?.target ?? "project" - const result = await this.getMarketplace().remove(message.mpItem, scope, workspace) - if (result.success) await this.disposeCliInstance(scope) + const result = + message.mpItem.type === "mode" + ? await this.removeModeViaCli(message.mpItem) + : await this.getMarketplace().remove(message.mpItem, scope, workspace) + if (result.success && message.mpItem.type !== "mode") await this.disposeCliInstance(scope) this.postMessage({ type: "marketplaceRemoveResult", success: result.success, @@ -1256,6 +1259,32 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper await this.fetchAndSendAgents() } + /** + * Remove a marketplace mode via the CLI removeAgent endpoint. + * Returns a RemoveResult so it plugs into the marketplace removal flow. + */ + private async removeModeViaCli(item: { + id: string + content: string + }): Promise<{ success: boolean; slug: string; error?: string }> { + if (!this.client) return { success: false, slug: item.id, error: "CLI not connected" } + const yaml = await import("yaml") + const mode = yaml.parse(item.content) as Record | undefined + const slug = (mode?.slug as string) ?? item.id + try { + const dir = this.getWorkspaceDirectory() + const result = await this.client.kilocode.removeAgent({ name: slug, directory: dir }) + if (result.error) { + console.error("[Kilo New] removeAgent (marketplace) returned error:", result.error) + return { success: false, slug: item.id, error: String(result.error) } + } + return { success: true, slug: item.id } + } catch (error) { + console.error("[Kilo New] Failed to remove mode (marketplace):", error) + return { success: false, slug: item.id, error: String(error) } + } + } + /** * Dispose the CLI backend instance so it re-reads config from disk. * Call after any marketplace install/remove that writes config files directly. diff --git a/packages/kilo-vscode/src/services/marketplace/installer.ts b/packages/kilo-vscode/src/services/marketplace/installer.ts index 7cf55351ef5..0b19073be49 100644 --- a/packages/kilo-vscode/src/services/marketplace/installer.ts +++ b/packages/kilo-vscode/src/services/marketplace/installer.ts @@ -6,6 +6,9 @@ import { promisify } from "util" import type { MarketplaceItem, SkillMarketplaceItem, + McpMarketplaceItem, + ModeMarketplaceItem, + McpInstallationMethod, InstallMarketplaceItemOptions, InstallResult, RemoveResult, @@ -24,9 +27,96 @@ export class MarketplaceInstaller { ): Promise { const scope = options.target ?? "project" if (item.type === "skill") return this.installSkill(item, scope, workspace) - return { success: false, slug: item.id, error: `${item.type} installation not yet implemented` } + if (item.type === "mcp") return this.installMcp(item, options, scope, workspace) + return this.installMode(item, scope, workspace) } + // ── MCP ───────────────────────────────────────────────────────────── + + async installMcp( + item: McpMarketplaceItem, + options: InstallMarketplaceItemOptions, + scope: "project" | "global", + workspace?: string, + ): Promise { + if (scope === "project" && !workspace) { + return { success: false, slug: item.id, error: "No workspace directory for project-scope install" } + } + + const config = await this.readConfig(scope, workspace) + if (!config.mcp) config.mcp = {} + + if (config.mcp[item.id]) { + return { success: false, slug: item.id, error: "MCP server already installed. Remove it first." } + } + + const content = this.resolveMcpContent(item, options) + if (!content) { + return { success: false, slug: item.id, error: "No installation content for MCP server" } + } + + const entry = this.buildMcpEntry(content, options.parameters) + config.mcp[item.id] = entry + + await this.writeConfig(scope, workspace, config) + return { success: true, slug: item.id } + } + + private resolveMcpContent(item: McpMarketplaceItem, options: InstallMarketplaceItemOptions): string | undefined { + if (typeof item.content === "string") return item.content + if (!Array.isArray(item.content) || item.content.length === 0) return undefined + const name = options.parameters?.__method as string | undefined + if (name) { + const found = item.content.find((m: McpInstallationMethod) => m.name === name) + if (found) return found.content + } + return item.content[0].content + } + + private buildMcpEntry(content: string, params?: Record): Record { + const filtered = Object.fromEntries(Object.entries(params ?? {}).filter(([k]) => k !== "__method")) + const replaced = Object.keys(filtered).length > 0 ? substituteParams(content, filtered) : content + + // Parse the content as JSON — the marketplace API provides JSON config blocks + try { + return JSON.parse(replaced) + } catch { + // Fallback: treat as a command string + return { command: replaced } + } + } + + // ── Mode ──────────────────────────────────────────────────────────── + + async installMode( + item: ModeMarketplaceItem, + scope: "project" | "global", + workspace?: string, + ): Promise { + if (scope === "project" && !workspace) { + return { success: false, slug: item.id, error: "No workspace directory for project-scope install" } + } + + const config = await this.readConfig(scope, workspace) + if (!config.agent) config.agent = {} + + if (config.agent[item.id]) { + return { success: false, slug: item.id, error: "Mode already installed. Remove it first." } + } + + // Mode content is a JSON blob for the agent config + try { + config.agent[item.id] = JSON.parse(item.content) + } catch { + config.agent[item.id] = { system: item.content } + } + + await this.writeConfig(scope, workspace, config) + return { success: true, slug: item.id } + } + + // ── Skill ─────────────────────────────────────────────────────────── + async installSkill( item: SkillMarketplaceItem, scope: "project" | "global", @@ -106,9 +196,34 @@ export class MarketplaceInstaller { } } + // ── Remove ────────────────────────────────────────────────────────── + async remove(item: MarketplaceItem, scope: "project" | "global", workspace?: string): Promise { if (item.type === "skill") return this.removeSkill(item, scope, workspace) - return { success: false, slug: item.id, error: `${item.type} removal not yet implemented` } + if (item.type === "mcp") return this.removeMcp(item, scope, workspace) + return this.removeMode(item, scope, workspace) + } + + async removeMcp(item: McpMarketplaceItem, scope: "project" | "global", workspace?: string): Promise { + const config = await this.readConfig(scope, workspace) + if (!config.mcp?.[item.id]) { + return { success: true, slug: item.id } + } + delete config.mcp[item.id] + if (Object.keys(config.mcp).length === 0) delete config.mcp + await this.writeConfig(scope, workspace, config) + return { success: true, slug: item.id } + } + + async removeMode(item: ModeMarketplaceItem, scope: "project" | "global", workspace?: string): Promise { + const config = await this.readConfig(scope, workspace) + if (!config.agent?.[item.id]) { + return { success: true, slug: item.id } + } + delete config.agent[item.id] + if (Object.keys(config.agent).length === 0) delete config.agent + await this.writeConfig(scope, workspace, config) + return { success: true, slug: item.id } } async removeSkill( @@ -136,6 +251,32 @@ export class MarketplaceInstaller { return { success: false, slug: item.id, error: String(err) } } } + + // ── Config helpers ────────────────────────────────────────────────── + + private async readConfig( + scope: "project" | "global", + workspace?: string, + ): Promise>> { + const filepath = this.paths.configPath(scope, workspace) + try { + const content = await fs.readFile(filepath, "utf-8") + return JSON.parse(content) + } catch (err) { + if ((err as NodeJS.ErrnoException).code === "ENOENT") return {} + throw err + } + } + + private async writeConfig( + scope: "project" | "global", + workspace: string | undefined, + config: Record, + ): Promise { + const filepath = this.paths.configPath(scope, workspace) + await fs.mkdir(path.dirname(filepath), { recursive: true }) + await fs.writeFile(filepath, JSON.stringify(config, null, 2) + "\n", "utf-8") + } } // ── Helpers ───────────────────────────────────────────────────────── @@ -145,6 +286,15 @@ function isSafeId(id: string): boolean { return /^[\w\-@.]+$/.test(id) } +function substituteParams(template: string, params: Record): string { + let result = template + for (const [key, value] of Object.entries(params)) { + result = result.replaceAll(`{{${key}}}`, String(value ?? "")) + result = result.replaceAll(`\${${key}}`, String(value ?? "")) + } + return result +} + async function findEscapedPaths(dir: string): Promise { const resolved = path.resolve(dir) const escaped: string[] = [] diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/InstallModal.tsx b/packages/kilo-vscode/webview-ui/src/components/marketplace/InstallModal.tsx index e070513b5de..3655739c762 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/InstallModal.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/InstallModal.tsx @@ -1,71 +1,109 @@ -import { Component, createSignal, onMount, onCleanup, Show, For } from "solid-js" +import { createSignal, createEffect, onCleanup, Show, For } from "solid-js" +import { Dialog } from "@kilocode/kilo-ui/dialog" import { Button } from "@kilocode/kilo-ui/button" import { RadioGroup } from "@kilocode/kilo-ui/radio-group" +import { Select } from "@kilocode/kilo-ui/select" +import { TextField } from "@kilocode/kilo-ui/text-field" +import { Spinner } from "@kilocode/kilo-ui/spinner" import { useVSCode } from "../../context/vscode" import { useServer } from "../../context/server" import { useLanguage } from "../../context/language" -import type { MarketplaceItem } from "../../types/marketplace" +import type { MarketplaceItem, McpMarketplaceItem, McpInstallationMethod, McpParameter } from "../../types/marketplace" interface ScopeOption { value: "project" | "global" label: string - disabled?: boolean } interface Props { item: MarketplaceItem onClose: () => void - onInstallResult?: (success: boolean) => void + onInstallResult: (success: boolean, scope: "project" | "global") => void } -const InstallModal: Component = (props) => { +export const InstallModal = (props: Props) => { const vscode = useVSCode() const server = useServer() const { t } = useLanguage() const workspace = () => server.workspaceDirectory() - const scopeOptions = (): ScopeOption[] => [ - { value: "project", label: t("marketplace.install.scope.project"), disabled: !workspace() }, - { value: "global", label: t("marketplace.install.scope.global") }, + const options = (): ScopeOption[] => [ + { value: "project", label: t("marketplace.scope.project") }, + { value: "global", label: t("marketplace.scope.global") }, ] - const [scope, setScope] = createSignal(workspace() ? scopeOptions()[0] : scopeOptions()[1]) + const initial = workspace() ? options()[0] : options()[1] + const [scope, setScope] = createSignal(initial) const [installing, setInstalling] = createSignal(false) const [result, setResult] = createSignal<{ success: boolean; error?: string } | null>(null) + const [params, setParams] = createSignal>({}) - const prerequisites = () => props.item.prerequisites ?? [] + // MCP installation methods + const methods = (): McpInstallationMethod[] => { + if (props.item.type !== "mcp") return [] + const mcp = props.item as McpMarketplaceItem + if (!Array.isArray(mcp.content)) return [] + return mcp.content + } - onMount(() => { - const handler = (event: MessageEvent) => { - const msg = event.data - if (msg?.type === "marketplaceInstallResult" && msg.slug === props.item.id) { + const [method, setMethod] = createSignal(methods()[0]) + + const prerequisites = (): string[] => { + if (method()?.prerequisites?.length) return method()!.prerequisites! + return props.item.prerequisites ?? [] + } + + const parameters = (): McpParameter[] => { + if (method()?.parameters?.length) return method()!.parameters! + if (props.item.type === "mcp") return (props.item as McpMarketplaceItem).parameters ?? [] + return [] + } + + const valid = () => { + for (const param of parameters()) { + if (!param.optional && !params()[param.key]?.trim()) return false + } + return true + } + + const setParam = (key: string, value: string) => { + setParams((prev) => ({ ...prev, [key]: value })) + } + + createEffect(() => { + const unsub = vscode.onMessage((msg) => { + if (msg.type === "marketplaceInstallResult" && msg.slug === props.item.id) { setInstalling(false) setResult({ success: msg.success, error: msg.error }) - props.onInstallResult?.(msg.success) + props.onInstallResult(msg.success, scope().value) } - } - window.addEventListener("message", handler) - onCleanup(() => window.removeEventListener("message", handler)) + }) + onCleanup(unsub) }) const handleInstall = () => { setInstalling(true) + const paramValues: Record = { ...params() } + if (method()) { + paramValues.__method = method()!.name + } vscode.postMessage({ type: "installMarketplaceItem", mpItem: props.item, - mpInstallOptions: { target: scope().value }, + mpInstallOptions: { + target: scope().value, + parameters: Object.keys(paramValues).length > 0 ? paramValues : undefined, + }, }) } return ( -
!installing() && props.onClose()}> -
e.stopPropagation()}> -

{t("marketplace.install.title", { name: props.item.name })}

- - -
- + + +
+
+ {t("marketplace.install.scope")} x.value} label={(x: ScopeOption) => x.label} @@ -73,46 +111,81 @@ const InstallModal: Component = (props) => { />
+ 1}> +
+ {t("marketplace.install.method")} + o.value} + label={(o: StatusOption) => o.label} + onSelect={(v: StatusOption | undefined) => v && setStatus(v)} + /> +
+ 0}> +
+ + {(tag) => ( + + )} + +
+
+ + +
+ } + > + 0} fallback={

{props.emptyMessage}

}> +
+ + {(item) => ( + {(tag) => {tag}}} + /> + )} + +
+
+
+
+ ) +} diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceView.tsx b/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceView.tsx index e821a6e0beb..7e546aad7b9 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceView.tsx @@ -1,168 +1,184 @@ -import { Component, createSignal, createMemo, onMount, onCleanup, Show } from "solid-js" +import { createSignal, createMemo, createEffect, onCleanup, Show } from "solid-js" import { Tabs } from "@kilocode/kilo-ui/tabs" +import { Card } from "@kilocode/kilo-ui/card" +import { Button } from "@kilocode/kilo-ui/button" import { useVSCode } from "../../context/vscode" import { useServer } from "../../context/server" import { useLanguage } from "../../context/language" -import type { MarketplaceItem, SkillMarketplaceItem, MarketplaceInstalledMetadata } from "../../types/marketplace" -import SkillsMarketplace from "./SkillsMarketplace" -import InstallModal from "./InstallModal" -import RemoveDialog from "./RemoveDialog" +import { useDialog } from "@kilocode/kilo-ui/context/dialog" +import type { + MarketplaceItem, + McpMarketplaceItem, + ModeMarketplaceItem, + SkillMarketplaceItem, + MarketplaceInstalledMetadata, +} from "../../types/marketplace" +import { SkillsMarketplace } from "./SkillsMarketplace" +import { MarketplaceListView } from "./MarketplaceListView" +import { InstallModal } from "./InstallModal" +import { RemoveDialog } from "./RemoveDialog" import "./marketplace.css" const EMPTY_METADATA: MarketplaceInstalledMetadata = { project: {}, global: {} } -const MarketplaceView: Component = () => { +export const MarketplaceView = () => { const vscode = useVSCode() const server = useServer() const { t } = useLanguage() + const dialog = useDialog() const [items, setItems] = createSignal([]) const [metadata, setMetadata] = createSignal(EMPTY_METADATA) const [fetching, setFetching] = createSignal(true) const [errors, setErrors] = createSignal([]) - const [tab, setTab] = createSignal("skill") - - // Install/Remove state - const [installItem, setInstallItem] = createSignal(null) - const [removeItem, setRemoveItem] = createSignal<{ item: MarketplaceItem; scope: "project" | "global" } | null>(null) - const [pendingRemove, setPendingRemove] = createSignal(null) - const [removeError, setRemoveError] = createSignal(null) + const [tab, setTab] = createSignal("mcp") const skills = createMemo(() => items().filter((i): i is SkillMarketplaceItem => i.type === "skill")) + const mcps = createMemo(() => items().filter((i): i is McpMarketplaceItem => i.type === "mcp")) + const modes = createMemo(() => items().filter((i): i is ModeMarketplaceItem => i.type === "mode")) const fetchData = () => { setFetching(true) vscode.postMessage({ type: "fetchMarketplaceData" }) } - onMount(() => { - fetchData() - - const handler = (event: MessageEvent) => { - const msg = event.data - if (msg?.type === "marketplaceData") { + // Listen for messages + createEffect(() => { + const unsub = vscode.onMessage((msg) => { + if (msg.type === "marketplaceData") { setItems(msg.marketplaceItems ?? []) setMetadata(msg.marketplaceInstalledMetadata ?? EMPTY_METADATA) setErrors(msg.errors ?? []) setFetching(false) } - if (msg?.type === "marketplaceRemoveResult") { - if (msg.slug === pendingRemove()) { - setPendingRemove(null) - if (!msg.success) { - setRemoveError(msg.error ?? "Failed to remove item") - } else { - fetchData() - } - } + if (msg.type === "marketplaceRemoveResult") { + if (msg.success) fetchData() } - // Re-fetch when agents or skills change (e.g. after install via CLI) - if (msg?.type === "agentsLoaded" || msg?.type === "skillsLoaded") { + if (msg.type === "agentsLoaded" || msg.type === "skillsLoaded") { fetchData() } - // Re-fetch when workspace directory changes - if (msg?.type === "workspaceDirectoryChanged") { + if (msg.type === "workspaceDirectoryChanged") { fetchData() } - } - window.addEventListener("message", handler) - onCleanup(() => window.removeEventListener("message", handler)) + }) + onCleanup(unsub) }) + // Re-fetch when workspace changes + createEffect(() => { + server.workspaceDirectory() + fetchData() + }) + + const telemetry = (event: string, properties?: Record) => { + vscode.postMessage({ type: "telemetry", event, properties: properties ?? {} }) + } + const handleInstall = (item: MarketplaceItem) => { - setInstallItem(item) + dialog.show(() => ( + dialog.close()} + onInstallResult={(success, scope) => { + if (success) { + telemetry("marketplace_item_installed", { + id: item.id, + type: item.type, + scope, + }) + dialog.close() + fetchData() + } + }} + /> + )) } const handleRemove = (item: MarketplaceItem, scope: "project" | "global") => { - setRemoveItem({ item, scope }) + dialog.show(() => ( + dialog.close()} + onConfirm={() => { + vscode.postMessage({ + type: "removeInstalledMarketplaceItem", + mpItem: item, + mpInstallOptions: { target: scope }, + }) + telemetry("marketplace_item_removed", { + id: item.id, + type: item.type, + scope, + }) + dialog.close() + }} + /> + )) } - const confirmRemove = () => { - const r = removeItem() - if (!r) return - setPendingRemove(r.item.id) - setRemoveError(null) - vscode.postMessage({ - type: "removeInstalledMarketplaceItem", - mpItem: r.item, - mpInstallOptions: { target: r.scope }, - }) - setRemoveItem(null) + const dismissError = (idx: number) => { + setErrors((prev) => prev.filter((_, i) => i !== idx)) } return (
- + 0}> + {errors().map((err, idx) => ( + + {err} + + + ))} + + + - {t("marketplace.tab.skills")} - {t("marketplace.tab.mcpServers")} + {t("marketplace.tab.mcp")} {t("marketplace.tab.modes")} + {t("marketplace.tab.skills")} - - - +
+ + + - -
-

{t("marketplace.placeholder")}

-
-
+ + + - -
-

{t("marketplace.placeholder")}

-
-
+ + + +
- - 0}> -
- {errors().map((e) => ( -

{e}

- ))} -
-
- - -
-

{removeError()}

-
-
- - - {(item) => ( - setInstallItem(null)} - onInstallResult={(success) => { - if (success) { - setInstallItem(null) - fetchData() - } - }} - /> - )} - - - - {(r) => ( - setRemoveItem(null)} - onConfirm={confirmRemove} - /> - )} -
) } - -export default MarketplaceView diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/RemoveDialog.tsx b/packages/kilo-vscode/webview-ui/src/components/marketplace/RemoveDialog.tsx index 4e544fac0cd..b092861b90e 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/RemoveDialog.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/RemoveDialog.tsx @@ -1,4 +1,4 @@ -import { Component } from "solid-js" +import { Dialog } from "@kilocode/kilo-ui/dialog" import { Button } from "@kilocode/kilo-ui/button" import type { MarketplaceItem } from "../../types/marketplace" import { useLanguage } from "../../context/language" @@ -10,25 +10,26 @@ interface Props { onConfirm: () => void } -const RemoveDialog: Component = (props) => { +export const RemoveDialog = (props: Props) => { const { t } = useLanguage() + const typeName = () => { + if (props.item.type === "mcp") return t("marketplace.type.mcp") + if (props.item.type === "mode") return t("marketplace.type.mode") + return t("marketplace.type.skill") + } + return ( -
-
e.stopPropagation()}> -

{t("marketplace.remove.title", { name: props.item.name })}

-

{t("marketplace.remove.confirm", { type: props.item.type, scope: props.scope })}

-
- - -
+ +

{t("marketplace.remove.confirm", { type: typeName(), scope: t(`marketplace.scope.${props.scope}`) })}

+
+ +
-
+ ) } - -export default RemoveDialog diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/SkillsMarketplace.tsx b/packages/kilo-vscode/webview-ui/src/components/marketplace/SkillsMarketplace.tsx index e55c4d3419c..d71a250266e 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/SkillsMarketplace.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/SkillsMarketplace.tsx @@ -1,11 +1,11 @@ -import { Component, createSignal, createMemo, For, Show } from "solid-js" +import { createSignal, createMemo, For, Show } from "solid-js" import { TextField } from "@kilocode/kilo-ui/text-field" import { RadioGroup } from "@kilocode/kilo-ui/radio-group" import { Tag } from "@kilocode/kilo-ui/tag" import { Spinner } from "@kilocode/kilo-ui/spinner" import type { SkillMarketplaceItem, MarketplaceInstalledMetadata, MarketplaceItem } from "../../types/marketplace" import { useLanguage } from "../../context/language" -import ItemCard from "./ItemCard" +import { ItemCard } from "./ItemCard" interface Props { items: SkillMarketplaceItem[] @@ -17,7 +17,7 @@ interface Props { const ALL = "__all__" -const SkillsMarketplace: Component = (props) => { +export const SkillsMarketplace = (props: Props) => { const { t } = useLanguage() const [search, setSearch] = createSignal("") const [category, setCategory] = createSignal(ALL) @@ -82,5 +82,3 @@ const SkillsMarketplace: Component = (props) => {
) } - -export default SkillsMarketplace diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/index.ts b/packages/kilo-vscode/webview-ui/src/components/marketplace/index.ts index 979b85151d1..e322a99a16f 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/index.ts +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/index.ts @@ -1 +1 @@ -export { default as MarketplaceView } from "./MarketplaceView" +export { MarketplaceView } from "./MarketplaceView" diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/marketplace.css b/packages/kilo-vscode/webview-ui/src/components/marketplace/marketplace.css index 49c95541ce4..5d958d7fdd2 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/marketplace.css +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/marketplace.css @@ -6,6 +6,19 @@ height: 100%; padding: 12px; overflow: auto; + gap: 8px; +} + +.marketplace-tabs-root { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; +} + +.marketplace-content { + flex: 1; + overflow: auto; } /* List layout */ @@ -23,10 +36,52 @@ align-items: center; } +.marketplace-search-field { + flex: 1; +} + .marketplace-category-filters { overflow-x: auto; } +/* Tag filters */ + +.marketplace-active-tags { + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +.marketplace-tag-filter { + all: unset; + cursor: pointer; + opacity: 0.5; + transition: opacity 0.15s ease; +} + +.marketplace-tag-filter:hover { + opacity: 0.8; +} + +.marketplace-tag-filter.active { + opacity: 1; +} + +/* Loading & empty */ + +.marketplace-loading { + display: flex; + justify-content: center; + align-items: center; + min-height: 200px; +} + +.marketplace-empty { + text-align: center; + color: var(--text-weak); + padding: 24px; +} + /* Grid */ .marketplace-grid { @@ -48,9 +103,6 @@ flex-direction: column; gap: 8px; padding: 12px; - border: 1px solid var(--border-base); - border-radius: 6px; - background: var(--background-base); } .marketplace-card-header { @@ -71,14 +123,6 @@ color: var(--text-base); } -.marketplace-card-link { - text-decoration: none; -} - -.marketplace-card-link:hover { - text-decoration: underline; -} - .marketplace-card-author { font-size: 12px; color: var(--text-weak); @@ -115,117 +159,109 @@ flex-shrink: 0; } -.marketplace-installed-tag { +/* Installed badge */ + +.marketplace-badge-installed { color: var(--text-on-success-base) !important; border-color: var(--border-success-base) !important; } +/* Remove button */ + .marketplace-remove-btn { color: var(--text-on-critical-base) !important; border-color: var(--border-critical-base) !important; } -/* Placeholder */ +/* Remove dialog actions */ -.marketplace-placeholder { - display: flex; - justify-content: center; - align-items: center; - min-height: 200px; - color: var(--text-weak); - font-size: 16px; -} - -/* Empty state */ - -.marketplace-empty { - text-align: center; - color: var(--text-weak); - padding: 24px; -} - -/* Modal / Dialog */ - -.marketplace-modal-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; -} - -.marketplace-modal { - background: var(--background-base); - border: 1px solid var(--border-base); - border-radius: 8px; - padding: 20px; - max-width: 480px; - width: 90%; - max-height: 80vh; - overflow-y: auto; -} - -.marketplace-modal h3 { - margin: 0 0 16px; - font-size: 16px; - color: var(--text-base); -} - -.marketplace-modal p { - margin: 0 0 12px; - color: var(--text-weak); - font-size: 13px; -} - -.marketplace-modal-section { - margin-bottom: 16px; -} - -.marketplace-modal-section > label { - display: block; - font-weight: 600; - font-size: 13px; - margin-bottom: 6px; - color: var(--text-base); -} - -.marketplace-modal-actions { +.marketplace-remove-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; } -.marketplace-modal-result { - text-align: center; - padding: 12px 0; +/* Danger button (remove confirm) */ + +.danger-btn { + color: var(--text-on-critical-base) !important; + border-color: var(--border-critical-base) !important; } -.marketplace-prerequisites { +/* Install modal */ + +.install-modal-body { + display: flex; + flex-direction: column; + gap: 16px; +} + +.install-modal-section { + display: flex; + flex-direction: column; + gap: 6px; +} + +.install-modal-label { + font-weight: 600; + font-size: 13px; + color: var(--text-base); +} + +.install-modal-prerequisites { margin: 0; padding-left: 20px; font-size: 13px; color: var(--text-weak); } -/* Error/Success messages */ - -.marketplace-errors { - margin-top: 12px; +.install-modal-param { + margin-bottom: 8px; } -.marketplace-error { - color: var(--text-on-critical-base); - font-size: 13px; +.install-modal-footer { + display: flex; + justify-content: flex-end; + gap: 8px; } -.marketplace-success { +.install-modal-result { + text-align: center; + padding: 12px 0; +} + +.install-modal-success { color: var(--text-on-success-base); font-size: 14px; font-weight: 600; + margin: 0 0 16px; +} + +.install-modal-error-msg { + color: var(--text-on-critical-base); + font-size: 13px; + margin: 0 0 16px; +} + +/* Error banner */ + +.marketplace-error-banner { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 8px 12px; + font-size: 13px; +} + +/* Link style */ + +.link { + color: var(--text-interactive-base); + cursor: pointer; +} + +.link:hover { + text-decoration: underline; } diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/utils.ts b/packages/kilo-vscode/webview-ui/src/components/marketplace/utils.ts index 264b722a54c..9504e734f37 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/utils.ts +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/utils.ts @@ -1,11 +1,19 @@ import type { MarketplaceInstalledMetadata } from "../../types/marketplace" +export function isInstalled( + id: string, + type: string, + metadata: MarketplaceInstalledMetadata, +): "project" | "global" | false { + return installedScopes(id, type, metadata)[0] ?? false +} + export function installedScopes( id: string, type: string, metadata: MarketplaceInstalledMetadata, -): Array<"project" | "global"> { - const scopes: Array<"project" | "global"> = [] +): ("project" | "global")[] { + const scopes: ("project" | "global")[] = [] if (metadata.project[id]?.type === type) scopes.push("project") if (metadata.global[id]?.type === type) scopes.push("global") return scopes diff --git a/packages/kilo-vscode/webview-ui/src/types/marketplace.ts b/packages/kilo-vscode/webview-ui/src/types/marketplace.ts index d41a7351266..943d2bec10a 100644 --- a/packages/kilo-vscode/webview-ui/src/types/marketplace.ts +++ b/packages/kilo-vscode/webview-ui/src/types/marketplace.ts @@ -1,3 +1,17 @@ +export interface McpParameter { + name: string + key: string + placeholder?: string + optional?: boolean +} + +export interface McpInstallationMethod { + name: string + content: string + parameters?: McpParameter[] + prerequisites?: string[] +} + export interface MarketplaceItemBase { id: string name: string @@ -11,7 +25,8 @@ export interface MarketplaceItemBase { export interface McpMarketplaceItem extends MarketplaceItemBase { type: "mcp" url: string - content: string | unknown[] + content: string | McpInstallationMethod[] + parameters?: McpParameter[] } export interface ModeMarketplaceItem extends MarketplaceItemBase { @@ -41,8 +56,7 @@ export interface MarketplaceInstalledMetadata { } export interface MarketplaceFilters { - search?: string type?: string - status?: string + search?: string tags?: string[] } From 9861f9d8ba4b23f4f3506eb2d330c535527081b5 Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 17 Mar 2026 10:45:16 +0100 Subject: [PATCH 40/74] refactor(vscode): merge SkillsMarketplace into MarketplaceListView --- .../marketplace/MarketplaceListView.tsx | 52 ++++++++---- .../marketplace/MarketplaceView.tsx | 6 +- .../marketplace/SkillsMarketplace.tsx | 84 ------------------- 3 files changed, 39 insertions(+), 103 deletions(-) delete mode 100644 packages/kilo-vscode/webview-ui/src/components/marketplace/SkillsMarketplace.tsx diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceListView.tsx b/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceListView.tsx index 5ebb22a9993..cfb0d174692 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceListView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceListView.tsx @@ -6,7 +6,7 @@ import { Spinner } from "@kilocode/kilo-ui/spinner" import type { MarketplaceItem, McpMarketplaceItem, - ModeMarketplaceItem, + SkillMarketplaceItem, MarketplaceInstalledMetadata, } from "../../types/marketplace" import { useLanguage } from "../../context/language" @@ -19,10 +19,10 @@ interface StatusOption { } interface Props { - items: (McpMarketplaceItem | ModeMarketplaceItem)[] + items: MarketplaceItem[] metadata: MarketplaceInstalledMetadata fetching: boolean - type: "mcp" | "mode" + type: "mcp" | "mode" | "skill" searchPlaceholder: string emptyMessage: string onInstall: (item: MarketplaceItem) => void @@ -41,10 +41,15 @@ export const MarketplaceListView = (props: Props) => { { value: "notInstalled", label: t("marketplace.filter.notInstalled") }, ] + const tagsFor = (item: MarketplaceItem): string[] => { + if (item.type === "skill") return [(item as SkillMarketplaceItem).displayCategory] + return item.tags ?? [] + } + const allTags = createMemo(() => { const set = new Set() for (const item of props.items) { - for (const tag of item.tags ?? []) set.add(tag) + for (const tag of tagsFor(item)) set.add(tag) } return Array.from(set).sort() }) @@ -65,13 +70,15 @@ export const MarketplaceListView = (props: Props) => { return props.items.filter((item) => { if (s === "installed" && !isInstalled(item.id, item.type, props.metadata)) return false if (s === "notInstalled" && isInstalled(item.id, item.type, props.metadata)) return false - if (active.length > 0 && !active.some((tag) => item.tags?.includes(tag))) return false + if (active.length > 0 && !active.some((tag) => tagsFor(item).includes(tag))) return false if (!q) return true + const skill = item.type === "skill" ? (item as SkillMarketplaceItem) : undefined return ( item.id.toLowerCase().includes(q) || item.name.toLowerCase().includes(q) || item.description.toLowerCase().includes(q) || - (item.author?.toLowerCase().includes(q) ?? false) + (item.author?.toLowerCase().includes(q) ?? false) || + (skill?.displayName.toLowerCase().includes(q) ?? false) ) }) }) @@ -116,17 +123,28 @@ export const MarketplaceListView = (props: Props) => { 0} fallback={

{props.emptyMessage}

}>
- {(item) => ( - {(tag) => {tag}}} - /> - )} + {(item) => { + const skill = item.type === "skill" ? (item as SkillMarketplaceItem) : undefined + const mcp = item.type === "mcp" ? (item as McpMarketplaceItem) : undefined + return ( + {(tag) => {tag}}} + /> + ) + }}
diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceView.tsx b/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceView.tsx index 7e546aad7b9..6746b0bebb6 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceView.tsx @@ -13,7 +13,6 @@ import type { SkillMarketplaceItem, MarketplaceInstalledMetadata, } from "../../types/marketplace" -import { SkillsMarketplace } from "./SkillsMarketplace" import { MarketplaceListView } from "./MarketplaceListView" import { InstallModal } from "./InstallModal" import { RemoveDialog } from "./RemoveDialog" @@ -169,10 +168,13 @@ export const MarketplaceView = () => { - diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/SkillsMarketplace.tsx b/packages/kilo-vscode/webview-ui/src/components/marketplace/SkillsMarketplace.tsx deleted file mode 100644 index d71a250266e..00000000000 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/SkillsMarketplace.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { createSignal, createMemo, For, Show } from "solid-js" -import { TextField } from "@kilocode/kilo-ui/text-field" -import { RadioGroup } from "@kilocode/kilo-ui/radio-group" -import { Tag } from "@kilocode/kilo-ui/tag" -import { Spinner } from "@kilocode/kilo-ui/spinner" -import type { SkillMarketplaceItem, MarketplaceInstalledMetadata, MarketplaceItem } from "../../types/marketplace" -import { useLanguage } from "../../context/language" -import { ItemCard } from "./ItemCard" - -interface Props { - items: SkillMarketplaceItem[] - metadata: MarketplaceInstalledMetadata - fetching: boolean - onInstall: (item: MarketplaceItem) => void - onRemove: (item: MarketplaceItem, scope: "project" | "global") => void -} - -const ALL = "__all__" - -export const SkillsMarketplace = (props: Props) => { - const { t } = useLanguage() - const [search, setSearch] = createSignal("") - const [category, setCategory] = createSignal(ALL) - - const categories = createMemo(() => { - const set = new Set() - for (const item of props.items) { - if (item.displayCategory) set.add(item.displayCategory) - } - return [ALL, ...Array.from(set).sort()] - }) - - const filtered = createMemo(() => { - const q = search().toLowerCase() - const cat = category() - return props.items.filter((item) => { - if (cat !== ALL && item.displayCategory !== cat) return false - if (!q) return true - return ( - item.id.toLowerCase().includes(q) || - item.name.toLowerCase().includes(q) || - item.displayName.toLowerCase().includes(q) || - item.description.toLowerCase().includes(q) || - item.displayCategory.toLowerCase().includes(q) - ) - }) - }) - - return ( -
-
- -
-
- c} - label={(c: string) => (c === ALL ? t("marketplace.category.all") : c)} - onSelect={(v: string | undefined) => v && setCategory(v)} - /> -
- }> - 0} fallback={

{t("marketplace.empty.skills")}

}> -
- - {(item) => ( - {item.displayCategory}} - /> - )} - -
-
-
-
- ) -} From 785b37883a574b8720681641e7a73bc11e13a485 Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 17 Mar 2026 10:46:10 +0100 Subject: [PATCH 41/74] fix(vscode): only show MCP tags used by at least 2 items --- .../src/components/marketplace/MarketplaceListView.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceListView.tsx b/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceListView.tsx index cfb0d174692..ee1f07b0486 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceListView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceListView.tsx @@ -47,11 +47,15 @@ export const MarketplaceListView = (props: Props) => { } const allTags = createMemo(() => { - const set = new Set() + const counts = new Map() for (const item of props.items) { - for (const tag of tagsFor(item)) set.add(tag) + for (const tag of tagsFor(item)) counts.set(tag, (counts.get(tag) ?? 0) + 1) } - return Array.from(set).sort() + const min = props.type === "mcp" ? 2 : 1 + return Array.from(counts.entries()) + .filter(([, n]) => n >= min) + .map(([tag]) => tag) + .sort() }) const toggleTag = (tag: string) => { From 02e7ddfd171515f8abcccae439d67f500a32a9e6 Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 17 Mar 2026 10:54:58 +0100 Subject: [PATCH 42/74] fix(i18n): use generic search/empty keys for all marketplace tabs The MCP and Modes tabs referenced non-existent per-type i18n keys (marketplace.search.mcp, marketplace.empty.modes, etc.), causing raw key names to render as placeholder text. Switch all three tabs to the generic marketplace.search / marketplace.empty keys and remove the now-unused marketplace.search.skills / marketplace.empty.skills keys from all 16 locales. --- packages/kilo-i18n/src/ar.ts | 2 -- packages/kilo-i18n/src/br.ts | 2 -- packages/kilo-i18n/src/bs.ts | 2 -- packages/kilo-i18n/src/da.ts | 2 -- packages/kilo-i18n/src/de.ts | 2 -- packages/kilo-i18n/src/en.ts | 2 -- packages/kilo-i18n/src/es.ts | 2 -- packages/kilo-i18n/src/fr.ts | 2 -- packages/kilo-i18n/src/ja.ts | 2 -- packages/kilo-i18n/src/ko.ts | 2 -- packages/kilo-i18n/src/no.ts | 2 -- packages/kilo-i18n/src/pl.ts | 2 -- packages/kilo-i18n/src/ru.ts | 2 -- packages/kilo-i18n/src/th.ts | 2 -- packages/kilo-i18n/src/zh.ts | 2 -- packages/kilo-i18n/src/zht.ts | 2 -- .../src/components/marketplace/MarketplaceView.tsx | 12 ++++++------ 17 files changed, 6 insertions(+), 38 deletions(-) diff --git a/packages/kilo-i18n/src/ar.ts b/packages/kilo-i18n/src/ar.ts index 6bd89a624c6..935a63b24d6 100644 --- a/packages/kilo-i18n/src/ar.ts +++ b/packages/kilo-i18n/src/ar.ts @@ -19,9 +19,7 @@ export const dict = { "marketplace.tab.skills": "المهارات", "marketplace.tab.mcpServers": "خوادم MCP", "marketplace.tab.modes": "الأوضاع", - "marketplace.search.skills": "البحث في المهارات...", "marketplace.category.all": "الكل", - "marketplace.empty.skills": "لم يتم العثور على مهارات", "marketplace.placeholder": "سيتم تنفيذه لاحقاً", "marketplace.card.installed": "مثبت", "marketplace.card.install": "تثبيت", diff --git a/packages/kilo-i18n/src/br.ts b/packages/kilo-i18n/src/br.ts index 6acda4e7942..9714a6e3c65 100644 --- a/packages/kilo-i18n/src/br.ts +++ b/packages/kilo-i18n/src/br.ts @@ -19,9 +19,7 @@ export const dict = { "marketplace.tab.skills": "Skills", "marketplace.tab.mcpServers": "Servidores MCP", "marketplace.tab.modes": "Modos", - "marketplace.search.skills": "Buscar skills...", "marketplace.category.all": "Todos", - "marketplace.empty.skills": "Nenhuma skill encontrada", "marketplace.placeholder": "A ser implementado", "marketplace.card.installed": "Instalado", "marketplace.card.install": "Instalar", diff --git a/packages/kilo-i18n/src/bs.ts b/packages/kilo-i18n/src/bs.ts index a2a8be0ed1f..f225ff43beb 100644 --- a/packages/kilo-i18n/src/bs.ts +++ b/packages/kilo-i18n/src/bs.ts @@ -24,9 +24,7 @@ export const dict = { "marketplace.tab.skills": "Vještine", "marketplace.tab.mcpServers": "MCP Serveri", "marketplace.tab.modes": "Modovi", - "marketplace.search.skills": "Pretraži vještine...", "marketplace.category.all": "Sve", - "marketplace.empty.skills": "Nema pronađenih vještina", "marketplace.placeholder": "Biće implementirano", "marketplace.card.installed": "Instalirano", "marketplace.card.install": "Instaliraj", diff --git a/packages/kilo-i18n/src/da.ts b/packages/kilo-i18n/src/da.ts index 8c5ff923725..35e7bda83f2 100644 --- a/packages/kilo-i18n/src/da.ts +++ b/packages/kilo-i18n/src/da.ts @@ -19,9 +19,7 @@ export const dict = { "marketplace.tab.skills": "Skills", "marketplace.tab.mcpServers": "MCP-servere", "marketplace.tab.modes": "Tilstande", - "marketplace.search.skills": "Søg efter skills...", "marketplace.category.all": "Alle", - "marketplace.empty.skills": "Ingen skills fundet", "marketplace.placeholder": "Skal implementeres", "marketplace.card.installed": "Installeret", "marketplace.card.install": "Installer", diff --git a/packages/kilo-i18n/src/de.ts b/packages/kilo-i18n/src/de.ts index 0ee1c8bd0c7..fed983ecbaf 100644 --- a/packages/kilo-i18n/src/de.ts +++ b/packages/kilo-i18n/src/de.ts @@ -19,9 +19,7 @@ export const dict = { "marketplace.tab.skills": "Skills", "marketplace.tab.mcpServers": "MCP-Server", "marketplace.tab.modes": "Modi", - "marketplace.search.skills": "Skills suchen...", "marketplace.category.all": "Alle", - "marketplace.empty.skills": "Keine Skills gefunden", "marketplace.placeholder": "Noch nicht implementiert", "marketplace.card.installed": "Installiert", "marketplace.card.install": "Installieren", diff --git a/packages/kilo-i18n/src/en.ts b/packages/kilo-i18n/src/en.ts index d8ae2dee47d..e1726bb2881 100644 --- a/packages/kilo-i18n/src/en.ts +++ b/packages/kilo-i18n/src/en.ts @@ -21,9 +21,7 @@ export const dict = { "marketplace.tab.skills": "Skills", "marketplace.tab.mcpServers": "MCP Servers", "marketplace.tab.modes": "Modes", - "marketplace.search.skills": "Search skills...", "marketplace.category.all": "All", - "marketplace.empty.skills": "No skills found", "marketplace.placeholder": "To be implemented", "marketplace.card.installed": "Installed", "marketplace.card.install": "Install", diff --git a/packages/kilo-i18n/src/es.ts b/packages/kilo-i18n/src/es.ts index 570e93f2672..2560cd4cf50 100644 --- a/packages/kilo-i18n/src/es.ts +++ b/packages/kilo-i18n/src/es.ts @@ -19,9 +19,7 @@ export const dict = { "marketplace.tab.skills": "Skills", "marketplace.tab.mcpServers": "Servidores MCP", "marketplace.tab.modes": "Modos", - "marketplace.search.skills": "Buscar skills...", "marketplace.category.all": "Todos", - "marketplace.empty.skills": "No se encontraron skills", "marketplace.placeholder": "Por implementar", "marketplace.card.installed": "Instalado", "marketplace.card.install": "Instalar", diff --git a/packages/kilo-i18n/src/fr.ts b/packages/kilo-i18n/src/fr.ts index 545f324f9e8..00bbb156e38 100644 --- a/packages/kilo-i18n/src/fr.ts +++ b/packages/kilo-i18n/src/fr.ts @@ -19,9 +19,7 @@ export const dict = { "marketplace.tab.skills": "Skills", "marketplace.tab.mcpServers": "Serveurs MCP", "marketplace.tab.modes": "Modes", - "marketplace.search.skills": "Rechercher des skills...", "marketplace.category.all": "Tout", - "marketplace.empty.skills": "Aucune skill trouvée", "marketplace.placeholder": "À implémenter", "marketplace.card.installed": "Installé", "marketplace.card.install": "Installer", diff --git a/packages/kilo-i18n/src/ja.ts b/packages/kilo-i18n/src/ja.ts index c368f713a5e..e7841a7afd6 100644 --- a/packages/kilo-i18n/src/ja.ts +++ b/packages/kilo-i18n/src/ja.ts @@ -18,9 +18,7 @@ export const dict = { "marketplace.tab.skills": "スキル", "marketplace.tab.mcpServers": "MCPサーバー", "marketplace.tab.modes": "モード", - "marketplace.search.skills": "スキルを検索...", "marketplace.category.all": "すべて", - "marketplace.empty.skills": "スキルが見つかりません", "marketplace.placeholder": "未実装", "marketplace.card.installed": "インストール済み", "marketplace.card.install": "インストール", diff --git a/packages/kilo-i18n/src/ko.ts b/packages/kilo-i18n/src/ko.ts index 857dd478ea6..3dd65e72189 100644 --- a/packages/kilo-i18n/src/ko.ts +++ b/packages/kilo-i18n/src/ko.ts @@ -18,9 +18,7 @@ export const dict = { "marketplace.tab.skills": "스킬", "marketplace.tab.mcpServers": "MCP 서버", "marketplace.tab.modes": "모드", - "marketplace.search.skills": "스킬 검색...", "marketplace.category.all": "전체", - "marketplace.empty.skills": "스킬을 찾을 수 없습니다", "marketplace.placeholder": "구현 예정", "marketplace.card.installed": "설치됨", "marketplace.card.install": "설치", diff --git a/packages/kilo-i18n/src/no.ts b/packages/kilo-i18n/src/no.ts index 6878d64b0c3..449f8749ec0 100644 --- a/packages/kilo-i18n/src/no.ts +++ b/packages/kilo-i18n/src/no.ts @@ -19,9 +19,7 @@ export const dict = { "marketplace.tab.skills": "Skills", "marketplace.tab.mcpServers": "MCP-servere", "marketplace.tab.modes": "Moduser", - "marketplace.search.skills": "Søk etter skills...", "marketplace.category.all": "Alle", - "marketplace.empty.skills": "Ingen skills funnet", "marketplace.placeholder": "Skal implementeres", "marketplace.card.installed": "Installert", "marketplace.card.install": "Installer", diff --git a/packages/kilo-i18n/src/pl.ts b/packages/kilo-i18n/src/pl.ts index 1cc4c5f420d..66a0c50c758 100644 --- a/packages/kilo-i18n/src/pl.ts +++ b/packages/kilo-i18n/src/pl.ts @@ -19,9 +19,7 @@ export const dict = { "marketplace.tab.skills": "Skills", "marketplace.tab.mcpServers": "Serwery MCP", "marketplace.tab.modes": "Tryby", - "marketplace.search.skills": "Szukaj skills...", "marketplace.category.all": "Wszystkie", - "marketplace.empty.skills": "Nie znaleziono skills", "marketplace.placeholder": "Do zaimplementowania", "marketplace.card.installed": "Zainstalowano", "marketplace.card.install": "Zainstaluj", diff --git a/packages/kilo-i18n/src/ru.ts b/packages/kilo-i18n/src/ru.ts index cf8a06e54a0..22936a6283d 100644 --- a/packages/kilo-i18n/src/ru.ts +++ b/packages/kilo-i18n/src/ru.ts @@ -19,9 +19,7 @@ export const dict = { "marketplace.tab.skills": "Навыки", "marketplace.tab.mcpServers": "MCP-серверы", "marketplace.tab.modes": "Режимы", - "marketplace.search.skills": "Поиск навыков...", "marketplace.category.all": "Все", - "marketplace.empty.skills": "Навыки не найдены", "marketplace.placeholder": "Будет реализовано", "marketplace.card.installed": "Установлено", "marketplace.card.install": "Установить", diff --git a/packages/kilo-i18n/src/th.ts b/packages/kilo-i18n/src/th.ts index 196fbbecca9..5fea8e60398 100644 --- a/packages/kilo-i18n/src/th.ts +++ b/packages/kilo-i18n/src/th.ts @@ -19,9 +19,7 @@ export const dict = { "marketplace.tab.skills": "ทักษะ", "marketplace.tab.mcpServers": "เซิร์ฟเวอร์ MCP", "marketplace.tab.modes": "โหมด", - "marketplace.search.skills": "ค้นหาทักษะ...", "marketplace.category.all": "ทั้งหมด", - "marketplace.empty.skills": "ไม่พบทักษะ", "marketplace.placeholder": "ยังไม่ได้ดำเนินการ", "marketplace.card.installed": "ติดตั้งแล้ว", "marketplace.card.install": "ติดตั้ง", diff --git a/packages/kilo-i18n/src/zh.ts b/packages/kilo-i18n/src/zh.ts index 3ddf2b8c988..034435c970a 100644 --- a/packages/kilo-i18n/src/zh.ts +++ b/packages/kilo-i18n/src/zh.ts @@ -17,9 +17,7 @@ export const dict = { "marketplace.tab.skills": "技能", "marketplace.tab.mcpServers": "MCP 服务器", "marketplace.tab.modes": "模式", - "marketplace.search.skills": "搜索技能...", "marketplace.category.all": "全部", - "marketplace.empty.skills": "未找到技能", "marketplace.placeholder": "待实现", "marketplace.card.installed": "已安装", "marketplace.card.install": "安装", diff --git a/packages/kilo-i18n/src/zht.ts b/packages/kilo-i18n/src/zht.ts index ff74dab6e2f..09404c94306 100644 --- a/packages/kilo-i18n/src/zht.ts +++ b/packages/kilo-i18n/src/zht.ts @@ -17,9 +17,7 @@ export const dict = { "marketplace.tab.skills": "技能", "marketplace.tab.mcpServers": "MCP 伺服器", "marketplace.tab.modes": "模式", - "marketplace.search.skills": "搜尋技能...", "marketplace.category.all": "全部", - "marketplace.empty.skills": "找不到技能", "marketplace.placeholder": "待實作", "marketplace.card.installed": "已安裝", "marketplace.card.install": "安裝", diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceView.tsx b/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceView.tsx index 6746b0bebb6..733e1e62d8b 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceView.tsx @@ -147,8 +147,8 @@ export const MarketplaceView = () => { metadata={metadata()} fetching={fetching()} type="mcp" - searchPlaceholder={t("marketplace.search.mcp")} - emptyMessage={t("marketplace.empty.mcp")} + searchPlaceholder={t("marketplace.search")} + emptyMessage={t("marketplace.empty")} onInstall={handleInstall} onRemove={handleRemove} /> @@ -160,8 +160,8 @@ export const MarketplaceView = () => { metadata={metadata()} fetching={fetching()} type="mode" - searchPlaceholder={t("marketplace.search.modes")} - emptyMessage={t("marketplace.empty.modes")} + searchPlaceholder={t("marketplace.search")} + emptyMessage={t("marketplace.empty")} onInstall={handleInstall} onRemove={handleRemove} /> @@ -173,8 +173,8 @@ export const MarketplaceView = () => { metadata={metadata()} fetching={fetching()} type="skill" - searchPlaceholder={t("marketplace.search.skills")} - emptyMessage={t("marketplace.empty.skills")} + searchPlaceholder={t("marketplace.search")} + emptyMessage={t("marketplace.empty")} onInstall={handleInstall} onRemove={handleRemove} /> From c038bcfb148da7f38f63d83252ec2edccaf9107d Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 17 Mar 2026 11:05:21 +0100 Subject: [PATCH 43/74] fix(vscode): raise MCP tag threshold to 5 to reduce tag clutter --- .../src/components/marketplace/MarketplaceListView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceListView.tsx b/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceListView.tsx index ee1f07b0486..15651a20e63 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceListView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceListView.tsx @@ -51,7 +51,7 @@ export const MarketplaceListView = (props: Props) => { for (const item of props.items) { for (const tag of tagsFor(item)) counts.set(tag, (counts.get(tag) ?? 0) + 1) } - const min = props.type === "mcp" ? 2 : 1 + const min = props.type === "mcp" ? 5 : 1 return Array.from(counts.entries()) .filter(([, n]) => n >= min) .map(([tag]) => tag) From 13341256f6ef81ff90d9bd904c1798a5ff8f5187 Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 17 Mar 2026 11:23:02 +0100 Subject: [PATCH 44/74] fix(vscode): cap marketplace card width with responsive auto-fill grid --- .../components/marketplace/marketplace.css | 9 ++----- .../src/stories/marketplace.stories.tsx | 27 ++++++++++++++----- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/marketplace.css b/packages/kilo-vscode/webview-ui/src/components/marketplace/marketplace.css index 5d958d7fdd2..c81470a5260 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/marketplace.css +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/marketplace.css @@ -86,14 +86,9 @@ .marketplace-grid { display: grid; - grid-template-columns: 1fr; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; -} - -@media (min-width: 500px) { - .marketplace-grid { - grid-template-columns: repeat(2, 1fr); - } + max-width: 1200px; } /* Card */ diff --git a/packages/kilo-vscode/webview-ui/src/stories/marketplace.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/marketplace.stories.tsx index 253d7b99018..c6c30fb9528 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/marketplace.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/marketplace.stories.tsx @@ -2,14 +2,14 @@ /** * Stories for Marketplace components. * - * Renders SkillsMarketplace and ItemCard directly with mock data + * Renders MarketplaceListView and ItemCard directly with mock data * so no API requests are made. */ import type { Meta, StoryObj } from "storybook-solidjs-vite" import { StoryProviders } from "./StoryProviders" -import SkillsMarketplace from "../components/marketplace/SkillsMarketplace" -import ItemCard from "../components/marketplace/ItemCard" +import { MarketplaceListView } from "../components/marketplace/MarketplaceListView" +import { ItemCard } from "../components/marketplace/ItemCard" import type { SkillMarketplaceItem, MarketplaceInstalledMetadata } from "../types/marketplace" import "../components/marketplace/marketplace.css" @@ -116,10 +116,13 @@ export const SkillsTabWithItems: Story = { render: () => (
- @@ -133,10 +136,13 @@ export const SkillsTabWithInstalled: Story = { render: () => (
- @@ -150,7 +156,16 @@ export const SkillsTabEmpty: Story = { render: () => (
- +
), From 60c6cbab692aca9d775e08bfcef20f495e000225 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 17 Mar 2026 10:39:42 +0000 Subject: [PATCH 45/74] chore: update kilo-vscode visual regression baselines --- .../marketplace/installed-skill-card-chromium-linux.png | 4 ++-- .../marketplace/skills-tab-with-installed-chromium-linux.png | 4 ++-- .../marketplace/skills-tab-with-items-chromium-linux.png | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/marketplace/installed-skill-card-chromium-linux.png b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/marketplace/installed-skill-card-chromium-linux.png index 2822662560f..86b45cb700e 100644 --- a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/marketplace/installed-skill-card-chromium-linux.png +++ b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/marketplace/installed-skill-card-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7bc9274e748f94e0ae979f688a87c52b9a3d579ac015a2a49f327f8c96d002db -size 11116 +oid sha256:0d5400d9db070953fcbf4cc3f8a4968b80c5bccb1a67ee63a463aa0539de1f41 +size 11190 diff --git a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/marketplace/skills-tab-with-installed-chromium-linux.png b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/marketplace/skills-tab-with-installed-chromium-linux.png index 204b46b36ef..b7d88719025 100644 --- a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/marketplace/skills-tab-with-installed-chromium-linux.png +++ b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/marketplace/skills-tab-with-installed-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be90b297cbdef33fa1c3c67be7a42e4c00a5f3d68147a5fbf54a04ab7b261d04 -size 52330 +oid sha256:782c1fae4bed19825b56570e573d09cc80aa2180ca5c6a4c495867f207d95beb +size 54728 diff --git a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/marketplace/skills-tab-with-items-chromium-linux.png b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/marketplace/skills-tab-with-items-chromium-linux.png index 1bda64ef203..899e1a2c4ad 100644 --- a/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/marketplace/skills-tab-with-items-chromium-linux.png +++ b/packages/kilo-vscode/tests/visual-regression.spec.ts-snapshots/marketplace/skills-tab-with-items-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d27dc2c85a377f7daab0b7de444a7d883a4503ab45c92c0c0a9155de89e2a221 -size 49775 +oid sha256:3d309302b177f97f198a2b889c96d08cf4578d219043d48a9d0251460be411c4 +size 52039 From e8575fbf02d9298b4222a31e85960b90b0ec62ef Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 17 Mar 2026 11:44:20 +0100 Subject: [PATCH 46/74] feat(vscode): increase marketplace card size and add expandable descriptions --- packages/kilo-i18n/src/ar.ts | 2 ++ packages/kilo-i18n/src/br.ts | 2 ++ packages/kilo-i18n/src/bs.ts | 2 ++ packages/kilo-i18n/src/da.ts | 2 ++ packages/kilo-i18n/src/de.ts | 2 ++ packages/kilo-i18n/src/en.ts | 2 ++ packages/kilo-i18n/src/es.ts | 2 ++ packages/kilo-i18n/src/fr.ts | 2 ++ packages/kilo-i18n/src/ja.ts | 2 ++ packages/kilo-i18n/src/ko.ts | 2 ++ packages/kilo-i18n/src/no.ts | 2 ++ packages/kilo-i18n/src/pl.ts | 2 ++ packages/kilo-i18n/src/ru.ts | 2 ++ packages/kilo-i18n/src/th.ts | 2 ++ packages/kilo-i18n/src/tr.ts | 4 ++++ packages/kilo-i18n/src/zh.ts | 2 ++ packages/kilo-i18n/src/zht.ts | 2 ++ .../src/components/marketplace/ItemCard.tsx | 18 ++++++++++++++++-- .../src/components/marketplace/marketplace.css | 18 +++++++++++++++++- 19 files changed, 69 insertions(+), 3 deletions(-) diff --git a/packages/kilo-i18n/src/ar.ts b/packages/kilo-i18n/src/ar.ts index 935a63b24d6..268cac7ca7d 100644 --- a/packages/kilo-i18n/src/ar.ts +++ b/packages/kilo-i18n/src/ar.ts @@ -25,6 +25,8 @@ export const dict = { "marketplace.card.install": "تثبيت", "marketplace.card.remove": "إزالة", "marketplace.card.removeScope": "إزالة ({{scope}})", + "marketplace.card.showMore": "عرض المزيد", + "marketplace.card.showLess": "عرض أقل", "marketplace.install.title": "تثبيت {{name}}", "marketplace.install.scope": "النطاق", "marketplace.install.scope.project": "المشروع", diff --git a/packages/kilo-i18n/src/br.ts b/packages/kilo-i18n/src/br.ts index 9714a6e3c65..577971ee9a4 100644 --- a/packages/kilo-i18n/src/br.ts +++ b/packages/kilo-i18n/src/br.ts @@ -25,6 +25,8 @@ export const dict = { "marketplace.card.install": "Instalar", "marketplace.card.remove": "Remover", "marketplace.card.removeScope": "Remover ({{scope}})", + "marketplace.card.showMore": "Mostrar mais", + "marketplace.card.showLess": "Mostrar menos", "marketplace.install.title": "Instalar {{name}}", "marketplace.install.scope": "Escopo", "marketplace.install.scope.project": "Projeto", diff --git a/packages/kilo-i18n/src/bs.ts b/packages/kilo-i18n/src/bs.ts index f225ff43beb..2a816969422 100644 --- a/packages/kilo-i18n/src/bs.ts +++ b/packages/kilo-i18n/src/bs.ts @@ -30,6 +30,8 @@ export const dict = { "marketplace.card.install": "Instaliraj", "marketplace.card.remove": "Ukloni", "marketplace.card.removeScope": "Ukloni ({{scope}})", + "marketplace.card.showMore": "Prikaži više", + "marketplace.card.showLess": "Prikaži manje", "marketplace.install.title": "Instaliraj {{name}}", "marketplace.install.scope": "Opseg", "marketplace.install.scope.project": "Projekat", diff --git a/packages/kilo-i18n/src/da.ts b/packages/kilo-i18n/src/da.ts index 35e7bda83f2..c0230f60270 100644 --- a/packages/kilo-i18n/src/da.ts +++ b/packages/kilo-i18n/src/da.ts @@ -25,6 +25,8 @@ export const dict = { "marketplace.card.install": "Installer", "marketplace.card.remove": "Fjern", "marketplace.card.removeScope": "Fjern ({{scope}})", + "marketplace.card.showMore": "Vis mere", + "marketplace.card.showLess": "Vis mindre", "marketplace.install.title": "Installer {{name}}", "marketplace.install.scope": "Omfang", "marketplace.install.scope.project": "Projekt", diff --git a/packages/kilo-i18n/src/de.ts b/packages/kilo-i18n/src/de.ts index fed983ecbaf..0484fbaf44a 100644 --- a/packages/kilo-i18n/src/de.ts +++ b/packages/kilo-i18n/src/de.ts @@ -25,6 +25,8 @@ export const dict = { "marketplace.card.install": "Installieren", "marketplace.card.remove": "Entfernen", "marketplace.card.removeScope": "Entfernen ({{scope}})", + "marketplace.card.showMore": "Mehr anzeigen", + "marketplace.card.showLess": "Weniger anzeigen", "marketplace.install.title": "{{name}} installieren", "marketplace.install.scope": "Bereich", "marketplace.install.scope.project": "Projekt", diff --git a/packages/kilo-i18n/src/en.ts b/packages/kilo-i18n/src/en.ts index e1726bb2881..4f306eab419 100644 --- a/packages/kilo-i18n/src/en.ts +++ b/packages/kilo-i18n/src/en.ts @@ -27,6 +27,8 @@ export const dict = { "marketplace.card.install": "Install", "marketplace.card.remove": "Remove", "marketplace.card.removeScope": "Remove ({{scope}})", + "marketplace.card.showMore": "Show more", + "marketplace.card.showLess": "Show less", "marketplace.install.title": "Install {{name}}", "marketplace.install.scope": "Scope", "marketplace.install.scope.project": "Project", diff --git a/packages/kilo-i18n/src/es.ts b/packages/kilo-i18n/src/es.ts index 2560cd4cf50..2fbbe34051e 100644 --- a/packages/kilo-i18n/src/es.ts +++ b/packages/kilo-i18n/src/es.ts @@ -25,6 +25,8 @@ export const dict = { "marketplace.card.install": "Instalar", "marketplace.card.remove": "Eliminar", "marketplace.card.removeScope": "Eliminar ({{scope}})", + "marketplace.card.showMore": "Mostrar más", + "marketplace.card.showLess": "Mostrar menos", "marketplace.install.title": "Instalar {{name}}", "marketplace.install.scope": "Alcance", "marketplace.install.scope.project": "Proyecto", diff --git a/packages/kilo-i18n/src/fr.ts b/packages/kilo-i18n/src/fr.ts index 00bbb156e38..d71544eaaf6 100644 --- a/packages/kilo-i18n/src/fr.ts +++ b/packages/kilo-i18n/src/fr.ts @@ -25,6 +25,8 @@ export const dict = { "marketplace.card.install": "Installer", "marketplace.card.remove": "Supprimer", "marketplace.card.removeScope": "Supprimer ({{scope}})", + "marketplace.card.showMore": "Afficher plus", + "marketplace.card.showLess": "Afficher moins", "marketplace.install.title": "Installer {{name}}", "marketplace.install.scope": "Portée", "marketplace.install.scope.project": "Projet", diff --git a/packages/kilo-i18n/src/ja.ts b/packages/kilo-i18n/src/ja.ts index e7841a7afd6..ac16c110b31 100644 --- a/packages/kilo-i18n/src/ja.ts +++ b/packages/kilo-i18n/src/ja.ts @@ -24,6 +24,8 @@ export const dict = { "marketplace.card.install": "インストール", "marketplace.card.remove": "削除", "marketplace.card.removeScope": "削除 ({{scope}})", + "marketplace.card.showMore": "もっと見る", + "marketplace.card.showLess": "折りたたむ", "marketplace.install.title": "{{name}} のインストール", "marketplace.install.scope": "スコープ", "marketplace.install.scope.project": "プロジェクト", diff --git a/packages/kilo-i18n/src/ko.ts b/packages/kilo-i18n/src/ko.ts index 3dd65e72189..4c4d3683b80 100644 --- a/packages/kilo-i18n/src/ko.ts +++ b/packages/kilo-i18n/src/ko.ts @@ -24,6 +24,8 @@ export const dict = { "marketplace.card.install": "설치", "marketplace.card.remove": "제거", "marketplace.card.removeScope": "제거 ({{scope}})", + "marketplace.card.showMore": "더 보기", + "marketplace.card.showLess": "접기", "marketplace.install.title": "{{name}} 설치", "marketplace.install.scope": "범위", "marketplace.install.scope.project": "프로젝트", diff --git a/packages/kilo-i18n/src/no.ts b/packages/kilo-i18n/src/no.ts index 449f8749ec0..82ae6fe678c 100644 --- a/packages/kilo-i18n/src/no.ts +++ b/packages/kilo-i18n/src/no.ts @@ -25,6 +25,8 @@ export const dict = { "marketplace.card.install": "Installer", "marketplace.card.remove": "Fjern", "marketplace.card.removeScope": "Fjern ({{scope}})", + "marketplace.card.showMore": "Vis mer", + "marketplace.card.showLess": "Vis mindre", "marketplace.install.title": "Installer {{name}}", "marketplace.install.scope": "Omfang", "marketplace.install.scope.project": "Prosjekt", diff --git a/packages/kilo-i18n/src/pl.ts b/packages/kilo-i18n/src/pl.ts index 66a0c50c758..f80fd5635c5 100644 --- a/packages/kilo-i18n/src/pl.ts +++ b/packages/kilo-i18n/src/pl.ts @@ -25,6 +25,8 @@ export const dict = { "marketplace.card.install": "Zainstaluj", "marketplace.card.remove": "Usuń", "marketplace.card.removeScope": "Usuń ({{scope}})", + "marketplace.card.showMore": "Pokaż więcej", + "marketplace.card.showLess": "Pokaż mniej", "marketplace.install.title": "Instalacja: {{name}}", "marketplace.install.scope": "Zakres", "marketplace.install.scope.project": "Projekt", diff --git a/packages/kilo-i18n/src/ru.ts b/packages/kilo-i18n/src/ru.ts index 22936a6283d..cb105f7f619 100644 --- a/packages/kilo-i18n/src/ru.ts +++ b/packages/kilo-i18n/src/ru.ts @@ -25,6 +25,8 @@ export const dict = { "marketplace.card.install": "Установить", "marketplace.card.remove": "Удалить", "marketplace.card.removeScope": "Удалить ({{scope}})", + "marketplace.card.showMore": "Показать больше", + "marketplace.card.showLess": "Показать меньше", "marketplace.install.title": "Установка {{name}}", "marketplace.install.scope": "Область", "marketplace.install.scope.project": "Проект", diff --git a/packages/kilo-i18n/src/th.ts b/packages/kilo-i18n/src/th.ts index 5fea8e60398..fff39198b1b 100644 --- a/packages/kilo-i18n/src/th.ts +++ b/packages/kilo-i18n/src/th.ts @@ -25,6 +25,8 @@ export const dict = { "marketplace.card.install": "ติดตั้ง", "marketplace.card.remove": "ลบ", "marketplace.card.removeScope": "ลบ ({{scope}})", + "marketplace.card.showMore": "แสดงเพิ่มเติม", + "marketplace.card.showLess": "แสดงน้อยลง", "marketplace.install.title": "ติดตั้ง {{name}}", "marketplace.install.scope": "ขอบเขต", "marketplace.install.scope.project": "โปรเจกต์", diff --git a/packages/kilo-i18n/src/tr.ts b/packages/kilo-i18n/src/tr.ts index 1b4c19c6fda..13c58a73cf8 100644 --- a/packages/kilo-i18n/src/tr.ts +++ b/packages/kilo-i18n/src/tr.ts @@ -14,4 +14,8 @@ export const dict = { // Reasoning block label "ui.reasoning.label": "Akıl Yürütme", + + // Marketplace card translations + "marketplace.card.showMore": "Daha fazla göster", + "marketplace.card.showLess": "Daha az göster", } diff --git a/packages/kilo-i18n/src/zh.ts b/packages/kilo-i18n/src/zh.ts index 034435c970a..eee8c336df0 100644 --- a/packages/kilo-i18n/src/zh.ts +++ b/packages/kilo-i18n/src/zh.ts @@ -23,6 +23,8 @@ export const dict = { "marketplace.card.install": "安装", "marketplace.card.remove": "移除", "marketplace.card.removeScope": "移除 ({{scope}})", + "marketplace.card.showMore": "展开", + "marketplace.card.showLess": "收起", "marketplace.install.title": "安装 {{name}}", "marketplace.install.scope": "作用域", "marketplace.install.scope.project": "项目", diff --git a/packages/kilo-i18n/src/zht.ts b/packages/kilo-i18n/src/zht.ts index 09404c94306..1fcb8749fb9 100644 --- a/packages/kilo-i18n/src/zht.ts +++ b/packages/kilo-i18n/src/zht.ts @@ -23,6 +23,8 @@ export const dict = { "marketplace.card.install": "安裝", "marketplace.card.remove": "移除", "marketplace.card.removeScope": "移除 ({{scope}})", + "marketplace.card.showMore": "展開", + "marketplace.card.showLess": "收起", "marketplace.install.title": "安裝 {{name}}", "marketplace.install.scope": "作用域", "marketplace.install.scope.project": "專案", diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/ItemCard.tsx b/packages/kilo-vscode/webview-ui/src/components/marketplace/ItemCard.tsx index 96dfc2c5f78..2c18fee39a8 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/ItemCard.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/ItemCard.tsx @@ -1,4 +1,4 @@ -import { Show, JSX } from "solid-js" +import { Show, JSX, createSignal, onMount } from "solid-js" import { Button } from "@kilocode/kilo-ui/button" import { Card } from "@kilocode/kilo-ui/card" import { Tag } from "@kilocode/kilo-ui/tag" @@ -24,6 +24,13 @@ export const ItemCard = (props: Props) => { const scopes = () => installedScopes(props.item.id, props.item.type, props.metadata) const installed = () => scopes().length > 0 const name = () => props.displayName ?? props.item.name + const [expanded, setExpanded] = createSignal(false) + const [clamped, setClamped] = createSignal(false) + let ref: HTMLParagraphElement | undefined + + onMount(() => { + if (ref && ref.scrollHeight > ref.clientHeight) setClamped(true) + }) const openLink = (url: string) => { vscode.postMessage({ type: "openExternal", url }) @@ -52,7 +59,14 @@ export const ItemCard = (props: Props) => {
-

{props.item.description}

+

+ {props.item.description} +

+ + + diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/RevertBanner.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/RevertBanner.tsx new file mode 100644 index 00000000000..fe23f966b0b --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/components/chat/RevertBanner.tsx @@ -0,0 +1,74 @@ +/** + * RevertBanner component + * Shows when the session is in a reverted state, displaying the number of + * reverted messages, per-file diff stats, and Redo / Redo All actions. + */ + +import { Component, For, Show } from "solid-js" +import { Button } from "@kilocode/kilo-ui/button" +import { Icon } from "@kilocode/kilo-ui/icon" +import { DiffChanges } from "@kilocode/kilo-ui/diff-changes" +import { useSession } from "../../context/session" +import { useLanguage } from "../../context/language" + +export const RevertBanner: Component = () => { + const session = useSession() + const language = useLanguage() + + const info = () => session.revert() + const count = () => session.revertedCount() + const diffs = () => session.summary()?.diffs + + const users = () => session.userMessages() + + const handleRedo = () => { + const boundary = info()?.messageID + if (!boundary) return + const next = users().find((m) => m.id > boundary) + if (!next) { + session.unrevertSession() + return + } + session.revertSession(next.id) + } + + return ( + +
+
+
+ + + {count() === 1 + ? language.t("revert.banner.count_one", { count: count() }) + : language.t("revert.banner.count_other", { count: count() })} + +
+
+ + 1}> + + +
+
+ +
+ + {(file) => ( +
+ {file.file} + +
+ )} +
+
+
+
{language.t("revert.banner.hint")}
+
+
+ ) +} diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx index e109f4791d8..c1092f1eaf5 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/VscodeSessionTurn.tsx @@ -29,6 +29,7 @@ import type { } from "@kilocode/sdk/v2" import { ErrorDisplay } from "./ErrorDisplay" import { useServer } from "../../context/server" +import { useSession } from "../../context/session" function getDirectory(path: string): string { const sep = path.includes("/") ? "/" : "\\" @@ -53,6 +54,7 @@ export const VscodeSessionTurn: Component = (props) => { const i18n = useI18n() const diffComponent = useDiffComponent() const server = useServer() + const session = useSession() const emptyMessages: SDKMessage[] = [] const emptyParts: SDKPart[] = [] @@ -168,6 +170,11 @@ export const VscodeSessionTurn: Component = (props) => { parts={parts() as unknown as Parameters[0]["parts"]} interrupted={interrupted()} queued={props.queued} + onRevert={ + assistantMessages().length > 0 && session.status() === "idle" && !session.revert() + ? () => session.revertSession(props.messageID) + : undefined + } />
diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index 091272f4b50..700d31cc41b 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -135,7 +135,14 @@ interface SessionContextValue { currentVariant: () => string | undefined selectVariant: (value: string) => void + // Revert/undo state for the current session + revert: Accessor + revertedCount: Accessor + summary: Accessor + // Actions + revertSession: (messageID: string) => void + unrevertSession: () => void sendMessage: (text: string, providerID?: string, modelID?: string, files?: FileAttachment[]) => void abort: () => void compact: () => void @@ -493,6 +500,10 @@ export const SessionProvider: ParentComponent = (props) => { handleSessionDeleted(message.sessionID) break + case "messageRemoved": + handleMessageRemoved(message.sessionID, message.messageID) + break + case "error": // Only clear loading if the error is for the current session // (or has no sessionID for backwards compatibility) @@ -903,6 +914,18 @@ export const SessionProvider: ParentComponent = (props) => { }) } + // Matches desktop app's event-reducer.ts: message.removed handler. + // Splices the message from the store and deletes its parts. + function handleMessageRemoved(sessionID: string, messageID: string) { + setStore("messages", sessionID, (msgs = []) => msgs.filter((m) => m.id !== messageID)) + setStore( + "parts", + produce((parts) => { + delete parts[messageID] + }), + ) + } + function handleCloudSessionDataLoaded(cloudSessionId: string, title: string, messages: Message[]) { if (cloudPreviewId() !== cloudSessionId) return const key = `cloud:${cloudSessionId}` @@ -1262,6 +1285,47 @@ export const SessionProvider: ParentComponent = (props) => { const userMessages = createMemo(() => messages().filter((m) => m.role === "user")) + const revert = createMemo(() => { + const id = currentSessionID() + // revert can be null (cleared by unrevert) or undefined (never set) — treat both as "no revert" + return id ? (store.sessions[id]?.revert ?? undefined) : undefined + }) + + const revertedCount = createMemo(() => { + const boundary = revert()?.messageID + if (!boundary) return 0 + return userMessages().filter((m) => m.id >= boundary).length + }) + + const summary = createMemo(() => { + const id = currentSessionID() + return id ? (store.sessions[id]?.summary ?? undefined) : undefined + }) + + function revertSession(messageID: string) { + const id = currentSessionID() + if (!id) return + // Restore the reverted user message's prompt text into the input. + // Dispatch as a window message so PromptInput picks it up via onMessage. + const parts = store.parts[messageID] + if (parts) { + const text = parts + .filter((p) => p.type === "text" && !(p as { synthetic?: boolean }).synthetic) + .map((p) => (p as { text: string }).text ?? "") + .join("") + if (text) window.postMessage({ type: "setChatBoxMessage", text }, "*") + } + vscode.postMessage({ type: "revertSession", sessionID: id, messageID }) + } + + function unrevertSession() { + const id = currentSessionID() + if (!id) return + // Clear the prompt input on full redo (matching TUI/desktop behavior) + window.postMessage({ type: "setChatBoxMessage", text: "" }, "*") + vscode.postMessage({ type: "unrevertSession", sessionID: id }) + } + function syncSession(sessionID: string) { vscode.postMessage({ type: "syncSession", sessionID }) } @@ -1360,6 +1424,11 @@ export const SessionProvider: ParentComponent = (props) => { variantList, currentVariant, selectVariant, + revert, + revertedCount, + summary, + revertSession, + unrevertSession, sendMessage, abort, compact, diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index b63df90802a..b064704bcac 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -78,6 +78,14 @@ export const dict = { "command.session.undo.description": "تراجع عن الرسالة الأخيرة", "command.session.redo": "إعادة", "command.session.redo.description": "إعادة الرسالة التي تم التراجع عنها", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "ضغط الجلسة", "command.session.compact.description": "تلخيص الجلسة لتقليل حجم السياق", "command.session.fork": "تشعب من الرسالة", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 5a1681d09d8..fcb2bd1b2d1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -78,6 +78,14 @@ export const dict = { "command.session.undo.description": "Desfazer a última mensagem", "command.session.redo": "Refazer", "command.session.redo.description": "Refazer a última mensagem desfeita", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "Compactar sessão", "command.session.compact.description": "Resumir a sessão para reduzir o tamanho do contexto", "command.session.fork": "Bifurcar da mensagem", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index 1ca43e55e50..b2ff6c5f3b8 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -78,6 +78,14 @@ export const dict = { "command.session.undo.description": "Poništi posljednju poruku", "command.session.redo": "Vrati", "command.session.redo.description": "Vrati posljednju poništenu poruku", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "Sažmi sesiju", "command.session.compact.description": "Sažmi sesiju kako bi se smanjio kontekst", "command.session.fork": "Fork iz poruke", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index 4fe53e1b592..7ebdbb2fac0 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -78,6 +78,14 @@ export const dict = { "command.session.undo.description": "Fortryd den sidste besked", "command.session.redo": "Omgør", "command.session.redo.description": "Omgør den sidste fortrudte besked", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "Komprimér session", "command.session.compact.description": "Opsummer sessionen for at reducere kontekststørrelsen", "command.session.fork": "Forgren fra besked", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 3eb052d0a77..29469221cd8 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -82,6 +82,14 @@ export const dict = { "command.session.undo.description": "Letzte Nachricht rückgängig machen", "command.session.redo": "Wiederherstellen", "command.session.redo.description": "Letzte rückgängig gemachte Nachricht wiederherstellen", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "Sitzung komprimieren", "command.session.compact.description": "Sitzung zusammenfassen, um die Kontextgröße zu reduzieren", "command.session.fork": "Von Nachricht abzweigen", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index eb40f32b141..ae2b5a4340f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -78,6 +78,14 @@ export const dict = { "command.session.undo.description": "Undo the last message", "command.session.redo": "Redo", "command.session.redo.description": "Redo the last undone message", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "Compact session", "command.session.compact.description": "Summarize the session to reduce context size", "command.session.fork": "Fork from message", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index 9f34b9094ea..4a67c75c9d6 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -78,6 +78,14 @@ export const dict = { "command.session.undo.description": "Deshacer el último mensaje", "command.session.redo": "Rehacer", "command.session.redo.description": "Rehacer el último mensaje deshecho", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "Compactar sesión", "command.session.compact.description": "Resumir la sesión para reducir el tamaño del contexto", "command.session.fork": "Bifurcar desde mensaje", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index dabceedd183..b343cb3876a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -79,6 +79,14 @@ export const dict = { "command.session.undo.description": "Annuler le dernier message", "command.session.redo": "Rétablir", "command.session.redo.description": "Rétablir le dernier message annulé", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "Compacter la session", "command.session.compact.description": "Résumer la session pour réduire la taille du contexte", "command.session.fork": "Bifurquer à partir du message", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 2dea7cfde2f..7fdb68e8de9 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -78,6 +78,14 @@ export const dict = { "command.session.undo.description": "最後のメッセージを元に戻す", "command.session.redo": "やり直す", "command.session.redo.description": "元に戻したメッセージをやり直す", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "セッションを圧縮", "command.session.compact.description": "セッションを要約してコンテキストサイズを削減", "command.session.fork": "メッセージからフォーク", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 8c3b8ce5db2..1389d5121ae 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -82,6 +82,14 @@ export const dict = { "command.session.undo.description": "마지막 메시지 실행 취소", "command.session.redo": "다시 실행", "command.session.redo.description": "마지막 실행 취소된 메시지 다시 실행", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "세션 압축", "command.session.compact.description": "컨텍스트 크기를 줄이기 위해 세션 요약", "command.session.fork": "메시지에서 분기", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index bd1981ca990..073781cdac7 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -81,6 +81,14 @@ export const dict = { "command.session.undo.description": "Angre siste melding", "command.session.redo": "Gjør om", "command.session.redo.description": "Gjør om siste angrede melding", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "Komprimer sesjon", "command.session.compact.description": "Oppsummer sesjonen for å redusere kontekststørrelsen", "command.session.fork": "Forgren fra melding", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index be0542a5e45..5af63b6d036 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -78,6 +78,14 @@ export const dict = { "command.session.undo.description": "Cofnij ostatnią wiadomość", "command.session.redo": "Ponów", "command.session.redo.description": "Ponów ostatnią cofniętą wiadomość", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "Kompaktuj sesję", "command.session.compact.description": "Podsumuj sesję, aby zmniejszyć rozmiar kontekstu", "command.session.fork": "Rozwidlij od wiadomości", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index e68ca6e9f6a..4f112c73096 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -78,6 +78,14 @@ export const dict = { "command.session.undo.description": "Отменить последнее сообщение", "command.session.redo": "Повторить", "command.session.redo.description": "Повторить отменённое сообщение", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "Сжать сессию", "command.session.compact.description": "Сократить сессию для уменьшения размера контекста", "command.session.fork": "Создать ответвление", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index 8112e4e2be6..7e974c6bc3c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -78,6 +78,14 @@ export const dict = { "command.session.undo.description": "ยกเลิกข้อความล่าสุด", "command.session.redo": "ทำซ้ำ", "command.session.redo.description": "ทำซ้ำข้อความที่ถูกยกเลิกล่าสุด", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "บีบอัดเซสชัน", "command.session.compact.description": "สรุปเซสชันเพื่อลดขนาดบริบท", "command.session.fork": "แตกแขนงจากข้อความ", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 2d24ab47542..d7c856ae4ee 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -82,6 +82,14 @@ export const dict = { "command.session.undo.description": "撤销上一条消息", "command.session.redo": "重做", "command.session.redo.description": "重做上一条撤销的消息", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "精简会话", "command.session.compact.description": "总结会话以减少上下文大小", "command.session.fork": "从消息分叉", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index eababfc302b..6746fe6c6cf 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -82,6 +82,14 @@ export const dict = { "command.session.undo.description": "復原上一則訊息", "command.session.redo": "重做", "command.session.redo.description": "重做上一則復原的訊息", + "revert.banner.count_one": "{{count}} message reverted", + "revert.banner.count_other": "{{count}} messages reverted", + "revert.banner.redo": "Redo", + "revert.banner.redo.all": "Redo All", + "revert.banner.hint": "Send a new message to make this permanent", + "revert.turn.revert": "Revert to here", + "revert.turn.copy": "Copy message", + "revert.bottom.undo": "Undo Changes", "command.session.compact": "精簡工作階段", "command.session.compact.description": "總結工作階段以減少上下文大小", "command.session.fork": "從訊息分支", diff --git a/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx b/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx index cdfc06c47ce..3cdf84c201a 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx @@ -152,6 +152,11 @@ export function mockSessionValue(overrides?: { getSessionModel: () => ({ providerID: "kilo", modelID: "anthropic/claude-sonnet-4-6" }), setSessionModel: noop, setSessionAgent: noop, + revert: () => undefined, + revertedCount: () => 0, + summary: () => undefined, + revertSession: noop, + unrevertSession: noop, variantList: () => [], currentVariant: () => undefined, selectVariant: noop, diff --git a/packages/kilo-vscode/webview-ui/src/styles/chat.css b/packages/kilo-vscode/webview-ui/src/styles/chat.css index ccf6db1df4c..604d54edc2e 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/chat.css +++ b/packages/kilo-vscode/webview-ui/src/styles/chat.css @@ -446,6 +446,75 @@ background: var(--vscode-button-hoverBackground); } +/* ============================================ + Revert Banner + ============================================ */ + +.revert-banner { + margin: 8px 12px; + padding: 8px 12px; + border-radius: 4px; + background: var(--surface-secondary, var(--vscode-editorWidget-background, #252526)); + border: 1px solid var(--border-base, var(--vscode-editorWidget-border, #454545)); + font-size: 12px; +} + +.revert-banner-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.revert-banner-info { + display: flex; + align-items: center; + gap: 6px; + color: var(--text-muted, var(--vscode-descriptionForeground)); +} + +.revert-banner-count { + font-weight: 500; +} + +.revert-banner-actions { + display: flex; + gap: 4px; + flex-shrink: 0; +} + +.revert-banner-files { + margin-top: 6px; + display: flex; + flex-direction: column; + gap: 2px; +} + +.revert-banner-file { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 1px 0; + color: var(--text-base, var(--vscode-foreground)); +} + +.revert-banner-filename { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + direction: rtl; + text-align: left; +} + +.revert-banner-hint { + margin-top: 6px; + color: var(--text-muted, var(--vscode-descriptionForeground)); + font-style: italic; + font-size: 11px; +} + /* ============================================ Prompt Input ============================================ */ diff --git a/packages/kilo-vscode/webview-ui/src/types/messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages.ts index 61ffec879d7..27600116722 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages.ts @@ -113,12 +113,34 @@ export interface Message { tokens?: TokenUsage } +// File diff info (matches Snapshot.FileDiff from CLI backend) +export interface SessionFileDiff { + file: string + before: string + after: string + additions: number + deletions: number + status?: "added" | "deleted" | "modified" +} + // Session info (simplified for webview) export interface SessionInfo { id: string title?: string createdAt: string updatedAt: string + revert?: { + messageID: string + partID?: string + snapshot?: string + diff?: string + } | null + summary?: { + additions: number + deletions: number + files: number + diffs?: SessionFileDiff[] + } | null } // Cloud session info (from Kilo cloud API) @@ -448,6 +470,12 @@ export interface SessionDeletedMessage { sessionID: string } +export interface MessageRemovedMessage { + type: "messageRemoved" + sessionID: string + messageID: string +} + export interface MessagesLoadedMessage { type: "messagesLoaded" sessionID: string @@ -1102,6 +1130,7 @@ export type ExtensionMessage = | SessionCreatedMessage | SessionUpdatedMessage | SessionDeletedMessage + | MessageRemovedMessage | MessagesLoadedMessage | MessageCreatedMessage | SessionsLoadedMessage @@ -1198,6 +1227,17 @@ export interface AbortRequest { sessionID: string } +export interface RevertSessionRequest { + type: "revertSession" + sessionID: string + messageID: string +} + +export interface UnrevertSessionRequest { + type: "unrevertSession" + sessionID: string +} + export interface PermissionResponseRequest { type: "permissionResponse" permissionId: string @@ -1679,6 +1719,8 @@ export interface SetDefaultBaseBranchRequest { export type WebviewMessage = | SendMessageRequest | AbortRequest + | RevertSessionRequest + | UnrevertSessionRequest | PermissionResponseRequest | CreateSessionRequest | ClearSessionRequest diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 961b27df1df..7b4b30d6dbf 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -81,14 +81,16 @@ "@gitlab/opencode-gitlab-auth": "1.3.3", "@hono/standard-validator": "0.1.5", "@hono/zod-validator": "catalog:", + "@kilocode/kilo-gateway": "workspace:*", + "@kilocode/kilo-telemetry": "workspace:*", + "@kilocode/plugin": "workspace:*", + "@kilocode/sdk": "workspace:*", "@modelcontextprotocol/sdk": "1.25.2", "@morphllm/morphsdk": "0.2.148", "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", - "@kilocode/plugin": "workspace:*", "@opencode-ai/script": "workspace:*", - "@kilocode/sdk": "workspace:*", "@opencode-ai/util": "workspace:*", "@openrouter/ai-sdk-provider": "1.5.4", "@opentui/core": "0.1.86", @@ -122,6 +124,7 @@ "opentui-spinner": "0.0.6", "partial-json": "0.1.7", "remeda": "catalog:", + "simple-git": "3.31.1", "solid-js": "catalog:", "strip-ansi": "7.1.2", "tree-sitter-bash": "0.25.0", @@ -133,10 +136,7 @@ "xdg-basedir": "5.1.0", "yargs": "18.0.0", "zod": "catalog:", - "zod-to-json-schema": "3.24.5", - "@kilocode/kilo-gateway": "workspace:*", - "@kilocode/kilo-telemetry": "workspace:*", - "simple-git": "3.31.1" + "zod-to-json-schema": "3.24.5" }, "overrides": { "drizzle-orm": "1.0.0-beta.12-a5629fb" diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 1a384237554..aead588d809 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -880,6 +880,7 @@ export function UserMessageDisplay(props: { parts: PartType[] interrupted?: boolean queued?: boolean + onRevert?: () => void }) { const data = useData() const dialog = useDialog() @@ -1018,6 +1019,21 @@ export function UserMessageDisplay(props: {
+ + + e.preventDefault()} + onClick={(event) => { + event.stopPropagation() + props.onRevert?.() + }} + aria-label={i18n.t("ui.message.revert")} + /> + + = { "ui.message.copyMessage": "Copy message", "ui.message.copyResponse": "Copy response", "ui.message.copied": "Copied", + "ui.message.revert": "Revert to here", "ui.message.interrupted": "Interrupted", "ui.message.queued": "Queued", "ui.message.attachment.alt": "attachment", diff --git a/packages/ui/src/i18n/es.ts b/packages/ui/src/i18n/es.ts index 9ee95d82459..5df36d5d92b 100644 --- a/packages/ui/src/i18n/es.ts +++ b/packages/ui/src/i18n/es.ts @@ -127,6 +127,7 @@ export const dict = { "ui.message.copyMessage": "Copiar mensaje", "ui.message.copyResponse": "Copiar respuesta", "ui.message.copied": "¡Copiado!", + "ui.message.revert": "Revert to here", "ui.message.interrupted": "Interrumpido", "ui.message.queued": "En cola", "ui.message.attachment.alt": "adjunto", diff --git a/packages/ui/src/i18n/fr.ts b/packages/ui/src/i18n/fr.ts index 431abe56830..df53d9c5584 100644 --- a/packages/ui/src/i18n/fr.ts +++ b/packages/ui/src/i18n/fr.ts @@ -127,6 +127,7 @@ export const dict = { "ui.message.copyMessage": "Copier le message", "ui.message.copyResponse": "Copier la réponse", "ui.message.copied": "Copié !", + "ui.message.revert": "Revert to here", "ui.message.interrupted": "Interrompu", "ui.message.queued": "En file", "ui.message.attachment.alt": "pièce jointe", diff --git a/packages/ui/src/i18n/ja.ts b/packages/ui/src/i18n/ja.ts index c6cb2ac401e..90448a8562b 100644 --- a/packages/ui/src/i18n/ja.ts +++ b/packages/ui/src/i18n/ja.ts @@ -126,6 +126,7 @@ export const dict = { "ui.message.copyMessage": "メッセージをコピー", "ui.message.copyResponse": "応答をコピー", "ui.message.copied": "コピーしました!", + "ui.message.revert": "Revert to here", "ui.message.interrupted": "中断", "ui.message.queued": "待機中", "ui.message.attachment.alt": "添付ファイル", diff --git a/packages/ui/src/i18n/ko.ts b/packages/ui/src/i18n/ko.ts index cd306e879e5..2eb297f860e 100644 --- a/packages/ui/src/i18n/ko.ts +++ b/packages/ui/src/i18n/ko.ts @@ -127,6 +127,7 @@ export const dict = { "ui.message.copyMessage": "메시지 복사", "ui.message.copyResponse": "응답 복사", "ui.message.copied": "복사됨!", + "ui.message.revert": "Revert to here", "ui.message.interrupted": "중단됨", "ui.message.queued": "대기 중", "ui.message.attachment.alt": "첨부 파일", diff --git a/packages/ui/src/i18n/no.ts b/packages/ui/src/i18n/no.ts index ddfe0946188..dc56d918cc9 100644 --- a/packages/ui/src/i18n/no.ts +++ b/packages/ui/src/i18n/no.ts @@ -130,6 +130,7 @@ export const dict: Record = { "ui.message.copyMessage": "Kopier melding", "ui.message.copyResponse": "Kopier svar", "ui.message.copied": "Kopiert!", + "ui.message.revert": "Revert to here", "ui.message.interrupted": "Avbrutt", "ui.message.queued": "I kø", "ui.message.attachment.alt": "vedlegg", diff --git a/packages/ui/src/i18n/pl.ts b/packages/ui/src/i18n/pl.ts index 73fa96afae3..9e34769bfa7 100644 --- a/packages/ui/src/i18n/pl.ts +++ b/packages/ui/src/i18n/pl.ts @@ -126,6 +126,7 @@ export const dict = { "ui.message.copyMessage": "Kopiuj wiadomość", "ui.message.copyResponse": "Kopiuj odpowiedź", "ui.message.copied": "Skopiowano!", + "ui.message.revert": "Revert to here", "ui.message.interrupted": "Przerwano", "ui.message.queued": "W kolejce", "ui.message.attachment.alt": "załącznik", diff --git a/packages/ui/src/i18n/ru.ts b/packages/ui/src/i18n/ru.ts index 085be28436f..0a747a24319 100644 --- a/packages/ui/src/i18n/ru.ts +++ b/packages/ui/src/i18n/ru.ts @@ -126,6 +126,7 @@ export const dict = { "ui.message.copyMessage": "Копировать сообщение", "ui.message.copyResponse": "Копировать ответ", "ui.message.copied": "Скопировано!", + "ui.message.revert": "Revert to here", "ui.message.interrupted": "Прервано", "ui.message.queued": "В очереди", "ui.message.attachment.alt": "вложение", diff --git a/packages/ui/src/i18n/th.ts b/packages/ui/src/i18n/th.ts index 705f68d1b4f..5d480880648 100644 --- a/packages/ui/src/i18n/th.ts +++ b/packages/ui/src/i18n/th.ts @@ -128,6 +128,7 @@ export const dict = { "ui.message.copyMessage": "คัดลอกข้อความ", "ui.message.copyResponse": "คัดลอกคำตอบ", "ui.message.copied": "คัดลอกแล้ว!", + "ui.message.revert": "Revert to here", "ui.message.interrupted": "ถูกขัดจังหวะ", "ui.message.queued": "อยู่ในคิว", "ui.message.attachment.alt": "ไฟล์แนบ", diff --git a/packages/ui/src/i18n/tr.ts b/packages/ui/src/i18n/tr.ts index 8e536c51768..8db027784ad 100644 --- a/packages/ui/src/i18n/tr.ts +++ b/packages/ui/src/i18n/tr.ts @@ -133,6 +133,7 @@ export const dict = { "ui.message.copyMessage": "Mesajı kopyala", "ui.message.copyResponse": "Yanıtı kopyala", "ui.message.copied": "Kopyalandı", + "ui.message.revert": "Revert to here", "ui.message.interrupted": "Kesildi", "ui.message.queued": "Sırada", "ui.message.attachment.alt": "ek", diff --git a/packages/ui/src/i18n/zh.ts b/packages/ui/src/i18n/zh.ts index 571574d92e6..7c3b1b1e38b 100644 --- a/packages/ui/src/i18n/zh.ts +++ b/packages/ui/src/i18n/zh.ts @@ -131,6 +131,7 @@ export const dict = { "ui.message.copyMessage": "复制消息", "ui.message.copyResponse": "复制回复", "ui.message.copied": "已复制!", + "ui.message.revert": "Revert to here", "ui.message.interrupted": "已中断", "ui.message.queued": "排队中", "ui.message.attachment.alt": "附件", diff --git a/packages/ui/src/i18n/zht.ts b/packages/ui/src/i18n/zht.ts index edbc96b12f4..6a5637d8a30 100644 --- a/packages/ui/src/i18n/zht.ts +++ b/packages/ui/src/i18n/zht.ts @@ -131,6 +131,7 @@ export const dict = { "ui.message.copyMessage": "複製訊息", "ui.message.copyResponse": "複製回覆", "ui.message.copied": "已複製!", + "ui.message.revert": "Revert to here", "ui.message.interrupted": "已中斷", "ui.message.queued": "排隊中", "ui.message.attachment.alt": "附件", From 59667f9a57ffc280ef8e2d8fc147c840cdd2ff0d Mon Sep 17 00:00:00 2001 From: Alex Alecu Date: Tue, 17 Mar 2026 18:07:19 +0200 Subject: [PATCH 70/74] fix(cli): use .cmd shim for local tsc on Windows --- packages/opencode/src/kilocode/ts-check.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/kilocode/ts-check.ts b/packages/opencode/src/kilocode/ts-check.ts index f4627454826..449609df830 100644 --- a/packages/opencode/src/kilocode/ts-check.ts +++ b/packages/opencode/src/kilocode/ts-check.ts @@ -107,11 +107,13 @@ export namespace TsCheck { return undefined } - // Walk up from root looking for node_modules/typescript/bin/tsc + // Walk up from root looking for a usable tsc binary. + // On Windows the JS entrypoint has no shebang support, so use the .cmd shim. async function local_tsc(root: string): Promise { + const shim = process.platform === "win32" ? path.join(".bin", "tsc.cmd") : path.join("typescript", "bin", "tsc") let dir = root while (true) { - const bin = path.join(dir, "node_modules", "typescript", "bin", "tsc") + const bin = path.join(dir, "node_modules", shim) if (await exists(bin)) return bin const parent = path.dirname(dir) if (parent === dir) break From 27895e07fd25eaa44d5f65c35fbe0b15e42d4a1a Mon Sep 17 00:00:00 2001 From: Thomas Brugman Date: Tue, 17 Mar 2026 17:22:21 +0100 Subject: [PATCH 71/74] fix(vscode): preview pasted images in chat input (#7052) * fix(vscode): preview pasted images in chat input * fix(vscode): support image preview in agent manager * fix(vscode): pass extension context to agent manager provider * fix(vscode): keep image preview cleanup best-effort --- packages/kilo-vscode/src/KiloProvider.ts | 42 ++++++++++ .../src/agent-manager/AgentManagerProvider.ts | 6 +- .../kilo-vscode/src/agent-manager/types.ts | 7 ++ packages/kilo-vscode/src/extension.ts | 2 +- packages/kilo-vscode/src/image-preview.ts | 83 +++++++++++++++++++ .../kilo-vscode/tests/setup/vscode-mock.ts | 8 ++ .../tests/unit/image-preview.test.ts | 61 ++++++++++++++ .../agent-manager/NewWorktreeDialog.tsx | 9 +- .../src/components/chat/PromptInput.tsx | 9 +- .../webview-ui/src/styles/chat.css | 1 + .../webview-ui/src/types/messages.ts | 8 ++ 11 files changed, 232 insertions(+), 4 deletions(-) create mode 100644 packages/kilo-vscode/src/image-preview.ts create mode 100644 packages/kilo-vscode/tests/unit/image-preview.test.ts diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 6393350eca2..a2b1b99792e 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -1,6 +1,7 @@ import * as path from "path" import * as vscode from "vscode" import { z } from "zod" +import { buildPreviewPath, getPreviewCommand, getPreviewDir, parseImage, trimEntries } from "./image-preview" import { isAbsolutePath } from "./path-utils" import type { KiloClient, @@ -438,6 +439,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper case "openSubAgentViewer": vscode.commands.executeCommand("kilo-code.new.openSubAgentViewer", message.sessionID, message.title) break + case "previewImage": + this.handlePreviewImage(message.dataUrl, message.filename) + break case "openFile": if (message.filePath) { this.handleOpenFile(message.filePath, message.line, message.column) @@ -2016,6 +2020,44 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper } } + private handlePreviewImage(dataUrl: string, filename: string): void { + const dir = this.extensionContext?.globalStorageUri + if (!dir) return + + const img = parseImage(dataUrl, filename) + if (!img) return + + const root = vscode.Uri.joinPath(dir, getPreviewDir()) + const uri = vscode.Uri.joinPath(dir, buildPreviewPath(img.name, Date.now())) + const clean = () => + vscode.workspace.fs.readDirectory(root).then( + (items) => { + const stale = trimEntries(items.map(([name]) => ({ path: name }))) + return Promise.all( + stale.map((name) => + Promise.resolve(vscode.workspace.fs.delete(vscode.Uri.joinPath(root, name), { recursive: true })).then( + undefined, + (err: unknown) => { + console.warn("[Kilo New] KiloProvider: Failed to delete stale preview:", err) + }, + ), + ), + ) + }, + () => [], + ) + const open = () => + vscode.commands + .executeCommand(...getPreviewCommand(uri)) + .then(undefined, () => vscode.commands.executeCommand("vscode.open", uri)) + + void vscode.workspace.fs + .createDirectory(root) + .then(() => vscode.workspace.fs.writeFile(uri, img.data)) + .then(() => clean()) + .then(open, (err) => console.error("[Kilo New] KiloProvider: Failed to preview image:", err)) + } + /** * Handle openFile request from the webview — open a file in the VS Code editor. * Resolves relative paths against the current session's directory (which may be diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts index 26d455c5b18..ee235e930fa 100644 --- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts +++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts @@ -66,6 +66,7 @@ export class AgentManagerProvider implements vscode.Disposable { constructor( private readonly extensionUri: vscode.Uri, private readonly connectionService: KiloConnectionService, + private readonly context: vscode.ExtensionContext, ) { this.outputChannel = vscode.window.createOutputChannel("Kilo Agent Manager") this.terminalManager = new SessionTerminalManager( @@ -147,7 +148,7 @@ export class AgentManagerProvider implements vscode.Disposable { panel.webview.html = this.getHtml(panel.webview) - this.provider = new KiloProvider(this.extensionUri, this.connectionService) + this.provider = new KiloProvider(this.extensionUri, this.connectionService, this.context) this.provider.attachToWebview(panel.webview, { onBeforeMessage: (msg) => this.onMessage(msg), }) @@ -232,6 +233,9 @@ export class AgentManagerProvider implements vscode.Disposable { this.openWorktreeDirectory(m.worktreeId) return null } + if (m.type === "previewImage") { + return msg + } if (m.type === "agentManager.showExistingLocalTerminal") { this.terminalManager.syncLocalOnSessionSwitch() return null diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts index b6cf1cc138d..91977a40469 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -379,6 +379,12 @@ interface GenericOpenFileIn { column?: number } +interface PreviewImageIn { + type: "previewImage" + dataUrl: string + filename: string +} + interface LoadMessagesIn { type: "loadMessages" sessionID: string @@ -434,6 +440,7 @@ export type AgentManagerInMessage = | StopDiffWatchIn | OpenFileIn | GenericOpenFileIn + | PreviewImageIn | LoadMessagesIn | ClearSessionIn | AbortIn diff --git a/packages/kilo-vscode/src/extension.ts b/packages/kilo-vscode/src/extension.ts index 457d0901d7f..c5fa591e2be 100644 --- a/packages/kilo-vscode/src/extension.ts +++ b/packages/kilo-vscode/src/extension.ts @@ -47,7 +47,7 @@ export function activate(context: vscode.ExtensionContext) { ) // Create Agent Manager provider for editor panel - const agentManagerProvider = new AgentManagerProvider(context.extensionUri, connectionService) + const agentManagerProvider = new AgentManagerProvider(context.extensionUri, connectionService, context) context.subscriptions.push(agentManagerProvider) // Register serializer so Agent Manager restores when VS Code restarts diff --git a/packages/kilo-vscode/src/image-preview.ts b/packages/kilo-vscode/src/image-preview.ts new file mode 100644 index 00000000000..c9abc2d4331 --- /dev/null +++ b/packages/kilo-vscode/src/image-preview.ts @@ -0,0 +1,83 @@ +import * as path from "path" + +const IMAGE_PREVIEW_ID = "imagePreview.previewEditor" +const PREVIEW_DIR = "image-preview" +const PREVIEW_LIMIT = 20 + +type Preview = { + data: Uint8Array + ext: string + name: string +} + +export function parseImage(dataUrl: string, filename: string): Preview | null { + const sep = dataUrl.indexOf(",") + if (sep === -1) return null + + const head = dataUrl.slice(0, sep) + const mime = head.match(/^data:(image\/[A-Za-z0-9.+-]+);base64$/)?.[1] + if (!mime) return null + + const data = parseBase64(dataUrl.slice(sep + 1)) + if (!data) return null + + const ext = getExt(mime) + const name = buildName(filename, ext) + return { data, ext, name } +} + +export function buildPreviewPath(name: string, now: number): string { + return path.posix.join(PREVIEW_DIR, `${now}-${name}`) +} + +export function getPreviewCommand(uri: { + toString(): string +}): [string, { resource: { toString(): string }; size: "contain" }] { + return [IMAGE_PREVIEW_ID, { resource: uri, size: "contain" }] +} + +export function getPreviewDir(): string { + return PREVIEW_DIR +} + +export function trimEntries(items: T[], limit = PREVIEW_LIMIT): string[] { + if (items.length <= limit) return [] + + return items + .slice() + .sort((a, b) => a.path.localeCompare(b.path)) + .slice(0, items.length - limit) + .map((item) => item.path) +} + +function parseBase64(value: string): Uint8Array | null { + try { + return Buffer.from(value, "base64") + } catch { + return null + } +} + +function getExt(mime: string): string { + const raw = mime.split("/")[1] ?? "png" + if (raw === "jpeg") return "jpg" + if (raw === "svg+xml") return "svg" + return raw +} + +function buildName(filename: string, ext: string): string { + const raw = path.basename(filename || "image") + const item = path.parse(raw) + const base = sanitize(item.name) || "image" + const tail = sanitize(item.ext.replace(/^\./, "")) + const clean = tail ? `${base}.${tail}` : base + if (path.extname(clean)) return clean + return `${clean}.${ext}` +} + +function sanitize(value: string): string { + return value + .replace(/[^A-Za-z0-9._-]+/g, "-") + .replace(/-+/g, "-") + .replace(/^-|-$/g, "") +} diff --git a/packages/kilo-vscode/tests/setup/vscode-mock.ts b/packages/kilo-vscode/tests/setup/vscode-mock.ts index adf1dfafda8..8fdba932d30 100644 --- a/packages/kilo-vscode/tests/setup/vscode-mock.ts +++ b/packages/kilo-vscode/tests/setup/vscode-mock.ts @@ -51,6 +51,14 @@ const mockVscode = { update: async () => {}, }), asRelativePath: (pathOrUri: string) => pathOrUri, + fs: { + createDirectory: async () => {}, + writeFile: async () => {}, + readFile: async () => new Uint8Array(), + readDirectory: async () => [], + delete: async () => {}, + stat: async () => ({ type: 1, ctime: 0, mtime: 0, size: 0 }), + }, }, window: { activeTextEditor: undefined, diff --git a/packages/kilo-vscode/tests/unit/image-preview.test.ts b/packages/kilo-vscode/tests/unit/image-preview.test.ts new file mode 100644 index 00000000000..6e52993b032 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/image-preview.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from "bun:test" +import { buildPreviewPath, getPreviewCommand, getPreviewDir, parseImage, trimEntries } from "../../src/image-preview" + +describe("parseImage", () => { + it("parses png data urls and preserves a clean extension", () => { + const img = parseImage("data:image/png;base64,aGVsbG8=", "screen") + + expect(img).not.toBeNull() + expect(img?.name).toBe("screen.png") + expect(img?.ext).toBe("png") + expect(Buffer.from(img?.data ?? []).toString("utf8")).toBe("hello") + }) + + it("sanitizes the basename and keeps the existing extension", () => { + const img = parseImage("data:image/jpeg;base64,aGVsbG8=", "../../bad name!!.jpeg") + + expect(img).not.toBeNull() + expect(img?.name).toBe("bad-name.jpeg") + expect(img?.ext).toBe("jpg") + }) + + it("returns null for non-image data urls", () => { + expect(parseImage("data:text/plain;base64,aGVsbG8=", "note.txt")).toBeNull() + }) + + it("returns null when the header is not base64", () => { + expect(parseImage("data:image/png,hello", "screen.png")).toBeNull() + }) +}) + +describe("buildPreviewPath", () => { + it("writes previews into a dedicated storage folder", () => { + expect(buildPreviewPath("screen.png", 42)).toBe("image-preview/42-screen.png") + }) +}) + +describe("getPreviewDir", () => { + it("returns the preview storage folder", () => { + expect(getPreviewDir()).toBe("image-preview") + }) +}) + +describe("trimEntries", () => { + it("drops the oldest preview paths once the limit is exceeded", () => { + const items = Array.from({ length: 22 }, (_, i) => ({ path: `${String(i).padStart(2, "0")}-screen.png` })) + + expect(trimEntries(items)).toEqual(["00-screen.png", "01-screen.png"]) + }) + + it("keeps all preview paths when below the limit", () => { + expect(trimEntries([{ path: "01-screen.png" }])).toEqual([]) + }) +}) + +describe("getPreviewCommand", () => { + it("targets the built-in preview editor with explicit sizing", () => { + const uri = { toString: () => "file:///tmp/screen.png" } + + expect(getPreviewCommand(uri)).toEqual(["imagePreview.previewEditor", { resource: uri, size: "contain" }]) + }) +}) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx index 274e38c9076..1fb48cb6c6b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx @@ -247,7 +247,14 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran {(img) => (
- {img.filename} + {img.filename} + vscode.postMessage({ type: "previewImage", dataUrl: img.dataUrl, filename: img.filename }) + } + /> -