From 6ad16cd86924cc5400bf1a02ec1007d9f896559c Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 3 Jul 2026 16:32:25 +0200 Subject: [PATCH 01/68] feat(vscode): prewarm microphone capture before showing voice input as recording --- .changeset/warm-speech-capture.md | 5 + .../kilo-vscode/src/services/input-tools.ts | 6 + .../kilo-vscode/src/speech-to-text/capture.ts | 24 +++- .../tests/unit/speech-to-text-prewarm.test.ts | 104 ++++++++++++++++++ .../tests/unit/use-speech-to-text.test.ts | 56 ++++++++++ .../agent-manager/AgentManagerApp.tsx | 2 + .../webview-ui/diff-viewer/DiffViewerApp.tsx | 2 + packages/kilo-vscode/webview-ui/src/App.tsx | 2 + .../speech-to-text/SpeechToTextButton.tsx | 19 +++- .../speech-to-text/SpeechToTextPrewarm.tsx | 20 ++++ .../speech-to-text/useSpeechToText.ts | 7 +- .../kilo-vscode/webview-ui/src/i18n/ar.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/br.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/bs.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/da.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/de.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/en.ts | 3 +- .../kilo-vscode/webview-ui/src/i18n/es.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/fr.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/it.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/ja.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/ko.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/nl.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/no.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/pl.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/ru.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/th.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/tr.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/uk.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/zh.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/zht.ts | 1 + .../src/types/messages/webview-messages.ts | 5 + 32 files changed, 264 insertions(+), 10 deletions(-) create mode 100644 .changeset/warm-speech-capture.md create mode 100644 packages/kilo-vscode/tests/unit/speech-to-text-prewarm.test.ts create mode 100644 packages/kilo-vscode/webview-ui/src/components/speech-to-text/SpeechToTextPrewarm.tsx diff --git a/.changeset/warm-speech-capture.md b/.changeset/warm-speech-capture.md new file mode 100644 index 0000000000..64d9be64cc --- /dev/null +++ b/.changeset/warm-speech-capture.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Wait for microphone capture to start before showing voice input as recording. diff --git a/packages/kilo-vscode/src/services/input-tools.ts b/packages/kilo-vscode/src/services/input-tools.ts index 7d722b684e..23479bbf20 100644 --- a/packages/kilo-vscode/src/services/input-tools.ts +++ b/packages/kilo-vscode/src/services/input-tools.ts @@ -1,6 +1,7 @@ import type { KiloConnectionService } from "./cli-backend/connection-service" import { routeAutocompleteMessage } from "./autocomplete/settings" import { handleSpeechToTextCancel, handleSpeechToTextStart, handleSpeechToTextStop } from "../speech-to-text/handler" +import { prewarmSpeechCapture } from "../speech-to-text/capture" type Msg = { type: string @@ -18,6 +19,11 @@ type Ctx = { export async function routeInputToolMessage(message: Msg, ctx: Ctx): Promise { if (await routeAutocompleteMessage(message, ctx.post)) return true + if (message.type === "speechToTextPrewarm") { + void prewarmSpeechCapture().catch((err) => console.warn("[Kilo New] Speech capture prewarm failed:", err)) + return true + } + if (message.type === "speechToTextStart") { if (!message.requestId) return true handleSpeechToTextStart( diff --git a/packages/kilo-vscode/src/speech-to-text/capture.ts b/packages/kilo-vscode/src/speech-to-text/capture.ts index 03efa67aaf..a2d61885bc 100644 --- a/packages/kilo-vscode/src/speech-to-text/capture.ts +++ b/packages/kilo-vscode/src/speech-to-text/capture.ts @@ -36,13 +36,18 @@ type Args = { let active: Recording | undefined let starting: string | undefined +let ffmpeg: Promise | undefined + +export async function prewarmSpeechCapture(): Promise { + await resolveFFmpeg() +} export async function startSpeechCapture(input: Input): Promise { if (active || starting) throw new Error("Speech recording is already in progress") starting = input.requestId try { - const bin = await findFFmpeg() + const bin = await resolveFFmpeg() const file = path.join(os.tmpdir(), `kilo-stt-${process.pid}-${Date.now()}.wav`) const state = await startWithArgs(bin, file, input, await inputArgSets(bin)) return !state.stopped @@ -229,6 +234,23 @@ function requireActive(requestId: string): Recording { return active } +async function resolveFFmpeg(): Promise { + if (ffmpeg) { + const bin = await ffmpeg + if (!path.isAbsolute(bin) || existsSync(bin)) return bin + ffmpeg = undefined + } + + const task = findFFmpeg() + ffmpeg = task + try { + return await task + } catch (err) { + if (ffmpeg === task) ffmpeg = undefined + throw err + } +} + async function findFFmpeg(): Promise { const paths = [ process.env.KILO_FFMPEG_PATH, diff --git a/packages/kilo-vscode/tests/unit/speech-to-text-prewarm.test.ts b/packages/kilo-vscode/tests/unit/speech-to-text-prewarm.test.ts new file mode 100644 index 0000000000..48288a0c53 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/speech-to-text-prewarm.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it } from "bun:test" +import path from "node:path" + +// Run from webview-ui so the child transpiles JSX with solid-js (its tsconfig sets +// jsxImportSource: solid-js). The package root tsconfig has no jsx setting, which makes +// bun fall back to react/jsx-dev-runtime and fail whenever react isn't hoisted nearby. +const WEBVIEW = path.resolve(import.meta.dir, "../../webview-ui") + +// solid-js effects only run under the browser export condition, so the component has to be +// exercised in a child process resolved with `--conditions=browser`. The child prints an +// explicit PASS/FAIL sentinel: a FAIL means the prewarm logic is wrong (fail immediately), +// while any other non-zero exit is a transient spawn failure under load and is retried so +// the suite stays deterministic. +const PASS = "PREWARM_PASS" +const FAIL = "PREWARM_FAIL:" + +const SCRIPT = ` + import { Window } from "happy-dom" + + const window = new Window() + globalThis.window = window + globalThis.document = window.document + globalThis.Node = window.Node + + const sent = [] + globalThis.acquireVsCodeApi = () => ({ + postMessage: (message) => sent.push(message), + getState: () => undefined, + setState: () => {}, + }) + + const { createComponent, createSignal } = await import("solid-js") + const { render } = await import("solid-js/web") + const { ConfigContext } = await import("./src/context/config.tsx") + const { ProviderContext } = await import("./src/context/provider.tsx") + const { SpeechToTextPrewarm } = await import( + "./src/components/speech-to-text/SpeechToTextPrewarm.tsx" + ) + + const fail = (reason) => { + console.log("${FAIL}" + reason) + process.exit(2) + } + + const [config, setConfig] = createSignal({ disabled_providers: ["kilo"] }) + const [auth, setAuth] = createSignal({}) + const root = document.createElement("div") + const dispose = render( + () => + createComponent(ProviderContext.Provider, { + value: { authStates: auth }, + get children() { + return createComponent(ConfigContext.Provider, { + value: { config }, + get children() { + return createComponent(SpeechToTextPrewarm, {}) + }, + }) + }, + }), + root, + ) + + if (sent.length !== 0) fail("prewarmed without Kilo access") + setAuth({ kilo: "api" }) + if (sent.length !== 0) fail("prewarmed while Kilo was disabled") + setConfig({}) + if (sent.length !== 1 || sent[0]?.type !== "speechToTextPrewarm") { + fail("did not prewarm after Kilo access became available") + } + setAuth({ kilo: "oauth" }) + if (sent.length !== 1) fail("prewarmed more than once") + dispose() + console.log("${PASS}") +` + +describe("speech-to-text prewarm", () => { + it("starts only after Kilo speech access becomes available", () => { + const attempts = 3 + const failures: string[] = [] + + for (let attempt = 1; attempt <= attempts; attempt++) { + const result = Bun.spawnSync(["bun", "--conditions=browser", "-e", SCRIPT], { + cwd: WEBVIEW, + stdout: "pipe", + stderr: "pipe", + }) + const output = result.stdout.toString() + result.stderr.toString() + + if (output.includes(PASS)) return + + const logic = output.indexOf(FAIL) + // A FAIL sentinel is a real assertion failure in the prewarm logic — surface it now. + if (logic !== -1) { + expect.unreachable(output.slice(logic + FAIL.length).split("\n")[0]?.trim()) + } + + // Otherwise the child died before it could run (starved/transient spawn) — retry. + failures.push(`attempt ${attempt} exit ${result.exitCode}: ${output.trim() || ""}`) + } + + expect.unreachable(`prewarm child never reported success:\n${failures.join("\n")}`) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/use-speech-to-text.test.ts b/packages/kilo-vscode/tests/unit/use-speech-to-text.test.ts index ae28f0b417..3b406e9cdd 100644 --- a/packages/kilo-vscode/tests/unit/use-speech-to-text.test.ts +++ b/packages/kilo-vscode/tests/unit/use-speech-to-text.test.ts @@ -41,6 +41,59 @@ function setup() { } describe("useSpeechToText", () => { + it("waits for microphone readiness before reporting recording", () => { + const ctx = setup() + + ctx.speech.start({ model: "scribe", insert: () => {} }) + const start = ctx.sent[0] + if (start?.type !== "speechToTextStart") throw new Error("speech start message missing") + + expect(ctx.speech.state()).toBe("starting") + expect(ctx.speech.active()).toBe(true) + + ctx.fire({ type: "speechToTextStarted", requestId: "another-request" }) + expect(ctx.speech.state()).toBe("starting") + + ctx.fire({ type: "speechToTextStarted", requestId: start.requestId }) + expect(ctx.speech.state()).toBe("recording") + ctx.dispose() + }) + + it("does not stop or start another recording while the microphone is starting", () => { + const ctx = setup() + + ctx.speech.start({ model: "scribe", insert: () => {} }) + const start = ctx.sent[0] + if (start?.type !== "speechToTextStart") throw new Error("speech start message missing") + + ctx.speech.stop() + ctx.speech.start({ model: "other", insert: () => {} }) + expect(ctx.speech.state()).toBe("starting") + expect(ctx.sent).toEqual([start]) + + ctx.fire({ type: "speechToTextStarted", requestId: start.requestId }) + ctx.speech.stop() + expect(ctx.speech.state()).toBe("transcribing") + expect(ctx.sent[1]).toEqual({ type: "speechToTextStop", requestId: start.requestId }) + ctx.dispose() + }) + + it("cancels a pending microphone startup and ignores its late acknowledgement", () => { + const ctx = setup() + + ctx.speech.start({ model: "scribe", insert: () => {} }) + const start = ctx.sent[0] + if (start?.type !== "speechToTextStart") throw new Error("speech start message missing") + + ctx.speech.cancel() + expect(ctx.sent[1]).toEqual({ type: "speechToTextCancel", requestId: start.requestId }) + expect(ctx.speech.state()).toBe("idle") + + ctx.fire({ type: "speechToTextStarted", requestId: start.requestId }) + expect(ctx.speech.state()).toBe("idle") + ctx.dispose() + }) + it("offers sign-in when stored credentials stop authenticating", () => { const ctx = setup() @@ -70,6 +123,7 @@ describe("useSpeechToText", () => { ctx.speech.start({ model: "scribe", insert: (value) => text.push(value) }) const start = ctx.sent[0] if (start?.type !== "speechToTextStart") throw new Error("speech start message missing") + ctx.fire({ type: "speechToTextStarted", requestId: start.requestId }) ctx.speech.stop({ done: () => done++ }) ctx.fire({ type: "speechToTextResult", requestId: start.requestId, text: "Recorded prompt" }) @@ -87,6 +141,7 @@ describe("useSpeechToText", () => { ctx.speech.start({ model: "scribe", insert: () => {} }) const start = ctx.sent[0] if (start?.type !== "speechToTextStart") throw new Error("speech start message missing") + ctx.fire({ type: "speechToTextStarted", requestId: start.requestId }) ctx.speech.stop({ done: () => done++ }) ctx.speech.cancel() @@ -105,6 +160,7 @@ describe("useSpeechToText", () => { ctx.speech.start({ model: "scribe", insert: (value) => text.push(value) }) const start = ctx.sent[0] if (start?.type !== "speechToTextStart") throw new Error("speech start message missing") + ctx.fire({ type: "speechToTextStarted", requestId: start.requestId }) ctx.speech.stop({ done: () => done++, ready: () => false }) ctx.fire({ type: "speechToTextResult", requestId: start.requestId, text: "Keep as draft" }) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index a092686fcb..1363d949cf 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -83,6 +83,7 @@ import { FeedbackProvider } from "../src/context/feedback" import { SessionProvider, useSession } from "../src/context/session" import { WorktreeModeProvider } from "../src/context/worktree-mode" import { ChatView } from "../src/components/chat" +import { SpeechToTextPrewarm } from "../src/components/speech-to-text/SpeechToTextPrewarm" import HistoryView from "../src/components/history/HistoryView" import { NewWorktreeDialog } from "./NewWorktreeDialog" import { DataBridge, MermaidDownloadBridge } from "../src/App" @@ -3131,6 +3132,7 @@ export const AgentManagerApp: Component = () => { + diff --git a/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx b/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx index a60338c62c..5ab642292e 100644 --- a/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx +++ b/packages/kilo-vscode/webview-ui/diff-viewer/DiffViewerApp.tsx @@ -23,6 +23,7 @@ import type { DiffSourceCapabilities, DiffSourceDescriptor } from "../../src/dif import type { DiffViewerNotice } from "../src/types/messages/extension-messages" import { DiffPickerHeader } from "./DiffPickerHeader" import { BaseBranchPicker } from "./BaseBranchPicker" +import { SpeechToTextPrewarm } from "../src/components/speech-to-text/SpeechToTextPrewarm" const NOTICE_KEYS: Record = { "snapshots-disabled": "diffViewer.notice.snapshotsDisabled", @@ -305,6 +306,7 @@ export const DiffViewerApp: Component = () => { + diff --git a/packages/kilo-vscode/webview-ui/src/App.tsx b/packages/kilo-vscode/webview-ui/src/App.tsx index de57030df6..d47cf5d952 100644 --- a/packages/kilo-vscode/webview-ui/src/App.tsx +++ b/packages/kilo-vscode/webview-ui/src/App.tsx @@ -25,6 +25,7 @@ import { ChatView } from "./components/chat" import { SidebarEmptyState } from "./components/chat/SidebarEmptyState" import { registerExpandedTaskTool } from "./components/chat/TaskToolExpanded" import { registerVscodeToolOverrides } from "./components/chat/VscodeToolOverrides" +import { SpeechToTextPrewarm } from "./components/speech-to-text/SpeechToTextPrewarm" // Override the upstream "task" tool renderer with the fully-expanded version // that shows child session parts inline in the VS Code sidebar. @@ -378,6 +379,7 @@ const App: Component = () => { + diff --git a/packages/kilo-vscode/webview-ui/src/components/speech-to-text/SpeechToTextButton.tsx b/packages/kilo-vscode/webview-ui/src/components/speech-to-text/SpeechToTextButton.tsx index 3da53666ec..ade6048443 100644 --- a/packages/kilo-vscode/webview-ui/src/components/speech-to-text/SpeechToTextButton.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/speech-to-text/SpeechToTextButton.tsx @@ -1,7 +1,7 @@ import { Button } from "@kilocode/kilo-ui/button" import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { Spinner } from "@kilocode/kilo-ui/spinner" -import type { Component } from "solid-js" +import { onCleanup, type Component } from "solid-js" import type { SpeechToText } from "./useSpeechToText" type Props = { @@ -12,8 +12,10 @@ type Props = { } export const SpeechToTextButton: Component = (props) => { - const disabled = () => !!props.disabled + const unavailable = () => !!props.disabled && props.speech.state() === "idle" + const busy = () => props.speech.state() === "starting" || props.speech.state() === "transcribing" const label = () => { + if (props.speech.state() === "starting") return props.label("speechToText.tooltip.starting") if (props.speech.state() === "recording") return props.label("speechToText.tooltip.stop") if (props.speech.state() === "transcribing") return props.label("speechToText.tooltip.transcribing") if (props.speech.state() === "error") return props.speech.error() || props.label("speechToText.tooltip.error") @@ -21,6 +23,7 @@ export const SpeechToTextButton: Component = (props) => { } const click = () => { + if (props.speech.state() === "starting") return if (props.speech.state() === "recording") { props.speech.stop() return @@ -33,22 +36,28 @@ export const SpeechToTextButton: Component = (props) => { props.speech.clear() return } - if (disabled()) return + if (unavailable()) return props.start() } + onCleanup(() => { + if (props.speech.active()) props.speech.cancel() + }) + return ( + + + {(path, index) => ( +
+ + {path} + + removePath(index())} /> +
+ )} +
+ + ) } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index d4435bcf03..a15b691327 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -1422,6 +1422,9 @@ export const dict = { "settings.sandboxing.network.title": "Restrict Network Access", "settings.sandboxing.network.description": "Block outbound network access from model-originated commands and HTTP tools. Local MCP servers and plugin hooks run outside this restriction. Provider and model inference traffic remains available.", + "settings.sandboxing.writablePaths.title": "Additional Writable Paths", + "settings.sandboxing.writablePaths.description": + "Extra filesystem paths the sandbox allows writes to (e.g. /tmp, /var/log). These are merged with the default writable paths when the sandbox is active.", "settings.experimental.mcpTimeout.title": "MCP Timeout (ms)", "settings.experimental.mcpTimeout.description": "Timeout for MCP server requests in milliseconds", "settings.experimental.remote.title": "Remote Control", diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/config.ts b/packages/kilo-vscode/webview-ui/src/types/messages/config.ts index 8ceed88a13..cf7fefb2cf 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/config.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/config.ts @@ -48,6 +48,7 @@ export interface ExperimentalConfig { mcp_timeout?: number sandbox?: boolean sandbox_restrict_network?: boolean + sandbox_writable_paths?: string[] } export interface CommitMessageConfig { diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index be259ff3d9..eae0c08f1c 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -418,6 +418,10 @@ export const Info = Schema.Struct({ description: "Restrict outbound network access for model-originated commands and first-party HTTP tools; local MCP servers and plugin hooks are not covered (default: true)", }), + sandbox_writable_paths: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({ + description: + "Additional filesystem paths the sandbox allows writes to (e.g. ['/tmp', '/var/log']). These are merged with the default writable paths when the sandbox is active.", + }), // kilocode_change end mcp_timeout: Schema.optional(PositiveInt).annotate({ description: "Timeout in milliseconds for model context protocol (MCP) requests", diff --git a/packages/opencode/src/kilocode/sandbox/policy.ts b/packages/opencode/src/kilocode/sandbox/policy.ts index c02d66e5bc..ad0611db72 100644 --- a/packages/opencode/src/kilocode/sandbox/policy.ts +++ b/packages/opencode/src/kilocode/sandbox/policy.ts @@ -92,7 +92,11 @@ function isolated(ctx: InstanceContext) { return linked(path.resolve(ctx.directory), path.resolve(ctx.worktree)) } -export function profile(ctx: InstanceContext, mode: Profile["network"]["mode"] = "deny"): Profile { +export function profile( + ctx: InstanceContext, + mode: Profile["network"]["mode"] = "deny", + extraWritable?: readonly string[], +): Profile { const project = isolated(ctx) ? [ctx.directory] : ctx.directory === ctx.worktree @@ -108,6 +112,7 @@ export function profile(ctx: InstanceContext, mode: Profile["network"]["mode"] = Global.Path.bin, Global.Path.log, Global.Path.repos, + ...(extraWritable ?? []), ].map(root) return { filesystem: { @@ -297,7 +302,9 @@ function execute(sessionID: SessionID, effect: Effect.Effect) const current = yield* snapshot(sessionID) const support = backendSupport({ mode: current.state.mode, allowedHosts: [] }) if (!current.state.enabled || !support.available) return yield* unrestricted(effect) - return yield* runSandbox(profile(yield* InstanceState.context, current.state.mode), effect) + const cfg = yield* (yield* Config.Service).get() + const extraWritable = cfg.experimental?.sandbox_writable_paths + return yield* runSandbox(profile(yield* InstanceState.context, current.state.mode, extraWritable), effect) }) } From 1fc36e2e83e911af25aab780082f0c07f8593889 Mon Sep 17 00:00:00 2001 From: trim21 Date: Tue, 7 Jul 2026 12:54:27 +0800 Subject: [PATCH 12/68] fix(i18n): translate sandbox writable paths for zh/zht and add keys to 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/bs.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/it.ts | 4 ++++ packages/kilo-vscode/webview-ui/src/i18n/ja.ts | 4 ++++ packages/kilo-vscode/webview-ui/src/i18n/ko.ts | 4 ++++ packages/kilo-vscode/webview-ui/src/i18n/nl.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/tr.ts | 4 ++++ packages/kilo-vscode/webview-ui/src/i18n/uk.ts | 4 ++++ packages/kilo-vscode/webview-ui/src/i18n/zh.ts | 4 ++++ packages/kilo-vscode/webview-ui/src/i18n/zht.ts | 4 ++++ 19 files changed, 76 insertions(+) diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index a87c8ac9b6..62c7b7bce9 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -1411,6 +1411,10 @@ export const dict = { "settings.sandboxing.network.title": "تقييد الوصول إلى الشبكة", "settings.sandboxing.network.description": "احظر الوصول الصادر إلى الشبكة من الأوامر الصادرة عن النموذج وأدوات HTTP. تعمل خوادم MCP المحلية وخطافات المكونات الإضافية خارج هذا التقييد. تظل حركة مرور استدلال الموفّر والنموذج متاحة.", + + "settings.sandboxing.writablePaths.title": "Additional Writable Paths", + "settings.sandboxing.writablePaths.description": + "Extra filesystem paths the sandbox allows writes to (e.g. /tmp, /var/log). These are merged with the default writable paths when the sandbox is active.", "settings.experimental.mcpTimeout.title": "مهلة MCP (مللي ثانية)", "settings.experimental.mcpTimeout.description": "مهلة طلبات خادم MCP بالمللي ثانية", "settings.experimental.remote.title": "التحكم Remote", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 5daf082020..531b64e519 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -1447,6 +1447,10 @@ export const dict = { "settings.sandboxing.network.title": "Restringir acesso à rede", "settings.sandboxing.network.description": "Bloqueie o acesso de saída à rede para comandos originados pelo modelo e ferramentas HTTP. Servidores MCP locais e hooks de plugins são executados fora dessa restrição. O tráfego de inferência de provedores e modelos permanece disponível.", + + "settings.sandboxing.writablePaths.title": "Additional Writable Paths", + "settings.sandboxing.writablePaths.description": + "Extra filesystem paths the sandbox allows writes to (e.g. /tmp, /var/log). These are merged with the default writable paths when the sandbox is active.", "settings.experimental.mcpTimeout.title": "Tempo limite MCP (ms)", "settings.experimental.mcpTimeout.description": "Tempo limite para solicitações do servidor MCP em milissegundos", "settings.experimental.remote.title": "Controle Remote", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index a5a894c7f0..08eac42dbb 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -1443,6 +1443,10 @@ export const dict = { "settings.sandboxing.network.title": "Ograniči pristup mreži", "settings.sandboxing.network.description": "Blokiraj odlazni mrežni pristup za naredbe koje potiču od modela i HTTP alate. Lokalni MCP serveri i hookovi dodataka izvršavaju se izvan ovog ograničenja. Saobraćaj za inferenciju pružatelja i modela ostaje dostupan.", + + "settings.sandboxing.writablePaths.title": "Additional Writable Paths", + "settings.sandboxing.writablePaths.description": + "Extra filesystem paths the sandbox allows writes to (e.g. /tmp, /var/log). These are merged with the default writable paths when the sandbox is active.", "settings.experimental.mcpTimeout.title": "MCP istek vremena (ms)", "settings.experimental.mcpTimeout.description": "Istek vremena za MCP server zahtjeve u milisekundama", "settings.experimental.remote.title": "Remote kontrola", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index 14b21e2ab8..8084198a5c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -1437,6 +1437,10 @@ export const dict = { "settings.sandboxing.network.title": "Begræns netværksadgang", "settings.sandboxing.network.description": "Bloker udgående netværksadgang fra kommandoer, der stammer fra modellen, og HTTP-værktøjer. Lokale MCP-servere og plugin-hooks er ikke underlagt denne begrænsning. Inferenstrafik til udbydere og modeller er fortsat tilgængelig.", + + "settings.sandboxing.writablePaths.title": "Additional Writable Paths", + "settings.sandboxing.writablePaths.description": + "Extra filesystem paths the sandbox allows writes to (e.g. /tmp, /var/log). These are merged with the default writable paths when the sandbox is active.", "settings.experimental.mcpTimeout.title": "MCP-timeout (ms)", "settings.experimental.mcpTimeout.description": "Timeout for MCP-serveranmodninger i millisekunder", "settings.experimental.remote.title": "Remote-styring", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 7c3ebe2962..e250600c9f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -1467,6 +1467,10 @@ export const dict = { "settings.sandboxing.network.title": "Netzwerkzugriff einschränken", "settings.sandboxing.network.description": "Blockiert den ausgehenden Netzwerkzugriff für vom Modell initiierte Befehle und HTTP-Tools. Lokale MCP-Server und Plugin-Hooks sind von dieser Einschränkung ausgenommen. Anbieter- und Modellinferenzdatenverkehr bleibt verfügbar.", + + "settings.sandboxing.writablePaths.title": "Additional Writable Paths", + "settings.sandboxing.writablePaths.description": + "Extra filesystem paths the sandbox allows writes to (e.g. /tmp, /var/log). These are merged with the default writable paths when the sandbox is active.", "settings.experimental.mcpTimeout.title": "MCP-Zeitlimit (ms)", "settings.experimental.mcpTimeout.description": "Zeitlimit für MCP-Server-Anfragen in Millisekunden", "settings.experimental.remote.title": "Remote-Steuerung", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index a5bf4b39bd..11a1c4eb7c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -1455,6 +1455,10 @@ export const dict = { "settings.sandboxing.network.title": "Restringir el acceso a la red", "settings.sandboxing.network.description": "Bloquea el acceso saliente a la red para los comandos iniciados por el modelo y las herramientas HTTP. Los servidores MCP locales y los hooks de plugins no están sujetos a esta restricción. El tráfico de proveedores y de inferencia de modelos sigue estando disponible.", + + "settings.sandboxing.writablePaths.title": "Additional Writable Paths", + "settings.sandboxing.writablePaths.description": + "Extra filesystem paths the sandbox allows writes to (e.g. /tmp, /var/log). These are merged with the default writable paths when the sandbox is active.", "settings.experimental.mcpTimeout.title": "Tiempo de espera MCP (ms)", "settings.experimental.mcpTimeout.description": "Tiempo de espera para solicitudes del servidor MCP en milisegundos", "settings.experimental.remote.title": "Control Remote", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index e793315324..a5cbfe1fff 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -1470,6 +1470,10 @@ export const dict = { "settings.sandboxing.network.title": "Restreindre l'accès au réseau", "settings.sandboxing.network.description": "Bloquer l'accès réseau sortant des commandes provenant du modèle et des outils HTTP. Les serveurs MCP locaux et les hooks de plugin ne sont pas soumis à cette restriction. Le trafic d'inférence des fournisseurs et des modèles reste disponible.", + + "settings.sandboxing.writablePaths.title": "Additional Writable Paths", + "settings.sandboxing.writablePaths.description": + "Extra filesystem paths the sandbox allows writes to (e.g. /tmp, /var/log). These are merged with the default writable paths when the sandbox is active.", "settings.experimental.mcpTimeout.title": "Délai MCP (ms)", "settings.experimental.mcpTimeout.description": "Délai des requêtes du serveur MCP en millisecondes", "settings.experimental.remote.title": "Contrôle Remote", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index 5e1079426a..0af26debd7 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -1229,6 +1229,10 @@ export const dict = { "settings.sandboxing.network.title": "Limita l'accesso alla rete", "settings.sandboxing.network.description": "Blocca l'accesso in uscita alla rete per i comandi avviati dal modello e gli strumenti HTTP. I server MCP locali e gli hook dei plugin operano al di fuori di questa restrizione. Il traffico verso i provider e per l'inferenza dei modelli rimane disponibile.", + + "settings.sandboxing.writablePaths.title": "Additional Writable Paths", + "settings.sandboxing.writablePaths.description": + "Extra filesystem paths the sandbox allows writes to (e.g. /tmp, /var/log). These are merged with the default writable paths when the sandbox is active.", "settings.experimental.mcpTimeout.title": "Timeout MCP (ms)", "settings.experimental.mcpTimeout.description": "Timeout per richieste server MCP in millisecondi", "settings.experimental.remote.title": "Controllo remoto", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 5483784d61..480fc818ee 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -1432,6 +1432,10 @@ export const dict = { "settings.sandboxing.network.title": "ネットワークアクセスを制限", "settings.sandboxing.network.description": "モデルから発行されたコマンドと HTTP ツールによる外部ネットワークアクセスをブロックします。ローカル MCP サーバーとプラグインフックは、この制限の対象外です。プロバイダーおよびモデルへの推論通信は引き続き利用できます。", + + "settings.sandboxing.writablePaths.title": "Additional Writable Paths", + "settings.sandboxing.writablePaths.description": + "Extra filesystem paths the sandbox allows writes to (e.g. /tmp, /var/log). These are merged with the default writable paths when the sandbox is active.", "settings.experimental.mcpTimeout.title": "MCPタイムアウト(ミリ秒)", "settings.experimental.mcpTimeout.description": "MCPサーバーリクエストのタイムアウト(ミリ秒)", "settings.experimental.remote.title": "Remote コントロール", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 1c68d5d743..592d78f9ff 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -1424,6 +1424,10 @@ export const dict = { "settings.sandboxing.network.title": "네트워크 액세스 제한", "settings.sandboxing.network.description": "모델이 실행한 명령과 HTTP 도구의 아웃바운드 네트워크 액세스를 차단합니다. 로컬 MCP 서버와 플러그인 훅에는 이 제한이 적용되지 않습니다. 공급자 및 모델 추론 트래픽은 계속 사용할 수 있습니다.", + + "settings.sandboxing.writablePaths.title": "Additional Writable Paths", + "settings.sandboxing.writablePaths.description": + "Extra filesystem paths the sandbox allows writes to (e.g. /tmp, /var/log). These are merged with the default writable paths when the sandbox is active.", "settings.experimental.mcpTimeout.title": "MCP 타임아웃 (ms)", "settings.experimental.mcpTimeout.description": "MCP 서버 요청의 타임아웃 시간 (밀리초)", "settings.experimental.remote.title": "Remote 제어", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index 845b5d5a16..2ad81ecb59 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -1441,6 +1441,10 @@ export const dict = { "settings.sandboxing.network.title": "Netwerktoegang beperken", "settings.sandboxing.network.description": "Blokkeer uitgaande netwerktoegang voor door het model geïnitieerde opdrachten en HTTP-tools. Lokale MCP-servers en plugin-hooks vallen buiten deze beperking. Netwerkverkeer voor providers en modelinferentie blijft beschikbaar.", + + "settings.sandboxing.writablePaths.title": "Additional Writable Paths", + "settings.sandboxing.writablePaths.description": + "Extra filesystem paths the sandbox allows writes to (e.g. /tmp, /var/log). These are merged with the default writable paths when the sandbox is active.", "settings.experimental.mcpTimeout.title": "MCP Timeout (ms)", "settings.experimental.mcpTimeout.description": "Timeout voor MCP-serververzoeken in milliseconden", "settings.experimental.remote.title": "Remote-bediening", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 775d9dc7e6..a6fb9502cb 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -1400,6 +1400,10 @@ export const dict = { "settings.sandboxing.network.title": "Begrens nettverkstilgang", "settings.sandboxing.network.description": "Blokker utgående nettverkstilgang fra kommandoer generert av modellen og HTTP-verktøy. Lokale MCP-servere og programtilleggskroker kjører utenfor denne begrensningen. Trafikk for leverandør- og modellinferens forblir tilgjengelig.", + + "settings.sandboxing.writablePaths.title": "Additional Writable Paths", + "settings.sandboxing.writablePaths.description": + "Extra filesystem paths the sandbox allows writes to (e.g. /tmp, /var/log). These are merged with the default writable paths when the sandbox is active.", "settings.experimental.mcpTimeout.title": "MCP-tidsavbrudd (ms)", "settings.experimental.mcpTimeout.description": "Tidsavbrudd for MCP-serverforespørsler i millisekunder", "settings.experimental.remote.title": "Remote-kontroll", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index d3fa876fb7..4c580f8b68 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -1400,6 +1400,10 @@ export const dict = { "settings.sandboxing.network.title": "Ogranicz dostęp do sieci", "settings.sandboxing.network.description": "Blokuj wychodzący dostęp do sieci z poleceń pochodzących od modelu i narzędzi HTTP. Lokalne serwery MCP i hooki wtyczek nie podlegają temu ograniczeniu. Ruch do dostawców i modeli na potrzeby wnioskowania pozostaje dostępny.", + + "settings.sandboxing.writablePaths.title": "Additional Writable Paths", + "settings.sandboxing.writablePaths.description": + "Extra filesystem paths the sandbox allows writes to (e.g. /tmp, /var/log). These are merged with the default writable paths when the sandbox is active.", "settings.experimental.mcpTimeout.title": "Limit czasu MCP (ms)", "settings.experimental.mcpTimeout.description": "Limit czasu żądań serwera MCP w milisekundach", "settings.experimental.remote.title": "Sterowanie Remote", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 6f616a81d8..f2abffcf8b 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -1441,6 +1441,10 @@ export const dict = { "settings.sandboxing.network.title": "Ограничить доступ к сети", "settings.sandboxing.network.description": "Блокировать исходящий доступ к сети для команд, инициированных моделью, и HTTP-инструментов. Локальные серверы MCP и хуки плагинов не подпадают под это ограничение. Трафик к провайдерам и моделям для инференса остаётся доступным.", + + "settings.sandboxing.writablePaths.title": "Additional Writable Paths", + "settings.sandboxing.writablePaths.description": + "Extra filesystem paths the sandbox allows writes to (e.g. /tmp, /var/log). These are merged with the default writable paths when the sandbox is active.", "settings.experimental.mcpTimeout.title": "Таймаут MCP (мс)", "settings.experimental.mcpTimeout.description": "Таймаут запросов MCP-сервера в миллисекундах", "settings.experimental.remote.title": "Управление Remote", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index e5ec7f48ed..360857fba2 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -1420,6 +1420,10 @@ export const dict = { "settings.sandboxing.network.title": "จำกัดการเข้าถึงเครือข่าย", "settings.sandboxing.network.description": "บล็อกการเข้าถึงเครือข่ายขาออกจากคำสั่งที่มาจากโมเดลและเครื่องมือ HTTP เซิร์ฟเวอร์ MCP ภายในเครื่องและฮุกของปลั๊กอินทำงานอยู่นอกข้อจำกัดนี้ การรับส่งข้อมูลสำหรับการอนุมานของผู้ให้บริการและโมเดลยังคงใช้งานได้", + + "settings.sandboxing.writablePaths.title": "Additional Writable Paths", + "settings.sandboxing.writablePaths.description": + "Extra filesystem paths the sandbox allows writes to (e.g. /tmp, /var/log). These are merged with the default writable paths when the sandbox is active.", "settings.experimental.mcpTimeout.title": "หมดเวลา MCP (มิลลิวินาที)", "settings.experimental.mcpTimeout.description": "หมดเวลาสำหรับคำขอเซิร์ฟเวอร์ MCP เป็นมิลลิวินาที", "settings.experimental.remote.title": "การควบคุม Remote", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index e57d018906..9337af1b82 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -1431,6 +1431,10 @@ export const dict = { "settings.sandboxing.network.title": "Ağ Erişimini Kısıtla", "settings.sandboxing.network.description": "Model tarafından başlatılan komutların ve HTTP araçlarının giden ağ erişimini engelleyin. Yerel MCP sunucuları ve eklenti kancaları bu kısıtlamanın dışında çalışır. Sağlayıcı ve model çıkarım trafiği kullanılabilir durumda kalır.", + + "settings.sandboxing.writablePaths.title": "Additional Writable Paths", + "settings.sandboxing.writablePaths.description": + "Extra filesystem paths the sandbox allows writes to (e.g. /tmp, /var/log). These are merged with the default writable paths when the sandbox is active.", "settings.experimental.mcpTimeout.title": "MCP Zaman Aşımı (ms)", "settings.experimental.mcpTimeout.description": "MCP sunucu istekleri için milisaniye cinsinden zaman aşımı", "settings.experimental.remote.title": "Remote Kontrolü", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index 06c0f00dfa..bd849a94b4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -1429,6 +1429,10 @@ export const dict = { "settings.sandboxing.network.title": "Обмежити доступ до мережі", "settings.sandboxing.network.description": "Блокуйте вихідний доступ до мережі для команд, ініційованих моделлю, та HTTP-інструментів. Локальні MCP-сервери й хуки плагінів працюють поза цим обмеженням. Трафік провайдерів та інференсу моделей залишається доступним.", + + "settings.sandboxing.writablePaths.title": "Additional Writable Paths", + "settings.sandboxing.writablePaths.description": + "Extra filesystem paths the sandbox allows writes to (e.g. /tmp, /var/log). These are merged with the default writable paths when the sandbox is active.", "settings.experimental.mcpTimeout.title": "Тайм-аут MCP (мс)", "settings.experimental.mcpTimeout.description": "Тайм-аут у мілісекундах для запитів до MCP-сервера", "settings.experimental.remote.title": "Керування Remote", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 3f0eafa95e..cf9dd5844e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -1394,6 +1394,10 @@ export const dict = { "settings.sandboxing.network.title": "限制网络访问", "settings.sandboxing.network.description": "阻止模型发起的命令和 HTTP 工具进行出站网络访问。本地 MCP 服务器和插件钩子不受此限制。提供商和模型推理流量仍然可用。", + + "settings.sandboxing.writablePaths.title": "额外可写路径", + "settings.sandboxing.writablePaths.description": + "沙盒允许写入的额外文件系统路径(例如 /tmp、/var/log)。沙盒启用后,这些路径会与默认可写路径合并。", "settings.experimental.mcpTimeout.title": "MCP 超时(毫秒)", "settings.experimental.mcpTimeout.description": "MCP 服务器请求的超时时间(毫秒)", "settings.experimental.remote.title": "Remote 控制", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index d50689f1f7..16e14f70e2 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -1358,6 +1358,10 @@ export const dict = { "settings.sandboxing.network.title": "限制網路存取", "settings.sandboxing.network.description": "封鎖模型發起的命令和 HTTP 工具的對外網路存取。本機 MCP 伺服器和外掛程式鉤子不受此限制。供應商與模型推論流量仍然可用。", + + "settings.sandboxing.writablePaths.title": "額外可寫路徑", + "settings.sandboxing.writablePaths.description": + "沙盒允許寫入的額外檔案系統路徑(例如 /tmp、/var/log)。沙盒啟用後,這些路徑會與預設可寫路徑合併。", "settings.experimental.mcpTimeout.title": "MCP 逾時(毫秒)", "settings.experimental.mcpTimeout.description": "MCP 伺服器請求的逾時時間(毫秒)", "settings.experimental.remote.title": "Remote 控制", From 889e2f4358042250d87817b41d085a20a7c24a9b Mon Sep 17 00:00:00 2001 From: trim21 Date: Tue, 7 Jul 2026 12:58:44 +0800 Subject: [PATCH 13/68] style: fix prettier formatting for SandboxingTab --- .../webview-ui/src/components/settings/SandboxingTab.tsx | 3 +-- 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/bs.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/it.ts | 4 ++-- packages/kilo-vscode/webview-ui/src/i18n/ja.ts | 4 ++-- packages/kilo-vscode/webview-ui/src/i18n/ko.ts | 4 ++-- packages/kilo-vscode/webview-ui/src/i18n/nl.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/tr.ts | 4 ++-- packages/kilo-vscode/webview-ui/src/i18n/uk.ts | 4 ++-- 18 files changed, 35 insertions(+), 36 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/SandboxingTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/SandboxingTab.tsx index 6e69d7b14d..74f6e656fa 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/SandboxingTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/SandboxingTab.tsx @@ -104,8 +104,7 @@ const SandboxingTab: Component = () => { "align-items": "center", "justify-content": "space-between", padding: "6px 0", - "border-bottom": - index() < writablePaths().length - 1 ? "1px solid var(--border-weak-base)" : "none", + "border-bottom": index() < writablePaths().length - 1 ? "1px solid var(--border-weak-base)" : "none", }} > Date: Tue, 7 Jul 2026 11:30:13 +0200 Subject: [PATCH 14/68] fix(cli): honor cloud-selected Kilo organization --- .changeset/selected-organization-default.md | 6 +++++ packages/kilo-gateway/src/api/profile.ts | 2 ++ packages/kilo-gateway/src/server/handlers.ts | 19 ++++++++++--- packages/kilo-gateway/src/server/routes.ts | 1 + packages/kilo-gateway/src/types.ts | 1 + .../src/services/cli-backend/types.ts | 1 + .../webview-ui/src/types/messages/profile.ts | 1 + .../server/httpapi/groups/kilo-gateway.ts | 1 + .../server/httpapi/handlers/kilo-gateway.ts | 27 ++++++++++++++++--- .../kilocode/server/httpapi-public.test.ts | 1 + packages/sdk/js/src/v2/gen/types.gen.ts | 1 + packages/sdk/openapi.json | 3 +++ 12 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 .changeset/selected-organization-default.md diff --git a/.changeset/selected-organization-default.md b/.changeset/selected-organization-default.md new file mode 100644 index 0000000000..1f4c262b29 --- /dev/null +++ b/.changeset/selected-organization-default.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": patch +"@kilocode/kilo-gateway": patch +--- + +Use the cloud-selected organization as the active Kilo account when provided. diff --git a/packages/kilo-gateway/src/api/profile.ts b/packages/kilo-gateway/src/api/profile.ts index aa717a64c4..ae7d46eeed 100644 --- a/packages/kilo-gateway/src/api/profile.ts +++ b/packages/kilo-gateway/src/api/profile.ts @@ -25,6 +25,7 @@ export async function fetchProfile(token: string): Promise { email?: string name?: string organizations?: Organization[] + selectedOrganizationId?: string | null } // Backend returns { user: { email, name, ... }, organizations } // Transform to flat KilocodeProfile structure @@ -32,6 +33,7 @@ export async function fetchProfile(token: string): Promise { email: data.user?.email ?? data.email ?? "", name: data.user?.name ?? data.name, organizations: data.organizations, + selectedOrganizationId: data.selectedOrganizationId ?? undefined, } } diff --git a/packages/kilo-gateway/src/server/handlers.ts b/packages/kilo-gateway/src/server/handlers.ts index 167bfbf4db..ebf5aa3734 100644 --- a/packages/kilo-gateway/src/server/handlers.ts +++ b/packages/kilo-gateway/src/server/handlers.ts @@ -68,12 +68,25 @@ export async function getProfile(auth: AuthStore): Promise { const info = await auth.get("kilo") if (!info || info.type !== "oauth") throw new UnauthorizedError("Not authenticated with Kilo Gateway") - const currentOrgId = info.accountId ?? null - const [profile, balance, kiloPass] = await Promise.all([ + const [profile, kiloPass] = await Promise.all([ fetchProfile(info.access), - fetchBalance(info.access, currentOrgId ?? undefined), fetchKiloPassState(info.access), ]) + + const selected = profile.selectedOrganizationId + const valid = selected && profile.organizations?.some((org) => org.id === selected) ? selected : undefined + const currentOrgId = valid ?? info.accountId ?? null + if (valid && valid !== info.accountId) { + await auth.set("kilo", { + type: "oauth", + refresh: info.refresh, + access: info.access, + expires: info.expires, + accountId: valid, + }) + } + + const balance = await fetchBalance(info.access, currentOrgId ?? undefined) return { profile, balance, kiloPass, currentOrgId } } diff --git a/packages/kilo-gateway/src/server/routes.ts b/packages/kilo-gateway/src/server/routes.ts index 40fb57937c..cc60ecc62c 100644 --- a/packages/kilo-gateway/src/server/routes.ts +++ b/packages/kilo-gateway/src/server/routes.ts @@ -101,6 +101,7 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { email: z.string(), name: z.string().optional(), organizations: z.array(Organization).optional(), + selectedOrganizationId: z.string().optional(), }) const Balance = z.object({ diff --git a/packages/kilo-gateway/src/types.ts b/packages/kilo-gateway/src/types.ts index 801464966b..c9014f1bc2 100644 --- a/packages/kilo-gateway/src/types.ts +++ b/packages/kilo-gateway/src/types.ts @@ -27,6 +27,7 @@ export interface KilocodeProfile { email: string name?: string organizations?: Organization[] + selectedOrganizationId?: string } export interface KilocodeBalance { diff --git a/packages/kilo-vscode/src/services/cli-backend/types.ts b/packages/kilo-vscode/src/services/cli-backend/types.ts index b675500454..99f52c80d3 100644 --- a/packages/kilo-vscode/src/services/cli-backend/types.ts +++ b/packages/kilo-vscode/src/services/cli-backend/types.ts @@ -31,6 +31,7 @@ export interface KilocodeProfile { email: string name?: string organizations?: KilocodeOrganization[] + selectedOrganizationId?: string } export interface KilocodeBalance { diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/profile.ts b/packages/kilo-vscode/webview-ui/src/types/messages/profile.ts index 6b05d3fb51..2d2a845a2b 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/profile.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/profile.ts @@ -30,6 +30,7 @@ export interface ProfileData { email: string name?: string organizations?: Array<{ id: string; name: string; role: string }> + selectedOrganizationId?: string } balance: KilocodeBalance | null kiloPass: KiloPassState | null diff --git a/packages/opencode/src/kilocode/server/httpapi/groups/kilo-gateway.ts b/packages/opencode/src/kilocode/server/httpapi/groups/kilo-gateway.ts index f807b480bd..8f255191b0 100644 --- a/packages/opencode/src/kilocode/server/httpapi/groups/kilo-gateway.ts +++ b/packages/opencode/src/kilocode/server/httpapi/groups/kilo-gateway.ts @@ -21,6 +21,7 @@ export const Profile = Schema.Struct({ email: Schema.String, name: Schema.optional(Schema.String), organizations: Schema.optional(Schema.Array(Organization)), + selectedOrganizationId: Schema.optional(Schema.String), }) export const Balance = Schema.Struct({ diff --git a/packages/opencode/src/kilocode/server/httpapi/handlers/kilo-gateway.ts b/packages/opencode/src/kilocode/server/httpapi/handlers/kilo-gateway.ts index dc37a5628a..334a819276 100644 --- a/packages/opencode/src/kilocode/server/httpapi/handlers/kilo-gateway.ts +++ b/packages/opencode/src/kilocode/server/httpapi/handlers/kilo-gateway.ts @@ -67,16 +67,37 @@ export const kiloGatewayHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilo", const info = yield* auth.get("kilo").pipe(Effect.mapError(() => new HttpApiError.BadRequest({}))) if (!info || info.type !== "oauth") return yield* Effect.fail(new HttpApiError.Unauthorized({})) - const currentOrgId = info.accountId ?? null - const [profile, balance, kiloPass] = yield* Effect.tryPromise({ + const [profile, kiloPass] = yield* Effect.tryPromise({ try: () => Promise.all([ fetchProfile(info.access), - fetchBalance(info.access, currentOrgId ?? undefined), fetchKiloPassState(info.access), ]), catch: () => new HttpApiError.BadRequest({}), }) + + const selected = profile.selectedOrganizationId + const valid = selected && profile.organizations?.some((org) => org.id === selected) ? selected : undefined + const currentOrgId = valid ?? info.accountId ?? null + if (valid && valid !== info.accountId) { + yield* auth + .set("kilo", { + type: "oauth", + refresh: info.refresh, + access: info.access, + expires: info.expires, + accountId: valid, + }) + .pipe(Effect.mapError(() => new HttpApiError.BadRequest({}))) + yield* cache.clear("kilo") + clearModesCache() + yield* store.disposeAll().pipe(Effect.mapError(() => new HttpApiError.BadRequest({}))) + } + + const balance = yield* Effect.tryPromise({ + try: () => fetchBalance(info.access, currentOrgId ?? undefined), + catch: () => new HttpApiError.BadRequest({}), + }) return { profile, balance, kiloPass, currentOrgId } }) diff --git a/packages/opencode/test/kilocode/server/httpapi-public.test.ts b/packages/opencode/test/kilocode/server/httpapi-public.test.ts index 66cf90a7ec..3adc9c77ee 100644 --- a/packages/opencode/test/kilocode/server/httpapi-public.test.ts +++ b/packages/opencode/test/kilocode/server/httpapi-public.test.ts @@ -189,6 +189,7 @@ describe("Kilo PublicApi OpenAPI contract", () => { const profile = response(KiloGatewayPaths.profile)?.properties expect(profile?.balance).toEqual({ anyOf: [expect.objectContaining({ type: "object" }), { type: "null" }] }) expect(profile?.kiloPass).toEqual({ anyOf: [expect.objectContaining({ type: "object" }), { type: "null" }] }) + expect(profile?.profile?.properties?.selectedOrganizationId).toEqual({ type: "string" }) const pass = profile?.kiloPass?.anyOf?.find((item) => item.type === "object")?.properties expect(pass?.nextBillingAt).toEqual({ anyOf: [{ type: "string" }, { type: "null" }] }) expect(profile?.currentOrgId).toEqual({ anyOf: [{ type: "string" }, { type: "null" }] }) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 5282b260f4..e7a7c7c3f9 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -10787,6 +10787,7 @@ export type KiloProfileResponses = { name: string role: string }> + selectedOrganizationId?: string } balance: { balance: number diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 267d53bb1d..b1bb912b56 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -13509,6 +13509,9 @@ "required": ["id", "name", "role"], "additionalProperties": false } + }, + "selectedOrganizationId": { + "type": "string" } }, "required": ["email"], From 81213b9f251286fb87d677c8af3dba304efea136 Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 7 Jul 2026 11:46:34 +0200 Subject: [PATCH 15/68] fix: respect unavailable personal Kilo accounts --- .changeset/selected-organization-default.md | 2 +- packages/kilo-gateway/src/api/profile.ts | 2 ++ packages/kilo-gateway/src/server/routes.ts | 1 + packages/kilo-gateway/src/tui/helpers.ts | 17 ++++++++----- packages/kilo-gateway/src/types.ts | 1 + .../kilocode/backend/rpc/KiloAppRpcApiImpl.kt | 1 + .../backend/app/KiloBackendAppServiceTest.kt | 2 ++ .../ui/account/SessionAccountOverlay.kt | 5 ++-- .../settings/profile/LoggedInProfileUi.kt | 7 +++--- .../ui/account/SessionAccountOverlayTest.kt | 18 +++++++++++++ .../ai/kilocode/rpc/dto/KiloAppStateDto.kt | 1 + .../src/services/cli-backend/types.ts | 1 + .../src/components/profile/ProfileView.tsx | 25 +++++++++++++++---- .../src/components/shared/AccountSwitcher.tsx | 23 +++++++++-------- .../webview-ui/src/types/messages/profile.ts | 1 + .../components/dialog-kilo-auto-method.tsx | 11 ++++---- .../components/dialog-kilo-organization.tsx | 3 ++- .../components/dialog-kilo-team-select.tsx | 7 +++++- .../opencode/src/kilocode/kilo-commands.tsx | 1 + .../server/httpapi/groups/kilo-gateway.ts | 1 + .../kilocode/server/httpapi-public.test.ts | 1 + packages/sdk/js/src/v2/gen/types.gen.ts | 1 + packages/sdk/openapi.json | 3 +++ 23 files changed, 100 insertions(+), 35 deletions(-) diff --git a/.changeset/selected-organization-default.md b/.changeset/selected-organization-default.md index 1f4c262b29..78a9cec07c 100644 --- a/.changeset/selected-organization-default.md +++ b/.changeset/selected-organization-default.md @@ -3,4 +3,4 @@ "@kilocode/kilo-gateway": patch --- -Use the cloud-selected organization as the active Kilo account when provided. +Use cloud account preferences to select the active Kilo organization and hide unavailable personal accounts. diff --git a/packages/kilo-gateway/src/api/profile.ts b/packages/kilo-gateway/src/api/profile.ts index ae7d46eeed..fb3413ddf8 100644 --- a/packages/kilo-gateway/src/api/profile.ts +++ b/packages/kilo-gateway/src/api/profile.ts @@ -26,6 +26,7 @@ export async function fetchProfile(token: string): Promise { name?: string organizations?: Organization[] selectedOrganizationId?: string | null + hasPersonalAccount?: boolean | null } // Backend returns { user: { email, name, ... }, organizations } // Transform to flat KilocodeProfile structure @@ -34,6 +35,7 @@ export async function fetchProfile(token: string): Promise { name: data.user?.name ?? data.name, organizations: data.organizations, selectedOrganizationId: data.selectedOrganizationId ?? undefined, + hasPersonalAccount: data.hasPersonalAccount ?? undefined, } } diff --git a/packages/kilo-gateway/src/server/routes.ts b/packages/kilo-gateway/src/server/routes.ts index cc60ecc62c..177674cd18 100644 --- a/packages/kilo-gateway/src/server/routes.ts +++ b/packages/kilo-gateway/src/server/routes.ts @@ -102,6 +102,7 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { name: z.string().optional(), organizations: z.array(Organization).optional(), selectedOrganizationId: z.string().optional(), + hasPersonalAccount: z.boolean().optional(), }) const Balance = z.object({ diff --git a/packages/kilo-gateway/src/tui/helpers.ts b/packages/kilo-gateway/src/tui/helpers.ts index 6884312e49..cc7a72311a 100644 --- a/packages/kilo-gateway/src/tui/helpers.ts +++ b/packages/kilo-gateway/src/tui/helpers.ts @@ -56,6 +56,7 @@ export function formatProfileInfo( export function getOrganizationOptions( organizations: Organization[], currentOrgId?: string, + hasPersonalAccount = true, ): Array<{ title: string value: string | null @@ -63,12 +64,16 @@ export function getOrganizationOptions( category: string }> { return [ - { - title: "Personal Account", - value: null, - description: !currentOrgId ? "→ (current)" : undefined, - category: "Accounts", - }, + ...(hasPersonalAccount + ? [ + { + title: "Personal Account", + value: null, + description: !currentOrgId ? "→ (current)" : undefined, + category: "Accounts", + }, + ] + : []), ...organizations.map((org) => ({ title: org.name, value: org.id, diff --git a/packages/kilo-gateway/src/types.ts b/packages/kilo-gateway/src/types.ts index c9014f1bc2..3100b1e630 100644 --- a/packages/kilo-gateway/src/types.ts +++ b/packages/kilo-gateway/src/types.ts @@ -28,6 +28,7 @@ export interface KilocodeProfile { name?: string organizations?: Organization[] selectedOrganizationId?: string + hasPersonalAccount?: boolean } export interface KilocodeBalance { diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAppRpcApiImpl.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAppRpcApiImpl.kt index b593cb5dba..5e02bbb881 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAppRpcApiImpl.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAppRpcApiImpl.kt @@ -143,6 +143,7 @@ internal fun profileDto(p: KiloProfile200Response): ProfileDto = ProfileDto( organizations = p.profile.organizations.orEmpty().map { org -> ProfileOrganizationDto(id = org.id, name = org.name, role = org.role) }, + hasPersonalAccount = p.profile.hasPersonalAccount ?: true, balance = p.balance?.let { ProfileBalanceDto(balance = it.balance) }, kiloPass = p.kiloPass?.let { ProfileKiloPassDto( diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendAppServiceTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendAppServiceTest.kt index 7ba64fec23..5cb922a1d3 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendAppServiceTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendAppServiceTest.kt @@ -790,6 +790,7 @@ class KiloBackendAppServiceTest { "profile":{ "email":"alice@test.com", "name":"Alice", + "hasPersonalAccount":false, "organizations":[{"id":"org_1","name":"Acme","role":"ADMIN"}] }, "balance":{"balance":42.5}, @@ -804,6 +805,7 @@ class KiloBackendAppServiceTest { assertEquals("alice@test.com", dto.profile?.email) assertEquals("Alice", dto.profile?.name) assertEquals("ADMIN", dto.profile?.organizations?.firstOrNull()?.role) + assertFalse(dto.profile?.hasPersonalAccount ?: true) assertEquals(42.5, dto.profile?.balance?.balance) assertEquals("org_1", dto.profile?.currentOrgId) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/account/SessionAccountOverlay.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/account/SessionAccountOverlay.kt index 82a0429911..7a05e01202 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/account/SessionAccountOverlay.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/account/SessionAccountOverlay.kt @@ -124,7 +124,8 @@ internal class SessionAccountOverlay( var layout = false val orgs = prof.organizations - val next = listOf(AccountChoice(null, KiloBundle.message("profile.personalAccount"))) + + val personal = prof.hasPersonalAccount + val next = (if (personal) listOf(AccountChoice(null, KiloBundle.message("profile.personalAccount"))) else emptyList()) + orgs.map { org -> AccountChoice(org.id, org.name) } if (next != choices) { choices = next @@ -133,7 +134,7 @@ internal class SessionAccountOverlay( if (currentOrgId != prof.currentOrgId) currentOrgId = prof.currentOrgId - val activeId = if (switching) target else prof.currentOrgId + val activeId = if (switching) target else prof.currentOrgId ?: if (personal) null else orgs.firstOrNull()?.id val active = choices.firstOrNull { it.org == activeId } ?: choices.firstOrNull() val title = "${active?.title ?: " "} ▾" if (picker.text != title) { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/LoggedInProfileUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/LoggedInProfileUi.kt index 520aa26ae7..63fa8589d6 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/LoggedInProfileUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/LoggedInProfileUi.kt @@ -331,12 +331,11 @@ internal class LoggedInProfileUi( @RequiresEdt private fun applyOrganizations(profile: ProfileDto) { val orgs = profile.organizations - val keys: List> = listOf(null to KiloBundle.message("profile.personalAccount")) + + val personal = profile.hasPersonalAccount + val keys: List> = (if (personal) listOf(null to KiloBundle.message("profile.personalAccount")) else emptyList()) + orgs.map { it.id to it.name } - val target = profile.currentOrgId - ?.let { id -> orgs.indexOfFirst { it.id == id }.takeIf { it >= 0 }?.plus(1) } - ?: 0 + val target = keys.indexOfFirst { it.first == profile.currentOrgId }.takeIf { it >= 0 } ?: 0 currentOrgId = profile.currentOrgId diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/account/SessionAccountOverlayTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/account/SessionAccountOverlayTest.kt index 4aaa851181..428f682fb6 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/account/SessionAccountOverlayTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/account/SessionAccountOverlayTest.kt @@ -42,10 +42,12 @@ class SessionAccountOverlayTest : SessionControllerTestBase() { organizations: List = emptyList(), balance: ProfileBalanceDto? = null, currentOrgId: String? = null, + hasPersonalAccount: Boolean = true, ) = ProfileDto( email = email, name = name, organizations = organizations, + hasPersonalAccount = hasPersonalAccount, balance = balance, currentOrgId = currentOrgId, ) @@ -106,6 +108,22 @@ class SessionAccountOverlayTest : SessionControllerTestBase() { } } + fun `test profile without personal account hides personal choice`() { + val acme = org("org_1", "Acme", "MEMBER") + val prof = profile( + email = "user@example.com", + organizations = listOf(acme), + currentOrgId = "org_1", + hasPersonalAccount = false, + ) + show(snap(prof)) + edt { + assertEquals("Acme", panel.accountTitle()) + assertEquals(1, panel.choiceCount()) + assertEquals(0, panel.selectedIndex()) + } + } + // --- test 4: programmatic update does not call select callback --- fun `test programmatic update does not call select callback`() { diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/KiloAppStateDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/KiloAppStateDto.kt index cbc72c6db7..9fe4d903c5 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/KiloAppStateDto.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/KiloAppStateDto.kt @@ -155,6 +155,7 @@ data class ProfileDto( val email: String, val name: String? = null, val organizations: List = emptyList(), + val hasPersonalAccount: Boolean = true, val balance: ProfileBalanceDto? = null, val kiloPass: ProfileKiloPassDto? = null, val currentOrgId: String? = null, diff --git a/packages/kilo-vscode/src/services/cli-backend/types.ts b/packages/kilo-vscode/src/services/cli-backend/types.ts index 99f52c80d3..9ff83a941f 100644 --- a/packages/kilo-vscode/src/services/cli-backend/types.ts +++ b/packages/kilo-vscode/src/services/cli-backend/types.ts @@ -32,6 +32,7 @@ export interface KilocodeProfile { name?: string organizations?: KilocodeOrganization[] selectedOrganizationId?: string + hasPersonalAccount?: boolean } export interface KilocodeBalance { diff --git a/packages/kilo-vscode/webview-ui/src/components/profile/ProfileView.tsx b/packages/kilo-vscode/webview-ui/src/components/profile/ProfileView.tsx index 05c1cb1bcc..e108724703 100644 --- a/packages/kilo-vscode/webview-ui/src/components/profile/ProfileView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/profile/ProfileView.tsx @@ -65,20 +65,23 @@ const ProfileView: Component = (props) => { const orgOptions = createMemo(() => { const orgs = props.profileData?.profile.organizations ?? [] if (orgs.length === 0) return [] + const personal = props.profileData?.profile.hasPersonalAccount !== false return [ - { value: PERSONAL, label: language.t("profile.personalAccount") }, + ...(personal ? [{ value: PERSONAL, label: language.t("profile.personalAccount") }] : []), ...orgs.map((org) => ({ value: org.id, label: org.name, description: org.role })), ] }) const currentOrg = createMemo(() => { - const id = props.profileData?.currentOrgId ?? PERSONAL + const personal = props.profileData?.profile.hasPersonalAccount !== false + const id = props.profileData?.currentOrgId ?? (personal ? PERSONAL : orgOptions()[0]?.value) return orgOptions().find((o) => o.value === id) }) const selectOrg = (option: OrgOption | undefined) => { if (!option) return - const current = props.profileData?.currentOrgId ?? PERSONAL + const personal = props.profileData?.profile.hasPersonalAccount !== false + const current = props.profileData?.currentOrgId ?? (personal ? PERSONAL : orgOptions()[0]?.value) if (option.value === current) return setTarget(option.value) vscode.postMessage({ @@ -258,7 +261,13 @@ const ProfileView: Component = (props) => { {/* Kilo Pass is part of personal credits, so only show it on the personal account */} - + {(pass) => (
= (props) => { {/* No active Kilo Pass on the personal account — nudge to subscribe */} - +
= (props) => { const profile = () => server.profileData() const orgs = () => profile()?.profile.organizations ?? [] + const personal = () => profile()?.profile.hasPersonalAccount !== false const visible = () => !!profile() && orgs().length > 0 - const current = () => profile()?.currentOrgId ?? PERSONAL + const current = () => profile()?.currentOrgId ?? (personal() ? PERSONAL : (orgs()[0]?.id ?? PERSONAL)) const selected = createMemo(() => { const id = current() @@ -119,15 +120,17 @@ export const AccountSwitcher: Component<{ class?: string }> = (props) => { {/* Settings tabs */} diff --git a/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts b/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts index 0ee4d79232..b7a95fd62b 100644 --- a/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts +++ b/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts @@ -158,6 +158,14 @@ export function useSlashCommand( action: sandbox.action, enabled: sandbox.enabled, }, + { + name: "reload", + description: "Reload config, skills, agents, and commands from disk", + hints: ["refresh"], + action: () => { + vscode.postMessage({ type: "reload" }) + }, + }, ] const excluded = () => { diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index d4435bcf03..823fe3232b 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -1156,6 +1156,7 @@ export const dict = { "common.retry": "Retry", "common.refresh": "Refresh", + "common.reload": "Reload", "profile.title": "Profile", "profile.notLoggedIn": "Not logged in", diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts index 6bf6ea39e9..cc4fa03261 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts @@ -924,6 +924,10 @@ export interface RetryConnectionRequest { type: "retryConnection" } +export interface ReloadRequest { + type: "reload" +} + // Open a sub-agent session in a read-only editor panel export interface OpenSubAgentViewerRequest { type: "openSubAgentViewer" @@ -1333,6 +1337,7 @@ export type WebviewMessage = | DiffViewerSetBaseBranchRequest | DiffVirtualSetMarkdownRenderRequest | RetryConnectionRequest + | ReloadRequest | OpenSubAgentViewerRequest | PreviewImageRequest | SaveImageRequest diff --git a/packages/opencode/src/cli/cmd/tui/plugin/internal.ts b/packages/opencode/src/cli/cmd/tui/plugin/internal.ts index 1b9481664b..343bb5eeb3 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/internal.ts +++ b/packages/opencode/src/cli/cmd/tui/plugin/internal.ts @@ -15,6 +15,7 @@ import KiloSidebarPr from "@/kilocode/plugins/sidebar-pr" import KiloSidebarUsage from "@/kilocode/plugins/sidebar-usage" import KiloSandbox from "@/kilocode/plugins/sandbox" import KiloRemote from "@/kilocode/plugins/remote" +import KiloReload from "@/kilocode/plugins/reload" // kilocode_change end import SidebarContext from "../feature-plugins/sidebar/context" import SidebarMcp from "../feature-plugins/sidebar/mcp" @@ -54,6 +55,7 @@ export function internalTuiPlugins(flags: Pick { + api.keymap.registerLayer({ + commands: [ + { + namespace: "palette", + name: "app.reload", + title: "Reload", + desc: "Reload config, skills, agents, and commands from disk", + category: "System", + slashName: "reload", + async run() { + try { + await api.client.instance.reload({}, { throwOnError: true }) + api.ui.toast({ message: "Reloaded", variant: "success" }) + } catch (err) { + api.ui.toast({ message: String(err), variant: "error", duration: 5000 }) + } + }, + }, + ], + }) +} + +const plugin: TuiPluginModule & { id: string } = { id, tui } + +export default plugin diff --git a/packages/opencode/src/kilocode/server/httpapi/groups/instance-reload.ts b/packages/opencode/src/kilocode/server/httpapi/groups/instance-reload.ts new file mode 100644 index 0000000000..64d04a598d --- /dev/null +++ b/packages/opencode/src/kilocode/server/httpapi/groups/instance-reload.ts @@ -0,0 +1,49 @@ +import { Authorization } from "@/server/routes/instance/httpapi/middleware/authorization" +import { InstanceContextMiddleware } from "@/server/routes/instance/httpapi/middleware/instance-context" +import { + WorkspaceRoutingMiddleware, + WorkspaceRoutingQuery, +} from "@/server/routes/instance/httpapi/middleware/workspace-routing" +import { described } from "@/server/routes/instance/httpapi/groups/metadata" +import { ConflictError } from "@/server/routes/instance/httpapi/errors" +import { Schema } from "effect" +import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" + +export const ReloadPaths = { + reload: "/instance/reload", +} as const + +export const InstanceReloadApi = HttpApi.make("instance-reload") + .add( + HttpApiGroup.make("instance-reload") + .add( + HttpApiEndpoint.post("reload", ReloadPaths.reload, { + query: WorkspaceRoutingQuery, + success: described(Schema.Boolean, "Instance reloaded"), + error: ConflictError, + }).annotateMerge( + OpenApi.annotations({ + identifier: "instance.reload", + summary: "Reload instance", + description: + "Atomically dispose and reboot the current Kilo instance, reloading config, skills, agents, commands, and MCP prompts from disk. Returns 409 if a session is actively running.", + }), + ), + ) + .annotateMerge( + OpenApi.annotations({ + title: "instance-reload", + description: "Kilo instance reload route.", + }), + ) + .middleware(InstanceContextMiddleware) + .middleware(WorkspaceRoutingMiddleware) + .middleware(Authorization), + ) + .annotateMerge( + OpenApi.annotations({ + title: "kilo HttpApi", + version: "0.0.1", + description: "Kilo HttpApi surface.", + }), + ) diff --git a/packages/opencode/src/kilocode/server/httpapi/handlers/instance-reload.ts b/packages/opencode/src/kilocode/server/httpapi/handlers/instance-reload.ts new file mode 100644 index 0000000000..db95453a2a --- /dev/null +++ b/packages/opencode/src/kilocode/server/httpapi/handlers/instance-reload.ts @@ -0,0 +1,40 @@ +import * as InstanceState from "@/effect/instance-state" +import { SessionStatus } from "@/session/status" +import { ConflictError } from "@/server/routes/instance/httpapi/errors" +import { markInstanceForReload } from "@/server/routes/instance/httpapi/lifecycle" +import { InstanceHttpApi } from "@/server/routes/instance/httpapi/api" +import { Effect } from "effect" +import { HttpApiBuilder } from "effect/unstable/httpapi" +import type { SessionID } from "@/session/schema" + +export function hasBusySession(statuses: Map): boolean { + for (const info of statuses.values()) { + if (info.type === "busy") return true + } + return false +} + +export const instanceReloadHandlers = HttpApiBuilder.group(InstanceHttpApi, "instance-reload", (handlers) => + Effect.gen(function* () { + const status = yield* SessionStatus.Service + + const reload = Effect.fn("InstanceReloadHttpApi.reload")(function* () { + const ctx = yield* InstanceState.context + if (hasBusySession(yield* status.list())) { + return yield* Effect.fail( + new ConflictError({ + message: "Cannot reload while a session is running. Wait for it to finish or abort it first.", + }), + ) + } + yield* markInstanceForReload(ctx, { + directory: ctx.directory, + worktree: ctx.worktree, + project: ctx.project, + }) + return true + }) + + return handlers.handle("reload", reload) + }), +) diff --git a/packages/opencode/src/kilocode/server/httpapi/server.ts b/packages/opencode/src/kilocode/server/httpapi/server.ts index 9681c31d19..a6bd9210c8 100644 --- a/packages/opencode/src/kilocode/server/httpapi/server.ts +++ b/packages/opencode/src/kilocode/server/httpapi/server.ts @@ -15,6 +15,7 @@ import { commitMessageHandlers } from "./handlers/commit-message" import { configConsoleHandlers } from "./handlers/config-console" import { enhancePromptHandlers } from "./handlers/enhance-prompt" import { indexingHandlers } from "./handlers/indexing" +import { instanceReloadHandlers } from "./handlers/instance-reload" import { interactiveTerminalHandlers } from "./handlers/interactive-terminal" import { kiloGatewayHandlers } from "./handlers/kilo-gateway" import { kilocodeHandlers } from "./handlers/kilocode" @@ -35,6 +36,7 @@ export const provide = Layer.provide([ configConsoleHandlers, enhancePromptHandlers, indexingHandlers, + instanceReloadHandlers, interactiveTerminalHandlers, kiloGatewayHandlers, kilocodeHandlers, diff --git a/packages/opencode/src/server/routes/instance/httpapi/api.ts b/packages/opencode/src/server/routes/instance/httpapi/api.ts index 984f9097c3..ab1edae6be 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/api.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/api.ts @@ -28,6 +28,7 @@ import { BackgroundProcessApi } from "@/kilocode/server/httpapi/groups/backgroun import { ConfigConsoleApi } from "@/kilocode/server/httpapi/groups/config-console" import { EnhancePromptApi } from "@/kilocode/server/httpapi/groups/enhance-prompt" import { IndexingApi } from "@/kilocode/server/httpapi/groups/indexing" +import { InstanceReloadApi } from "@/kilocode/server/httpapi/groups/instance-reload" import { InteractiveTerminalApi } from "@/kilocode/server/httpapi/groups/interactive-terminal" import { KiloGatewayApi } from "@/kilocode/server/httpapi/groups/kilo-gateway" import { KilocodeApi } from "@/kilocode/server/httpapi/groups/kilocode" @@ -76,6 +77,7 @@ export const InstanceHttpApi = HttpApi.make("opencode-instance") .addHttpApi(ConfigConsoleApi) .addHttpApi(EnhancePromptApi) .addHttpApi(IndexingApi) + .addHttpApi(InstanceReloadApi) .addHttpApi(InteractiveTerminalApi) .addHttpApi(KiloGatewayApi) .addHttpApi(KilocodeApi) diff --git a/packages/opencode/test/kilocode/instance-reload.test.ts b/packages/opencode/test/kilocode/instance-reload.test.ts new file mode 100644 index 0000000000..b537811bcc --- /dev/null +++ b/packages/opencode/test/kilocode/instance-reload.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "bun:test" +import { hasBusySession } from "@/kilocode/server/httpapi/handlers/instance-reload" +import type { SessionStatus } from "@/session/status" +import { SessionID } from "@/session/schema" + +const entries = (items: [string, SessionStatus.Info][]) => + items.map(([k, v]) => [k as SessionID, v] as [SessionID, SessionStatus.Info]) + +describe("instance-reload hasBusySession", () => { + it("returns false for an empty map", () => { + expect(hasBusySession(new Map(entries([])))).toBe(false) + }) + + it("returns false when all sessions are idle", () => { + expect( + hasBusySession( + new Map( + entries([ + ["s1", { type: "idle" }], + ["s2", { type: "idle" }], + ]), + ), + ), + ).toBe(false) + }) + + it("returns true when any session is busy", () => { + expect( + hasBusySession( + new Map( + entries([ + ["s1", { type: "idle" }], + ["s2", { type: "busy" }], + ]), + ), + ), + ).toBe(true) + }) + + it("returns true when the only session is busy", () => { + expect(hasBusySession(new Map(entries([["s1", { type: "busy" }]])))).toBe(true) + }) +}) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index c315c6edc8..fd6e3cfc52 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -134,6 +134,8 @@ import type { IndexingWarningsResponses, InstanceDisposeErrors, InstanceDisposeResponses, + InstanceReloadErrors, + InstanceReloadResponses, InteractiveTerminalCloseErrors, InteractiveTerminalCloseResponses, InteractiveTerminalGetErrors, @@ -2257,6 +2259,36 @@ export class Instance extends HeyApiClient { ...params, }) } + + /** + * Reload instance + * + * Atomically dispose and reboot the current Kilo instance, reloading config, skills, agents, commands, and MCP prompts from disk. Returns 409 if a session is actively running. + */ + public reload( + 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).post({ + url: "/instance/reload", + ...options, + ...params, + }) + } } export class Path extends HeyApiClient { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 5282b260f4..05b86ba1d5 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2571,6 +2571,12 @@ export type KiloEmbeddingModelCatalog = { } } +export type ConflictError = { + _tag: "ConflictError" + message: string + resource?: string +} + export type InteractiveTerminalSnapshot = { info: InteractiveTerminalInfo output: string @@ -10588,6 +10594,38 @@ export type IndexingModelsResponses = { export type IndexingModelsResponse = IndexingModelsResponses[keyof IndexingModelsResponses] +export type InstanceReloadData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/instance/reload" +} + +export type InstanceReloadErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * ConflictError + */ + 409: ConflictError +} + +export type InstanceReloadError = InstanceReloadErrors[keyof InstanceReloadErrors] + +export type InstanceReloadResponses = { + /** + * Instance reloaded + */ + 200: boolean +} + +export type InstanceReloadResponse = InstanceReloadResponses[keyof InstanceReloadResponses] + export type InteractiveTerminalListData = { body?: never path?: never diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 267d53bb1d..e99bbffe08 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -13085,6 +13085,71 @@ ] } }, + "/instance/reload": { + "post": { + "tags": ["instance-reload"], + "operationId": "instance.reload", + "parameters": [ + { + "name": "directory", + "in": "query", + "schema": { + "type": "string" + }, + "required": false + }, + { + "name": "workspace", + "in": "query", + "schema": { + "type": "string" + }, + "required": false + } + ], + "responses": { + "200": { + "description": "Instance reloaded", + "content": { + "application/json": { + "schema": { + "type": "boolean", + "description": "Instance reloaded" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "409": { + "description": "ConflictError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ConflictError" + } + } + } + } + }, + "description": "Atomically dispose and reboot the current Kilo instance, reloading config, skills, agents, commands, and MCP prompts from disk. Returns 409 if a session is actively running.", + "summary": "Reload instance", + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\"\n\nconst client = createKiloClient()\nawait client.instance.reload({\n ...\n})" + } + ] + } + }, "/interactive-terminal": { "get": { "tags": ["interactive-terminal"], @@ -27801,6 +27866,23 @@ "required": ["defaultModel", "models", "aliases"], "additionalProperties": false }, + "ConflictError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["ConflictError"] + }, + "message": { + "type": "string" + }, + "resource": { + "type": "string" + } + }, + "required": ["_tag", "message"], + "additionalProperties": false + }, "InteractiveTerminalSnapshot": { "type": "object", "properties": { @@ -36967,6 +37049,10 @@ "name": "indexing", "description": "Kilo indexing routes." }, + { + "name": "instance-reload", + "description": "Kilo instance reload route." + }, { "name": "interactive-terminal", "description": "Kilo human-driven interactive terminal routes." From 64deacf66c59224b7c54a8995ed1540424d8e18a Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 7 Jul 2026 12:45:11 +0200 Subject: [PATCH 20/68] feat: translate common.reload, use circular reload icon, add tooltip - Add common.reload translations for all 18 non-English locales - Add a circular reload icon to kilo-ui's icon map (the standard two-arrow refresh symbol) and use it on both the task header and settings reload buttons instead of the reset (undo) icon - Add a tooltip to the settings reload button reusing common.reload, matching the existing tooltip on the task header button --- packages/kilo-ui/src/components/icon.tsx | 4 ++++ .../webview-ui/src/components/chat/TaskHeader.tsx | 2 +- .../src/components/settings/Settings.tsx | 15 +++++++-------- packages/kilo-vscode/webview-ui/src/i18n/ar.ts | 1 + packages/kilo-vscode/webview-ui/src/i18n/br.ts | 1 + packages/kilo-vscode/webview-ui/src/i18n/bs.ts | 1 + packages/kilo-vscode/webview-ui/src/i18n/da.ts | 1 + packages/kilo-vscode/webview-ui/src/i18n/de.ts | 1 + packages/kilo-vscode/webview-ui/src/i18n/es.ts | 1 + packages/kilo-vscode/webview-ui/src/i18n/fr.ts | 1 + packages/kilo-vscode/webview-ui/src/i18n/it.ts | 1 + packages/kilo-vscode/webview-ui/src/i18n/ja.ts | 1 + packages/kilo-vscode/webview-ui/src/i18n/ko.ts | 1 + packages/kilo-vscode/webview-ui/src/i18n/nl.ts | 1 + packages/kilo-vscode/webview-ui/src/i18n/no.ts | 1 + packages/kilo-vscode/webview-ui/src/i18n/pl.ts | 1 + packages/kilo-vscode/webview-ui/src/i18n/ru.ts | 1 + packages/kilo-vscode/webview-ui/src/i18n/th.ts | 1 + packages/kilo-vscode/webview-ui/src/i18n/tr.ts | 1 + packages/kilo-vscode/webview-ui/src/i18n/uk.ts | 1 + packages/kilo-vscode/webview-ui/src/i18n/zh.ts | 1 + packages/kilo-vscode/webview-ui/src/i18n/zht.ts | 1 + 22 files changed, 31 insertions(+), 9 deletions(-) diff --git a/packages/kilo-ui/src/components/icon.tsx b/packages/kilo-ui/src/components/icon.tsx index 773d17d1f5..87172ed841 100644 --- a/packages/kilo-ui/src/components/icon.tsx +++ b/packages/kilo-ui/src/components/icon.tsx @@ -42,6 +42,10 @@ const icons: Record = { viewBox: "0 0 16 16", path: ``, }, + reload: { + viewBox: "0 0 24 24", + path: ``, + }, } type Name = keyof typeof icons diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx index cd43ce3280..cccaa7ab72 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx @@ -214,7 +214,7 @@ export const TaskHeader: Component = (props) => { vscode.postMessage({ type: "reload" })} diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx index 9564a0fe75..e65e7005ed 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/Settings.tsx @@ -2,6 +2,7 @@ import { Component, createSignal, createEffect, createMemo, on, Show } from "sol import { Icon } from "@kilocode/kilo-ui/icon" import { Tabs } from "@kilocode/kilo-ui/tabs" import { Button } from "@kilocode/kilo-ui/button" +import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { showToast } from "@kilocode/kilo-ui/toast" import { useVSCode } from "../../context/vscode" import { useLanguage } from "../../context/language" @@ -143,14 +144,12 @@ const Settings: Component = (props) => { - + + +
{/* Settings tabs */} diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index a87c8ac9b6..29504be472 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -1183,6 +1183,7 @@ export const dict = { "common.retry": "إعادة المحاولة", "common.refresh": "تحديث", + "common.reload": "إعادة التحميل", "profile.title": "الملف الشخصي", "profile.notLoggedIn": "لم يتم تسجيل الدخول", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 5daf082020..c19f9f4aa1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -1204,6 +1204,7 @@ export const dict = { "common.retry": "Tentar novamente", "common.refresh": "Atualizar", + "common.reload": "Recarregar", "profile.title": "Perfil", "profile.notLoggedIn": "Não conectado", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index a5a894c7f0..5aa114866a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -1245,6 +1245,7 @@ export const dict = { "common.retry": "Pokušaj ponovo", "common.refresh": "Osvježi", + "common.reload": "Učitaj ponovo", "profile.title": "Profil", "profile.notLoggedIn": "Niste prijavljeni", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index 14b21e2ab8..8a8c840097 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -1238,6 +1238,7 @@ export const dict = { "common.retry": "Prøv igen", "common.refresh": "Opdatér", + "common.reload": "Genindlæs", "profile.title": "Profil", "profile.notLoggedIn": "Ikke logget ind", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 7c3ebe2962..8e2eb24669 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -1259,6 +1259,7 @@ export const dict = { "common.retry": "Erneut versuchen", "common.refresh": "Aktualisieren", + "common.reload": "Neu laden", "profile.title": "Profil", "profile.notLoggedIn": "Nicht angemeldet", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index a5bf4b39bd..58221c028d 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -1252,6 +1252,7 @@ export const dict = { "common.retry": "Reintentar", "common.refresh": "Actualizar", + "common.reload": "Recargar", "profile.title": "Perfil", "profile.notLoggedIn": "No conectado", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index e793315324..df7e722b0b 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -1262,6 +1262,7 @@ export const dict = { "common.retry": "Réessayer", "common.refresh": "Actualiser", + "common.reload": "Recharger", "profile.title": "Profil", "profile.notLoggedIn": "Non connecté", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index 5e1079426a..dba78c79c9 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -1006,6 +1006,7 @@ export const dict = { "deviceAuth.error.detailsTitle": "Dettagli errore accesso", "common.retry": "Riprova", "common.refresh": "Aggiorna", + "common.reload": "Ricarica", "profile.title": "Profilo", "profile.notLoggedIn": "Accesso non effettuato", "profile.action.login": "Accedi con Kilo Code", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 5483784d61..e51d83a2ea 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -1233,6 +1233,7 @@ export const dict = { "common.retry": "再試行", "common.refresh": "更新", + "common.reload": "再読み込み", "profile.title": "プロフィール", "profile.notLoggedIn": "ログインしていません", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 1c68d5d743..d07c77309f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -1190,6 +1190,7 @@ export const dict = { "common.retry": "재시도", "common.refresh": "새로고침", + "common.reload": "새로고침", "profile.title": "프로필", "profile.notLoggedIn": "로그인하지 않음", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index 845b5d5a16..968029377c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -1198,6 +1198,7 @@ export const dict = { "common.retry": "Opnieuw proberen", "common.refresh": "Vernieuwen", + "common.reload": "Herladen", "profile.title": "Profiel", "profile.notLoggedIn": "Niet ingelogd", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 775d9dc7e6..f31d7ad458 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -1202,6 +1202,7 @@ export const dict = { "common.retry": "Prøv igjen", "common.refresh": "Oppdater", + "common.reload": "Last inn på nytt", "profile.title": "Profil", "profile.notLoggedIn": "Ikke pålogget", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index d3fa876fb7..91201db107 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -1201,6 +1201,7 @@ export const dict = { "common.retry": "Ponów", "common.refresh": "Odśwież", + "common.reload": "Przeładuj", "profile.title": "Profil", "profile.notLoggedIn": "Nie zalogowano", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 6f616a81d8..1655da1576 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -1244,6 +1244,7 @@ export const dict = { "common.retry": "Повторить", "common.refresh": "Обновить", + "common.reload": "Перезагрузить", "profile.title": "Профиль", "profile.notLoggedIn": "Не авторизован", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index e5ec7f48ed..ade94cd672 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -1226,6 +1226,7 @@ export const dict = { "common.retry": "ลองอีกครั้ง", "common.refresh": "รีเฟรช", + "common.reload": "โหลดใหม่", "profile.title": "โปรไฟล์", "profile.notLoggedIn": "ยังไม่ได้เข้าสู่ระบบ", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index e57d018906..a0e4038b14 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -1195,6 +1195,7 @@ export const dict = { "common.retry": "Tekrar Dene", "common.refresh": "Yenile", + "common.reload": "Yeniden yükle", "profile.title": "Profil", "profile.notLoggedIn": "Giriş yapılmadı", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index 06c0f00dfa..d0609a1dea 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -1193,6 +1193,7 @@ export const dict = { "common.retry": "Спробувати ще раз", "common.refresh": "Оновити", + "common.reload": "Перезавантажити", "profile.title": "Профіль", "profile.notLoggedIn": "Не увійшов", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 3f0eafa95e..50a25a2ede 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -1206,6 +1206,7 @@ export const dict = { "common.retry": "重试", "common.refresh": "刷新", + "common.reload": "重新加载", "profile.title": "个人资料", "profile.notLoggedIn": "未登录", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index d50689f1f7..3659703f72 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -1170,6 +1170,7 @@ export const dict = { "common.retry": "重試", "common.refresh": "重新整理", + "common.reload": "重新載入", "profile.title": "個人資料", "profile.notLoggedIn": "尚未登入", From 1c31d7ea4925fd9993bb127abe67b6cfeee06f32 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 7 Jul 2026 13:22:30 +0200 Subject: [PATCH 21/68] fix: address review feedback on reload handler - Block reload for retry and offline session states, not just busy (hasActiveSession rejects any non-idle state) - Close TOCTOU race by calling store.reload inline instead of deferring to a pre-response handler, so the busy check and reload run in the same effect - Avoid double-firing reloadAfterAuthChange: only call it eagerly for worktree sessions where the SSE directory check won't match; for the workspace root the existing SSE handler already fires it - Replace err: any with unknown + narrowing - Add in-flight guard to the CLI /reload plugin to prevent overlapping reloads from rapid double-invocation - Remove the TaskHeader reload button (redundant with /reload slash command, avoids chat visual regression baseline churn) --- packages/kilo-vscode/src/KiloProvider.ts | 11 ++++-- .../src/components/chat/TaskHeader.tsx | 9 ----- .../opencode/src/kilocode/plugins/reload.tsx | 5 +++ .../httpapi/handlers/instance-reload.ts | 11 +++--- .../test/kilocode/instance-reload.test.ts | 38 +++++++++++++++---- 5 files changed, 49 insertions(+), 25 deletions(-) diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index c044c6f637..8358930f05 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -3620,8 +3620,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper const dir = this.getWorkspaceDirectory(this.currentSession?.id) try { await this.client.instance.reload({ directory: dir }, { throwOnError: true }) - } catch (err: any) { - const status = err?.response?.status + } catch (err) { + const status = + err && typeof err === "object" && "response" in err + ? (err as { response?: { status?: number } }).response?.status + : undefined if (status === 409) { vscode.window.showWarningMessage( "Cannot reload while a session is running. Wait for it to finish or abort it first.", @@ -3633,7 +3636,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper return } this.clearCommandsCache() - await this.reloadAfterAuthChange() + if (!sameDirectory(dir, this.getWorkspaceDirectory())) { + await this.reloadAfterAuthChange() + } } /** Public reload entry point for VS Code commands. */ diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx index cccaa7ab72..96ac022f93 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx @@ -212,15 +212,6 @@ export const TaskHeader: Component = (props) => { aria-label={language.t("command.session.compact")} /> - - vscode.postMessage({ type: "reload" })} - aria-label={language.t("common.reload")} - /> -
- +
- - - - {(path, index) => ( -
- + {(path, index) => ( +
- {path} - - removePath(index())} /> -
- )} - -
- + + {path} + + removePath(index())} /> + + )} +
+ + + ) } diff --git a/packages/opencode/test/kilocode/config/config.test.ts b/packages/opencode/test/kilocode/config/config.test.ts index e405398721..467282a346 100644 --- a/packages/opencode/test/kilocode/config/config.test.ts +++ b/packages/opencode/test/kilocode/config/config.test.ts @@ -242,6 +242,41 @@ describe("kilocode indexing config", () => { }) }) +describe("kilocode sandbox writable paths config", () => { + test("honors sandbox_writable_paths from global config only, ignoring project config", async () => { + await using globalTmp = await tmpdir() + await using tmp = await tmpdir({ git: true }) + + const prev = Global.Path.config + ;(Global.Path as { config: string }).config = globalTmp.path + await clear() + await disposeAllInstances() + + try { + await writeConfig(globalTmp.path, { + $schema: "https://app.kilo.ai/config.json", + experimental: { sandbox_writable_paths: ["/tmp/global"] }, + }) + // A project kilo.json must not widen the sandbox: its writable paths are dropped at merge time. + await writeConfig(tmp.path, { + experimental: { sandbox_writable_paths: ["/tmp/project"] }, + }) + + await provideTestInstance({ + directory: tmp.path, + fn: async () => { + const config = await load() + expect(config.experimental?.sandbox_writable_paths).toEqual(["/tmp/global"]) + }, + }) + } finally { + ;(Global.Path as { config: string }).config = prev + await clear() + await disposeAllInstances() + } + }) +}) + describe("custom provider model config", () => { test("persists and removes reasoning across a global config reload", async () => { await using globalTmp = await tmpdir() diff --git a/packages/opencode/test/kilocode/sandbox/policy.test.ts b/packages/opencode/test/kilocode/sandbox/policy.test.ts index 0352352699..240c1e1f38 100644 --- a/packages/opencode/test/kilocode/sandbox/policy.test.ts +++ b/packages/opencode/test/kilocode/sandbox/policy.test.ts @@ -234,4 +234,34 @@ describe("sandbox policy", () => { expect(roots(ctx)).not.toContain(dirs.approved) expect(Exit.isFailure(result)).toBe(true) }) + + test("makes configured extra writable paths writable while unlisted paths stay denied", async () => { + await using tmp = await fixture() + const dirs = tmp.extra + const ctx = context(dirs.a, dirs.a, dirs) + const policy = profile(ctx, "deny", [dirs.approved]) + const result = await Effect.runPromise( + Effect.all({ + extra: runSandbox(policy, assertWrite(path.join(dirs.approved, "allowed.txt")).pipe(Effect.exit)), + other: runSandbox(policy, assertWrite(path.join(dirs.b, "denied.txt")).pipe(Effect.exit)), + }), + ) + + expect(policy.filesystem.allowWrite.map((rule) => rule.path)).toContain(dirs.approved) + expect(roots(ctx)).not.toContain(dirs.approved) + expect(Exit.isSuccess(result.extra)).toBe(true) + expect(Exit.isFailure(result.other)).toBe(true) + }) + + test("keeps .git denied inside a configured extra writable path", async () => { + await using tmp = await fixture() + const dirs = tmp.extra + const ctx = context(dirs.a, dirs.a, dirs) + const policy = profile(ctx, "deny", [dirs.approved]) + const result = await Effect.runPromise( + runSandbox(policy, assertWrite(path.join(dirs.approved, ".git", "config")).pipe(Effect.exit)), + ) + + expect(Exit.isFailure(result)).toBe(true) + }) }) From d70977fd92dba79a52176a1a7ddaa4cd11a7de00 Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 7 Jul 2026 14:38:02 +0200 Subject: [PATCH 34/68] test: cover Kilo account selection defaults --- packages/kilo-gateway/src/index.ts | 1 + packages/kilo-gateway/src/server/handlers.ts | 36 +++++++---- .../kilo-gateway/test/server/handlers.test.ts | 59 +++++++++++++++++++ .../src/components/profile/ProfileView.tsx | 8 +-- .../components/dialog-kilo-auto-method.tsx | 12 ++-- .../server/httpapi/handlers/kilo-gateway.ts | 16 ++--- 6 files changed, 102 insertions(+), 30 deletions(-) create mode 100644 packages/kilo-gateway/test/server/handlers.test.ts diff --git a/packages/kilo-gateway/src/index.ts b/packages/kilo-gateway/src/index.ts index 850047c4dd..034902c614 100644 --- a/packages/kilo-gateway/src/index.ts +++ b/packages/kilo-gateway/src/index.ts @@ -75,6 +75,7 @@ export { getCloudSessions, getNotifications, getProfile, + resolveProfileOrganization, getToken, normalizeClawStatus, setOrganization, diff --git a/packages/kilo-gateway/src/server/handlers.ts b/packages/kilo-gateway/src/server/handlers.ts index ac8b5d2907..dc0a5424fd 100644 --- a/packages/kilo-gateway/src/server/handlers.ts +++ b/packages/kilo-gateway/src/server/handlers.ts @@ -36,6 +36,11 @@ export interface OrganizationDeps { dispose(): Promise } +export interface OrganizationResolution { + currentOrgId: string | null + persistOrgId: string | null +} + export interface CloudSessionsInput { cursor?: string limit?: number @@ -64,6 +69,22 @@ export function getOrganizationId(auth: KiloAuth | undefined) { return undefined } +export function resolveProfileOrganization(profile: KilocodeProfile, auth: Extract): OrganizationResolution { + const selected = profile.selectedOrganizationId + const orgs = profile.organizations ?? [] + const valid = selected && orgs.some((org) => org.id === selected) ? selected : undefined + const cloud = valid ?? (profile.hasPersonalAccount === false ? orgs[0]?.id : undefined) + if (auth.accountSelection === "manual" && (auth.accountId || profile.hasPersonalAccount !== false)) { + return { currentOrgId: auth.accountId ?? null, persistOrgId: null } + } + + const local = auth.accountSelection === "cloud" ? undefined : auth.accountId + const currentOrgId = local ?? cloud ?? auth.accountId ?? null + const invalid = auth.accountSelection === "manual" && !auth.accountId && profile.hasPersonalAccount === false + const persistOrgId = currentOrgId && !local && (auth.accountSelection !== "manual" || invalid) && currentOrgId !== auth.accountId ? currentOrgId : null + return { currentOrgId, persistOrgId } +} + export async function getProfile(auth: AuthStore): Promise { const info = await auth.get("kilo") if (!info || info.type !== "oauth") throw new UnauthorizedError("Not authenticated with Kilo Gateway") @@ -73,25 +94,20 @@ export async function getProfile(auth: AuthStore): Promise { fetchKiloPassState(info.access), ]) - const selected = profile.selectedOrganizationId - const orgs = profile.organizations ?? [] - const valid = selected && orgs.some((org) => org.id === selected) ? selected : undefined - const cloud = valid ?? (profile.hasPersonalAccount === false ? orgs[0]?.id : undefined) - const local = info.accountSelection === "cloud" ? undefined : info.accountId - const currentOrgId = info.accountSelection === "manual" ? (info.accountId ?? null) : (local ?? cloud ?? info.accountId ?? null) - if (currentOrgId && !local && info.accountSelection !== "manual" && currentOrgId !== info.accountId) { + const org = resolveProfileOrganization(profile, info) + if (org.persistOrgId) { await auth.set("kilo", { type: "oauth", refresh: info.refresh, access: info.access, expires: info.expires, - accountId: currentOrgId, + accountId: org.persistOrgId, accountSelection: "cloud", }).catch((err) => console.warn("Failed to persist cloud account selection:", err)) } - const balance = await fetchBalance(info.access, currentOrgId ?? undefined) - return { profile, balance, kiloPass, currentOrgId } + const balance = await fetchBalance(info.access, org.currentOrgId ?? undefined) + return { profile, balance, kiloPass, currentOrgId: org.currentOrgId } } export async function getNotifications(auth: AuthStore) { diff --git a/packages/kilo-gateway/test/server/handlers.test.ts b/packages/kilo-gateway/test/server/handlers.test.ts new file mode 100644 index 0000000000..0b888c9ca8 --- /dev/null +++ b/packages/kilo-gateway/test/server/handlers.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, test } from "bun:test" +import { resolveProfileOrganization, type KiloAuth } from "../../src/server/handlers.js" +import type { KilocodeProfile } from "../../src/types.js" + +const oauth = (input: Partial> = {}): Extract => ({ + type: "oauth", + access: "token", + refresh: "token", + expires: 1, + ...input, +}) + +const profile = (input: Partial = {}): KilocodeProfile => ({ + email: "user@example.com", + organizations: [{ id: "org_1", name: "Acme", role: "MEMBER" }], + ...input, +}) + +describe("resolveProfileOrganization", () => { + test("uses cloud selected organization only when no local selection exists", () => { + expect(resolveProfileOrganization(profile({ selectedOrganizationId: "org_1" }), oauth())).toEqual({ + currentOrgId: "org_1", + persistOrgId: "org_1", + }) + }) + + test("preserves a manual organization selection over the cloud default", () => { + expect( + resolveProfileOrganization( + profile({ + selectedOrganizationId: "org_1", + organizations: [ + { id: "org_1", name: "Acme", role: "MEMBER" }, + { id: "org_2", name: "Beta", role: "MEMBER" }, + ], + }), + oauth({ accountId: "org_2", accountSelection: "manual" }), + ), + ).toEqual({ currentOrgId: "org_2", persistOrgId: null }) + }) + + test("preserves explicit personal selection when personal is available", () => { + expect( + resolveProfileOrganization( + profile({ selectedOrganizationId: "org_1", hasPersonalAccount: true }), + oauth({ accountSelection: "manual" }), + ), + ).toEqual({ currentOrgId: null, persistOrgId: null }) + }) + + test("uses the first organization when personal is unavailable and no valid cloud selection exists", () => { + expect( + resolveProfileOrganization( + profile({ selectedOrganizationId: "missing", hasPersonalAccount: false }), + oauth({ accountSelection: "manual" }), + ), + ).toEqual({ currentOrgId: "org_1", persistOrgId: "org_1" }) + }) +}) diff --git a/packages/kilo-vscode/webview-ui/src/components/profile/ProfileView.tsx b/packages/kilo-vscode/webview-ui/src/components/profile/ProfileView.tsx index bed2bfbc98..436784c75c 100644 --- a/packages/kilo-vscode/webview-ui/src/components/profile/ProfileView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/profile/ProfileView.tsx @@ -44,6 +44,8 @@ const ProfileView: Component = (props) => { const language = useLanguage() const [target, setTarget] = createSignal(null) + const personal = createMemo(() => props.profileData?.profile.hasPersonalAccount !== false) + // Always fetch fresh profile+balance when navigating to this view onMount(() => { vscode.postMessage({ type: "refreshProfile" }) @@ -58,16 +60,14 @@ const ProfileView: Component = (props) => { const orgOptions = createMemo(() => { const orgs = props.profileData?.profile.organizations ?? [] if (orgs.length === 0) return [] - const personal = props.profileData?.profile.hasPersonalAccount !== false return [ - ...(personal ? [{ value: PERSONAL, label: language.t("profile.personalAccount") }] : []), + ...(personal() ? [{ value: PERSONAL, label: language.t("profile.personalAccount") }] : []), ...orgs.map((org) => ({ value: org.id, label: org.name, description: org.role })), ] }) const currentId = createMemo(() => { - const personal = props.profileData?.profile.hasPersonalAccount !== false - return props.profileData?.currentOrgId ?? (personal ? PERSONAL : orgOptions()[0]?.value) + return props.profileData?.currentOrgId ?? (personal() ? PERSONAL : orgOptions()[0]?.value) }) const switching = createMemo(() => { diff --git a/packages/opencode/src/kilocode/components/dialog-kilo-auto-method.tsx b/packages/opencode/src/kilocode/components/dialog-kilo-auto-method.tsx index 533571d02e..239ea52eec 100644 --- a/packages/opencode/src/kilocode/components/dialog-kilo-auto-method.tsx +++ b/packages/opencode/src/kilocode/components/dialog-kilo-auto-method.tsx @@ -83,12 +83,12 @@ export function KiloAutoMethod(props: KiloAutoMethodProps) { await sync.bootstrap() dialog.replace(() => ( - diff --git a/packages/opencode/src/kilocode/server/httpapi/handlers/kilo-gateway.ts b/packages/opencode/src/kilocode/server/httpapi/handlers/kilo-gateway.ts index bd610d3595..7e4d9968c3 100644 --- a/packages/opencode/src/kilocode/server/httpapi/handlers/kilo-gateway.ts +++ b/packages/opencode/src/kilocode/server/httpapi/handlers/kilo-gateway.ts @@ -7,6 +7,7 @@ import { getToken, importSessionToDb, normalizeClawStatus, + resolveProfileOrganization, } from "@kilocode/kilo-gateway" import { HEADER_FEATURE, @@ -76,28 +77,23 @@ export const kiloGatewayHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilo", catch: () => new HttpApiError.BadRequest({}), }) - const selected = profile.selectedOrganizationId - const orgs = profile.organizations ?? [] - const valid = selected && orgs.some((org) => org.id === selected) ? selected : undefined - const cloud = valid ?? (profile.hasPersonalAccount === false ? orgs[0]?.id : undefined) - const local = info.accountSelection === "cloud" ? undefined : info.accountId - const currentOrgId = info.accountSelection === "manual" ? (info.accountId ?? null) : (local ?? cloud ?? info.accountId ?? null) - if (currentOrgId && !local && info.accountSelection !== "manual" && currentOrgId !== info.accountId) { + const org = resolveProfileOrganization(profile, info) + if (org.persistOrgId) { yield* auth.set("kilo", { type: "oauth", refresh: info.refresh, access: info.access, expires: info.expires, - accountId: currentOrgId, + accountId: org.persistOrgId, accountSelection: "cloud", }).pipe(Effect.catch((err) => Effect.sync(() => log.warn("failed to persist cloud account selection", { err })))) } const balance = yield* Effect.tryPromise({ - try: () => fetchBalance(info.access, currentOrgId ?? undefined), + try: () => fetchBalance(info.access, org.currentOrgId ?? undefined), catch: () => new HttpApiError.BadRequest({}), }) - return { profile, balance, kiloPass, currentOrgId } + return { profile, balance, kiloPass, currentOrgId: org.currentOrgId } }) const authStatus = Effect.fn("KiloGatewayHttpApi.authStatus")(function* () { From 21848889cdee0f0e485780bd2fd97be1cc68ef61 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 7 Jul 2026 14:41:05 +0200 Subject: [PATCH 35/68] feat(vscode): render read-tool images inline in chat view The read tool already produces base64 data URL attachments for image files and threads them through SSE into the webview part store, but the tool renderer never received them. Thread part.state.attachments into ToolProps and render image attachments inline below the read tool card, reusing the existing user-attachment image pattern with click-to-open preview. No backend, schema, or SDK changes needed. --- .changeset/inline-read-images.md | 5 ++++ .../kilo-ui/src/components/message-part.css | 29 +++++++++++++++++++ .../kilo-ui/src/components/message-part.tsx | 25 ++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 .changeset/inline-read-images.md diff --git a/.changeset/inline-read-images.md b/.changeset/inline-read-images.md new file mode 100644 index 0000000000..7d4a511f0d --- /dev/null +++ b/.changeset/inline-read-images.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Render images inline in the chat view when the agent reads an image file. Images appear below the read tool card and can be clicked to open a full-size preview. diff --git a/packages/kilo-ui/src/components/message-part.css b/packages/kilo-ui/src/components/message-part.css index 5a3fd732da..d55bc2c4ce 100644 --- a/packages/kilo-ui/src/components/message-part.css +++ b/packages/kilo-ui/src/components/message-part.css @@ -412,6 +412,35 @@ html[data-theme="kilo-vscode"] [data-component="bash-output"] { margin-bottom: 0px; } +[data-slot="tool-read-images"] { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding-top: 8px; + max-width: min(100%, 64ch); +} + +[data-slot="tool-read-image"] { + max-width: 200px; + max-height: 200px; + border-radius: var(--radius-sm, 6px); + overflow: hidden; + border: 1px solid var(--border-weak-base); + cursor: pointer; + transition: border-color 0.15s ease; + + &:hover { + border-color: var(--border-strong-base); + } +} + +[data-slot="tool-read-image-img"] { + display: block; + max-width: 100%; + max-height: 200px; + object-fit: contain; +} + [data-component="todos"] { [data-slot="message-part-todo-hidden"] { padding: 2px 0 2px 28px; diff --git a/packages/kilo-ui/src/components/message-part.tsx b/packages/kilo-ui/src/components/message-part.tsx index 7fca610608..a5ab17ed51 100644 --- a/packages/kilo-ui/src/components/message-part.tsx +++ b/packages/kilo-ui/src/components/message-part.tsx @@ -995,6 +995,7 @@ export interface ToolProps { callID?: string output?: string status?: string + attachments?: FilePart[] hideDetails?: boolean defaultOpen?: boolean forceOpen?: boolean @@ -1243,6 +1244,8 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { // @ts-expect-error output={part.state.output} status={part.state.status} + // @ts-expect-error + attachments={part.state.attachments} hideDetails={props.hideDetails} defaultOpen={props.defaultOpen} animate @@ -1746,6 +1749,7 @@ ToolRegistry.register({ render(props) { const data = useData() const i18n = useI18n() + const dialog = useDialog() const args: string[] = [] if (props.input.offset) args.push("offset=" + props.input.offset) if (props.input.limit) args.push("limit=" + props.input.limit) @@ -1755,6 +1759,10 @@ ToolRegistry.register({ return value.filter((p): p is string => typeof p === "string") }) const pending = createMemo(() => busy(props.status)) + const images = createMemo(() => + (props.attachments ?? []).filter((f) => f.mime.startsWith("image/") && f.url), + ) + const preview = (url: string, alt?: string) => dialog.show(() => ) return ( <> )} + 0}> +
+ + {(file) => ( +
preview(file.url, file.filename)}> + {file.filename +
+ )} +
+
+
) }, From 0ab3702cf706bd2b50dc528871f773a0182cca0d Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 7 Jul 2026 14:35:14 +0200 Subject: [PATCH 36/68] fix(vscode): render variant dropdown story inline for visual regression The dialog.show() approach portaled the dialog to document.body (outside #storybook-root), and Kobalte's modal set aria-hidden on #storybook-root, causing Playwright's toHaveScreenshot to time out (element not visible). Replace with an inline fixture that reproduces the real CSS clipping chain: .am-nv-dialog > .am-nv-dialog-content > .am-prompt-input-container (position: relative, overflow: hidden from agent-manager.css) with the real inline ThinkingSelectorBase (portal={false}). The variant picker opens upward; with the overflow escape fix the popover renders fully above the container, without it floating-ui repositions the popover lower. The screenshot baseline captures the fixed state and catches any regression. --- .../src/stories/agent-manager.stories.tsx | 70 +++++++++++++------ 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx index b35303e127..4f2aef6e2d 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx @@ -10,8 +10,6 @@ import { FileTree } from "../../diff-viewer/FileTree" import { DiffPanel } from "../../agent-manager/DiffPanel" import { FullScreenDiffView } from "../../diff-viewer/FullScreenDiffView" import { WorktreeItem } from "../../agent-manager/WorktreeItem" -import { NewWorktreeDialog } from "../../agent-manager/NewWorktreeDialog" -import { useDialog } from "@kilocode/kilo-ui/context/dialog" import { ChatView } from "../components/chat/ChatView" import { registerVscodeToolOverrides } from "../components/chat/VscodeToolOverrides" import { SessionContext } from "../context/session" @@ -25,6 +23,7 @@ import { IconButton } from "@kilocode/kilo-ui/icon-button" import { Icon } from "@kilocode/kilo-ui/icon" import { TooltipKeybind } from "@kilocode/kilo-ui/tooltip" import { ContextMenu } from "@kilocode/kilo-ui/context-menu" +import { ThinkingSelectorBase } from "../components/shared/ThinkingSelector" import { createSignal, onCleanup, onMount, type JSX } from "solid-js" import type { WorktreeFileDiff, WorktreeState, WorktreeGitStats, PRStatus } from "../types/messages" import type { ReviewComment } from "../../diff-viewer/review-comments" @@ -932,40 +931,67 @@ export const TabBarSingleTab: Story = { } // --------------------------------------------------------------------------- -// NewWorktreeDialog — variant dropdown must escape the dialog scroll containers -// (regression: inline popovers were clipped by .am-nv-dialog-content overflow) +// NewWorktreeDialog — inline selector popovers must escape the dialog scroll +// containers. Regression: the reasoning-variant and mode pickers were clipped +// by .am-nv-dialog-content (overflow-y: auto) and .am-prompt-input-container +// (overflow: hidden) because the overflow escape hatch only covered the model +// picker. This fixture reproduces the real clipping chain (same CSS classes + +// the real inline ThinkingSelectorBase with portal={false}) so a screenshot +// baseline catches any future regression. Rendered inline (no dialog portal) +// because the visual-regression harness screenshots #storybook-root. // --------------------------------------------------------------------------- -const NewWorktreeVariantOpener = () => { - const dialog = useDialog() +const VariantPickerOpener = () => { + let frame = 0 + let attempts = 0 const open = () => { if (document.querySelector("[data-component='popover-content']")) return + if (attempts++ >= 120) return window.dispatchEvent(new CustomEvent("openVariantPicker")) - requestAnimationFrame(open) + frame = requestAnimationFrame(open) } onMount(() => { - dialog.show(() => {}} />) - requestAnimationFrame(open) + frame = requestAnimationFrame(open) }) + onCleanup(() => cancelAnimationFrame(frame)) return null } export const NewWorktreeVariantDropdown1280: Story = { name: "NewWorktreeDialog — variant dropdown open", parameters: { layout: "fullscreen" }, - render: () => { - const session = { - ...mockSessionValue(), - configModel: () => ({ providerID: "kilo", modelID: "anthropic/claude-sonnet-4-6" }), - } - return ( - - - - - - ) - }, + render: () => ( + + {/* Filler pushes the prompt container to the bottom of the dialog content. + The variant popover opens upward from the trigger, extending above the + container's top edge. Without the overflow escape fix, .am-prompt-input-container + (overflow: hidden + position: relative) clips the top of the popover. */} +
+
+
+
+
+
+
+ {}} + portal={false} + deferDismiss + /> +
+
+
+
+
+
+ + + ), } const searchSection = { id: "polish", name: "Polish", color: "Blue", order: 0, collapsed: false } From 62c41e21c6cff2ef9686de5ef678de33173c54bc Mon Sep 17 00:00:00 2001 From: kirillk Date: Tue, 7 Jul 2026 08:48:21 -0400 Subject: [PATCH 37/68] fix(jetbrains): prune old CLI binaries from the download cache Each pinned CLI version was cached under the IDE system dir forever, so old versions accumulated across plugin updates. After a version resolves successfully, delete every other cached version and drop the downloaded archive after extraction. Reinstall still deletes and re-downloads the active version. --- .changeset/jetbrains-prune-old-cli.md | 5 ++ .../kilocode/backend/cli/KiloCliDownloader.kt | 22 +++++++++ .../backend/cli/KiloCliDownloaderTest.kt | 46 +++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 .changeset/jetbrains-prune-old-cli.md diff --git a/.changeset/jetbrains-prune-old-cli.md b/.changeset/jetbrains-prune-old-cli.md new file mode 100644 index 0000000000..07600c3f7e --- /dev/null +++ b/.changeset/jetbrains-prune-old-cli.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Remove old JetBrains CLI binaries so they no longer accumulate in the IDE cache. Only the active version is kept, the downloaded archive is deleted after extraction, and reinstalling re-downloads a fresh binary. diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDownloader.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDownloader.kt index f7a6e06381..b50204f2ed 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDownloader.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDownloader.kt @@ -51,6 +51,7 @@ class KiloCliDownloader( if (!force && exe.isFile && cached != null && cached.matches(DIGEST)) { log.info("Using cached Kilo CLI $version for $platform at ${exe.absolutePath}") if (!SystemInfo.isWindows) exe.setExecutable(true) + prune(version) return@withContext exe } @@ -75,8 +76,12 @@ class KiloCliDownloader( extract(archive, dir) if (!exe.isFile) throw IllegalStateException("Downloaded CLI archive did not contain bin/${KiloCliPlatform.exe()}") if (!SystemInfo.isWindows) exe.setExecutable(true) + if (archive.exists() && !archive.delete()) { + log.warn("Failed to delete extracted Kilo CLI archive ${archive.absolutePath}") + } done.writeText("$digest\n") onProgress(CliDownload(100, version, platform)) + prune(version) exe } @@ -223,6 +228,23 @@ class KiloCliDownloader( } } + /** + * Delete every cached CLI version under [root] except [keep]. Each plugin update pins a new + * CLI version, so without this old versions would accumulate in the IDE system directory + * indefinitely. Runs only after the active version resolved successfully so a failed download + * never wipes the last working copy. + */ + private fun prune(keep: String) { + val entries = root.listFiles() ?: return + for (entry in entries) { + if (!entry.isDirectory || entry.name == keep) continue + log.info("Removing stale Kilo CLI version ${entry.absolutePath}") + if (!entry.deleteRecursively()) { + log.warn("Failed to remove stale Kilo CLI version ${entry.absolutePath}") + } + } + } + private fun url(version: String, platform: String, ext: String) = "${baseUrl.trimEnd('/')}/v$version/kilo-$platform.$ext" } diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDownloaderTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDownloaderTest.kt index 6a2e94bc02..b2cc0369ef 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDownloaderTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDownloaderTest.kt @@ -87,6 +87,52 @@ class KiloCliDownloaderTest { } } + @Test + fun `prunes stale versions and removes the extracted archive`() = runBlocking { + MockWebServer().use { server -> + val stale = File(File(dir, "0.0.1"), KiloCliPlatform.current()) + assertTrue(stale.mkdirs()) + File(stale, "leftover").writeText("old") + + val bytes = archive() + server.enqueue(metadata(bytes)) + server.enqueue(MockResponse().setResponseCode(200).setBody(Buffer().write(bytes))) + val cli = KiloCliDownloader( + root = dir, + baseUrl = server.url("/release").toString(), + api = server.url("/api").toString(), + ).resolve("1.2.3") + + assertTrue(cli.isFile) + assertFalse(File(dir, "0.0.1").exists()) + val archived = File(cli.parentFile.parentFile, "kilo-${KiloCliPlatform.current()}.${KiloCliPlatform.archive()}") + assertFalse(archived.exists()) + } + } + + @Test + fun `forced resolve re-downloads and keeps only the active version`() = runBlocking { + MockWebServer().use { server -> + val bytes = archive() + repeat(2) { + server.enqueue(metadata(bytes)) + server.enqueue(MockResponse().setResponseCode(200).setBody(Buffer().write(bytes))) + } + val cli = KiloCliDownloader( + root = dir, + baseUrl = server.url("/release").toString(), + api = server.url("/api").toString(), + ) + cli.resolve("1.2.3") + assertEquals(2, server.requestCount) + + val forced = cli.resolve("1.2.3", force = true) + assertTrue(forced.isFile) + assertEquals(4, server.requestCount) + assertEquals(listOf("1.2.3"), dir.listFiles()?.filter { it.isDirectory }?.map { it.name }) + } + } + @Test fun `rejects cli archive with mismatched digest`() = runBlocking { MockWebServer().use { server -> From a659bb2571bd5f7b52eceadbb42a975fe31aa7c1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 7 Jul 2026 12:50:04 +0000 Subject: [PATCH 38/68] chore: update kilo-vscode visual regression baselines --- .../new-worktree-variant-dropdown-1280-chromium-linux.png | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/new-worktree-variant-dropdown-1280-chromium-linux.png diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/new-worktree-variant-dropdown-1280-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/new-worktree-variant-dropdown-1280-chromium-linux.png new file mode 100644 index 0000000000..2b594d86ac --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/new-worktree-variant-dropdown-1280-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f7edbc1bf6f10260626bf21406bb02e32dc17bf969c83e8b56251c50f755a1f +size 7737 From bd29b94d20bb6639d55f6ac009c793f6e313030d Mon Sep 17 00:00:00 2001 From: kirillk Date: Tue, 7 Jul 2026 08:52:25 -0400 Subject: [PATCH 39/68] fix(jetbrains): report exact CLI release resolution failure The collapsed metadata lookup threw a misleading "did not include" error whether the asset was absent, had no digest, or had a malformed digest. Split into distinct, logged messages so the actual cause is clear, including the nullable-digest case where GitHub has not published a checksum. --- .../kilocode/backend/cli/KiloCliDownloader.kt | 21 +++++++-- .../backend/cli/KiloCliDownloaderTest.kt | 46 +++++++++++++++++++ 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDownloader.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDownloader.kt index b50204f2ed..269331e13c 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDownloader.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDownloader.kt @@ -85,6 +85,11 @@ class KiloCliDownloader( exe } + private fun fail(message: String): Nothing { + log.warn(message) + throw IllegalStateException(message) + } + private fun asset(version: String, platform: String, ext: String): String { val name = "kilo-$platform.$ext" val url = "${api.trimEnd('/')}/v$version" @@ -112,12 +117,18 @@ class KiloCliDownloader( log.debug { "GitHub metadata OK for Kilo CLI $version (${rate(response)})" } val body = response.body?.string() ?: throw IllegalStateException("Failed to fetch Kilo CLI release metadata for $version: empty response body") - val digest = JSON.parseToJsonElement(body).jsonObject["assets"]?.jsonArray - ?.firstOrNull { it.jsonObject["name"]?.jsonPrimitive?.contentOrNull == name } - ?.jsonObject?.get("digest")?.jsonPrimitive?.contentOrNull - ?: throw IllegalStateException("Kilo CLI release $version did not include $name") + val assets = JSON.parseToJsonElement(body).jsonObject["assets"]?.jsonArray + val entry = assets?.firstOrNull { it.jsonObject["name"]?.jsonPrimitive?.contentOrNull == name } + if (entry == null) { + val names = assets?.mapNotNull { it.jsonObject["name"]?.jsonPrimitive?.contentOrNull }?.joinToString(", ") + fail("Kilo CLI release $version has no asset named $name (available assets: ${names ?: "none"})") + } + val digest = entry.jsonObject["digest"]?.jsonPrimitive?.contentOrNull + if (digest == null) { + fail("Kilo CLI release $version asset $name has no digest yet; GitHub has not published a SHA-256 checksum for it") + } if (!digest.matches(DIGEST)) { - throw IllegalStateException("Kilo CLI release $version asset $name has invalid digest") + fail("Kilo CLI release $version asset $name has a malformed digest '$digest'; expected sha256:<64 hex chars>") } return digest } diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDownloaderTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDownloaderTest.kt index b2cc0369ef..face684e5f 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDownloaderTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDownloaderTest.kt @@ -153,6 +153,52 @@ class KiloCliDownloaderTest { } } + @Test + fun `fails clearly and logs when the release has no matching asset`() = runBlocking { + MockWebServer().use { server -> + server.enqueue( + MockResponse().setResponseCode(200).setBody( + """{"assets":[{"name":"other.zip","digest":"sha256:${"a".repeat(64)}"}]}""" + ) + ) + val log = TestLog() + val ex = assertFailsWith { + KiloCliDownloader( + log = log, + root = dir, + baseUrl = server.url("/release").toString(), + api = server.url("/api").toString(), + ).resolve("1.2.3") + } + val name = "kilo-${KiloCliPlatform.current()}.${KiloCliPlatform.archive()}" + assertContains(ex.message.orEmpty(), "has no asset named $name") + assertContains(ex.message.orEmpty(), "other.zip") + assertTrue(log.messages.any { it.contains("has no asset named $name") }) + } + } + + @Test + fun `fails clearly and logs when the asset has no digest`() = runBlocking { + MockWebServer().use { server -> + server.enqueue( + MockResponse().setResponseCode(200).setBody( + """{"assets":[{"name":"kilo-${KiloCliPlatform.current()}.${KiloCliPlatform.archive()}"}]}""" + ) + ) + val log = TestLog() + val ex = assertFailsWith { + KiloCliDownloader( + log = log, + root = dir, + baseUrl = server.url("/release").toString(), + api = server.url("/api").toString(), + ).resolve("1.2.3") + } + assertContains(ex.message.orEmpty(), "has no digest yet") + assertTrue(log.messages.any { it.contains("has no digest yet") }) + } + } + @Test fun `reports github api rate limits while resolving metadata`() = runBlocking { MockWebServer().use { server -> From b0348cbc01438f603f767117ca6f2e15370e099e Mon Sep 17 00:00:00 2001 From: Johnny Eric Amancio Date: Tue, 7 Jul 2026 15:44:24 +0200 Subject: [PATCH 40/68] feat(vscode): integrate project memory into the extension (#11954) * feat(vscode): integrate project memory into the extension Wire the kilo-memory system into the VS Code extension host and webview, building on the shared client helpers and /memory command catalog in @kilocode/kilo-memory. Extension host: - KiloProviderMemory bridges the SDK memory client (status/show/enable/ disable/configure/rebuild/remember/correct/forget/purge, plus status and edit) with serialized operations and a small per-directory cache - memory.status/updated/error events fan out to the active and tracked sessions and refresh the webview - showMemory / toggleMemory commands, routed through the sidebar provider or the Agent Manager panel depending on which is active Webview: - MemoryProvider context, memory status controls in the Context settings tab, task-header and assistant-message affordances, and the /memory prompt command (help/show/operation) driven by the shared catalog - message types and i18n strings across all locales * fix(vscode): address memory PR review suggestions - serialize toggleMemory's status pre-check via KiloProviderMemory.toggle() so rapid toggles can't double-apply the same operation - collapse duplicate enabled/active memos in memory context - extract shared formatCompactCount util (was triplicated K/M formatter) - drop dead reject handler in serial() and never-produced member from MemoryOperationResultMessage.result union * fix(memory): audit cleanups: inspect accuracy, shared marker decoder, dead i18n key * chore: update kilo-vscode visual regression baselines --------- Co-authored-by: github-actions[bot] --- .changeset/kilo-memory-vscode.md | 5 + bun.lock | 1 + ...task-header-with-memory-chromium-linux.png | 3 + packages/kilo-memory/src/marker-meta.ts | 5 +- packages/kilo-memory/test/decisions.test.ts | 14 + packages/kilo-vscode/package.json | 11 + packages/kilo-vscode/src/KiloProvider.ts | 77 +++ .../src/agent-manager/AgentManagerProvider.ts | 60 ++- .../kilo-vscode/src/agent-manager/host.ts | 2 + .../src/agent-manager/vscode-host.ts | 2 + packages/kilo-vscode/src/extension.ts | 20 + .../kilo-vscode/src/kilo-provider/memory.ts | 483 ++++++++++++++++++ .../kilo-vscode/tests/setup/vscode-mock.ts | 6 + .../agent-manager-memory-commands.test.ts | 102 ++++ .../unit/kilo-provider-memory-events.test.ts | 183 +++++++ .../tests/unit/kilo-provider-memory.test.ts | 212 ++++++++ .../tests/unit/memory-command.test.ts | 68 +++ .../agent-manager/AgentManagerApp.tsx | 17 +- packages/kilo-vscode/webview-ui/src/App.tsx | 13 +- .../src/components/chat/AssistantMessage.tsx | 64 +++ .../src/components/chat/ContextProgress.tsx | 7 +- .../src/components/chat/PromptInput.tsx | 55 ++ .../src/components/chat/TaskHeader.tsx | 77 +++ .../src/components/chat/TaskUsage.tsx | 7 +- .../src/components/settings/ContextTab.tsx | 80 ++- .../webview-ui/src/context/memory.tsx | 220 ++++++++ .../kilo-vscode/webview-ui/src/i18n/ar.ts | 37 ++ .../kilo-vscode/webview-ui/src/i18n/br.ts | 37 ++ .../kilo-vscode/webview-ui/src/i18n/bs.ts | 37 ++ .../kilo-vscode/webview-ui/src/i18n/da.ts | 37 ++ .../kilo-vscode/webview-ui/src/i18n/de.ts | 37 ++ .../kilo-vscode/webview-ui/src/i18n/en.ts | 36 ++ .../kilo-vscode/webview-ui/src/i18n/es.ts | 37 ++ .../kilo-vscode/webview-ui/src/i18n/fr.ts | 37 ++ .../kilo-vscode/webview-ui/src/i18n/it.ts | 37 ++ .../kilo-vscode/webview-ui/src/i18n/ja.ts | 37 ++ .../kilo-vscode/webview-ui/src/i18n/ko.ts | 37 ++ .../kilo-vscode/webview-ui/src/i18n/nl.ts | 37 ++ .../kilo-vscode/webview-ui/src/i18n/no.ts | 37 ++ .../kilo-vscode/webview-ui/src/i18n/pl.ts | 37 ++ .../kilo-vscode/webview-ui/src/i18n/ru.ts | 37 ++ .../kilo-vscode/webview-ui/src/i18n/th.ts | 37 ++ .../kilo-vscode/webview-ui/src/i18n/tr.ts | 37 ++ .../kilo-vscode/webview-ui/src/i18n/uk.ts | 37 ++ .../kilo-vscode/webview-ui/src/i18n/zh.ts | 36 ++ .../kilo-vscode/webview-ui/src/i18n/zht.ts | 36 ++ .../webview-ui/src/stories/StoryProviders.tsx | 51 +- .../webview-ui/src/stories/chat.stories.tsx | 47 ++ .../webview-ui/src/styles/chat-layout.css | 8 + .../webview-ui/src/styles/task-header.css | 36 ++ .../src/types/messages/extension-messages.ts | 4 + .../webview-ui/src/types/messages/index.ts | 1 + .../webview-ui/src/types/messages/memory.ts | 103 ++++ .../src/types/messages/webview-messages.ts | 5 + .../webview-ui/src/utils/format.ts | 8 + .../webview-ui/src/utils/memory-command.ts | 11 + 56 files changed, 2752 insertions(+), 53 deletions(-) create mode 100644 .changeset/kilo-memory-vscode.md create mode 100644 packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/task-header-with-memory-chromium-linux.png create mode 100644 packages/kilo-vscode/src/kilo-provider/memory.ts create mode 100644 packages/kilo-vscode/tests/unit/agent-manager-memory-commands.test.ts create mode 100644 packages/kilo-vscode/tests/unit/kilo-provider-memory-events.test.ts create mode 100644 packages/kilo-vscode/tests/unit/kilo-provider-memory.test.ts create mode 100644 packages/kilo-vscode/tests/unit/memory-command.test.ts create mode 100644 packages/kilo-vscode/webview-ui/src/context/memory.tsx create mode 100644 packages/kilo-vscode/webview-ui/src/types/messages/memory.ts create mode 100644 packages/kilo-vscode/webview-ui/src/utils/format.ts create mode 100644 packages/kilo-vscode/webview-ui/src/utils/memory-command.ts diff --git a/.changeset/kilo-memory-vscode.md b/.changeset/kilo-memory-vscode.md new file mode 100644 index 0000000000..6dc668d915 --- /dev/null +++ b/.changeset/kilo-memory-vscode.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Integrate project memory into the VS Code extension: memory status and controls in the Context settings tab, task-header and assistant-message affordances, the `/memory` prompt command, and Show/Toggle Project Memory command-palette entries. diff --git a/bun.lock b/bun.lock index a1f8b9ef20..e0c5b1329c 100644 --- a/bun.lock +++ b/bun.lock @@ -338,6 +338,7 @@ "@kilocode/kilo-gateway": "workspace:*", "@kilocode/kilo-i18n": "workspace:*", "@kilocode/kilo-indexing": "workspace:*", + "@kilocode/kilo-memory": "workspace:*", "@kilocode/kilo-ui": "workspace:*", "@kilocode/plugin": "workspace:*", "@kilocode/sdk": "workspace:*", diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/task-header-with-memory-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/task-header-with-memory-chromium-linux.png new file mode 100644 index 0000000000..1ae814a650 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/task-header-with-memory-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:92cf06c01b19386f2041e378a08afdaf880fdc5c3e19132350fbda05684dbeec +size 5775 diff --git a/packages/kilo-memory/src/marker-meta.ts b/packages/kilo-memory/src/marker-meta.ts index 727b147da4..b2db71714f 100644 --- a/packages/kilo-memory/src/marker-meta.ts +++ b/packages/kilo-memory/src/marker-meta.ts @@ -20,6 +20,7 @@ export namespace MemoryMarkerMeta { type: Type tokens: number count: number + files: string[] } export function metadata(marker: Info) { @@ -30,7 +31,6 @@ export namespace MemoryMarkerMeta { tokens: marker.tokens, count: marker.count, files: marker.files, - sources: marker.files, }, } } @@ -89,13 +89,14 @@ export namespace MemoryMarkerMeta { const value = meta as { type?: unknown; tokens?: unknown; count?: unknown; files?: unknown; sources?: unknown } const type = value.type === "startup" ? "startup" : "recall" const tokens = typeof value.tokens === "number" ? value.tokens : 0 + // `sources` fallback covers parts persisted before the key was dropped from metadata(). const files = Array.isArray(value.files) ? value.files.filter((item) => typeof item === "string") : Array.isArray(value.sources) ? value.sources.filter((item) => typeof item === "string") : [] const count = typeof value.count === "number" ? value.count : files.length - return { type, tokens, count } + return { type, tokens, count, files } } return undefined } diff --git a/packages/kilo-memory/test/decisions.test.ts b/packages/kilo-memory/test/decisions.test.ts index f75731af0d..c5eb563ebd 100644 --- a/packages/kilo-memory/test/decisions.test.ts +++ b/packages/kilo-memory/test/decisions.test.ts @@ -162,6 +162,20 @@ describe("memory marker metadata", () => { type: "recall", tokens: 4, count: 2, + files: ["project.md", "environment.md"], + }) + }) + + test("decodes legacy metadata that only carries sources", () => { + expect( + MemoryMarkerMeta.fromParts([ + { type: "text", metadata: { kiloMemory: { type: "startup", tokens: 3, sources: ["project.md"] } } }, + ]), + ).toEqual({ + type: "startup", + tokens: 3, + count: 1, + files: ["project.md"], }) }) diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json index ada2fcae6a..b24e746306 100644 --- a/packages/kilo-vscode/package.json +++ b/packages/kilo-vscode/package.json @@ -195,6 +195,16 @@ "title": "Migrate Settings from Legacy Version", "category": "Kilo Code" }, + { + "command": "kilo-code.new.showMemory", + "title": "Show Project Memory", + "category": "Kilo Code" + }, + { + "command": "kilo-code.new.toggleMemory", + "title": "Toggle Project Memory", + "category": "Kilo Code" + }, { "command": "kilo-code.new.autocomplete.generateSuggestions", "title": "Generate Suggested Edits", @@ -1147,6 +1157,7 @@ "@kilocode/kilo-gateway": "workspace:*", "@kilocode/kilo-i18n": "workspace:*", "@kilocode/kilo-indexing": "workspace:*", + "@kilocode/kilo-memory": "workspace:*", "@kilocode/kilo-ui": "workspace:*", "@kilocode/plugin": "workspace:*", "@kilocode/sdk": "workspace:*", diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 8358930f05..f9199c6e22 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -138,6 +138,7 @@ import { import { fetchAndSendPendingSuggestions } from "./kilo-provider/handlers/suggestion" import { nativeTitle } from "./kilo-provider/native-tab-title" import { parseReview, reviewMetadata, type ReviewMessageData } from "./shared/review-comments" +import { KiloProviderMemory } from "./kilo-provider/memory" import { buildActionContext, @@ -386,6 +387,14 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper private readonly confirmations = new MessageConfirmation() private readonly costs = new MaxCostNudge() private readonly activeAlerts = new Map() // sid -> limit currently shown in UI + private readonly memory = new KiloProviderMemory({ + client: () => this.client ?? undefined, + session: () => this.currentSession ?? undefined, + // Honor disabled project scope (null in a multi-root panel): no workspace fallback, + // so memory operations never silently target an arbitrary folder. + dir: (sessionID) => this.getProjectDirectory(sessionID), + post: (message) => this.postMessage(message), + }) private unsubscribeEvent: (() => void) | null = null private unsubscribeState: (() => void) | null = null /** Cached migration data so migration doesn't re-read from disk/SecretStorage. */ // legacy-migration @@ -931,6 +940,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper } if (await this.handleModelSelectorExpandedMessage(message)) return this.visibleTaskStreams.handle(message) + if (await this.handleMemoryMessage(message)) return switch (message.type) { case "webviewReady": console.log("[Kilo New] KiloProvider: ✅ webviewReady received") @@ -1515,6 +1525,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper // Remote status events are global and should always pass through if (event.type === "kilo-sessions.remote-status-changed") return true + if (event.type === "memory.status" || event.type === "memory.updated" || event.type === "memory.error") + return true const sessionId = this.resolveEventSessionId(event) // message.part.* events are always session-scoped; drop if session unknown. @@ -1659,6 +1671,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.fetchAndSendConfig(), this.fetchAndSendIndexingStatus(), this.fetchAndSendNotifications(), + this.memory.fetch(), this.seedSessionStatusMap(), ]) this.cachedGitRepo = await hasGit(this.client!, this.getWorkspaceDirectory()) @@ -2390,6 +2403,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper } } + private async handleMemoryMessage(message: Record): Promise { + return this.memory.handle(message) + } + /** * Fetch backend config and send to webview. */ @@ -3762,6 +3779,48 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper return } + if (event.type === "memory.status" || event.type === "memory.updated" || event.type === "memory.error") { + const props = event.properties as { sessionID?: unknown; detail?: unknown; reason?: unknown } + const eventSessionID = typeof props.sessionID === "string" ? props.sessionID : undefined + const active = this.currentSession?.id + const local = + !directory || sameDirectory(directory, this.getProjectDirectory(active) ?? this.getWorkspaceDirectory(active)) + const trackedById = Boolean(eventSessionID && this.trackedSessionIds.has(eventSessionID)) + // Directory-scoped events (enable/disable/rebuild/configure/purge) carry no + // sessionID, so also match any tracked session sharing the event directory — + // e.g. a non-active Agent Manager tab on the same worktree. + const trackedByDir = directory + ? [...this.sessionDirectories.entries()] + .filter(([sid, dir]) => this.trackedSessionIds.has(sid) && sameDirectory(directory, dir)) + .map(([sid]) => sid) + : [] + const tracked = trackedById || trackedByDir.length > 0 + if (!local && !tracked) return + if (trackedById && eventSessionID && directory) this.trackDirectory(eventSessionID, directory) + const targets = new Set() + if (trackedById && eventSessionID) targets.add(eventSessionID) + for (const sid of trackedByDir) targets.add(sid) + if (local && active) targets.add(active) + if (targets.size === 0 && local) targets.add(undefined) + const detail = + props.detail && typeof props.detail === "object" + ? props.detail + : event.type === "memory.error" && typeof props.reason === "string" + ? { type: "error", message: props.reason, reason: props.reason } + : undefined + for (const sessionID of targets) { + if (detail) { + this.postMessage({ + type: "memoryEvent", + sessionID, + detail, + }) + } + void this.memory.fetch(sessionID, false) + } + return + } + // Drop session events from other projects before any tracking logic. // This must come first: the trackedSessionIds guard below would otherwise // let a foreign session through if it was accidentally tracked. @@ -4007,6 +4066,22 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.flushPendingReviewComments() } + public async showMemory(sessionID?: string): Promise { + await this.memory.show(sessionID ?? this.currentSession?.id) + } + + public async toggleMemory(sessionID?: string): Promise { + try { + const operation = await this.memory.toggle(sessionID ?? this.currentSession?.id) + if (operation) { + void vscode.window.showInformationMessage(`Project memory ${operation === "enable" ? "enabled" : "disabled"}.`) + } + } catch (error) { + console.error("[Kilo New] KiloProvider: Failed to toggle memory:", error) + void vscode.window.showErrorMessage(getErrorMessage(error) || "Failed to toggle memory") + } + } + private flushPendingReviewComments(): void { if (!this.webview || !this.isWebviewReady || this.pendingReviewComments.length === 0) return @@ -4298,6 +4373,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.visibleTaskStreams.clear() this.streams.dispose() this.isWebviewReady = false + // Release any waitForReady() awaiters so their callers don't hang after disposal. + this.readyResolvers.splice(0).forEach((r) => r()) this.promptRecoveryQueued = false clearNetworkWaits(this.trackedSessionIds) this.trackedSessionIds.clear() diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts index e906a76607..837af5ae74 100644 --- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts +++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts @@ -1804,6 +1804,60 @@ export class AgentManagerProvider implements Disposable { return this.panel?.active === true } + private async waitForPanel(panel: PanelContext, promise: Promise): Promise { + const done = promise.then(() => true) + let sub: Disposable | undefined + const disposed = new Promise((resolve) => { + sub = panel.onDidDispose(() => { + sub?.dispose() + resolve(false) + }) + }) + void done.finally(() => sub?.dispose()) + const ok = await Promise.race([done, disposed]) + return ok && this.panel === panel + } + + private waitForPanelReady(panel: PanelContext): Promise { + return this.waitForPanel(panel, panel.waitForReady()) + } + + private waitForPanelActive(panel: PanelContext): Promise { + return this.waitForPanel(panel, panel.waitForActive()) + } + + public async showMemory(): Promise { + const panel = this.panel + const sid = this.activeSessionId + if (!panel || !sid) { + this.host.showError("No active Agent Manager session") + return + } + if (!(await this.waitForPanelReady(panel))) return + if (this.activeSessionId !== sid) return + try { + await panel.sessions.showMemory(sid) + } catch (error) { + this.host.showError(getErrorMessage(error) || "Failed to show memory") + } + } + + public async toggleMemory(): Promise { + const panel = this.panel + const sid = this.activeSessionId + if (!panel || !sid) { + this.host.showError("No active Agent Manager session") + return + } + if (!(await this.waitForPanelReady(panel))) return + if (this.activeSessionId !== sid) return + try { + await panel.sessions.toggleMemory(sid) + } catch (error) { + this.host.showError(getErrorMessage(error) || "Failed to toggle memory") + } + } + /** Expose worktree session→directory mappings for the auto-approve toggle. */ public getSessionDirectories(): ReadonlyMap { return this.panel?.sessions.getSessionDirectories() ?? new Map() @@ -1869,7 +1923,7 @@ export class AgentManagerProvider implements Disposable { this.openPanel() const panel = this.panel if (!panel) return - await panel.waitForReady() + if (!(await this.waitForPanelReady(panel))) return await this.waitForStateReady("createFromSidebar") await this.onCreateWorktree(baseBranch, branchName) } @@ -1878,8 +1932,8 @@ export class AgentManagerProvider implements Disposable { this.openPanel() const panel = this.panel if (!panel) return - await panel.waitForActive() - await panel.waitForReady() + if (!(await this.waitForPanelActive(panel))) return + if (!(await this.waitForPanelReady(panel))) return await this.waitForStateReady("openAdvancedWorktree") queueMicrotask(() => this.postToWebview({ type: "action", action: "advancedWorktree" })) } diff --git a/packages/kilo-vscode/src/agent-manager/host.ts b/packages/kilo-vscode/src/agent-manager/host.ts index 2d9b4f6bf4..fc5be00112 100644 --- a/packages/kilo-vscode/src/agent-manager/host.ts +++ b/packages/kilo-vscode/src/agent-manager/host.ts @@ -44,6 +44,8 @@ export interface SessionProvider { * The callback receives the new session and its directory so the Agent Manager * can route it to the correct worktree instead of LOCAL. */ onFollowupAdopted(cb: (session: Session, directory: string) => void): void + showMemory(sessionID?: string): Promise + toggleMemory(sessionID?: string): Promise dispose(): void } diff --git a/packages/kilo-vscode/src/agent-manager/vscode-host.ts b/packages/kilo-vscode/src/agent-manager/vscode-host.ts index 79d8821879..24d9640877 100644 --- a/packages/kilo-vscode/src/agent-manager/vscode-host.ts +++ b/packages/kilo-vscode/src/agent-manager/vscode-host.ts @@ -119,6 +119,8 @@ export class VscodeHost implements Host { registerSession: (s) => provider.registerSession(s), recoverPendingPrompts: () => provider.recoverPendingPrompts(), onFollowupAdopted: (cb) => provider.onFollowupAdopted(cb), + showMemory: (id) => provider.showMemory(id), + toggleMemory: (id) => provider.toggleMemory(id), dispose: () => provider.dispose(), } diff --git a/packages/kilo-vscode/src/extension.ts b/packages/kilo-vscode/src/extension.ts index 8539bf4d47..3e3aebd125 100644 --- a/packages/kilo-vscode/src/extension.ts +++ b/packages/kilo-vscode/src/extension.ts @@ -398,6 +398,26 @@ export function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand("kilo-code.new.openIndexingSettings", () => { settingsEditorProvider.openPanel("settings", "indexing") }), + vscode.commands.registerCommand("kilo-code.new.showMemory", async () => { + if (agentManagerProvider.isActive()) { + await agentManagerProvider.showMemory() + return + } + const target = activeTabProvider() ?? provider + if (target === provider) await vscode.commands.executeCommand("kilo-code.SidebarProvider.focus") + await target.waitForReady() + await target.showMemory() + }), + vscode.commands.registerCommand("kilo-code.new.toggleMemory", async () => { + if (agentManagerProvider.isActive()) { + await agentManagerProvider.toggleMemory() + return + } + const target = activeTabProvider() ?? provider + if (target === provider) await vscode.commands.executeCommand("kilo-code.SidebarProvider.focus") + await target.waitForReady() + await target.toggleMemory() + }), // legacy-migration start vscode.commands.registerCommand("kilo-code.new.openMigrationWizard", () => { provider.postMessage({ type: "migrationState", needed: true, source: "legacy" }) diff --git a/packages/kilo-vscode/src/kilo-provider/memory.ts b/packages/kilo-vscode/src/kilo-provider/memory.ts new file mode 100644 index 0000000000..fc0988bd98 --- /dev/null +++ b/packages/kilo-vscode/src/kilo-provider/memory.ts @@ -0,0 +1,483 @@ +import * as vscode from "vscode" +import * as path from "node:path" +import { + isMemoryOperation, + isMemoryPromptOperation, + type MemoryOperation, + type MemoryPromptOperation, +} from "@kilocode/kilo-memory/commands" +import { MemorySchema } from "@kilocode/kilo-memory/schema" +import type { KiloClient, Session } from "@kilocode/sdk/v2/client" +import { retry } from "../services/cli-backend/retry" +import { getErrorMessage } from "../kilo-provider-utils" + +type MemorySourceFile = MemorySchema.Source +type MemoryApi = KiloClient["memory"] +const CACHE_LIMIT = 8 +const NO_PROJECT = "No active project for memory. Open a file in the target folder to manage its memory." + +export type KiloProviderMemoryMessage = { + operation: MemoryOperation + sessionID?: string + mode?: "status" | "on" | "off" + confirm?: boolean + text?: string + query?: string + key?: string + file?: MemorySourceFile + section?: string +} + +export type KiloProviderMemoryInput = { + client(): KiloClient | undefined + session(): Session | undefined + /** Project directory for memory operations, or undefined when project scope is disabled. */ + dir(sessionID?: string): string | undefined + post(message: unknown): void +} + +function file(value: unknown): MemorySourceFile | undefined { + return MemorySchema.source(value) +} + +function operation(value: unknown): MemoryOperation | undefined { + return isMemoryOperation(value) ? value : undefined +} + +function mode(value: unknown) { + if (value === "status" || value === "on" || value === "off") return value + return undefined +} + +function memory(client: KiloClient | undefined): MemoryApi | undefined { + return (client as { memory?: MemoryApi } | undefined)?.memory +} + +function request(input: Record): { value: KiloProviderMemoryMessage } | { error: string } { + const op = operation(input.operation) + if (!op) return { error: "Unknown memory operation" } + const source = file(input.file) + if (input.file !== undefined && !source) return { error: "Invalid memory source file" } + return { + value: { + operation: op, + sessionID: typeof input.sessionID === "string" ? input.sessionID : undefined, + mode: mode(input.mode), + confirm: input.confirm === true, + text: typeof input.text === "string" ? input.text : undefined, + query: typeof input.query === "string" ? input.query : undefined, + key: typeof input.key === "string" ? input.key : undefined, + file: source, + section: typeof input.section === "string" ? input.section : undefined, + }, + } +} + +export class KiloProviderMemory { + private readonly cached = new Map() + private tail = Promise.resolve() + + constructor(private readonly input: KiloProviderMemoryInput) {} + + private cache(dir: string, msg: unknown) { + this.cached.delete(dir) + this.cached.set(dir, msg) + while (this.cached.size > CACHE_LIMIT) { + const key = this.cached.keys().next().value + if (typeof key !== "string") return + this.cached.delete(key) + } + } + + private serial(fn: () => Promise) { + // this.tail is always reassigned below to a never-rejecting promise, so it + // never settles rejected — a single fulfillment handler is sufficient. + const next = this.tail.then(fn) + this.tail = next.then( + () => undefined, + () => undefined, + ) + return next + } + + async handle(message: Record): Promise { + if (message.type === "requestMemory") { + this.fetch( + typeof message.sessionID === "string" ? message.sessionID : undefined, + message.includeSources === true, + ).catch((err: unknown) => console.error("[Kilo New] fetchAndSendMemory failed:", err)) + return true + } + if (message.type === "memoryShow") { + await this.show(typeof message.sessionID === "string" ? message.sessionID : undefined) + return true + } + if (message.type === "memoryOperation") { + const parsed = request(message) + if ("error" in parsed) { + this.input.post({ + type: "memoryOperationResult", + operation: typeof message.operation === "string" ? message.operation : "unknown", + sessionID: typeof message.sessionID === "string" ? message.sessionID : undefined, + ok: false, + error: parsed.error, + }) + return true + } + await this.run(parsed.value) + return true + } + if (message.type === "memoryPrompt") { + const op = isMemoryPromptOperation(message.operation) ? message.operation : undefined + if (!op) return true + await this.prompt(op, typeof message.sessionID === "string" ? message.sessionID : undefined) + return true + } + return false + } + + fetch(sessionID?: string, includeSources = false): Promise { + return this.serial(() => this.load(sessionID, includeSources)) + } + + /** Resolves once the serialized operation queue has drained. */ + idle(): Promise { + return this.tail + } + + private async load(sessionID?: string, includeSources = false): Promise { + try { + const directory = this.input.dir(sessionID ?? this.input.session()?.id) + const client = this.input.client() + if (!client) { + const cached = directory ? this.cached.get(directory) : undefined + if (cached && typeof cached === "object" && !Array.isArray(cached)) this.input.post({ ...cached, sessionID }) + else this.input.post({ type: "memoryLoaded", sessionID, error: "Not connected to CLI backend" }) + return + } + + const api = memory(client) + if (!api) { + this.input.post({ type: "memoryLoaded", sessionID, error: "Memory unavailable in CLI backend" }) + return + } + + if (!directory) { + this.input.post({ type: "memoryLoaded", sessionID, error: NO_PROJECT }) + return + } + + const { data: status } = await retry(() => api.status({ directory }, { throwOnError: true })) + const show = includeSources + ? (await retry(() => api.show({ directory }, { throwOnError: true }))).data + : undefined + const msg = { + type: "memoryLoaded", + sessionID, + status, + ...(show ? { show } : {}), + } + this.cache(directory, msg) + this.input.post(msg) + } catch (err) { + console.error("[Kilo New] KiloProvider: Failed to fetch memory:", err) + this.input.post({ + type: "memoryLoaded", + sessionID, + error: getErrorMessage(err) || "Failed to load memory", + }) + } + } + + async prompt(value: MemoryPromptOperation, sessionID?: string): Promise { + const title = value === "remember" ? "Remember in project memory" : "Forget project memory" + const placeHolder = value === "remember" ? "Project fact, command, or correction" : "Text to remove" + const text = await vscode.window.showInputBox({ title, placeHolder, ignoreFocusOut: true }) + if (!text?.trim()) { + // Clear the webview's pending state for this action when the input is dismissed. + this.input.post({ type: "memoryOperationResult", operation: value, sessionID, ok: true }) + return + } + await this.run({ + operation: value, + sessionID, + ...(value === "remember" ? { text: text.trim() } : { query: text.trim() }), + }) + } + + show(sessionID?: string): Promise { + return this.serial(() => this.doShow(sessionID)) + } + + private async doShow(sessionID?: string): Promise { + const client = this.input.client() + if (!client) { + this.input.post({ + type: "memoryLoaded", + sessionID, + error: "Not connected to CLI backend", + }) + return + } + + const api = memory(client) + + if (!api) { + this.input.post({ + type: "memoryLoaded", + sessionID, + error: "Memory unavailable in CLI backend", + }) + return + } + + try { + const directory = this.input.dir(sessionID ?? this.input.session()?.id) + if (!directory) { + this.input.post({ type: "memoryLoaded", sessionID, error: NO_PROJECT }) + return + } + const { data: show } = await retry(() => api.show({ directory }, { throwOnError: true })) + const { data: status } = await retry(() => api.status({ directory }, { throwOnError: true })) + const current = sessionID ?? this.input.session()?.id + const startup = + current && status.state.stats.lastInjectedSessionID === current ? status.state.stats.lastInjectedTokens : 0 + const content = [ + "# Kilo Memory", + "", + `Root: ${show.root}`, + `Enabled: ${show.state.enabled ? "yes" : "no"}`, + `Auto-save: ${show.state.autoConsolidate ? "on" : "off"}`, + `Startup context: ${show.state.autoInject ? "on" : "off"}`, + `Stored index tokens: ${status.index.estimatedTokens}`, + `Startup context tokens for this session: ${startup}`, + `Last auto-save model usage: ${status.state.stats.lastConsolidationTokens} tokens`, + "", + "## project.md", + show.sources.project.trim(), + "", + "## environment.md", + show.sources.environment.trim(), + "", + "## corrections.md", + show.sources.corrections.trim(), + "", + "## index.kmem", + show.index.trim(), + "", + "## items", + show.items.trim(), + "", + "## changes", + show.changes.trim(), + "", + "## decisions.jsonl", + show.decisions.trim(), + "", + ].join("\n") + await vscode.workspace + .openTextDocument({ content, language: "markdown" }) + .then((doc) => vscode.window.showTextDocument(doc, { preview: true })) + const msg = { + type: "memoryLoaded", + sessionID, + status, + show, + } + this.cache(directory, msg) + this.input.post(msg) + } catch (err) { + console.error("[Kilo New] KiloProvider: Failed to show memory:", err) + this.input.post({ + type: "memoryLoaded", + sessionID, + error: getErrorMessage(err) || "Failed to show memory", + }) + } + } + + run(message: KiloProviderMemoryMessage): Promise { + return this.serial(() => this.execute(message)) + } + + /** + * Serialized status read + enable/disable, so two rapid toggles can't both + * read the same pre-toggle state and apply the same operation twice. + * Returns the applied operation, or undefined when it failed (the failure is + * already posted to the webview by execute()). + */ + toggle(sessionID?: string): Promise { + return this.serial(async () => { + const client = this.input.client() + if (!client) throw new Error("Not connected to CLI backend") + const api = memory(client) + if (!api) throw new Error("Memory unavailable in CLI backend") + const directory = this.input.dir(sessionID ?? this.input.session()?.id) + if (!directory) throw new Error(NO_PROJECT) + const { data: status } = await retry(() => api.status({ directory }, { throwOnError: true })) + const operation = status.state.enabled ? "disable" : "enable" + return (await this.execute({ operation, sessionID })) ? operation : undefined + }) + } + + private async execute(message: KiloProviderMemoryMessage): Promise { + const client = this.input.client() + if (!client) { + this.input.post({ + type: "memoryOperationResult", + operation: message.operation, + sessionID: message.sessionID, + ok: false, + error: "Not connected to CLI backend", + }) + return false + } + + const api = memory(client) + if (!api) { + this.input.post({ + type: "memoryOperationResult", + operation: message.operation, + sessionID: message.sessionID, + ok: false, + error: "Memory unavailable in CLI backend", + }) + return false + } + + try { + const directory = this.input.dir(message.sessionID ?? this.input.session()?.id) + if (!directory) { + this.input.post({ + type: "memoryOperationResult", + operation: message.operation, + sessionID: message.sessionID, + ok: false, + error: NO_PROJECT, + }) + return false + } + const data = await this.action(api, directory, message) + const refreshed = await Promise.all([ + retry(() => api.status({ directory }, { throwOnError: true })), + retry(() => api.show({ directory }, { throwOnError: true })), + ]).catch((err: unknown) => { + console.warn("[Kilo New] Memory changed but refresh failed:", err) + return undefined + }) + const status = refreshed?.[0].data + const show = refreshed?.[1].data + const result = { + type: "memoryOperationResult", + operation: message.operation, + sessionID: message.sessionID, + ok: true, + ...(status ? { status } : {}), + ...(show ? { show } : {}), + result: data, + } + this.input.post(result) + if (status && show) { + const loaded = { + type: "memoryLoaded", + sessionID: message.sessionID, + status, + show, + } + this.cache(directory, loaded) + this.input.post(loaded) + } else { + // Mutation succeeded but the refresh failed: drop the now-stale cached + // entry so a later offline read doesn't report pre-mutation state. + this.cached.delete(directory) + } + return true + } catch (err) { + console.error("[Kilo New] KiloProvider: Failed memory operation:", err) + this.input.post({ + type: "memoryOperationResult", + operation: message.operation, + sessionID: message.sessionID, + ok: false, + error: getErrorMessage(err) || "Memory operation failed", + }) + return false + } + } + + private async action(api: MemoryApi, directory: string, message: KiloProviderMemoryMessage) { + const op = message.operation + if (op === "enable") return (await api.enable({ directory }, { throwOnError: true })).data + if (op === "status") return (await api.status({ directory }, { throwOnError: true })).data + if (op === "edit") return this.edit(api, directory) + if (op === "disable") return (await api.disable({ directory }, { throwOnError: true })).data + if (op === "rebuild") return (await api.rebuild({ directory }, { throwOnError: true })).data + if (op === "purge") return this.purge(api, directory, message) + if (op === "auto") return this.auto(api, directory, message) + if (op === "remember") return this.remember(api, directory, message) + if (op === "correct") return this.correct(api, directory, message) + return this.forget(api, directory, message) + } + + private async remember(api: MemoryApi, directory: string, message: KiloProviderMemoryMessage) { + const text = message.text?.trim() + if (!text) throw new Error("Memory text is required") + return ( + await api.remember( + { + directory, + text, + key: message.key, + file: message.file, + section: message.section, + sessionID: message.sessionID, + }, + { throwOnError: true }, + ) + ).data + } + + private async correct(api: MemoryApi, directory: string, message: KiloProviderMemoryMessage) { + const text = message.text?.trim() + if (!text) throw new Error("Correction text is required") + return ( + await api.correct( + { + directory, + text, + key: message.key, + sessionID: message.sessionID, + }, + { throwOnError: true }, + ) + ).data + } + + private async forget(api: MemoryApi, directory: string, message: KiloProviderMemoryMessage) { + const query = message.query?.trim() + if (!query) throw new Error("Forget query is required") + return (await api.forget({ directory, query, sessionID: message.sessionID }, { throwOnError: true })).data + } + + private async edit(api: MemoryApi, directory: string) { + const { data: status } = await retry(() => api.status({ directory }, { throwOnError: true })) + if (!status.state.enabled) throw new Error("Memory is disabled. Run /memory on first.") + const uri = vscode.Uri.file(path.join(status.root, "project.md")) + const doc = await vscode.workspace.openTextDocument(uri) + await vscode.window.showTextDocument(doc, { preview: false }) + return status + } + + private async purge(api: MemoryApi, directory: string, message: KiloProviderMemoryMessage) { + if (message.confirm !== true) throw new Error("Memory purge requires confirmation") + return (await api.purge({ directory, confirm: true }, { throwOnError: true })).data + } + + private async auto(api: MemoryApi, directory: string, message: KiloProviderMemoryMessage) { + if (message.mode === "status") return (await retry(() => api.status({ directory }, { throwOnError: true }))).data + if (message.mode === "on" || message.mode === "off") { + return (await api.configure({ directory, autoConsolidate: message.mode === "on" }, { throwOnError: true })).data + } + throw new Error("Auto-save mode is required") + } +} diff --git a/packages/kilo-vscode/tests/setup/vscode-mock.ts b/packages/kilo-vscode/tests/setup/vscode-mock.ts index 5cb1d09741..d85485821f 100644 --- a/packages/kilo-vscode/tests/setup/vscode-mock.ts +++ b/packages/kilo-vscode/tests/setup/vscode-mock.ts @@ -50,8 +50,13 @@ const mockVscode = { notebookDocuments: [] as Array, onDidOpenTextDocument: () => ({ dispose: noop }), onDidChangeTextDocument: () => ({ dispose: noop }), + onDidSaveTextDocument: () => ({ dispose: noop }), onDidCloseTextDocument: () => ({ dispose: noop }), onDidChangeConfiguration: () => ({ dispose: noop }), + openTextDocument: async (input: { content?: string; language?: string }) => ({ + getText: () => input.content ?? "", + languageId: input.language ?? "plaintext", + }), getConfiguration: () => ({ get: (_key: string, value?: T) => value, update: async () => {}, @@ -81,6 +86,7 @@ const mockVscode = { tabGroups: { all: [] }, showTextDocument: async () => {}, showInformationMessage: async () => undefined, + showErrorMessage: async () => undefined, showWarningMessage: async () => undefined, createTerminal: () => ({ show: noop, sendText: noop, dispose: noop }), createOutputChannel: () => ({ diff --git a/packages/kilo-vscode/tests/unit/agent-manager-memory-commands.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-memory-commands.test.ts new file mode 100644 index 0000000000..1ac5616097 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/agent-manager-memory-commands.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, it, mock } from "bun:test" + +const { AgentManagerProvider } = await import("../../src/agent-manager/AgentManagerProvider") + +type Manager = { + host: { showError: (message: string) => void } + panel: + | { + waitForReady: () => Promise + onDidDispose: (cb: () => void) => { dispose: () => void } + sessions: { + showMemory: (sessionID?: string) => Promise + toggleMemory: (sessionID?: string) => Promise + } + } + | undefined + activeSessionId: string | undefined + showMemory: () => Promise + toggleMemory: () => Promise +} + +function deferred() { + let done: (() => void) | undefined + const promise = new Promise((resolve) => { + done = resolve + }) + return { + promise, + resolve: () => done?.(), + } +} + +function manager() { + const calls: unknown[] = [] + const disposers: Array<() => void> = [] + const item = Object.create(AgentManagerProvider.prototype) as Manager + item.host = { showError: mock((message) => calls.push(["error", message])) } + item.panel = { + waitForReady: mock(async () => calls.push("ready")), + onDidDispose: mock((cb) => { + disposers.push(cb) + return { dispose: mock(() => {}) } + }), + sessions: { + showMemory: mock(async (sessionID) => calls.push(["show", sessionID])), + toggleMemory: mock(async (sessionID) => calls.push(["toggle", sessionID])), + }, + } + item.activeSessionId = "ses_agent_manager" + return { + item, + calls, + dispose: () => { + for (const cb of disposers) cb() + }, + } +} + +describe("AgentManagerProvider memory commands", () => { + it("routes memory commands to the active Agent Manager session", async () => { + const ctx = manager() + + await ctx.item.showMemory() + await ctx.item.toggleMemory() + + expect(ctx.calls).toEqual(["ready", ["show", "ses_agent_manager"], "ready", ["toggle", "ses_agent_manager"]]) + }) + + it("does not fall back when Agent Manager has no active session", async () => { + const ctx = manager() + ctx.item.activeSessionId = undefined + + await ctx.item.showMemory() + + expect(ctx.calls).toEqual([["error", "No active Agent Manager session"]]) + }) + + it("does not hang when the panel is disposed before it is ready", async () => { + const ctx = manager() + const wait = deferred() + ctx.item.panel!.waitForReady = mock(() => wait.promise) + + const pending = ctx.item.showMemory() + ctx.dispose() + await pending + + expect(ctx.calls).toEqual([]) + }) + + it("does not target a stale session after readiness resolves", async () => { + const ctx = manager() + const wait = deferred() + ctx.item.panel!.waitForReady = mock(() => wait.promise) + + const pending = ctx.item.toggleMemory() + ctx.item.activeSessionId = "ses_other" + wait.resolve() + await pending + + expect(ctx.calls).toEqual([]) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/kilo-provider-memory-events.test.ts b/packages/kilo-vscode/tests/unit/kilo-provider-memory-events.test.ts new file mode 100644 index 0000000000..f4f417fad0 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/kilo-provider-memory-events.test.ts @@ -0,0 +1,183 @@ +import { describe, expect, it } from "bun:test" +import type { KiloClient } from "@kilocode/sdk/v2/client" + +// vscode mock is provided by the shared preload (tests/setup/vscode-mock.ts) +const { KiloProvider } = await import("../../src/KiloProvider") + +type Internals = { + currentSession: { id: string } | null + trackedSessionIds: Set + webview: { postMessage(message: unknown): Promise } | null + handleEvent(event: unknown, directory?: string): void + memory: { idle(): Promise } +} + +function status(root: string) { + return { + root: `${root}/.kilo/memory`, + state: { + enabled: true, + autoConsolidate: true, + stats: { + lastInjectedSessionID: "", + lastInjectedTokens: 0, + lastOperationCount: 0, + }, + }, + index: { estimatedTokens: 0 }, + } +} + +function show(root: string) { + return { + root: `${root}/.kilo/memory`, + state: status(root).state, + sources: { project: "", environment: "", corrections: "" }, + index: "", + items: "", + changes: "", + decisions: "", + } +} + +describe("KiloProvider memory events", () => { + it("routes tracked background memory events to their session directory", async () => { + const calls: string[] = [] + const posts: unknown[] = [] + const client = { + memory: { + status: async (input: { directory: string }) => { + calls.push(input.directory) + return { data: status(input.directory) } + }, + }, + } as unknown as KiloClient + const provider = new KiloProvider( + {} as never, + { + getClient: () => client, + } as never, + ) + const item = provider as unknown as Internals + item.webview = { postMessage: async (message) => posts.push(message) } + item.currentSession = { id: "ses_active" } + item.trackedSessionIds.add("ses_active") + item.trackedSessionIds.add("ses_bg") + provider.setSessionDirectory("ses_bg", "/worktree") + + item.handleEvent( + { + type: "memory.updated", + properties: { + sessionID: "ses_bg", + detail: { type: "saved", message: "Saved project memory" }, + }, + }, + "/worktree", + ) + await item.memory.idle() + + expect(posts).toContainEqual({ + type: "memoryEvent", + sessionID: "ses_bg", + detail: { type: "saved", message: "Saved project memory" }, + }) + expect(posts).toContainEqual(expect.objectContaining({ type: "memoryLoaded", sessionID: "ses_bg" })) + expect(posts).not.toContainEqual(expect.objectContaining({ type: "memoryEvent", sessionID: "ses_active" })) + expect(calls).toEqual(["/worktree"]) + }) + + it("also refreshes the active session for same-directory memory events", async () => { + const calls: string[] = [] + const posts: unknown[] = [] + const client = { + memory: { + status: async (input: { directory: string }) => { + calls.push(input.directory) + return { data: status(input.directory) } + }, + }, + } as unknown as KiloClient + const provider = new KiloProvider( + {} as never, + { + getClient: () => client, + } as never, + ) + const item = provider as unknown as Internals + item.webview = { postMessage: async (message) => posts.push(message) } + item.currentSession = { id: "ses_active" } + item.trackedSessionIds.add("ses_active") + item.trackedSessionIds.add("ses_bg") + provider.setSessionDirectory("ses_active", "/repo") + provider.setSessionDirectory("ses_bg", "/repo") + + item.handleEvent( + { + type: "memory.updated", + properties: { + sessionID: "ses_bg", + detail: { type: "saved", message: "Saved project memory" }, + }, + }, + "/repo", + ) + await item.memory.idle() + + expect(posts).toContainEqual({ + type: "memoryEvent", + sessionID: "ses_bg", + detail: { type: "saved", message: "Saved project memory" }, + }) + expect(posts).toContainEqual({ + type: "memoryEvent", + sessionID: "ses_active", + detail: { type: "saved", message: "Saved project memory" }, + }) + expect(posts).toContainEqual(expect.objectContaining({ type: "memoryLoaded", sessionID: "ses_bg" })) + expect(posts).toContainEqual(expect.objectContaining({ type: "memoryLoaded", sessionID: "ses_active" })) + expect(calls).toEqual(["/repo", "/repo"]) + }) + + it("uses the project directory when toggling memory", async () => { + const calls: unknown[] = [] + const client = { + memory: { + status: async (input: { directory: string }) => { + calls.push(["status", input.directory]) + return { data: status(input.directory) } + }, + disable: async (input: { directory: string }) => { + calls.push(["disable", input.directory]) + return { data: { root: `${input.directory}/.kilo/memory`, state: status(input.directory).state } } + }, + show: async (input: { directory: string }) => { + calls.push(["show", input.directory]) + return { data: show(input.directory) } + }, + }, + } as unknown as KiloClient + const posts: unknown[] = [] + const provider = new KiloProvider( + {} as never, + { + getClient: () => client, + } as never, + undefined, + { projectDirectory: "/repo/project" }, + ) + const item = provider as unknown as Internals + item.webview = { postMessage: async (message) => posts.push(message) } + item.currentSession = { id: "ses_active" } + + await provider.toggleMemory("ses_active") + + expect(calls).toEqual([ + ["status", "/repo/project"], + ["disable", "/repo/project"], + ["status", "/repo/project"], + ["show", "/repo/project"], + ]) + expect(posts).toContainEqual(expect.objectContaining({ type: "memoryLoaded", sessionID: "ses_active" })) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/kilo-provider-memory.test.ts b/packages/kilo-vscode/tests/unit/kilo-provider-memory.test.ts new file mode 100644 index 0000000000..3630e80790 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/kilo-provider-memory.test.ts @@ -0,0 +1,212 @@ +import { describe, expect, it } from "bun:test" +import type { KiloClient } from "@kilocode/sdk/v2/client" +import { KiloProviderMemory } from "../../src/kilo-provider/memory" + +function subject(client: KiloClient | undefined) { + const posts: unknown[] = [] + const memory = new KiloProviderMemory({ + client: () => client, + session: () => undefined, + dir: () => "/repo", + post: (message) => posts.push(message), + }) + return { memory, posts } +} + +function status(root: string) { + return { + root: `${root}/.kilo/memory`, + state: { + enabled: true, + autoConsolidate: true, + stats: { + lastInjectedSessionID: "", + lastInjectedTokens: 0, + lastOperationCount: 0, + }, + }, + index: { estimatedTokens: 0 }, + } +} + +function show(root: string) { + return { + root: `${root}/.kilo/memory`, + state: status(root).state, + sources: { project: "", environment: "", corrections: "" }, + index: "", + items: "", + changes: "", + decisions: "", + } +} + +describe("KiloProviderMemory", () => { + it("handles clients without memory endpoints gracefully", async () => { + const item = subject({} as KiloClient) + + await item.memory.fetch("ses_memoryless") + await item.memory.show("ses_memoryless") + await item.memory.run({ operation: "enable", sessionID: "ses_memoryless" }) + + expect(item.posts).toEqual([ + { + type: "memoryLoaded", + sessionID: "ses_memoryless", + error: "Memory unavailable in CLI backend", + }, + { + type: "memoryLoaded", + sessionID: "ses_memoryless", + error: "Memory unavailable in CLI backend", + }, + { + type: "memoryOperationResult", + operation: "enable", + sessionID: "ses_memoryless", + ok: false, + error: "Memory unavailable in CLI backend", + }, + ]) + }) + + it("posts a load error when no client or cache exists", async () => { + const item = subject(undefined) + + await item.memory.fetch("ses_disconnected") + + expect(item.posts).toEqual([ + { + type: "memoryLoaded", + sessionID: "ses_disconnected", + error: "Not connected to CLI backend", + }, + ]) + }) + + it("evicts older cached memory payloads", async () => { + let client: KiloClient | undefined + const posts: unknown[] = [] + const item = new KiloProviderMemory({ + client: () => client, + session: () => undefined, + dir: (sid) => `/repo/${sid ?? "current"}`, + post: (message) => posts.push(message), + }) + client = { + memory: { + status: async (input: { directory: string }) => ({ data: status(input.directory) }), + show: async (input: { directory: string }) => ({ data: show(input.directory) }), + }, + } as unknown as KiloClient + + for (let i = 0; i < 9; i++) { + await item.show(`ses_${i}`) + } + + posts.length = 0 + client = undefined + await item.fetch("ses_0") + await item.fetch("ses_8") + + expect(posts[0]).toEqual({ + type: "memoryLoaded", + sessionID: "ses_0", + error: "Not connected to CLI backend", + }) + expect(posts[1]).toMatchObject({ + type: "memoryLoaded", + sessionID: "ses_8", + show: { root: "/repo/ses_8/.kilo/memory" }, + }) + }) + + it("does not send ignored placement fields to correction endpoint", async () => { + const calls: unknown[] = [] + const state = status("/repo") + const view = show("/repo") + const item = subject({ + memory: { + correct: async (input: unknown) => { + calls.push(input) + return { data: { operationCount: 1, added: 1, removed: 0, skipped: [], index: { tokens: 0 } } } + }, + status: async () => ({ data: state }), + show: async () => ({ data: view }), + }, + } as unknown as KiloClient) + + await item.memory.run({ + operation: "correct", + sessionID: "ses_correct", + text: "Prefer corrections.", + key: "correction_key", + file: "project.md", + section: "Facts", + }) + + expect(calls).toEqual([ + { + directory: "/repo", + text: "Prefer corrections.", + key: "correction_key", + sessionID: "ses_correct", + }, + ]) + }) + + it("routes status operations without mutating memory", async () => { + const calls: string[] = [] + const state = status("/repo") + const view = show("/repo") + const item = subject({ + memory: { + status: async () => { + calls.push("status") + return { data: state } + }, + show: async () => { + calls.push("show") + return { data: view } + }, + }, + } as unknown as KiloClient) + + await item.memory.run({ operation: "status", sessionID: "ses_memory" }) + + expect(calls).toEqual(["status", "status", "show"]) + expect(item.posts).toContainEqual( + expect.objectContaining({ type: "memoryOperationResult", operation: "status", ok: true }), + ) + }) + + it("routes auto-save and purge operations with explicit payloads", async () => { + const calls: unknown[] = [] + const state = status("/repo") + const view = show("/repo") + state.state.autoConsolidate = false + const item = subject({ + memory: { + configure: async (input: unknown) => { + calls.push(["configure", input]) + return { data: { root: "/repo/.kilo/memory", state: state.state } } + }, + purge: async (input: unknown) => { + calls.push(["purge", input]) + return { data: { root: "/repo/.kilo/memory", purged: true } } + }, + status: async () => ({ data: state }), + show: async () => ({ data: view }), + }, + } as unknown as KiloClient) + + await item.memory.run({ operation: "auto", mode: "off", sessionID: "ses_memory" }) + await item.memory.run({ operation: "purge", confirm: true, sessionID: "ses_memory" }) + + expect(calls).toEqual([ + ["configure", { directory: "/repo", autoConsolidate: false }], + ["purge", { directory: "/repo", confirm: true }], + ]) + expect(item.posts.filter((post) => (post as { type?: string }).type === "memoryOperationResult")).toHaveLength(2) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/memory-command.test.ts b/packages/kilo-vscode/tests/unit/memory-command.test.ts new file mode 100644 index 0000000000..1bd1df1e5d --- /dev/null +++ b/packages/kilo-vscode/tests/unit/memory-command.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from "bun:test" +import { parseMemoryCommand, type ParsedMemoryCommand } from "../../webview-ui/src/utils/memory-command" + +type MemoryOperation = + | "enable" + | "status" + | "edit" + | "disable" + | "rebuild" + | "remember" + | "correct" + | "forget" + | "purge" + | "auto" +type Case = { + name: string + input: string + result: "none" | "help" | "show" | "operation" | "usage" + operation?: MemoryOperation + mode?: "status" | "on" | "off" + confirm?: boolean + text?: string + query?: string + reason?: string +} + +const cases = (await Bun.file( + new URL("../../../kilo-memory/test/command-cases.json", import.meta.url), +).json()) as Case[] + +function expected(item: Case): ParsedMemoryCommand | undefined { + if (item.result === "none") return + if (item.result === "help") return { kind: "help" } + if (item.result === "show") return { kind: "show" } + if (item.result === "usage") return { kind: "usage", reason: item.reason ?? "" } + if (!item.operation) throw new Error(`Missing operation for fixture: ${item.name}`) + if (item.operation === "remember" || item.operation === "correct") { + if (!item.text) throw new Error(`Missing text for fixture: ${item.name}`) + return { kind: "operation", operation: item.operation, text: item.text } + } + if (item.operation === "forget") { + if (!item.query) throw new Error(`Missing query for fixture: ${item.name}`) + return { kind: "operation", operation: item.operation, query: item.query } + } + if (item.operation === "auto") { + if (!item.mode) throw new Error(`Missing mode for fixture: ${item.name}`) + return { kind: "operation", operation: item.operation, mode: item.mode } + } + if (item.operation === "purge") { + if (item.confirm !== true) throw new Error(`Missing confirmation for fixture: ${item.name}`) + return { kind: "operation", operation: item.operation, confirm: true } + } + return { kind: "operation", operation: item.operation } +} + +describe("parseMemoryCommand", () => { + it("matches shared command fixtures", () => { + for (const item of cases) { + const parsed = parseMemoryCommand(item.input) + if (item.result === "usage") { + expect(parsed?.kind, item.name).toBe("usage") + expect(parsed && "reason" in parsed ? parsed.reason : "", item.name).toContain(item.reason ?? "") + continue + } + expect(parsed, item.name).toEqual(expected(item)) + } + }) +}) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index a02d0e9dc4..b85a96390d 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -80,6 +80,7 @@ import { DisplayProvider } from "../src/context/display" import { KiloEmbeddingModelsProvider } from "../src/context/kilo-embedding-models" import { NotificationsProvider } from "../src/context/notifications" import { FeedbackProvider } from "../src/context/feedback" +import { MemoryProvider } from "../src/context/memory" import { SessionProvider, useSession } from "../src/context/session" import { AgentRequirementsProvider } from "../src/context/agent-requirements" import { WorktreeModeProvider } from "../src/context/worktree-mode" @@ -3140,13 +3141,15 @@ export const AgentManagerApp: Component = () => { - - - - - - - + + + + + + + + + diff --git a/packages/kilo-vscode/webview-ui/src/App.tsx b/packages/kilo-vscode/webview-ui/src/App.tsx index 358fc0cd8e..64e4b94eb3 100644 --- a/packages/kilo-vscode/webview-ui/src/App.tsx +++ b/packages/kilo-vscode/webview-ui/src/App.tsx @@ -20,6 +20,7 @@ import { DisplayProvider } from "./context/display" import { WorkStyleProvider } from "./context/work-style" import { IndexingProvider } from "./context/indexing" import { AgentRequirementsProvider } from "./context/agent-requirements" +import { MemoryProvider } from "./context/memory" import { SessionProvider, useSession } from "./context/session" import { LanguageBridge } from "./context/language-bridge" import { ChatView } from "./components/chat" @@ -418,11 +419,13 @@ const App: Component = () => { - - - - - + + + + + + + diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx index adb44b6071..b87ce38294 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx @@ -11,6 +11,7 @@ import { Component, For, Show, createMemo } from "solid-js" import { Dynamic } from "solid-js/web" import { Part, PART_MAPPING, ToolRegistry } from "@kilocode/kilo-ui/message-part" import type { MessageFeedbackControls } from "@kilocode/kilo-ui/message-part" +import { Tooltip } from "@kilocode/kilo-ui/tooltip" import type { AssistantMessage as SDKAssistantMessage, Part as SDKPart, @@ -22,9 +23,11 @@ import { useSession } from "../../context/session" import { useDisplay } from "../../context/display" import { useConfig } from "../../context/config" import { useLanguage } from "../../context/language" +import { useMemory } from "../../context/memory" import { useServer } from "../../context/server" import { snapshotProgress } from "../../context/session-utils" import { planDisplayPath } from "../../utils/plan-path" +import { MemoryMarkerMeta } from "@kilocode/kilo-memory/marker-meta" import { QuestionDock } from "./QuestionDock" import { SuggestBar } from "./SuggestBar" @@ -131,6 +134,8 @@ type ToolStateProps = { status?: string } +type MemoryItem = MemoryMarkerMeta.Decoded + function TodoToolCard(props: { part: ToolPart }) { const render = ToolRegistry.render(props.part.tool) const state = () => props.part.state as ToolStateProps @@ -183,6 +188,8 @@ export const AssistantMessage: Component = (props) => { const data = useData() const session = useSession() const display = useDisplay() + const mem = useMemory() + const language = useLanguage() const { config } = useConfig() const open = createMemo(() => config().terminal_command_display !== "collapsed") const edit = createMemo(() => config().code_edit_display === "expanded") @@ -198,6 +205,54 @@ export const AssistantMessage: Component = (props) => { return !!matchToolRequest(part, "question", session.questions()) }) }) + const meta = createMemo(() => + MemoryMarkerMeta.fromParts((props.parts ?? data.store.part?.[props.message.id] ?? []) as MemoryMarkerMeta.Part[]), + ) + const fmt = (value: number) => value.toLocaleString(language.locale()) + const count = (item: MemoryItem) => fmt(item.count) + const tokens = (item: MemoryItem) => fmt(item.tokens) + const label = (item: MemoryItem) => + item.type === "startup" ? language.t("chat.memory.badge.injected") : language.t("chat.memory.badge.recalled") + const detail = (item: MemoryItem) => + item.type === "startup" + ? language.t("chat.memory.badge.startupCtx") + : language.t("chat.memory.badge.items", { count: count(item) }) + const tip = (item: MemoryItem) => { + const err = mem.error() + if (err) return {err} + const status = mem.status() + if (!status) return {language.t("chat.memory.status.loading")} + const ops = status.state.stats.lastOperationCount + const total = mem.totalTokens().toLocaleString(language.locale()) + return ( +
+
+ {item.type === "startup" + ? language.t("chat.memory.session.tokens", { tokens: tokens(item) }) + : language.t("chat.memory.badge.recalledDetail", { count: count(item), tokens: tokens(item) })} +
+
{language.t("chat.memory.total.tokens", { tokens: total })}
+
+ {!status.state.enabled + ? language.t("chat.memory.project.disabled") + : language.t("chat.memory.project.enabled")} +
+ 0}> +
+ {language.t("chat.memory.savedOperations", { + count: ops.toLocaleString(language.locale()), + })} +
+
+ +
{mem.show()!.changes.split("\n").filter(Boolean).slice(-1)[0]}
+
+ 0}> +
{language.t("chat.memory.badge.files", { files: item.files.join(", ") })}
+
+
+ ) + } return ( <> @@ -290,6 +345,15 @@ export const AssistantMessage: Component = (props) => { ) }} + + {(item) => ( + +
+ {label(item())} · {detail(item())} · {language.t("chat.memory.badge.tokens", { tokens: tokens(item()) })} +
+
+ )} +
) } diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/ContextProgress.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/ContextProgress.tsx index b066d05982..69a311a59a 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/ContextProgress.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/ContextProgress.tsx @@ -13,12 +13,7 @@ import { Component, createMemo, Show } from "solid-js" import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { useSession } from "../../context/session" import { useProvider } from "../../context/provider" - -function fmt(n: number): string { - if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M` - if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K` - return String(n) -} +import { formatCompactCount as fmt } from "../../utils/format" export const ContextProgress: Component = () => { const session = useSession() diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx index d9c346cb4b..1593dadf0c 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx @@ -61,6 +61,8 @@ import { drafts, imageDrafts, reviewDrafts } from "../../utils/draft-store" import { ReviewComments } from "./ReviewComments" import { partReview, reviewBody } from "../../../../src/shared/review-comments" import { isEnterKeyCommitNotIme } from "../../utils/ime-enter" +import { MEMORY_USAGE, parseMemoryCommand } from "../../utils/memory-command" +import { useMemory } from "../../context/memory" const scrolls = new Map() function mergeReviewComments(current: ReviewComment[], incoming: ReviewComment[]): ReviewComment[] { @@ -91,6 +93,7 @@ export const PromptInput: Component = (props) => { const provider = useProvider() const language = useLanguage() const vscode = useVSCode() + const projectMemory = useMemory() const sid = () => session.currentSessionID() ?? props.pendingSessionID ?? session.draftSessionID() ?? undefined const ctx = () => { const id = props.boxId @@ -936,9 +939,61 @@ export const PromptInput: Component = (props) => { transcribeAndSend() } + const runMemory = (memory: NonNullable>) => { + if (memory.kind === "usage") { + showToast({ variant: "error", title: language.t("chat.memory.command.failed"), description: memory.reason }) + return false + } + if (memory.kind === "help") { + showToast({ variant: "default", title: "/memory", description: MEMORY_USAGE }) + return true + } + if (isDisabled() || speech.active() || terminal.pending() || git.pending() || props.blocked?.()) return false + const status = projectMemory.status() + if ( + memory.kind === "operation" && + (memory.operation === "remember" || memory.operation === "correct" || memory.operation === "forget") && + status && + !status.state.enabled + ) { + showToast({ variant: "error", title: language.t("chat.memory.project.disabled") }) + return false + } + if (memory.kind === "show") vscode.postMessage({ type: "memoryShow", sessionID: sid() }) + if (memory.kind === "operation") { + vscode.postMessage({ + type: "memoryOperation", + operation: memory.operation, + sessionID: sid(), + ...(memory.operation === "auto" ? { mode: memory.mode } : {}), + ...(memory.operation === "purge" ? { confirm: memory.confirm } : {}), + ...(memory.operation === "remember" || memory.operation === "correct" ? { text: memory.text } : {}), + ...(memory.operation === "forget" ? { query: memory.query } : {}), + }) + } + return true + } + const handleSend = async () => { const draft = text().trim() + const memory = parseMemoryCommand(draft) + if (memory) { + if (!runMemory(memory)) return + history.append(draft) + setText("") + clearReviewComments() + imageAttach.clear() + mention.closeMention() + slash.close() + drafts.delete(draftKey()) + reviewDrafts.delete(draftKey()) + imageDrafts.delete(draftKey()) + scrolls.delete(draftKey()) + if (textareaRef) textareaRef.style.height = "auto" + return + } + // Detect slash command (hoisted for both client and server command checks). // Prioritize exact name matches over hint/alias matches so that a server // command named e.g. "continue" is not hijacked by a client alias. diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx index 96ac022f93..468fe45d5a 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx @@ -14,6 +14,7 @@ import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { Icon } from "@kilocode/kilo-ui/icon" import { Checkbox } from "@kilocode/kilo-ui/checkbox" import { useSession } from "../../context/session" +import { useMemory } from "../../context/memory" import { calcTokenUsage, collapseCostBreakdown } from "../../context/session-utils" import { useLanguage } from "../../context/language" import { useVSCode } from "../../context/vscode" @@ -24,6 +25,7 @@ import { hasModelUsage, tokenSummary } from "../../context/model-usage" import { SessionRenameEditor } from "../shared/SessionRenameEditor" import { target as todoTarget } from "../../context/todo-revert" import type { Part, TodoItem, ExtensionMessage } from "../../types/messages" +import { formatCompactCount } from "../../utils/format" interface TaskHeaderProps { readonly?: boolean @@ -31,6 +33,7 @@ interface TaskHeaderProps { export const TaskHeader: Component = (props) => { const session = useSession() + const memory = useMemory() const language = useLanguage() const title = createMemo(() => session.currentSession()?.title ?? language.t("command.session.new")) @@ -83,6 +86,17 @@ export const TaskHeader: Component = (props) => { return false }) + const memoryLabel = createMemo(() => { + const count = memory.sessionTokens() + return count > 0 + ? language.t("chat.memory.label", { tokens: formatCompactCount(count) }) + : language.t("chat.memory.on") + }) + + const memoryTooltip = createMemo(() => + language.t("chat.memory.session.tokens", { tokens: formatCompactCount(memory.sessionTokens()) }), + ) + const vscode = useVSCode() const [expanded, setExpanded] = createSignal(true) @@ -235,6 +249,69 @@ export const TaskHeader: Component = (props) => { {(tk) => }
+ +
+ + + + {memoryLabel()} + + + + + memory.showMemory()} + aria-label={language.t("chat.memory.inspect")} + /> + + + memory.remember()} + aria-label={language.t("chat.memory.remember")} + /> + + + memory.forget()} + aria-label={language.t("chat.memory.forget")} + /> + + + memory.rebuild()} + aria-label={language.t("chat.memory.rebuild")} + /> + + {/* Strip only mounts when enabled, so this is always the disable action; re-enable lives in Settings > Context. */} + + memory.disable()} + aria-label={language.t("chat.memory.disable")} + /> + + +
+
+ memory.rebuild()} + aria-label={language.t("settings.context.memory.rebuild")} + /> +
+ + + {(err) => ( +
+ {err()} +
+ )} +
+ + {/* Compaction settings */} +

+ {language.t("settings.context.compaction.title")} +

+ show: Accessor + loading: Accessor + pending: Accessor + error: Accessor + enabled: Accessor + sessionTokens: Accessor + totalTokens: Accessor + refresh: (includeSources?: boolean) => void + showMemory: () => void + enable: () => void + disable: () => void + auto: (mode: "on" | "off") => void + rebuild: () => void + remember: () => void + forget: () => void +} + +export const MemoryContext = createContext() +const EVENT_DEDUPE_MS = 1000 + +export const MemoryProvider: ParentComponent = (props) => { + const vscode = useVSCode() + const server = useServer() + const session = useSession() + const language = useLanguage() + const [status, setStatus] = createSignal() + const [show, setShow] = createSignal() + const [loading, setLoading] = createSignal(false) + const [pending, setPending] = createSignal() + const [error, setError] = createSignal() + + const id = () => session.currentSessionID() + const key = (sid?: string) => sid ?? "" + const current = (sid?: string) => { + if (!sid) return true + // A response can be addressed to a draft session that hasn't been promoted to + // currentSessionID yet (PromptInput posts with the draft id), so match both. + return sid === id() || sid === session.draftSessionID() + } + let last: { key: string; time: number } | undefined + let scope = "" + + const clear = () => { + setStatus(undefined) + setShow(undefined) + setError(undefined) + setPending(undefined) + last = undefined + } + + const refresh = (includeSources = false) => { + if (!server.isConnected()) return + setLoading(true) + setError(undefined) + vscode.postMessage({ type: "requestMemory", sessionID: id(), includeSources }) + } + + const operation = (op: "enable" | "disable" | "rebuild") => { + if (!server.isConnected()) return + setPending(key(id())) + setError(undefined) + vscode.postMessage({ type: "memoryOperation", operation: op, sessionID: id() }) + } + + const auto = (mode: "on" | "off") => { + if (!server.isConnected()) return + setPending(key(id())) + setError(undefined) + vscode.postMessage({ type: "memoryOperation", operation: "auto", mode, sessionID: id() }) + } + + const prompt = (op: "remember" | "forget") => { + if (!server.isConnected()) return + setPending(key(id())) + setError(undefined) + vscode.postMessage({ type: "memoryPrompt", operation: op, sessionID: id() }) + } + + const showMemory = () => { + if (!server.isConnected()) return + setLoading(true) + setError(undefined) + vscode.postMessage({ type: "memoryShow", sessionID: id() }) + } + + const event = (message: Extract) => { + if (!current(message.sessionID)) return + if (message.detail.type === "skipped") return + if (!message.detail.message) return + const dedupeKey = `${message.sessionID ?? ""}:${message.detail.type ?? ""}:${message.detail.message}` + const now = Date.now() + if (last?.key === dedupeKey && now - last.time < EVENT_DEDUPE_MS) return + last = { key: dedupeKey, time: now } + showToast({ + ...(message.detail.type === "saved" + ? { variant: "success" as const } + : message.detail.type === "error" + ? { variant: "error" as const } + : {}), + title: message.detail.message, + }) + } + + const loaded = (message: Extract) => { + if (!current(message.sessionID)) return + setLoading(false) + if (message.error) { + setError(message.error) + setStatus(undefined) + setShow(undefined) + return + } + if (message.status) setStatus(message.status) + if (message.show) setShow(message.show) + setError(undefined) + } + + const done = (message: Extract) => { + if (pending() === key(message.sessionID)) setPending(undefined) + if (!current(message.sessionID)) return + setLoading(false) + if (!message.ok) { + const err = message.error ?? language.t("chat.memory.command.failed") + setError(err) + showToast({ variant: "error", title: err }) + return + } + if (message.status) setStatus(message.status) + if (message.show) setShow(message.show) + setError(undefined) + } + + const receive = (message: ExtensionMessage) => { + if (message.type === "memoryEvent") { + event(message) + return + } + if (message.type === "memoryLoaded") { + loaded(message) + return + } + if (message.type === "memoryOperationResult") { + done(message) + return + } + if (message.type === "extensionDataReady" && server.isConnected() && !status()) refresh(false) + } + + const unsubscribe = vscode.onMessage(receive) + + onCleanup(unsubscribe) + + createEffect(() => { + const sid = id() + const dir = server.workspaceDirectory() + const connected = server.isConnected() + const next = `${connected ? "1" : "0"}:${sid ?? ""}:${dir ?? ""}` + if (scope !== next) { + scope = next + clear() + } + if (!connected) { + setLoading(false) + return + } + refresh(false) + }) + + const sessionTokens = (snapshot?: MemoryStatusResponse) => { + const sid = id() + if (!snapshot?.state.enabled) return 0 + if (!sid || snapshot.state.stats.lastInjectedSessionID !== sid) return 0 + return snapshot.state.stats.lastInjectedTokens + } + + const total = createMemo(() => status()?.index.estimatedTokens ?? 0) + + const sessionTotal = createMemo(() => sessionTokens(status())) + + const value: MemoryContextValue = { + status, + show, + loading, + pending: createMemo(() => pending() === key(id())), + error, + enabled: createMemo(() => status()?.state.enabled ?? false), + sessionTokens: sessionTotal, + totalTokens: total, + refresh, + showMemory, + enable: () => operation("enable"), + disable: () => operation("disable"), + auto, + rebuild: () => operation("rebuild"), + remember: () => prompt("remember"), + forget: () => prompt("forget"), + } + + return {props.children} +} + +export function useMemory(): MemoryContextValue { + const context = useContext(MemoryContext) + if (!context) { + throw new Error("useMemory must be used within a MemoryProvider") + } + return context +} diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 5a66075adc..9dd3f6eb34 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -1590,6 +1590,7 @@ export const dict = { "settings.checkpoints.enable.description": "إنشاء نقاط فحص قبل تحرير الملفات", "settings.context.autoCompaction.title": "ضغط تلقائي", "settings.context.autoCompaction.description": "ضغط السياق تلقائياً قبل أن يصل إلى الحد", + "settings.context.compaction.title": "الضغط", "settings.context.compactionLimit.title": "حد الضغط التلقائي", "settings.context.compactionLimit.description": "اضغط عندما يصل السياق إلى هذه النسبة المئوية من نافذة النموذج. اتركه فارغاً لاستخدام هامش الأمان فقط.", @@ -1598,6 +1599,42 @@ export const dict = { "settings.context.watcherPatterns": "أنماط تجاهل مراقب الملفات", "settings.context.watcherPatterns.description": "أنماط glob للملفات التي يجب على المراقب تجاهلها", + "settings.context.memory.title": "الذاكرة", + "settings.context.memory.project.title": "ذاكرة المشروع", + "settings.context.memory.autoSave.title": "حفظ ذاكرة المشروع تلقائيًا", + "settings.context.memory.autoSave.description": + "حفظ حقائق المشروع الدائمة تلقائيًا من الجولات المكتملة عند تفعيل الذاكرة.", + "settings.context.memory.index.title": "فهرس الذاكرة", + "settings.context.memory.status.notLoaded": "غير محمّلة", + "settings.context.memory.status.disabled": "معطّلة", + "settings.context.memory.status.enabledTokensOps": + "مفعّلة - ~{{session}} رموز سياق بدء التشغيل في هذه الجلسة - ~{{tokens}} رموز في الفهرس المخزّن - آخر عملية {{ops}}", + "settings.context.memory.index.path": "{{path}}/index.kmem", + "settings.context.memory.index.enable": "فعّل الذاكرة لإنشاء ملفات ذاكرة المشروع.", + "settings.context.memory.inspect": "فحص", + "settings.context.memory.rebuild": "إعادة بناء فهرس الذاكرة", + "chat.memory.on": "الذاكرة مفعّلة", + "chat.memory.label": "الذاكرة · {{tokens}} رمز", + "chat.memory.status.loading": "جارٍ تحميل حالة الذاكرة", + "chat.memory.session.tokens": "سياق بدء التشغيل في هذه الجلسة: {{tokens}} رموز", + "chat.memory.total.tokens": "الفهرس المخزّن: {{tokens}} رموز", + "chat.memory.project.enabled": "ذاكرة المشروع مفعّلة", + "chat.memory.project.disabled": "ذاكرة المشروع معطّلة", + "chat.memory.command.failed": "فشل أمر الذاكرة", + "chat.memory.savedOperations": "آخر عملية ذاكرة: {{count}} عمليات", + "chat.memory.inspect": "فحص الذاكرة", + "chat.memory.remember": "تذكّر", + "chat.memory.forget": "انسَ", + "chat.memory.rebuild": "إعادة بناء الفهرس", + "chat.memory.disable": "تعطيل الذاكرة", + "chat.memory.badge.injected": "تم حقن الذاكرة", + "chat.memory.badge.recalled": "تم استدعاء الذاكرة", + "chat.memory.badge.startupCtx": "سياق البدء", + "chat.memory.badge.items": "{{count}} عناصر", + "chat.memory.badge.tokens": "{{tokens}} رموز", + "chat.memory.badge.recalledDetail": "تم استدعاء الذاكرة: {{count}} عناصر - {{tokens}} رموز", + "chat.memory.badge.files": "ملفات الذاكرة: {{files}}", + "settings.commitMessage.title": "Commit Message", "settings.commitMessage.override.title": "استخدام prompt مخصص", "settings.commitMessage.override.description": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index f738a64eae..8f7c0867cd 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -1638,6 +1638,7 @@ export const dict = { "settings.checkpoints.enable.description": "Criar pontos de verificação antes de editar arquivos", "settings.context.autoCompaction.title": "Compactação automática", "settings.context.autoCompaction.description": "Compactar automaticamente o contexto antes que atinja o limite", + "settings.context.compaction.title": "Compactação", "settings.context.compactionLimit.title": "Limite de compactação automática", "settings.context.compactionLimit.description": "Compacte quando o contexto atingir esta porcentagem da janela do modelo. Deixe em branco para usar apenas a margem de segurança.", @@ -1646,6 +1647,42 @@ export const dict = { "settings.context.watcherPatterns": "Padrões de ignorar do observador", "settings.context.watcherPatterns.description": "Padrões glob para arquivos que o observador deve ignorar", + "settings.context.memory.title": "Memória", + "settings.context.memory.project.title": "Memória do projeto", + "settings.context.memory.autoSave.title": "Salvar memória do projeto automaticamente", + "settings.context.memory.autoSave.description": + "Salva automaticamente fatos duradouros do projeto a partir de turnos concluídos quando a memória está ativada.", + "settings.context.memory.index.title": "Índice da memória", + "settings.context.memory.status.notLoaded": "Não carregada", + "settings.context.memory.status.disabled": "Desativada", + "settings.context.memory.status.enabledTokensOps": + "Ativada - ~{{session}} tokens de contexto inicial nesta sessão - ~{{tokens}} tokens no índice armazenado - última op. {{ops}}", + "settings.context.memory.index.path": "{{path}}/index.kmem", + "settings.context.memory.index.enable": "Ative a memória para criar arquivos de memória do projeto.", + "settings.context.memory.inspect": "Inspecionar", + "settings.context.memory.rebuild": "Reconstruir índice da memória", + "chat.memory.on": "Memória ligada", + "chat.memory.label": "Memória · {{tokens}} tokens", + "chat.memory.status.loading": "Carregando status da memória", + "chat.memory.session.tokens": "Contexto inicial desta sessão: {{tokens}} tokens", + "chat.memory.total.tokens": "Índice armazenado: {{tokens}} tokens", + "chat.memory.project.enabled": "Memória do projeto ativada", + "chat.memory.project.disabled": "Memória do projeto desativada", + "chat.memory.command.failed": "Comando de memória falhou", + "chat.memory.savedOperations": "Última operação de memória: {{count}} ops", + "chat.memory.inspect": "Inspecionar memória", + "chat.memory.remember": "Lembrar", + "chat.memory.forget": "Esquecer", + "chat.memory.rebuild": "Reconstruir índice", + "chat.memory.disable": "Desativar memória", + "chat.memory.badge.injected": "Memória injetada", + "chat.memory.badge.recalled": "Memória recuperada", + "chat.memory.badge.startupCtx": "ctx inicial", + "chat.memory.badge.items": "{{count}} itens", + "chat.memory.badge.tokens": "{{tokens}} tokens", + "chat.memory.badge.recalledDetail": "Memória recuperada: {{count}} itens - {{tokens}} tokens", + "chat.memory.badge.files": "Arquivos de memória: {{files}}", + "settings.commitMessage.title": "Commit Message", "settings.commitMessage.override.title": "Usar prompt personalizado", "settings.commitMessage.override.description": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index 13674b2f11..96d098fd3e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -1631,6 +1631,7 @@ export const dict = { "settings.checkpoints.enable.description": "Kreiraj kontrolne točke prije uređivanja datoteka", "settings.context.autoCompaction.title": "Automatska kompresija", "settings.context.autoCompaction.description": "Automatski komprimiraj kontekst prije nego dostigne limit", + "settings.context.compaction.title": "Kompresija", "settings.context.compactionLimit.title": "Limit automatske kompresije", "settings.context.compactionLimit.description": "Komprimiraj kada kontekst dostigne ovaj procenat prozora modela. Ostavite prazno da koristite samo sigurnosnu rezervu.", @@ -1639,6 +1640,42 @@ export const dict = { "settings.context.watcherPatterns": "Uzorci ignoriranja za promatrač datoteka", "settings.context.watcherPatterns.description": "Glob uzorci za datoteke koje promatrač treba ignorirati", + "settings.context.memory.title": "Memorija", + "settings.context.memory.project.title": "Memorija projekta", + "settings.context.memory.autoSave.title": "Automatsko spremanje memorije projekta", + "settings.context.memory.autoSave.description": + "Automatski sprema trajne činjenice projekta iz završenih koraka kada je memorija uključena.", + "settings.context.memory.index.title": "Indeks memorije", + "settings.context.memory.status.notLoaded": "Nije učitana", + "settings.context.memory.status.disabled": "Onemogućena", + "settings.context.memory.status.enabledTokensOps": + "Omogućena - ~{{session}} tokena početnog konteksta u ovoj sesiji - ~{{tokens}} tokena spremljenog indeksa - zadnja op. {{ops}}", + "settings.context.memory.index.path": "{{path}}/index.kmem", + "settings.context.memory.index.enable": "Omogućite memoriju za kreiranje datoteka memorije projekta.", + "settings.context.memory.inspect": "Pregledaj", + "settings.context.memory.rebuild": "Ponovo izgradi indeks memorije", + "chat.memory.on": "Memorija uključena", + "chat.memory.label": "Memorija · {{tokens}} tokena", + "chat.memory.status.loading": "Učitavanje statusa memorije", + "chat.memory.session.tokens": "Početni kontekst ove sesije: {{tokens}} tokena", + "chat.memory.total.tokens": "Spremljeni indeks: {{tokens}} tokena", + "chat.memory.project.enabled": "Memorija projekta omogućena", + "chat.memory.project.disabled": "Memorija projekta onemogućena", + "chat.memory.command.failed": "Komanda memorije nije uspjela", + "chat.memory.savedOperations": "Zadnja operacija memorije: {{count}} op.", + "chat.memory.inspect": "Pregledaj memoriju", + "chat.memory.remember": "Zapamti", + "chat.memory.forget": "Zaboravi", + "chat.memory.rebuild": "Ponovo izgradi indeks", + "chat.memory.disable": "Onemogući memoriju", + "chat.memory.badge.injected": "Memorija ubačena", + "chat.memory.badge.recalled": "Memorija opozvana", + "chat.memory.badge.startupCtx": "početni ctx", + "chat.memory.badge.items": "{{count}} stavki", + "chat.memory.badge.tokens": "{{tokens}} tokena", + "chat.memory.badge.recalledDetail": "Memorija opozvana: {{count}} stavki - {{tokens}} tokena", + "chat.memory.badge.files": "Datoteke memorije: {{files}}", + "settings.commitMessage.title": "Commit Message", "settings.commitMessage.override.title": "Koristi prilagođeni prompt", "settings.commitMessage.override.description": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index c68095f7b6..c9ac7fe483 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -1621,6 +1621,7 @@ export const dict = { "settings.checkpoints.enable.description": "Opret kontrolpunkter før filredigeringer", "settings.context.autoCompaction.title": "Automatisk komprimering", "settings.context.autoCompaction.description": "Komprimér automatisk kontekst, før den når grænsen", + "settings.context.compaction.title": "Komprimering", "settings.context.compactionLimit.title": "Grænse for automatisk komprimering", "settings.context.compactionLimit.description": "Komprimér, når konteksten når denne procentdel af modelvinduet. Lad feltet være tomt for kun at bruge sikkerhedsbufferen.", @@ -1629,6 +1630,42 @@ export const dict = { "settings.context.watcherPatterns": "Filvagt-ignormønstre", "settings.context.watcherPatterns.description": "Glob-mønstre for filer, som vagten skal ignorere", + "settings.context.memory.title": "Hukommelse", + "settings.context.memory.project.title": "Projekthukommelse", + "settings.context.memory.autoSave.title": "Gem projekthukommelse automatisk", + "settings.context.memory.autoSave.description": + "Gemmer automatisk varige projektfakta fra afsluttede ture, når hukommelse er aktiveret.", + "settings.context.memory.index.title": "Hukommelsesindeks", + "settings.context.memory.status.notLoaded": "Ikke indlæst", + "settings.context.memory.status.disabled": "Deaktiveret", + "settings.context.memory.status.enabledTokensOps": + "Aktiveret - ~{{session}} opstartstokens i denne session - ~{{tokens}} tokens i gemt indeks - seneste handling {{ops}}", + "settings.context.memory.index.path": "{{path}}/index.kmem", + "settings.context.memory.index.enable": "Aktivér hukommelse for at oprette projektets hukommelsesfiler.", + "settings.context.memory.inspect": "Inspicér", + "settings.context.memory.rebuild": "Genopbyg hukommelsesindeks", + "chat.memory.on": "Hukommelse til", + "chat.memory.label": "Hukommelse · {{tokens}} tokens", + "chat.memory.status.loading": "Indlæser hukommelsesstatus", + "chat.memory.session.tokens": "Opstartskontekst i denne session: {{tokens}} tokens", + "chat.memory.total.tokens": "Gemt indeks: {{tokens}} tokens", + "chat.memory.project.enabled": "Projekthukommelse aktiveret", + "chat.memory.project.disabled": "Projekthukommelse deaktiveret", + "chat.memory.command.failed": "Hukommelseskommando mislykkedes", + "chat.memory.savedOperations": "Seneste hukommelseshandling: {{count}} handlinger", + "chat.memory.inspect": "Inspicér hukommelse", + "chat.memory.remember": "Husk", + "chat.memory.forget": "Glem", + "chat.memory.rebuild": "Genopbyg indeks", + "chat.memory.disable": "Deaktivér hukommelse", + "chat.memory.badge.injected": "Hukommelse injiceret", + "chat.memory.badge.recalled": "Hukommelse genkaldt", + "chat.memory.badge.startupCtx": "opstartsctx", + "chat.memory.badge.items": "{{count}} elementer", + "chat.memory.badge.tokens": "{{tokens}} tokens", + "chat.memory.badge.recalledDetail": "Hukommelse genkaldt: {{count}} elementer - {{tokens}} tokens", + "chat.memory.badge.files": "Hukommelsesfiler: {{files}}", + "settings.commitMessage.title": "Commit Message", "settings.commitMessage.override.title": "Brug brugerdefineret prompt", "settings.commitMessage.override.description": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 5748b0116f..7a22c29c89 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -1657,6 +1657,7 @@ export const dict = { "Prüfpunkte vor Dateibearbeitungen erstellen, um vorherige Zustände wiederherstellen zu können", "settings.context.autoCompaction.title": "Automatische Komprimierung", "settings.context.autoCompaction.description": "Kontext automatisch komprimieren, bevor er das Limit erreicht", + "settings.context.compaction.title": "Komprimierung", "settings.context.compactionLimit.title": "Limit für automatische Komprimierung", "settings.context.compactionLimit.description": "Komprimieren, wenn der Kontext diesen Prozentsatz des Modellfensters erreicht. Leer lassen, um nur den Sicherheitspuffer zu verwenden.", @@ -1665,6 +1666,42 @@ export const dict = { "settings.context.watcherPatterns": "Datei-Watcher-Ignorierungsmuster", "settings.context.watcherPatterns.description": "Glob-Muster für Dateien, die der Watcher ignorieren soll", + "settings.context.memory.title": "Speicher", + "settings.context.memory.project.title": "Projektspeicher", + "settings.context.memory.autoSave.title": "Projektspeicher automatisch speichern", + "settings.context.memory.autoSave.description": + "Dauerhafte Projektfakten aus abgeschlossenen Durchläufen automatisch speichern, wenn Speicher aktiviert ist.", + "settings.context.memory.index.title": "Speicherindex", + "settings.context.memory.status.notLoaded": "Nicht geladen", + "settings.context.memory.status.disabled": "Deaktiviert", + "settings.context.memory.status.enabledTokensOps": + "Aktiviert - ~{{session}} Startkontext-Tokens in dieser Sitzung - ~{{tokens}} Tokens im gespeicherten Index - letzte Operation {{ops}}", + "settings.context.memory.index.path": "{{path}}/index.kmem", + "settings.context.memory.index.enable": "Aktivieren Sie den Speicher, um Projektspeicherdateien zu erstellen.", + "settings.context.memory.inspect": "Prüfen", + "settings.context.memory.rebuild": "Speicherindex neu erstellen", + "chat.memory.on": "Speicher an", + "chat.memory.label": "Speicher · {{tokens}} Tokens", + "chat.memory.status.loading": "Speicherstatus wird geladen", + "chat.memory.session.tokens": "Startkontext dieser Sitzung: {{tokens}} Tokens", + "chat.memory.total.tokens": "Gespeicherter Index: {{tokens}} Tokens", + "chat.memory.project.enabled": "Projektspeicher aktiviert", + "chat.memory.project.disabled": "Projektspeicher deaktiviert", + "chat.memory.command.failed": "Speicherbefehl fehlgeschlagen", + "chat.memory.savedOperations": "Letzte Speicheroperation: {{count}} Vorgänge", + "chat.memory.inspect": "Speicher prüfen", + "chat.memory.remember": "Merken", + "chat.memory.forget": "Vergessen", + "chat.memory.rebuild": "Index neu erstellen", + "chat.memory.disable": "Speicher deaktivieren", + "chat.memory.badge.injected": "Speicher eingefügt", + "chat.memory.badge.recalled": "Speicher abgerufen", + "chat.memory.badge.startupCtx": "Startkontext", + "chat.memory.badge.items": "{{count}} Elemente", + "chat.memory.badge.tokens": "{{tokens}} Tokens", + "chat.memory.badge.recalledDetail": "Speicher abgerufen: {{count}} Elemente - {{tokens}} Tokens", + "chat.memory.badge.files": "Speicherdateien: {{files}}", + "settings.commitMessage.title": "Commit Message", "settings.commitMessage.override.title": "Benutzerdefinierten prompt verwenden", "settings.commitMessage.override.description": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index c379dd5c76..ae7a17800b 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -1602,6 +1602,7 @@ export const dict = { "settings.context.autoCompaction.title": "Auto Compaction", "settings.context.autoCompaction.description": "Automatically compact context before it reaches the limit", + "settings.context.compaction.title": "Compaction", "settings.context.compactionLimit.title": "Auto Compaction Limit", "settings.context.compactionLimit.description": "Compact when context reaches this percentage of the model window. Leave blank to use the safety buffer only.", @@ -1609,6 +1610,41 @@ export const dict = { "settings.context.prune.description": "Remove old tool outputs during compaction", "settings.context.watcherPatterns": "File Watcher Ignore Patterns", "settings.context.watcherPatterns.description": "Glob patterns for files the watcher should ignore", + "settings.context.memory.title": "Memory", + "settings.context.memory.project.title": "Project memory", + "settings.context.memory.autoSave.title": "Auto-save project memory", + "settings.context.memory.autoSave.description": + "Automatically save durable project facts from completed turns when memory is enabled.", + "settings.context.memory.index.title": "Memory index", + "settings.context.memory.status.notLoaded": "Not loaded", + "settings.context.memory.status.disabled": "Disabled", + "settings.context.memory.status.enabledTokensOps": + "Enabled - ~{{session}} startup tokens this session - ~{{tokens}} stored index tokens - last op {{ops}}", + "settings.context.memory.index.path": "{{path}}/index.kmem", + "settings.context.memory.index.enable": "Enable memory to create project memory files.", + "settings.context.memory.inspect": "Inspect", + "settings.context.memory.rebuild": "Rebuild memory index", + "chat.memory.on": "Memory on", + "chat.memory.label": "Memory · {{tokens}} tokens", + "chat.memory.status.loading": "Memory status loading", + "chat.memory.session.tokens": "Startup context this session: {{tokens}} tokens", + "chat.memory.total.tokens": "Stored index: {{tokens}} tokens", + "chat.memory.project.enabled": "Project memory enabled", + "chat.memory.project.disabled": "Project memory disabled", + "chat.memory.command.failed": "Memory command failed", + "chat.memory.savedOperations": "Last memory operation: {{count}} ops", + "chat.memory.inspect": "Inspect memory", + "chat.memory.remember": "Remember", + "chat.memory.forget": "Forget", + "chat.memory.rebuild": "Rebuild index", + "chat.memory.disable": "Disable memory", + "chat.memory.badge.injected": "Memory injected", + "chat.memory.badge.recalled": "Memory recalled", + "chat.memory.badge.startupCtx": "startup ctx", + "chat.memory.badge.items": "{{count}} items", + "chat.memory.badge.tokens": "{{tokens}} tokens", + "chat.memory.badge.recalledDetail": "Memory recalled: {{count}} items - {{tokens}} tokens", + "chat.memory.badge.files": "Memory files: {{files}}", "settings.commitMessage.title": "Commit Message", "settings.commitMessage.override.title": "Use Custom Prompt", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index 5188521433..bdeaf5939a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -1646,6 +1646,7 @@ export const dict = { "settings.checkpoints.enable.description": "Crear puntos de control antes de editar archivos", "settings.context.autoCompaction.title": "Compactación automática", "settings.context.autoCompaction.description": "Compactar automáticamente el contexto antes de que alcance el límite", + "settings.context.compaction.title": "Compactación", "settings.context.compactionLimit.title": "Límite de compactación automática", "settings.context.compactionLimit.description": "Compactar cuando el contexto alcance este porcentaje de la ventana del modelo. Déjalo en blanco para usar solo el búfer de seguridad.", @@ -1654,6 +1655,42 @@ export const dict = { "settings.context.watcherPatterns": "Patrones de ignorar del observador", "settings.context.watcherPatterns.description": "Patrones glob para archivos que el observador debe ignorar", + "settings.context.memory.title": "Memoria", + "settings.context.memory.project.title": "Memoria del proyecto", + "settings.context.memory.autoSave.title": "Guardado automático de memoria del proyecto", + "settings.context.memory.autoSave.description": + "Guardar automáticamente hechos duraderos del proyecto desde turnos completados cuando la memoria está activada.", + "settings.context.memory.index.title": "Índice de memoria", + "settings.context.memory.status.notLoaded": "No cargada", + "settings.context.memory.status.disabled": "Desactivada", + "settings.context.memory.status.enabledTokensOps": + "Activada - ~{{session}} tokens de contexto inicial en esta sesión - ~{{tokens}} tokens del índice almacenado - última op. {{ops}}", + "settings.context.memory.index.path": "{{path}}/index.kmem", + "settings.context.memory.index.enable": "Activa la memoria para crear archivos de memoria del proyecto.", + "settings.context.memory.inspect": "Inspeccionar", + "settings.context.memory.rebuild": "Reconstruir índice de memoria", + "chat.memory.on": "Memoria activada", + "chat.memory.label": "Memoria · {{tokens}} tokens", + "chat.memory.status.loading": "Cargando estado de la memoria", + "chat.memory.session.tokens": "Contexto inicial de esta sesión: {{tokens}} tokens", + "chat.memory.total.tokens": "Índice almacenado: {{tokens}} tokens", + "chat.memory.project.enabled": "Memoria del proyecto activada", + "chat.memory.project.disabled": "Memoria del proyecto desactivada", + "chat.memory.command.failed": "Error en el comando de memoria", + "chat.memory.savedOperations": "Última operación de memoria: {{count}} ops", + "chat.memory.inspect": "Inspeccionar memoria", + "chat.memory.remember": "Recordar", + "chat.memory.forget": "Olvidar", + "chat.memory.rebuild": "Reconstruir índice", + "chat.memory.disable": "Desactivar memoria", + "chat.memory.badge.injected": "Memoria inyectada", + "chat.memory.badge.recalled": "Memoria recuperada", + "chat.memory.badge.startupCtx": "ctx inicial", + "chat.memory.badge.items": "{{count}} elementos", + "chat.memory.badge.tokens": "{{tokens}} tokens", + "chat.memory.badge.recalledDetail": "Memoria recuperada: {{count}} elementos - {{tokens}} tokens", + "chat.memory.badge.files": "Archivos de memoria: {{files}}", + "settings.commitMessage.title": "Commit Message", "settings.commitMessage.override.title": "Usar prompt personalizado", "settings.commitMessage.override.description": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index f05e4c68fb..95fff423ce 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -1666,6 +1666,7 @@ export const dict = { "settings.context.autoCompaction.title": "Compaction automatique", "settings.context.autoCompaction.description": "Compacter automatiquement le contexte avant qu'il n'atteigne la limite", + "settings.context.compaction.title": "Compactage", "settings.context.compactionLimit.title": "Limite de compactage automatique", "settings.context.compactionLimit.description": "Compacter lorsque le contexte atteint ce pourcentage de la fenêtre du modèle. Laissez vide pour utiliser uniquement la marge de sécurité.", @@ -1674,6 +1675,42 @@ export const dict = { "settings.context.watcherPatterns": "Motifs d'ignorance de l'observateur", "settings.context.watcherPatterns.description": "Motifs glob pour les fichiers que l'observateur doit ignorer", + "settings.context.memory.title": "Mémoire", + "settings.context.memory.project.title": "Mémoire du projet", + "settings.context.memory.autoSave.title": "Enregistrement automatique de la mémoire du projet", + "settings.context.memory.autoSave.description": + "Enregistrer automatiquement les faits durables du projet à partir des tours terminés lorsque la mémoire est activée.", + "settings.context.memory.index.title": "Index de mémoire", + "settings.context.memory.status.notLoaded": "Non chargée", + "settings.context.memory.status.disabled": "Désactivée", + "settings.context.memory.status.enabledTokensOps": + "Activée - ~{{session}} tokens de contexte initial dans cette session - ~{{tokens}} tokens dans l’index stocké - dernière opération {{ops}}", + "settings.context.memory.index.path": "{{path}}/index.kmem", + "settings.context.memory.index.enable": "Activez la mémoire pour créer les fichiers de mémoire du projet.", + "settings.context.memory.inspect": "Inspecter", + "settings.context.memory.rebuild": "Reconstruire l’index de mémoire", + "chat.memory.on": "Mémoire activée", + "chat.memory.label": "Mémoire · {{tokens}} tokens", + "chat.memory.status.loading": "Chargement de l’état de la mémoire", + "chat.memory.session.tokens": "Contexte initial de cette session : {{tokens}} tokens", + "chat.memory.total.tokens": "Index stocké : {{tokens}} tokens", + "chat.memory.project.enabled": "Mémoire du projet activée", + "chat.memory.project.disabled": "Mémoire du projet désactivée", + "chat.memory.command.failed": "La commande de mémoire a échoué", + "chat.memory.savedOperations": "Dernière opération de mémoire : {{count}} ops", + "chat.memory.inspect": "Inspecter la mémoire", + "chat.memory.remember": "Mémoriser", + "chat.memory.forget": "Oublier", + "chat.memory.rebuild": "Reconstruire l’index", + "chat.memory.disable": "Désactiver la mémoire", + "chat.memory.badge.injected": "Mémoire injectée", + "chat.memory.badge.recalled": "Mémoire rappelée", + "chat.memory.badge.startupCtx": "ctx initial", + "chat.memory.badge.items": "{{count}} éléments", + "chat.memory.badge.tokens": "{{tokens}} tokens", + "chat.memory.badge.recalledDetail": "Mémoire rappelée : {{count}} éléments - {{tokens}} tokens", + "chat.memory.badge.files": "Fichiers mémoire : {{files}}", + "settings.commitMessage.title": "Commit Message", "settings.commitMessage.override.title": "Utiliser un prompt personnalisé", "settings.commitMessage.override.description": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index d37b19b83c..9291b14d1b 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -1428,10 +1428,47 @@ export const dict = { "Crea checkpoint prima delle modifiche ai file così puoi ripristinare stati precedenti", "settings.context.autoCompaction.title": "Compattazione automatica", "settings.context.autoCompaction.description": "Compatta automaticamente il contesto quando è pieno", + "settings.context.compaction.title": "Compattazione", "settings.context.prune.title": "Riduci output vecchi", "settings.context.prune.description": "Rimuovi output tool vecchi durante la compattazione", "settings.context.watcherPatterns": "Pattern ignore file watcher", "settings.context.watcherPatterns.description": "Pattern glob per file che il watcher deve ignorare", + "settings.context.memory.title": "Memoria", + "settings.context.memory.project.title": "Memoria del progetto", + "settings.context.memory.autoSave.title": "Salvataggio automatico memoria progetto", + "settings.context.memory.autoSave.description": + "Salva automaticamente fatti durevoli del progetto dai turni completati quando la memoria è attiva.", + "settings.context.memory.index.title": "Indice della memoria", + "settings.context.memory.status.notLoaded": "Non caricata", + "settings.context.memory.status.disabled": "Disattivata", + "settings.context.memory.status.enabledTokensOps": + "Attivata - ~{{session}} token di contesto iniziale in questa sessione - ~{{tokens}} token nell’indice salvato - ultima operazione {{ops}}", + "settings.context.memory.index.path": "{{path}}/index.kmem", + "settings.context.memory.index.enable": "Attiva la memoria per creare i file di memoria del progetto.", + "settings.context.memory.inspect": "Ispeziona", + "settings.context.memory.rebuild": "Ricostruisci indice della memoria", + "chat.memory.on": "Memoria attivata", + "chat.memory.label": "Memoria · {{tokens}} token", + "chat.memory.status.loading": "Caricamento stato memoria", + "chat.memory.session.tokens": "Contesto iniziale di questa sessione: {{tokens}} token", + "chat.memory.total.tokens": "Indice salvato: {{tokens}} token", + "chat.memory.project.enabled": "Memoria del progetto attivata", + "chat.memory.project.disabled": "Memoria del progetto disattivata", + "chat.memory.command.failed": "Comando memoria non riuscito", + "chat.memory.savedOperations": "Ultima operazione di memoria: {{count}} ops", + "chat.memory.inspect": "Ispeziona memoria", + "chat.memory.remember": "Ricorda", + "chat.memory.forget": "Dimentica", + "chat.memory.rebuild": "Ricostruisci indice", + "chat.memory.disable": "Disattiva memoria", + "chat.memory.badge.injected": "Memoria iniettata", + "chat.memory.badge.recalled": "Memoria richiamata", + "chat.memory.badge.startupCtx": "ctx iniziale", + "chat.memory.badge.items": "{{count}} elementi", + "chat.memory.badge.tokens": "{{tokens}} token", + "chat.memory.badge.recalledDetail": "Memoria richiamata: {{count}} elementi - {{tokens}} token", + "chat.memory.badge.files": "File di memoria: {{files}}", + "settings.commitMessage.title": "Messaggio commit", "settings.commitMessage.override.title": "Usa prompt personalizzato", "settings.commitMessage.override.description": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 9b807d5d00..bc8570701a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -1617,6 +1617,7 @@ export const dict = { "settings.checkpoints.enable.description": "ファイル編集前にチェックポイントを作成して以前の状態を復元可能にする", "settings.context.autoCompaction.title": "自動圧縮", "settings.context.autoCompaction.description": "コンテキストが上限に達する前に自動的に圧縮", + "settings.context.compaction.title": "圧縮", "settings.context.compactionLimit.title": "自動圧縮の上限", "settings.context.compactionLimit.description": "コンテキストがモデルウィンドウのこの割合に達したら圧縮します。安全バッファーのみを使用するには空欄のままにしてください。", @@ -1625,6 +1626,42 @@ export const dict = { "settings.context.watcherPatterns": "ファイルウォッチャー無視パターン", "settings.context.watcherPatterns.description": "ウォッチャーが無視すべきファイルのglobパターン", + "settings.context.memory.title": "メモリ", + "settings.context.memory.project.title": "プロジェクトメモリ", + "settings.context.memory.autoSave.title": "プロジェクトメモリを自動保存", + "settings.context.memory.autoSave.description": + "メモリが有効なとき、完了したターンから永続的なプロジェクト情報を自動保存します。", + "settings.context.memory.index.title": "メモリインデックス", + "settings.context.memory.status.notLoaded": "未読み込み", + "settings.context.memory.status.disabled": "無効", + "settings.context.memory.status.enabledTokensOps": + "有効 - このセッションの起動コンテキスト ~{{session}} トークン - 保存済みインデックス ~{{tokens}} トークン - 最後の操作 {{ops}}", + "settings.context.memory.index.path": "{{path}}/index.kmem", + "settings.context.memory.index.enable": "メモリを有効にしてプロジェクトメモリファイルを作成します。", + "settings.context.memory.inspect": "検査", + "settings.context.memory.rebuild": "メモリインデックスを再構築", + "chat.memory.on": "メモリ オン", + "chat.memory.label": "メモリ · {{tokens}} トークン", + "chat.memory.status.loading": "メモリ状態を読み込み中", + "chat.memory.session.tokens": "このセッションの起動コンテキスト: {{tokens}} トークン", + "chat.memory.total.tokens": "保存済みインデックス: {{tokens}} トークン", + "chat.memory.project.enabled": "プロジェクトメモリが有効です", + "chat.memory.project.disabled": "プロジェクトメモリが無効です", + "chat.memory.command.failed": "メモリコマンドに失敗しました", + "chat.memory.savedOperations": "最後のメモリ操作: {{count}} 件", + "chat.memory.inspect": "メモリを検査", + "chat.memory.remember": "記憶", + "chat.memory.forget": "忘れる", + "chat.memory.rebuild": "インデックスを再構築", + "chat.memory.disable": "メモリを無効化", + "chat.memory.badge.injected": "メモリを注入しました", + "chat.memory.badge.recalled": "メモリを呼び出しました", + "chat.memory.badge.startupCtx": "起動ctx", + "chat.memory.badge.items": "{{count}} 件", + "chat.memory.badge.tokens": "{{tokens}} トークン", + "chat.memory.badge.recalledDetail": "メモリを呼び出しました: {{count}} 件 - {{tokens}} トークン", + "chat.memory.badge.files": "メモリファイル: {{files}}", + "settings.commitMessage.title": "Commit Message", "settings.commitMessage.override.title": "カスタム prompt を使用", "settings.commitMessage.override.description": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 203f58d32b..92c95acdab 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -1600,6 +1600,7 @@ export const dict = { "settings.checkpoints.enable.description": "파일 편집 전 체크포인트를 생성하여 이전 상태를 복원할 수 있습니다", "settings.context.autoCompaction.title": "자동 압축", "settings.context.autoCompaction.description": "컨텍스트가 한도에 도달하기 전에 자동으로 압축", + "settings.context.compaction.title": "압축", "settings.context.compactionLimit.title": "자동 압축 한도", "settings.context.compactionLimit.description": "컨텍스트가 모델 창의 이 비율에 도달하면 압축합니다. 안전 버퍼만 사용하려면 비워 두세요.", @@ -1608,6 +1609,42 @@ export const dict = { "settings.context.watcherPatterns": "파일 감시자 무시 패턴", "settings.context.watcherPatterns.description": "감시자가 무시해야 할 파일의 글로브 패턴", + "settings.context.memory.title": "메모리", + "settings.context.memory.project.title": "프로젝트 메모리", + "settings.context.memory.autoSave.title": "프로젝트 메모리 자동 저장", + "settings.context.memory.autoSave.description": + "메모리가 활성화되면 완료된 턴에서 지속적인 프로젝트 사실을 자동으로 저장합니다.", + "settings.context.memory.index.title": "메모리 인덱스", + "settings.context.memory.status.notLoaded": "로드되지 않음", + "settings.context.memory.status.disabled": "비활성화됨", + "settings.context.memory.status.enabledTokensOps": + "활성화됨 - 이 세션의 시작 컨텍스트 ~{{session}} 토큰 - 저장된 인덱스 ~{{tokens}} 토큰 - 마지막 작업 {{ops}}", + "settings.context.memory.index.path": "{{path}}/index.kmem", + "settings.context.memory.index.enable": "메모리를 활성화하여 프로젝트 메모리 파일을 만듭니다.", + "settings.context.memory.inspect": "검사", + "settings.context.memory.rebuild": "메모리 인덱스 다시 빌드", + "chat.memory.on": "메모리 켜짐", + "chat.memory.label": "메모리 · {{tokens}} 토큰", + "chat.memory.status.loading": "메모리 상태 로드 중", + "chat.memory.session.tokens": "이 세션의 시작 컨텍스트: {{tokens}} 토큰", + "chat.memory.total.tokens": "저장된 인덱스: {{tokens}} 토큰", + "chat.memory.project.enabled": "프로젝트 메모리 활성화됨", + "chat.memory.project.disabled": "프로젝트 메모리 비활성화됨", + "chat.memory.command.failed": "메모리 명령 실패", + "chat.memory.savedOperations": "마지막 메모리 작업: {{count}}개 작업", + "chat.memory.inspect": "메모리 검사", + "chat.memory.remember": "기억", + "chat.memory.forget": "잊기", + "chat.memory.rebuild": "인덱스 다시 빌드", + "chat.memory.disable": "메모리 비활성화", + "chat.memory.badge.injected": "메모리 주입됨", + "chat.memory.badge.recalled": "메모리 불러옴", + "chat.memory.badge.startupCtx": "시작 ctx", + "chat.memory.badge.items": "{{count}}개 항목", + "chat.memory.badge.tokens": "{{tokens}} 토큰", + "chat.memory.badge.recalledDetail": "메모리 불러옴: {{count}}개 항목 - {{tokens}} 토큰", + "chat.memory.badge.files": "메모리 파일: {{files}}", + "settings.commitMessage.title": "Commit Message", "settings.commitMessage.override.title": "사용자 지정 prompt 사용", "settings.commitMessage.override.description": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index 136df47605..5f93bd1575 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -1598,6 +1598,7 @@ export const dict = { "settings.context.autoCompaction.title": "Automatische Compactie", "settings.context.autoCompaction.description": "Context automatisch compacteren voordat deze de limiet bereikt", + "settings.context.compaction.title": "Compactie", "settings.context.compactionLimit.title": "Limiet voor automatisch compacteren", "settings.context.compactionLimit.description": "Compacteer wanneer de context dit percentage van het modelvenster bereikt. Laat leeg om alleen de veiligheidsbuffer te gebruiken.", @@ -1606,6 +1607,42 @@ export const dict = { "settings.context.watcherPatterns": "File Watcher Negeer Patronen", "settings.context.watcherPatterns.description": "Glob-patronen voor bestanden die de watcher moet negeren", + "settings.context.memory.title": "Geheugen", + "settings.context.memory.project.title": "Projectgeheugen", + "settings.context.memory.autoSave.title": "Projectgeheugen automatisch opslaan", + "settings.context.memory.autoSave.description": + "Sla duurzame projectfeiten automatisch op uit voltooide beurten wanneer geheugen is ingeschakeld.", + "settings.context.memory.index.title": "Geheugenindex", + "settings.context.memory.status.notLoaded": "Niet geladen", + "settings.context.memory.status.disabled": "Uitgeschakeld", + "settings.context.memory.status.enabledTokensOps": + "Ingeschakeld - ~{{session}} startcontexttokens in deze sessie - ~{{tokens}} tokens in opgeslagen index - laatste bewerking {{ops}}", + "settings.context.memory.index.path": "{{path}}/index.kmem", + "settings.context.memory.index.enable": "Schakel geheugen in om projectgeheugenbestanden te maken.", + "settings.context.memory.inspect": "Inspecteren", + "settings.context.memory.rebuild": "Geheugenindex opnieuw opbouwen", + "chat.memory.on": "Geheugen aan", + "chat.memory.label": "Geheugen · {{tokens}} tokens", + "chat.memory.status.loading": "Geheugenstatus laden", + "chat.memory.session.tokens": "Startcontext deze sessie: {{tokens}} tokens", + "chat.memory.total.tokens": "Opgeslagen index: {{tokens}} tokens", + "chat.memory.project.enabled": "Projectgeheugen ingeschakeld", + "chat.memory.project.disabled": "Projectgeheugen uitgeschakeld", + "chat.memory.command.failed": "Geheugenopdracht mislukt", + "chat.memory.savedOperations": "Laatste geheugenbewerking: {{count}} bewerkingen", + "chat.memory.inspect": "Geheugen inspecteren", + "chat.memory.remember": "Onthouden", + "chat.memory.forget": "Vergeten", + "chat.memory.rebuild": "Index opnieuw opbouwen", + "chat.memory.disable": "Geheugen uitschakelen", + "chat.memory.badge.injected": "Geheugen geïnjecteerd", + "chat.memory.badge.recalled": "Geheugen opgehaald", + "chat.memory.badge.startupCtx": "startctx", + "chat.memory.badge.items": "{{count}} items", + "chat.memory.badge.tokens": "{{tokens}} tokens", + "chat.memory.badge.recalledDetail": "Geheugen opgehaald: {{count}} items - {{tokens}} tokens", + "chat.memory.badge.files": "Geheugenbestanden: {{files}}", + "settings.commitMessage.title": "Commit Message", "settings.commitMessage.override.title": "Aangepaste prompt gebruiken", "settings.commitMessage.override.description": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index f568a6f3d2..6e5c43cdc5 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -1620,6 +1620,7 @@ export const dict = { "settings.checkpoints.enable.description": "Opprett kontrollpunkter før filredigeringer", "settings.context.autoCompaction.title": "Automatisk komprimering", "settings.context.autoCompaction.description": "Komprimer automatisk kontekst før den når grensen", + "settings.context.compaction.title": "Komprimering", "settings.context.compactionLimit.title": "Grense for automatisk komprimering", "settings.context.compactionLimit.description": "Komprimer når konteksten når denne prosentandelen av modellvinduet. La stå tomt for å bare bruke sikkerhetsbufferen.", @@ -1628,6 +1629,42 @@ export const dict = { "settings.context.watcherPatterns": "Filvakt-ignormønstre", "settings.context.watcherPatterns.description": "Glob-mønstre for filer som vakten skal ignorere", + "settings.context.memory.title": "Minne", + "settings.context.memory.project.title": "Prosjektminne", + "settings.context.memory.autoSave.title": "Lagre prosjektminne automatisk", + "settings.context.memory.autoSave.description": + "Lagrer varige prosjektfakta automatisk fra fullførte turer når minne er aktivert.", + "settings.context.memory.index.title": "Minneindeks", + "settings.context.memory.status.notLoaded": "Ikke lastet", + "settings.context.memory.status.disabled": "Deaktivert", + "settings.context.memory.status.enabledTokensOps": + "Aktivert - ~{{session}} oppstartstokener i denne økten - ~{{tokens}} tokener i lagret indeks - siste operasjon {{ops}}", + "settings.context.memory.index.path": "{{path}}/index.kmem", + "settings.context.memory.index.enable": "Aktiver minne for å opprette prosjektets minnefiler.", + "settings.context.memory.inspect": "Inspiser", + "settings.context.memory.rebuild": "Bygg minneindeks på nytt", + "chat.memory.on": "Minne på", + "chat.memory.label": "Minne · {{tokens}} tokens", + "chat.memory.status.loading": "Laster minnestatus", + "chat.memory.session.tokens": "Oppstartskontekst denne økten: {{tokens}} tokener", + "chat.memory.total.tokens": "Lagret indeks: {{tokens}} tokener", + "chat.memory.project.enabled": "Prosjektminne aktivert", + "chat.memory.project.disabled": "Prosjektminne deaktivert", + "chat.memory.command.failed": "Minnekommando mislyktes", + "chat.memory.savedOperations": "Siste minneoperasjon: {{count}} operasjoner", + "chat.memory.inspect": "Inspiser minne", + "chat.memory.remember": "Husk", + "chat.memory.forget": "Glem", + "chat.memory.rebuild": "Bygg indeks på nytt", + "chat.memory.disable": "Deaktiver minne", + "chat.memory.badge.injected": "Minne injisert", + "chat.memory.badge.recalled": "Minne hentet", + "chat.memory.badge.startupCtx": "oppstartsctx", + "chat.memory.badge.items": "{{count}} elementer", + "chat.memory.badge.tokens": "{{tokens}} tokener", + "chat.memory.badge.recalledDetail": "Minne hentet: {{count}} elementer - {{tokens}} tokener", + "chat.memory.badge.files": "Minnefiler: {{files}}", + "settings.commitMessage.title": "Commit Message", "settings.commitMessage.override.title": "Bruk egendefinert prompt", "settings.commitMessage.override.description": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index cac6e6766e..f7513a620e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -1629,6 +1629,7 @@ export const dict = { "settings.checkpoints.enable.description": "Twórz punkty kontrolne przed edycją plików", "settings.context.autoCompaction.title": "Automatyczna kompakcja", "settings.context.autoCompaction.description": "Automatycznie kompaktuj kontekst, zanim osiągnie limit", + "settings.context.compaction.title": "Kompaktowanie", "settings.context.compactionLimit.title": "Limit automatycznego kompaktowania", "settings.context.compactionLimit.description": "Kompaktuj, gdy kontekst osiągnie ten procent okna modelu. Pozostaw puste, aby używać tylko bufora bezpieczeństwa.", @@ -1637,6 +1638,42 @@ export const dict = { "settings.context.watcherPatterns": "Wzorce ignorowania obserwatora plików", "settings.context.watcherPatterns.description": "Wzorce glob dla plików do ignorowania", + "settings.context.memory.title": "Pamięć", + "settings.context.memory.project.title": "Pamięć projektu", + "settings.context.memory.autoSave.title": "Automatycznie zapisuj pamięć projektu", + "settings.context.memory.autoSave.description": + "Automatycznie zapisuje trwałe fakty projektu z zakończonych tur, gdy pamięć jest włączona.", + "settings.context.memory.index.title": "Indeks pamięci", + "settings.context.memory.status.notLoaded": "Nie wczytano", + "settings.context.memory.status.disabled": "Wyłączona", + "settings.context.memory.status.enabledTokensOps": + "Włączona - ~{{session}} tokenów kontekstu startowego w tej sesji - ~{{tokens}} tokenów w zapisanym indeksie - ostatnia operacja {{ops}}", + "settings.context.memory.index.path": "{{path}}/index.kmem", + "settings.context.memory.index.enable": "Włącz pamięć, aby utworzyć pliki pamięci projektu.", + "settings.context.memory.inspect": "Sprawdź", + "settings.context.memory.rebuild": "Odbuduj indeks pamięci", + "chat.memory.on": "Pamięć włączona", + "chat.memory.label": "Pamięć · {{tokens}} tokenów", + "chat.memory.status.loading": "Ładowanie stanu pamięci", + "chat.memory.session.tokens": "Kontekst startowy tej sesji: {{tokens}} tokenów", + "chat.memory.total.tokens": "Zapisany indeks: {{tokens}} tokenów", + "chat.memory.project.enabled": "Pamięć projektu włączona", + "chat.memory.project.disabled": "Pamięć projektu wyłączona", + "chat.memory.command.failed": "Polecenie pamięci nie powiodło się", + "chat.memory.savedOperations": "Ostatnia operacja pamięci: {{count}} operacji", + "chat.memory.inspect": "Sprawdź pamięć", + "chat.memory.remember": "Zapamiętaj", + "chat.memory.forget": "Zapomnij", + "chat.memory.rebuild": "Odbuduj indeks", + "chat.memory.disable": "Wyłącz pamięć", + "chat.memory.badge.injected": "Pamięć wstrzyknięta", + "chat.memory.badge.recalled": "Pamięć przywołana", + "chat.memory.badge.startupCtx": "ctx startowy", + "chat.memory.badge.items": "{{count}} elementów", + "chat.memory.badge.tokens": "{{tokens}} tokenów", + "chat.memory.badge.recalledDetail": "Pamięć przywołana: {{count}} elementów - {{tokens}} tokenów", + "chat.memory.badge.files": "Pliki pamięci: {{files}}", + "settings.commitMessage.title": "Commit Message", "settings.commitMessage.override.title": "Użyj niestandardowego prompt", "settings.commitMessage.override.description": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 09820f33d4..6087656085 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -1628,6 +1628,7 @@ export const dict = { "settings.checkpoints.enable.description": "Создавать контрольные точки перед редактированием файлов", "settings.context.autoCompaction.title": "Автоматическое сжатие", "settings.context.autoCompaction.description": "Автоматически сжимать контекст до достижения лимита", + "settings.context.compaction.title": "Сжатие", "settings.context.compactionLimit.title": "Лимит автоматического сжатия", "settings.context.compactionLimit.description": "Сжимать, когда контекст достигает этого процента окна модели. Оставьте пустым, чтобы использовать только буфер безопасности.", @@ -1636,6 +1637,42 @@ export const dict = { "settings.context.watcherPatterns": "Шаблоны игнорирования наблюдателя файлов", "settings.context.watcherPatterns.description": "Glob-шаблоны для файлов, которые наблюдатель должен игнорировать", + "settings.context.memory.title": "Память", + "settings.context.memory.project.title": "Память проекта", + "settings.context.memory.autoSave.title": "Автосохранение памяти проекта", + "settings.context.memory.autoSave.description": + "Автоматически сохраняет устойчивые факты проекта из завершённых ходов, когда память включена.", + "settings.context.memory.index.title": "Индекс памяти", + "settings.context.memory.status.notLoaded": "Не загружена", + "settings.context.memory.status.disabled": "Отключена", + "settings.context.memory.status.enabledTokensOps": + "Включена - ~{{session}} токенов начального контекста в этой сессии - ~{{tokens}} токенов в сохранённом индексе - последняя операция {{ops}}", + "settings.context.memory.index.path": "{{path}}/index.kmem", + "settings.context.memory.index.enable": "Включите память, чтобы создать файлы памяти проекта.", + "settings.context.memory.inspect": "Проверить", + "settings.context.memory.rebuild": "Перестроить индекс памяти", + "chat.memory.on": "Память включена", + "chat.memory.label": "Память · {{tokens}} токенов", + "chat.memory.status.loading": "Загрузка состояния памяти", + "chat.memory.session.tokens": "Начальный контекст этой сессии: {{tokens}} токенов", + "chat.memory.total.tokens": "Сохранённый индекс: {{tokens}} токенов", + "chat.memory.project.enabled": "Память проекта включена", + "chat.memory.project.disabled": "Память проекта отключена", + "chat.memory.command.failed": "Команда памяти не выполнена", + "chat.memory.savedOperations": "Последняя операция памяти: {{count}} операций", + "chat.memory.inspect": "Проверить память", + "chat.memory.remember": "Запомнить", + "chat.memory.forget": "Забыть", + "chat.memory.rebuild": "Перестроить индекс", + "chat.memory.disable": "Отключить память", + "chat.memory.badge.injected": "Память внедрена", + "chat.memory.badge.recalled": "Память извлечена", + "chat.memory.badge.startupCtx": "начальный ctx", + "chat.memory.badge.items": "{{count}} элементов", + "chat.memory.badge.tokens": "{{tokens}} токенов", + "chat.memory.badge.recalledDetail": "Память извлечена: {{count}} элементов - {{tokens}} токенов", + "chat.memory.badge.files": "Файлы памяти: {{files}}", + "settings.commitMessage.title": "Commit Message", "settings.commitMessage.override.title": "Использовать пользовательский prompt", "settings.commitMessage.override.description": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index 1bda1a2b38..75cbec15a1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -1598,6 +1598,7 @@ export const dict = { "settings.checkpoints.enable.description": "สร้างจุดตรวจก่อนแก้ไขไฟล์", "settings.context.autoCompaction.title": "การบีบอัดอัตโนมัติ", "settings.context.autoCompaction.description": "บีบอัดบริบทอัตโนมัติก่อนถึงขีดจำกัด", + "settings.context.compaction.title": "การบีบอัด", "settings.context.compactionLimit.title": "ขีดจำกัดการบีบอัดอัตโนมัติ", "settings.context.compactionLimit.description": "บีบอัดเมื่อบริบทถึงเปอร์เซ็นต์นี้ของหน้าต่างโมเดล เว้นว่างไว้เพื่อใช้เฉพาะบัฟเฟอร์ความปลอดภัย", @@ -1606,6 +1607,42 @@ export const dict = { "settings.context.watcherPatterns": "รูปแบบการละเว้นตัวเฝ้าดูไฟล์", "settings.context.watcherPatterns.description": "รูปแบบ glob สำหรับไฟล์ที่ตัวเฝ้าดูควรละเว้น", + "settings.context.memory.title": "ความจำ", + "settings.context.memory.project.title": "ความจำของโปรเจกต์", + "settings.context.memory.autoSave.title": "บันทึกความจำของโปรเจกต์อัตโนมัติ", + "settings.context.memory.autoSave.description": + "บันทึกข้อเท็จจริงถาวรของโปรเจกต์จากรอบที่เสร็จแล้วโดยอัตโนมัติเมื่อเปิดใช้ความจำ", + "settings.context.memory.index.title": "ดัชนีความจำ", + "settings.context.memory.status.notLoaded": "ยังไม่ได้โหลด", + "settings.context.memory.status.disabled": "ปิดใช้งาน", + "settings.context.memory.status.enabledTokensOps": + "เปิดใช้งาน - ~{{session}} โทเค็นบริบทเริ่มต้นในเซสชันนี้ - ~{{tokens}} โทเค็นในดัชนีที่บันทึกไว้ - การทำงานล่าสุด {{ops}}", + "settings.context.memory.index.path": "{{path}}/index.kmem", + "settings.context.memory.index.enable": "เปิดใช้ความจำเพื่อสร้างไฟล์ความจำของโปรเจกต์", + "settings.context.memory.inspect": "ตรวจสอบ", + "settings.context.memory.rebuild": "สร้างดัชนีความจำใหม่", + "chat.memory.on": "เปิดความจำ", + "chat.memory.label": "ความจำ · {{tokens}} โทเค็น", + "chat.memory.status.loading": "กำลังโหลดสถานะความจำ", + "chat.memory.session.tokens": "บริบทเริ่มต้นของเซสชันนี้: {{tokens}} โทเค็น", + "chat.memory.total.tokens": "ดัชนีที่บันทึกไว้: {{tokens}} โทเค็น", + "chat.memory.project.enabled": "เปิดใช้ความจำของโปรเจกต์แล้ว", + "chat.memory.project.disabled": "ปิดใช้ความจำของโปรเจกต์แล้ว", + "chat.memory.command.failed": "คำสั่งความจำล้มเหลว", + "chat.memory.savedOperations": "การทำงานความจำล่าสุด: {{count}} รายการ", + "chat.memory.inspect": "ตรวจสอบความจำ", + "chat.memory.remember": "จำ", + "chat.memory.forget": "ลืม", + "chat.memory.rebuild": "สร้างดัชนีใหม่", + "chat.memory.disable": "ปิดใช้ความจำ", + "chat.memory.badge.injected": "แทรกความจำแล้ว", + "chat.memory.badge.recalled": "เรียกคืนความจำแล้ว", + "chat.memory.badge.startupCtx": "ctx เริ่มต้น", + "chat.memory.badge.items": "{{count}} รายการ", + "chat.memory.badge.tokens": "{{tokens}} โทเค็น", + "chat.memory.badge.recalledDetail": "เรียกคืนความจำแล้ว: {{count}} รายการ - {{tokens}} โทเค็น", + "chat.memory.badge.files": "ไฟล์ความจำ: {{files}}", + "settings.commitMessage.title": "Commit Message", "settings.commitMessage.override.title": "ใช้ prompt แบบกำหนดเอง", "settings.commitMessage.override.description": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index 4ea7408914..0b9bb8900b 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -1585,6 +1585,7 @@ export const dict = { "settings.context.autoCompaction.title": "Otomatik Sıkıştırma", "settings.context.autoCompaction.description": "Bağlam sınıra ulaşmadan önce otomatik olarak sıkıştır", + "settings.context.compaction.title": "Sıkıştırma", "settings.context.compactionLimit.title": "Otomatik sıkıştırma sınırı", "settings.context.compactionLimit.description": "Bağlam model penceresinin bu yüzdesine ulaştığında sıkıştır. Yalnızca güvenlik tamponunu kullanmak için boş bırakın.", @@ -1593,6 +1594,42 @@ export const dict = { "settings.context.watcherPatterns": "Dosya İzleyici Yok Sayma Kalıpları", "settings.context.watcherPatterns.description": "İzleyicinin yok sayması gereken dosyalar için glob kalıpları", + "settings.context.memory.title": "Bellek", + "settings.context.memory.project.title": "Proje belleği", + "settings.context.memory.autoSave.title": "Proje belleğini otomatik kaydet", + "settings.context.memory.autoSave.description": + "Bellek açıkken tamamlanan turlardan kalıcı proje gerçeklerini otomatik olarak kaydeder.", + "settings.context.memory.index.title": "Bellek indeksi", + "settings.context.memory.status.notLoaded": "Yüklenmedi", + "settings.context.memory.status.disabled": "Devre dışı", + "settings.context.memory.status.enabledTokensOps": + "Etkin - bu oturumda ~{{session}} başlangıç bağlamı tokenı - depolanan indekste ~{{tokens}} token - son işlem {{ops}}", + "settings.context.memory.index.path": "{{path}}/index.kmem", + "settings.context.memory.index.enable": "Proje belleği dosyalarını oluşturmak için belleği etkinleştirin.", + "settings.context.memory.inspect": "İncele", + "settings.context.memory.rebuild": "Bellek indeksini yeniden oluştur", + "chat.memory.on": "Bellek açık", + "chat.memory.label": "Bellek · {{tokens}} token", + "chat.memory.status.loading": "Bellek durumu yükleniyor", + "chat.memory.session.tokens": "Bu oturumdaki başlangıç bağlamı: {{tokens}} token", + "chat.memory.total.tokens": "Depolanan indeks: {{tokens}} token", + "chat.memory.project.enabled": "Proje belleği etkin", + "chat.memory.project.disabled": "Proje belleği devre dışı", + "chat.memory.command.failed": "Bellek komutu başarısız oldu", + "chat.memory.savedOperations": "Son bellek işlemi: {{count}} işlem", + "chat.memory.inspect": "Belleği incele", + "chat.memory.remember": "Hatırla", + "chat.memory.forget": "Unut", + "chat.memory.rebuild": "İndeksi yeniden oluştur", + "chat.memory.disable": "Belleği devre dışı bırak", + "chat.memory.badge.injected": "Bellek eklendi", + "chat.memory.badge.recalled": "Bellek geri çağrıldı", + "chat.memory.badge.startupCtx": "başlangıç ctx", + "chat.memory.badge.items": "{{count}} öğe", + "chat.memory.badge.tokens": "{{tokens}} token", + "chat.memory.badge.recalledDetail": "Bellek geri çağrıldı: {{count}} öğe - {{tokens}} token", + "chat.memory.badge.files": "Bellek dosyaları: {{files}}", + "settings.commitMessage.title": "Commit Message", "settings.commitMessage.override.title": "Özel prompt Kullan", "settings.commitMessage.override.description": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index d59d2b1d57..cfb0d8e646 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -1582,6 +1582,7 @@ export const dict = { "settings.context.autoCompaction.title": "Автоматичне стиснення", "settings.context.autoCompaction.description": "Автоматично стискати контекст до досягнення ліміту", + "settings.context.compaction.title": "Стискання", "settings.context.compactionLimit.title": "Ліміт автоматичного стискання", "settings.context.compactionLimit.description": "Стискати, коли контекст досягає цього відсотка вікна моделі. Залиште порожнім, щоб використовувати лише буфер безпеки.", @@ -1590,6 +1591,42 @@ export const dict = { "settings.context.watcherPatterns": "Шаблони ігнорування спостерігача файлів", "settings.context.watcherPatterns.description": "Glob-шаблони для файлів, які спостерігач має ігнорувати", + "settings.context.memory.title": "Пам’ять", + "settings.context.memory.project.title": "Пам’ять проєкту", + "settings.context.memory.autoSave.title": "Автозбереження пам’яті проєкту", + "settings.context.memory.autoSave.description": + "Автоматично зберігає сталі факти проєкту із завершених ходів, коли пам’ять увімкнено.", + "settings.context.memory.index.title": "Індекс пам’яті", + "settings.context.memory.status.notLoaded": "Не завантажено", + "settings.context.memory.status.disabled": "Вимкнено", + "settings.context.memory.status.enabledTokensOps": + "Увімкнено - ~{{session}} токенів стартового контексту в цій сесії - ~{{tokens}} токенів у збереженому індексі - остання операція {{ops}}", + "settings.context.memory.index.path": "{{path}}/index.kmem", + "settings.context.memory.index.enable": "Увімкніть пам’ять, щоб створити файли пам’яті проєкту.", + "settings.context.memory.inspect": "Перевірити", + "settings.context.memory.rebuild": "Перебудувати індекс пам’яті", + "chat.memory.on": "Пам’ять увімкнено", + "chat.memory.label": "Пам’ять · {{tokens}} токенів", + "chat.memory.status.loading": "Завантаження стану пам’яті", + "chat.memory.session.tokens": "Стартовий контекст цієї сесії: {{tokens}} токенів", + "chat.memory.total.tokens": "Збережений індекс: {{tokens}} токенів", + "chat.memory.project.enabled": "Пам’ять проєкту увімкнено", + "chat.memory.project.disabled": "Пам’ять проєкту вимкнено", + "chat.memory.command.failed": "Команду пам’яті не виконано", + "chat.memory.savedOperations": "Остання операція пам’яті: {{count}} операцій", + "chat.memory.inspect": "Перевірити пам’ять", + "chat.memory.remember": "Запам’ятати", + "chat.memory.forget": "Забути", + "chat.memory.rebuild": "Перебудувати індекс", + "chat.memory.disable": "Вимкнути пам’ять", + "chat.memory.badge.injected": "Пам’ять додано", + "chat.memory.badge.recalled": "Пам’ять відновлено", + "chat.memory.badge.startupCtx": "стартовий ctx", + "chat.memory.badge.items": "{{count}} елементів", + "chat.memory.badge.tokens": "{{tokens}} токенів", + "chat.memory.badge.recalledDetail": "Пам’ять відновлено: {{count}} елементів - {{tokens}} токенів", + "chat.memory.badge.files": "Файли пам’яті: {{files}}", + "settings.commitMessage.title": "Commit Message", "settings.commitMessage.override.title": "Використовувати власний prompt", "settings.commitMessage.override.description": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 987d2f2ccb..1d07cf0ed7 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -1557,6 +1557,7 @@ export const dict = { "settings.checkpoints.enable.description": "在文件编辑前创建检查点,以便恢复之前的状态", "settings.context.autoCompaction.title": "自动压缩", "settings.context.autoCompaction.description": "在上下文达到限制前自动压缩", + "settings.context.compaction.title": "压缩", "settings.context.compactionLimit.title": "自动压缩限制", "settings.context.compactionLimit.description": "当上下文达到模型窗口的此百分比时进行压缩。留空则仅使用安全缓冲区。", "settings.context.prune.title": "修剪旧输出", @@ -1564,6 +1565,41 @@ export const dict = { "settings.context.watcherPatterns": "文件监视器忽略模式", "settings.context.watcherPatterns.description": "监视器应忽略的文件的 glob 模式", + "settings.context.memory.title": "记忆", + "settings.context.memory.project.title": "项目记忆", + "settings.context.memory.autoSave.title": "自动保存项目记忆", + "settings.context.memory.autoSave.description": "启用记忆时,自动从已完成轮次保存持久项目事实。", + "settings.context.memory.index.title": "记忆索引", + "settings.context.memory.status.notLoaded": "未加载", + "settings.context.memory.status.disabled": "已禁用", + "settings.context.memory.status.enabledTokensOps": + "已启用 - 本会话启动上下文约 {{session}} 个 token - 已存储索引约 {{tokens}} 个 token - 上次操作 {{ops}}", + "settings.context.memory.index.path": "{{path}}/index.kmem", + "settings.context.memory.index.enable": "启用记忆以创建项目记忆文件。", + "settings.context.memory.inspect": "检查", + "settings.context.memory.rebuild": "重建记忆索引", + "chat.memory.on": "记忆开启", + "chat.memory.label": "记忆 · {{tokens}} 个 token", + "chat.memory.status.loading": "正在加载记忆状态", + "chat.memory.session.tokens": "本会话启动上下文:{{tokens}} 个 token", + "chat.memory.total.tokens": "已存储索引:{{tokens}} 个 token", + "chat.memory.project.enabled": "项目记忆已启用", + "chat.memory.project.disabled": "项目记忆已禁用", + "chat.memory.command.failed": "记忆命令失败", + "chat.memory.savedOperations": "上次记忆操作:{{count}} 次操作", + "chat.memory.inspect": "检查记忆", + "chat.memory.remember": "记住", + "chat.memory.forget": "忘记", + "chat.memory.rebuild": "重建索引", + "chat.memory.disable": "禁用记忆", + "chat.memory.badge.injected": "已注入记忆", + "chat.memory.badge.recalled": "已召回记忆", + "chat.memory.badge.startupCtx": "启动 ctx", + "chat.memory.badge.items": "{{count}} 项", + "chat.memory.badge.tokens": "{{tokens}} 个 token", + "chat.memory.badge.recalledDetail": "已召回记忆:{{count}} 项 - {{tokens}} 个 token", + "chat.memory.badge.files": "记忆文件:{{files}}", + "settings.commitMessage.title": "Commit Message", "settings.commitMessage.override.title": "使用自定义 prompt", "settings.commitMessage.override.description": diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index f596429f96..e5d49f7fbe 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -1524,6 +1524,7 @@ export const dict = { "settings.checkpoints.enable.description": "在檔案編輯前建立檢查點,以便恢復之前的狀態", "settings.context.autoCompaction.title": "自動壓縮", "settings.context.autoCompaction.description": "在上下文達到限制前自動壓縮", + "settings.context.compaction.title": "壓縮", "settings.context.compactionLimit.title": "自動壓縮限制", "settings.context.compactionLimit.description": "當上下文達到模型視窗的此百分比時進行壓縮。留空則僅使用安全緩衝區。", "settings.context.prune.title": "修剪舊輸出", @@ -1531,6 +1532,41 @@ export const dict = { "settings.context.watcherPatterns": "檔案監視器忽略模式", "settings.context.watcherPatterns.description": "監視器應忽略的檔案的 glob 模式", + "settings.context.memory.title": "記憶", + "settings.context.memory.project.title": "專案記憶", + "settings.context.memory.autoSave.title": "自動儲存專案記憶", + "settings.context.memory.autoSave.description": "啟用記憶時,自動從已完成回合儲存持久專案事實。", + "settings.context.memory.index.title": "記憶索引", + "settings.context.memory.status.notLoaded": "未載入", + "settings.context.memory.status.disabled": "已停用", + "settings.context.memory.status.enabledTokensOps": + "已啟用 - 此工作階段啟動內容約 {{session}} 個 token - 已儲存索引約 {{tokens}} 個 token - 上次操作 {{ops}}", + "settings.context.memory.index.path": "{{path}}/index.kmem", + "settings.context.memory.index.enable": "啟用記憶以建立專案記憶檔案。", + "settings.context.memory.inspect": "檢查", + "settings.context.memory.rebuild": "重建記憶索引", + "chat.memory.on": "記憶開啟", + "chat.memory.label": "記憶 · {{tokens}} 個 token", + "chat.memory.status.loading": "正在載入記憶狀態", + "chat.memory.session.tokens": "此工作階段啟動內容:{{tokens}} 個 token", + "chat.memory.total.tokens": "已儲存索引:{{tokens}} 個 token", + "chat.memory.project.enabled": "專案記憶已啟用", + "chat.memory.project.disabled": "專案記憶已停用", + "chat.memory.command.failed": "記憶命令失敗", + "chat.memory.savedOperations": "上次記憶操作:{{count}} 次操作", + "chat.memory.inspect": "檢查記憶", + "chat.memory.remember": "記住", + "chat.memory.forget": "忘記", + "chat.memory.rebuild": "重建索引", + "chat.memory.disable": "停用記憶", + "chat.memory.badge.injected": "已注入記憶", + "chat.memory.badge.recalled": "已召回記憶", + "chat.memory.badge.startupCtx": "啟動 ctx", + "chat.memory.badge.items": "{{count}} 項", + "chat.memory.badge.tokens": "{{tokens}} 個 token", + "chat.memory.badge.recalledDetail": "已召回記憶:{{count}} 項 - {{tokens}} 個 token", + "chat.memory.badge.files": "記憶檔案:{{files}}", + "settings.commitMessage.title": "Commit Message", "settings.commitMessage.override.title": "使用自訂 prompt", "settings.commitMessage.override.description": diff --git a/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx b/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx index 71bd4db4f3..03dfa684a6 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx @@ -34,6 +34,7 @@ import { NotificationsContext } from "../context/notifications" import { LanguageContext } from "../context/language" import { IndexingProvider } from "../context/indexing" import { KiloEmbeddingModelsProvider } from "../context/kilo-embedding-models" +import { MemoryProvider } from "../context/memory" import { dict as uiEn } from "@kilocode/kilo-ui/i18n/en" import { dict as appEn } from "../i18n/en" import { dict as amEn } from "../../agent-manager/i18n/en" @@ -439,30 +440,32 @@ export const StoryProviders: ParentComponent = (props) => { - - - - - - - - {props.noPadding ? ( - props.children - ) : ( -
{props.children}
- )} -
-
-
-
-
-
-
+ + + + + + + + + {props.noPadding ? ( + props.children + ) : ( +
{props.children}
+ )} +
+
+
+
+
+
+
+
diff --git a/packages/kilo-vscode/webview-ui/src/stories/chat.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/chat.stories.tsx index 7d5e6705e9..4ede4db139 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/chat.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/chat.stories.tsx @@ -20,6 +20,7 @@ import { MessageList } from "../components/chat/MessageList" import { VscodeUserMessage } from "../components/chat/VscodeUserMessage" import { TurnOutcome } from "../components/shared/TurnOutcome" import { SessionContext } from "../context/session" +import { MemoryContext, type MemoryContextValue } from "../context/memory" import { ProviderContext } from "../context/provider" import { ServerContext } from "../context/server" import { WorktreeModeProvider } from "../context/worktree-mode" @@ -988,6 +989,52 @@ export const TaskHeaderWithTodosAllDone: Story = { }, } +const mockMemory: MemoryContextValue = { + status: () => ({}) as any, + show: () => undefined, + loading: () => false, + pending: () => false, + error: () => undefined, + enabled: () => true, + sessionTokens: () => 533, + totalTokens: () => 12_400, + refresh: () => {}, + showMemory: () => {}, + enable: () => {}, + disable: () => {}, + auto: () => {}, + rebuild: () => {}, + remember: () => {}, + forget: () => {}, +} + +export const TaskHeaderWithMemory: Story = { + name: "TaskHeader — with memory enabled", + render: () => { + const session = { + ...mockSessionValue({ id: SESSION_ID, status: "idle" }), + messages: () => [{ id: "msg-001" }] as any[], + currentSession: () => ({ + id: SESSION_ID, + title: "Integrate project memory", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }), + } + return ( + + + +
+ +
+
+
+
+ ) + }, +} + const usageTokens = { input: 25_900_000, output: 52_000, reasoning: 4_100, cache: { read: 10_500_000, write: 80_000 } } const usageData = { sessionIDs: [SESSION_ID, "story-subagent-001"], diff --git a/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css b/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css index dcb17a5a6a..670cbd07e6 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css +++ b/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css @@ -228,6 +228,14 @@ width: 100%; } +[data-component="assistant-memory-badge"] { + align-self: flex-start; + color: var(--vscode-descriptionForeground); + font-family: var(--font-family-sans); + font-size: var(--font-size-small); + line-height: var(--line-height-normal); +} + .vscode-session-turn-diffs { width: 100%; } diff --git a/packages/kilo-vscode/webview-ui/src/styles/task-header.css b/packages/kilo-vscode/webview-ui/src/styles/task-header.css index 61151d7f5f..c64587fe08 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/task-header.css +++ b/packages/kilo-vscode/webview-ui/src/styles/task-header.css @@ -353,6 +353,42 @@ font-variant-numeric: tabular-nums; } +[data-slot="task-header-memory"] { + display: flex; + align-items: center; + gap: 6px; + padding: 0 8px; + font-size: var(--kilo-font-size-11); + color: var(--vscode-descriptionForeground); + min-width: 0; +} + +/* Passed to the shared Tooltip via its `class` prop (no data-slot API), so the trigger stretches. */ +.task-header-memory-tooltip { + display: inline-flex; + flex: 1; + min-width: 0; +} + +[data-slot="task-header-memory-status"] { + display: inline-flex; + align-items: center; + gap: 4px; + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--vscode-foreground); +} + +[data-slot="task-header-memory-actions"] { + display: inline-flex; + align-items: center; + gap: 2px; + flex-shrink: 0; +} + /* Expand toggle */ [data-slot="task-header-expand"] { all: unset; diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts index 75aa20b8aa..8dee79be45 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts @@ -47,6 +47,7 @@ import type { MigrationSessionProgressMessage, MigrationStateMessage, } from "./migration" +import type { MemoryEventMessage, MemoryLoadedMessage, MemoryOperationResultMessage } from "./memory" // ============================================ // Messages FROM extension TO webview @@ -1218,3 +1219,6 @@ export type ExtensionMessage = | TelemetryStateMessage | RemoteStatusMessage | ValidateFilesResultMessage + | MemoryLoadedMessage + | MemoryEventMessage + | MemoryOperationResultMessage diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/index.ts b/packages/kilo-vscode/webview-ui/src/types/messages/index.ts index f74b20db68..0a8a6bcfe7 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/index.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/index.ts @@ -13,5 +13,6 @@ export * from "./config" export * from "./profile" export * from "./agent-manager" export * from "./migration" +export * from "./memory" export * from "./extension-messages" export * from "./webview-messages" diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/memory.ts b/packages/kilo-vscode/webview-ui/src/types/messages/memory.ts new file mode 100644 index 0000000000..022dd73c08 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/types/messages/memory.ts @@ -0,0 +1,103 @@ +import type { + MemoryCorrectResponse, + MemoryConfigureResponse, + MemoryDisableResponse, + MemoryEnableResponse, + MemoryForgetResponse, + MemoryPurgeResponse, + MemoryRememberResponse, + MemoryRebuildResponse, + MemoryShowResponse, + MemoryStatusResponse, +} from "@kilocode/sdk/v2" +import type { + MemoryOperation as SharedMemoryOperation, + MemoryPromptOperation as SharedMemoryPromptOperation, +} from "@kilocode/kilo-memory/commands" +import type { MemorySchema } from "@kilocode/kilo-memory/schema" + +export type MemorySourceFile = MemorySchema.Source + +export type MemoryOperation = SharedMemoryOperation + +export type MemoryResultOperation = MemoryOperation + +export type MemoryPromptOperation = SharedMemoryPromptOperation + +export type MemoryOperationResponse = + | MemoryEnableResponse + | MemoryConfigureResponse + | MemoryDisableResponse + | MemoryStatusResponse + | MemoryRebuildResponse + | MemoryRememberResponse + | MemoryCorrectResponse + | MemoryForgetResponse + | MemoryPurgeResponse + +export interface MemoryLoadedMessage { + type: "memoryLoaded" + sessionID?: string + status?: MemoryStatusResponse + show?: MemoryShowResponse + error?: string +} + +export interface MemoryEventDetail { + type?: "saved" | "skipped" | "recalled" | "error" + message?: string + reason?: string + duplicateOf?: string + tokens?: number + operationCount?: number + skippedCount?: number + sources?: string[] + files?: string[] +} + +export interface MemoryEventMessage { + type: "memoryEvent" + sessionID?: string + detail: MemoryEventDetail +} + +export interface MemoryOperationResultMessage { + type: "memoryOperationResult" + operation: MemoryResultOperation + sessionID?: string + ok: boolean + status?: MemoryStatusResponse + show?: MemoryShowResponse + result?: MemoryOperationResponse + error?: string +} + +export interface RequestMemoryMessage { + type: "requestMemory" + sessionID?: string + includeSources?: boolean +} + +export interface MemoryShowMessage { + type: "memoryShow" + sessionID?: string +} + +export interface MemoryOperationMessage { + type: "memoryOperation" + operation: MemoryOperation + sessionID?: string + mode?: "status" | "on" | "off" + confirm?: boolean + text?: string + query?: string + key?: string + file?: MemorySourceFile + section?: string +} + +export interface MemoryPromptMessage { + type: "memoryPrompt" + operation: MemoryPromptOperation + sessionID?: string +} diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts index 5652ca59fb..3946fff8df 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts @@ -15,6 +15,7 @@ import type { SkipLegacyMigrationMessage, StartMigrationMessage, } from "./migration" +import type { MemoryShowMessage, MemoryOperationMessage, MemoryPromptMessage, RequestMemoryMessage } from "./memory" // ============================================ // Messages FROM webview TO extension @@ -1379,6 +1380,10 @@ export type WebviewMessage = | SetRemoteEnabledMessage | RequestRemoteStatusMessage | ContinueInWorktreeRequest + | RequestMemoryMessage + | MemoryShowMessage + | MemoryOperationMessage + | MemoryPromptMessage | CreateSectionRequest | RenameSectionRequest | DeleteSectionRequest diff --git a/packages/kilo-vscode/webview-ui/src/utils/format.ts b/packages/kilo-vscode/webview-ui/src/utils/format.ts new file mode 100644 index 0000000000..38d1f1cb3d --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/utils/format.ts @@ -0,0 +1,8 @@ +/** Compact K/M formatting for token counts, e.g. 1234 -> "1.2K". */ +export function formatCompactCount(n: number): string { + // Thresholds account for toFixed(1) rounding up at the unit boundary, so e.g. + // 999_950 renders "1.0M" rather than "1000.0K". + if (n >= 999_950) return `${(n / 1_000_000).toFixed(1)}M` + if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K` + return String(n) +} diff --git a/packages/kilo-vscode/webview-ui/src/utils/memory-command.ts b/packages/kilo-vscode/webview-ui/src/utils/memory-command.ts new file mode 100644 index 0000000000..68c18f50bd --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/utils/memory-command.ts @@ -0,0 +1,11 @@ +import { MEMORY_USAGE, parseMemoryCommand as parse } from "@kilocode/kilo-memory/commands" +import type { ParsedMemoryCommand as SharedMemoryCommand } from "@kilocode/kilo-memory/commands" + +export type ParsedMemoryCommand = SharedMemoryCommand +export { MEMORY_USAGE } + +export function parseMemoryCommand(input: string): ParsedMemoryCommand | undefined { + const parsed = parse(input) + if (parsed?.kind !== "usage") return parsed + return { ...parsed, reason: `${parsed.reason}\n${MEMORY_USAGE}` } +} From 251cc9fe567a58080e5e1ce0789993907d48f88e Mon Sep 17 00:00:00 2001 From: "kilo-maintainer[bot]" Date: Tue, 7 Jul 2026 13:48:06 +0000 Subject: [PATCH 41/68] release(jetbrains): v7.0.2-rc.1 --- packages/kilo-jetbrains/CHANGELOG.md | 24 +++++++++++++++++++++++ packages/kilo-jetbrains/gradle.properties | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/kilo-jetbrains/CHANGELOG.md b/packages/kilo-jetbrains/CHANGELOG.md index 082d91d37d..d438d7269d 100644 --- a/packages/kilo-jetbrains/CHANGELOG.md +++ b/packages/kilo-jetbrains/CHANGELOG.md @@ -152,6 +152,30 @@ ## [Unreleased] +## [7.0.2-rc.1] - 2026-07-07 + +### Added +- feat(cli): add vim modal editing to the prompt input by @drye in https://github.com/Kilo-Org/kilocode/pull/11428 +- feat(sandbox): add configurable writable paths option by @trim21 in https://github.com/Kilo-Org/kilocode/pull/11995 +- feat(vscode): prewarm microphone capture before showing voice input as recording by @marius-kilocode in https://github.com/Kilo-Org/kilocode/pull/12001 +- feat: add /reload action to reboot the instance from disk by @marius-kilocode in https://github.com/Kilo-Org/kilocode/pull/12004 +- feat(vscode): render read-tool images inline in chat view by @marius-kilocode in https://github.com/Kilo-Org/kilocode/pull/12010 +- feat(jetbrains): download pinned Kilo Core releases at runtime by @kirillk in https://github.com/Kilo-Org/kilocode/pull/11975 +- feat(vscode): integrate project memory into the extension by @johnnyeric in https://github.com/Kilo-Org/kilocode/pull/11954 + +### Fixed +- fix(cli): import cloud sessions before validation by @maphew in https://github.com/Kilo-Org/kilocode/pull/11223 +- fix(vscode): show routed model name for auto-routed free sessions by @kilo-code-bot[bot] in https://github.com/Kilo-Org/kilocode/pull/12000 +- fix(cli): block file/env references in untrusted project config by @markijbema in https://github.com/Kilo-Org/kilocode/pull/11886 +- fix(vscode): prevent selector popover clipping in new worktree dialog by @marius-kilocode in https://github.com/Kilo-Org/kilocode/pull/12007 + +### Changed +- release(jetbrains): v7.0.1 by @kilo-maintainer[bot] in https://github.com/Kilo-Org/kilocode/pull/11971 +- CLI - Show Remote Badge In TUI Prompt by @eshurakov in https://github.com/Kilo-Org/kilocode/pull/11976 +- docs: add deprecation notice to App Builder page by @eshurakov in https://github.com/Kilo-Org/kilocode/pull/11997 +- Show actually used models when using fable by @chrarnoldus in https://github.com/Kilo-Org/kilocode/pull/11969 + + ## [7.0.1] - 2026-07-06 ### Added diff --git a/packages/kilo-jetbrains/gradle.properties b/packages/kilo-jetbrains/gradle.properties index 437f551148..23aa553936 100644 --- a/packages/kilo-jetbrains/gradle.properties +++ b/packages/kilo-jetbrains/gradle.properties @@ -1,5 +1,5 @@ kotlin.stdlib.default.dependency=false -kilo.jetbrains.version=7.0.1 +kilo.jetbrains.version=7.0.2-rc.1 org.gradle.configuration-cache=true org.gradle.caching=true org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m From 497819684494e1608054b8b5ee55cadf87a7b2ed Mon Sep 17 00:00:00 2001 From: Kirill Kalishev Date: Tue, 7 Jul 2026 09:55:32 -0400 Subject: [PATCH 42/68] docs(jetbrains): edit changelog for v7.0.2-rc.1 --- packages/kilo-jetbrains/CHANGELOG.md | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/packages/kilo-jetbrains/CHANGELOG.md b/packages/kilo-jetbrains/CHANGELOG.md index d438d7269d..dd47ea6971 100644 --- a/packages/kilo-jetbrains/CHANGELOG.md +++ b/packages/kilo-jetbrains/CHANGELOG.md @@ -155,26 +155,7 @@ ## [7.0.2-rc.1] - 2026-07-07 ### Added -- feat(cli): add vim modal editing to the prompt input by @drye in https://github.com/Kilo-Org/kilocode/pull/11428 -- feat(sandbox): add configurable writable paths option by @trim21 in https://github.com/Kilo-Org/kilocode/pull/11995 -- feat(vscode): prewarm microphone capture before showing voice input as recording by @marius-kilocode in https://github.com/Kilo-Org/kilocode/pull/12001 -- feat: add /reload action to reboot the instance from disk by @marius-kilocode in https://github.com/Kilo-Org/kilocode/pull/12004 -- feat(vscode): render read-tool images inline in chat view by @marius-kilocode in https://github.com/Kilo-Org/kilocode/pull/12010 -- feat(jetbrains): download pinned Kilo Core releases at runtime by @kirillk in https://github.com/Kilo-Org/kilocode/pull/11975 -- feat(vscode): integrate project memory into the extension by @johnnyeric in https://github.com/Kilo-Org/kilocode/pull/11954 - -### Fixed -- fix(cli): import cloud sessions before validation by @maphew in https://github.com/Kilo-Org/kilocode/pull/11223 -- fix(vscode): show routed model name for auto-routed free sessions by @kilo-code-bot[bot] in https://github.com/Kilo-Org/kilocode/pull/12000 -- fix(cli): block file/env references in untrusted project config by @markijbema in https://github.com/Kilo-Org/kilocode/pull/11886 -- fix(vscode): prevent selector popover clipping in new worktree dialog by @marius-kilocode in https://github.com/Kilo-Org/kilocode/pull/12007 - -### Changed -- release(jetbrains): v7.0.1 by @kilo-maintainer[bot] in https://github.com/Kilo-Org/kilocode/pull/11971 -- CLI - Show Remote Badge In TUI Prompt by @eshurakov in https://github.com/Kilo-Org/kilocode/pull/11976 -- docs: add deprecation notice to App Builder page by @eshurakov in https://github.com/Kilo-Org/kilocode/pull/11997 -- Show actually used models when using fable by @chrarnoldus in https://github.com/Kilo-Org/kilocode/pull/11969 - +- Download the pinned Kilo Core release at runtime instead of bundling every CLI binary in the JetBrains plugin, keeping the Marketplace package smaller while still verifying downloaded artifacts. ## [7.0.1] - 2026-07-06 From f3d4e493e80d313300ac3458d19027cc4e376617 Mon Sep 17 00:00:00 2001 From: "kilo-maintainer[bot]" Date: Tue, 7 Jul 2026 14:01:04 +0000 Subject: [PATCH 43/68] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 6790015e90..387012efc6 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-jTNTUFR9PuOqU3HVmA9tWR6E07O+LgQyubZOvQOxZ5I=", - "aarch64-linux": "sha256-afSECQ+kAsWirSpUAPe8sVcsal4Cu37MBWG/klc5I4Q=", - "aarch64-darwin": "sha256-z2LNeKVnKu95c6W2vl1x0qHTmZK9nwJlneurDibLjYw=", - "x86_64-darwin": "sha256-9dRFffouzJNfewnMm9Hm0FgIbYeUMBgv9mJUEwlSZS0=" + "x86_64-linux": "sha256-5/NgXEBhaNEEqIz9GVCXxQaLJwkqWXAAZOPoa6GyHTI=", + "aarch64-linux": "sha256-qt7PKn0SAhDCcYtXRY9+1TBp9x3Far7kK24jhUzoMf8=", + "aarch64-darwin": "sha256-3FN/i3chx01S52lAv0tjxVtRpUlPkIfInphpq+69uWA=", + "x86_64-darwin": "sha256-+rOacJLx+qOCRGW4F+AjvFcc9XUE2biB0BRSy3dTWGI=" } } From 837c61570bb3eb087508da508d772de239685590 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 7 Jul 2026 16:02:34 +0200 Subject: [PATCH 44/68] fix(vscode): show sandbox tooltip immediately on hover --- .../webview-ui/src/components/shared/SandboxButton.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/SandboxButton.tsx b/packages/kilo-vscode/webview-ui/src/components/shared/SandboxButton.tsx index a8de4cfed2..2e307cb8c3 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/SandboxButton.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/shared/SandboxButton.tsx @@ -69,7 +69,12 @@ export const SandboxButtonBase: Component = (props) => { language.t(props.enabled ? "prompt.action.sandbox.enabled" : "prompt.action.sandbox.disabled")) return ( - + - + = (props) => { props.start() } - onCleanup(() => { - if (props.speech.active()) props.speech.cancel() - }) + onCleanup(() => cleanupSpeechCapture(props.speech)) return ( diff --git a/packages/kilo-vscode/webview-ui/src/components/speech-to-text/speech-cleanup.ts b/packages/kilo-vscode/webview-ui/src/components/speech-to-text/speech-cleanup.ts new file mode 100644 index 0000000000..fa97233f3e --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/components/speech-to-text/speech-cleanup.ts @@ -0,0 +1,5 @@ +import type { SpeechToText } from "./useSpeechToText" + +export function cleanupSpeechCapture(speech: Pick) { + if (speech.active()) speech.cancel() +} diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 9b3c279fea..f14577ce02 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1738,11 +1738,8 @@ function StepFinishPart(props: { last: boolean; part: StepFinishPart; message: A const { theme } = useTheme() const info = useContext(RoutedModelMeta.Context) const routed = createMemo(() => { - if (props.message.providerID !== "kilo") return undefined - if (!props.message.modelID.startsWith("kilo-auto/")) return undefined const model = props.part.model - if (!model) return undefined - if (model.providerID === props.message.providerID && model.modelID === props.message.modelID) return undefined + if (!RoutedModelMeta.routed(model, props.message)) return undefined return RoutedModelMeta.label(ctx.providers(), model) }) const consumed = createMemo(() => info().consumed.has(props.part.id)) diff --git a/packages/opencode/src/kilocode/cli/cmd/tui/component/prompt/vim.ts b/packages/opencode/src/kilocode/cli/cmd/tui/component/prompt/vim.ts index 2b91606c86..ad1d0b58d9 100644 --- a/packages/opencode/src/kilocode/cli/cmd/tui/component/prompt/vim.ts +++ b/packages/opencode/src/kilocode/cli/cmd/tui/component/prompt/vim.ts @@ -299,7 +299,7 @@ function resolveMotion(text: string, pos: number, key: string, count: number, aw case "$": { let t = pos for (let i = 0; i < reps; i++) t = lineEnd(text, lineEnd(text, t) + (i === 0 ? 0 : 1)) - return { target: lineEnd(text, t), inclusive: true, linewise: false } + return { target: Math.max(lineStart(text, t), lineEnd(text, t) - 1), inclusive: true, linewise: false } } case "G": { let target = 0 diff --git a/packages/opencode/src/kilocode/cli/cmd/tui/routes/session/routed-model-meta.tsx b/packages/opencode/src/kilocode/cli/cmd/tui/routes/session/routed-model-meta.tsx index 61b964b270..12179aba52 100644 --- a/packages/opencode/src/kilocode/cli/cmd/tui/routes/session/routed-model-meta.tsx +++ b/packages/opencode/src/kilocode/cli/cmd/tui/routes/session/routed-model-meta.tsx @@ -39,10 +39,10 @@ export namespace RoutedModelMeta { return KiloRoutedModel.displayName(text) } - function routed(model: StepFinishPart["model"], message: Message) { + export function routed(model: StepFinishPart["model"], message: Message) { if (!model) return undefined if (message.providerID !== "kilo") return undefined - if (!message.modelID.startsWith("kilo-auto/")) return undefined + if (!KiloRoutedModel.routedSelection(message.modelID)) return undefined if (model.providerID === message.providerID && model.modelID === message.modelID) return undefined return model } diff --git a/packages/opencode/src/kilocode/session/routed-model.ts b/packages/opencode/src/kilocode/session/routed-model.ts index eace803049..ba474dad40 100644 --- a/packages/opencode/src/kilocode/session/routed-model.ts +++ b/packages/opencode/src/kilocode/session/routed-model.ts @@ -47,10 +47,15 @@ export namespace KiloRoutedModel { input: { providerID: ProviderID; modelID: string; selected?: string }, ) { if (input.providerID !== ProviderID.kilo) return undefined - if (!input.modelID.startsWith("kilo-auto/") && !input.modelID.includes("fable")) return undefined + if (!routedSelection(input.modelID)) return undefined const model = read(meta, input.providerID) if (!model) return undefined if (model.modelID === input.modelID || model.modelID === input.selected) return undefined return model } + + export function routedSelection(modelID: string) { + const id = modelID.trim() + return id.startsWith("kilo-auto/") || /(?:^|[/.])claude-fable(?:[-.:/]|$)/.test(id) + } } diff --git a/packages/opencode/test/kilocode/cli/cmd/tui/prompt/vim.test.ts b/packages/opencode/test/kilocode/cli/cmd/tui/prompt/vim.test.ts index 43a2330d61..878ffed953 100644 --- a/packages/opencode/test/kilocode/cli/cmd/tui/prompt/vim.test.ts +++ b/packages/opencode/test/kilocode/cli/cmd/tui/prompt/vim.test.ts @@ -199,6 +199,28 @@ describe("vim edits", () => { expect(doc.text).toBe("bar baz") }) + test("d$ deletes line content without consuming the newline", () => { + const doc = new MockDoc("abc\ndef", 0) + const state = createVimState("normal") + feed(doc, state, "d$") + expect(doc.text).toBe("\ndef") + }) + + test("c$ changes line content without consuming the newline", () => { + const doc = new MockDoc("abc\ndef", 0) + const state = createVimState("normal") + feed(doc, state, "c$") + expect(doc.text).toBe("\ndef") + expect(state.mode).toBe("insert") + }) + + test("y$ yanks line content without the newline", () => { + const doc = new MockDoc("abc\ndef", 0) + const state = createVimState("normal") + feed(doc, state, "y$") + expect(state.register).toEqual({ text: "abc", linewise: false }) + }) + test("dd deletes the current line", () => { const doc = new MockDoc("one\ntwo\nthree", 4) const state = createVimState("normal") diff --git a/packages/opencode/test/kilocode/config/config.test.ts b/packages/opencode/test/kilocode/config/config.test.ts index 467282a346..b8a41edcc6 100644 --- a/packages/opencode/test/kilocode/config/config.test.ts +++ b/packages/opencode/test/kilocode/config/config.test.ts @@ -243,6 +243,51 @@ describe("kilocode indexing config", () => { }) describe("kilocode sandbox writable paths config", () => { + test("honors sandbox_writable_paths from global config", async () => { + await using globalTmp = await tmpdir() + await using tmp = await tmpdir({ git: true }) + + const prev = Global.Path.config + ;(Global.Path as { config: string }).config = globalTmp.path + await clear() + await disposeAllInstances() + + try { + await writeConfig(globalTmp.path, { + $schema: "https://app.kilo.ai/config.json", + experimental: { sandbox_writable_paths: ["/tmp/global"] }, + }) + + await provideTestInstance({ + directory: tmp.path, + fn: async () => { + const config = await load() + expect(config.experimental?.sandbox_writable_paths).toEqual(["/tmp/global"]) + }, + }) + } finally { + ;(Global.Path as { config: string }).config = prev + await clear() + await disposeAllInstances() + } + }) + + test("drops sandbox_writable_paths from project config", async () => { + await using tmp = await tmpdir({ git: true }) + + await writeConfig(tmp.path, { + experimental: { sandbox_writable_paths: ["/tmp/project"] }, + }) + + await provideTestInstance({ + directory: tmp.path, + fn: async () => { + const config = await load() + expect(config.experimental?.sandbox_writable_paths).toBeUndefined() + }, + }) + }) + test("honors sandbox_writable_paths from global config only, ignoring project config", async () => { await using globalTmp = await tmpdir() await using tmp = await tmpdir({ git: true }) diff --git a/packages/opencode/test/kilocode/session-routed-model.test.ts b/packages/opencode/test/kilocode/session-routed-model.test.ts index 64981bad90..de86c5af4b 100644 --- a/packages/opencode/test/kilocode/session-routed-model.test.ts +++ b/packages/opencode/test/kilocode/session-routed-model.test.ts @@ -103,7 +103,7 @@ describe("session routed model", () => { expect(KiloRoutedModel.displayName("o3")).toBe("o3") }) - test("shows compact labels only for Kilo auto selections", () => { + test("shows compact labels only for routed Kilo selections", () => { const model = { providerID: "openai", modelID: "gpt-5.5" } const parts = [reason, finish(model)] @@ -115,6 +115,13 @@ describe("session routed model", () => { expect(routed.footer).toBe("gpt-5.5") expect(routed.consumed.has("finish")).toBe(true) + const fable = RoutedModelMeta.info(undefined, parts, false, { + providerID: "kilo", + modelID: "anthropic/claude-fable-5", + }) + expect(fable.labels.get("reasoning")).toBe("gpt-5.5") + expect(fable.footer).toBe("gpt-5.5") + const explicit = RoutedModelMeta.info(undefined, parts, false, { providerID: "openai", modelID: "gpt-5.5", @@ -169,7 +176,7 @@ describe("session routed model", () => { expect(routed.consumed.has("last")).toBe(false) }) - test("reads routed model only for selected Kilo auto models", () => { + test("reads routed model only for selected Kilo routed models", () => { const meta = { kilocode: { routedModelID: "openai/gpt-5.5-20260423" } } expect( @@ -182,6 +189,22 @@ describe("session routed model", () => { modelID: ModelID.make("openai/gpt-5.5-20260423"), }) + expect( + KiloRoutedModel.readAuto(meta, { + providerID: ProviderID.kilo, + modelID: "anthropic.claude-fable-5", + }), + ).toEqual({ + providerID: ProviderID.kilo, + modelID: ModelID.make("openai/gpt-5.5-20260423"), + }) + + expect( + KiloRoutedModel.readAuto(meta, { + providerID: ProviderID.kilo, + modelID: "openai/affable-model", + }), + ).toBeUndefined() expect( KiloRoutedModel.readAuto(meta, { providerID: ProviderID.kilo, From cb329c6bdb4be807f25206079e915400cae976b6 Mon Sep 17 00:00:00 2001 From: kirillk Date: Tue, 7 Jul 2026 14:13:35 -0400 Subject: [PATCH 65/68] chore: drop unrelated review fixes --- .../code-with-ai/agents/custom-models.md | 4 +- .../pages/code-with-ai/platforms/cli.md | 4 +- .../tests/unit/config-scope.test.ts | 15 ------- .../tests/unit/speech-to-text-button.test.ts | 30 ------------- .../src/components/chat/PromptInput.tsx | 2 +- .../speech-to-text/SpeechToTextButton.tsx | 5 ++- .../speech-to-text/speech-cleanup.ts | 5 --- .../src/cli/cmd/tui/routes/session/index.tsx | 5 ++- .../cli/cmd/tui/component/prompt/vim.ts | 2 +- .../tui/routes/session/routed-model-meta.tsx | 4 +- .../src/kilocode/session/routed-model.ts | 7 +-- .../kilocode/cli/cmd/tui/prompt/vim.test.ts | 22 --------- .../test/kilocode/config/config.test.ts | 45 ------------------- .../kilocode/session-routed-model.test.ts | 27 +---------- 14 files changed, 18 insertions(+), 159 deletions(-) delete mode 100644 packages/kilo-vscode/tests/unit/speech-to-text-button.test.ts delete mode 100644 packages/kilo-vscode/webview-ui/src/components/speech-to-text/speech-cleanup.ts diff --git a/packages/kilo-docs/pages/code-with-ai/agents/custom-models.md b/packages/kilo-docs/pages/code-with-ai/agents/custom-models.md index 065672280e..d489dc63eb 100644 --- a/packages/kilo-docs/pages/code-with-ai/agents/custom-models.md +++ b/packages/kilo-docs/pages/code-with-ai/agents/custom-models.md @@ -390,8 +390,8 @@ You can also set options that apply to all models from a provider: | `timeout` | `number \| false` | Request timeout in milliseconds. Defaults to `300000` (5 minutes); set to `false` to disable | | `chunkTimeout` | `number` | Timeout in milliseconds between streamed response chunks. If no chunk arrives within this window, the request is aborted and retried. This catches silent provider dropouts where the TCP connection stays open but SSE streaming stops. Recommended: `15000`–`30000` (15–30 seconds) for providers with unreliable streaming. | -{% callout type="warning" title="{env:} requires trusted config; {file:} is project-confined" %} -`{env:VAR}` references in `apiKey` (or any option) resolve **only** when the config lives in a trusted location: your global config (`~/.config/kilo`), a config passed via `KILO_CONFIG` / `KILO_CONFIG_CONTENT`, or organization/MDM-managed config. A project-level `kilo.json` / `opencode.json` committed to a repository **cannot** resolve `{env:VAR}`; if one appears anywhere in that file, Kilo drops the entire file's config and logs a warning, so unrelated settings in the same file stop applying too. This prevents a malicious repository from exfiltrating your secrets to an attacker-controlled `baseURL` just by being opened. `{file:...}` works in trusted config and still works in project config, but only for files that resolve inside the project root — references that leave it (absolute paths outside the root, `../` traversal, and symlink escapes) are rejected. Keep provider credentials in your global config. +{% callout type="warning" title="{env:} / {file:} only resolve in trusted config" %} +`{env:VAR}` and `{file:...}` references in `apiKey` (or any option) are resolved **only** when the config lives in a trusted location: your global config (`~/.config/kilo`), a config passed via `KILO_CONFIG` / `KILO_CONFIG_CONTENT`, or organization/MDM-managed config. A project-level `kilo.json` / `opencode.json` committed to a repository **cannot** resolve `{env:VAR}` — the reference is ignored and a warning is logged, so a provider configured this way in a repo will not authenticate. This prevents a malicious repository from exfiltrating your secrets to an attacker-controlled `baseURL` just by being opened. `{file:...}` still works in project config, but only for files that resolve inside the project root — references that leave it (absolute paths outside the root, `../` traversal, and symlink escapes) are rejected. Keep provider credentials in your global config. {% /callout %} ## Filtering Available Models diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/cli.md b/packages/kilo-docs/pages/code-with-ai/platforms/cli.md index be2bafc75d..d785539288 100644 --- a/packages/kilo-docs/pages/code-with-ai/platforms/cli.md +++ b/packages/kilo-docs/pages/code-with-ai/platforms/cli.md @@ -465,8 +465,8 @@ Use `{env:VARIABLE_NAME}` syntax in config files to reference environment variab } ``` -{% callout type="warning" title="{env:} requires trusted config; {file:} is project-confined" %} -`{env:VAR}` references resolve **only** in trusted config: your global config (`~/.config/kilo`), a config passed via `KILO_CONFIG` / `KILO_CONFIG_CONTENT`, or organization/MDM-managed config. A project-level `kilo.json` / `opencode.json` committed to a repository **cannot** use `{env:VAR}`; if one appears anywhere in that file, Kilo drops the entire file's config and logs a warning, so unrelated settings in the same file stop applying too. This prevents a malicious repository from exfiltrating your secrets to an attacker-controlled `baseURL` simply by being opened. `{file:...}` works in trusted config and still works in project config, but only for files that resolve inside the project root — references that leave it (absolute paths outside the root, `../` traversal, and symlink escapes) are rejected. +{% callout type="warning" title="Only works in trusted config" %} +`{env:VAR}` (and `{file:...}`) references are resolved **only** in trusted config: your global config (`~/.config/kilo`), a config passed via `KILO_CONFIG` / `KILO_CONFIG_CONTENT`, or organization/MDM-managed config. A project-level `kilo.json` / `opencode.json` committed to a repository **cannot** use `{env:VAR}` — the reference is ignored and a warning is logged. This prevents a malicious repository from exfiltrating your secrets to an attacker-controlled `baseURL` simply by being opened. `{file:...}` still works in project config, but only for files that resolve inside the project root — references that leave it (absolute paths outside the root, `../` traversal, and symlink escapes) are rejected. {% /callout %} For full details on all configuration options including compaction, file watchers, plugins, and experimental features, see the [OpenCode Config documentation](https://opencode.ai/docs/config). diff --git a/packages/kilo-vscode/tests/unit/config-scope.test.ts b/packages/kilo-vscode/tests/unit/config-scope.test.ts index aee2c488ce..eca588a5d1 100644 --- a/packages/kilo-vscode/tests/unit/config-scope.test.ts +++ b/packages/kilo-vscode/tests/unit/config-scope.test.ts @@ -47,19 +47,4 @@ describe("splitConfigByScope", () => { }) expect(split.project).toEqual({}) }) - - it("writes sandbox writable paths to global config", () => { - const split = splitConfigByScope({ - experimental: { - sandbox_writable_paths: ["/tmp/kilo"], - }, - }) - - expect(split.global).toEqual({ - experimental: { - sandbox_writable_paths: ["/tmp/kilo"], - }, - }) - expect(split.project).toEqual({}) - }) }) diff --git a/packages/kilo-vscode/tests/unit/speech-to-text-button.test.ts b/packages/kilo-vscode/tests/unit/speech-to-text-button.test.ts deleted file mode 100644 index 56db776d55..0000000000 --- a/packages/kilo-vscode/tests/unit/speech-to-text-button.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, expect, it } from "bun:test" -import { cleanupSpeechCapture } from "../../webview-ui/src/components/speech-to-text/speech-cleanup" - -describe("speech-to-text button", () => { - it("cancels active capture on component cleanup", () => { - let cancels = 0 - - cleanupSpeechCapture({ - active: () => true, - cancel: () => { - cancels += 1 - }, - }) - - expect(cancels).toBe(1) - }) - - it("does not cancel idle capture on component cleanup", () => { - let cancels = 0 - - cleanupSpeechCapture({ - active: () => false, - cancel: () => { - cancels += 1 - }, - }) - - expect(cancels).toBe(0) - }) -}) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx index 0298535f8a..1593dadf0c 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx @@ -1394,7 +1394,7 @@ export const PromptInput: Component = (props) => { - + = (props) => { props.start() } - onCleanup(() => cleanupSpeechCapture(props.speech)) + onCleanup(() => { + if (props.speech.active()) props.speech.cancel() + }) return ( diff --git a/packages/kilo-vscode/webview-ui/src/components/speech-to-text/speech-cleanup.ts b/packages/kilo-vscode/webview-ui/src/components/speech-to-text/speech-cleanup.ts deleted file mode 100644 index fa97233f3e..0000000000 --- a/packages/kilo-vscode/webview-ui/src/components/speech-to-text/speech-cleanup.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { SpeechToText } from "./useSpeechToText" - -export function cleanupSpeechCapture(speech: Pick) { - if (speech.active()) speech.cancel() -} diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index f14577ce02..9b3c279fea 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1738,8 +1738,11 @@ function StepFinishPart(props: { last: boolean; part: StepFinishPart; message: A const { theme } = useTheme() const info = useContext(RoutedModelMeta.Context) const routed = createMemo(() => { + if (props.message.providerID !== "kilo") return undefined + if (!props.message.modelID.startsWith("kilo-auto/")) return undefined const model = props.part.model - if (!RoutedModelMeta.routed(model, props.message)) return undefined + if (!model) return undefined + if (model.providerID === props.message.providerID && model.modelID === props.message.modelID) return undefined return RoutedModelMeta.label(ctx.providers(), model) }) const consumed = createMemo(() => info().consumed.has(props.part.id)) diff --git a/packages/opencode/src/kilocode/cli/cmd/tui/component/prompt/vim.ts b/packages/opencode/src/kilocode/cli/cmd/tui/component/prompt/vim.ts index ad1d0b58d9..2b91606c86 100644 --- a/packages/opencode/src/kilocode/cli/cmd/tui/component/prompt/vim.ts +++ b/packages/opencode/src/kilocode/cli/cmd/tui/component/prompt/vim.ts @@ -299,7 +299,7 @@ function resolveMotion(text: string, pos: number, key: string, count: number, aw case "$": { let t = pos for (let i = 0; i < reps; i++) t = lineEnd(text, lineEnd(text, t) + (i === 0 ? 0 : 1)) - return { target: Math.max(lineStart(text, t), lineEnd(text, t) - 1), inclusive: true, linewise: false } + return { target: lineEnd(text, t), inclusive: true, linewise: false } } case "G": { let target = 0 diff --git a/packages/opencode/src/kilocode/cli/cmd/tui/routes/session/routed-model-meta.tsx b/packages/opencode/src/kilocode/cli/cmd/tui/routes/session/routed-model-meta.tsx index 12179aba52..61b964b270 100644 --- a/packages/opencode/src/kilocode/cli/cmd/tui/routes/session/routed-model-meta.tsx +++ b/packages/opencode/src/kilocode/cli/cmd/tui/routes/session/routed-model-meta.tsx @@ -39,10 +39,10 @@ export namespace RoutedModelMeta { return KiloRoutedModel.displayName(text) } - export function routed(model: StepFinishPart["model"], message: Message) { + function routed(model: StepFinishPart["model"], message: Message) { if (!model) return undefined if (message.providerID !== "kilo") return undefined - if (!KiloRoutedModel.routedSelection(message.modelID)) return undefined + if (!message.modelID.startsWith("kilo-auto/")) return undefined if (model.providerID === message.providerID && model.modelID === message.modelID) return undefined return model } diff --git a/packages/opencode/src/kilocode/session/routed-model.ts b/packages/opencode/src/kilocode/session/routed-model.ts index ba474dad40..eace803049 100644 --- a/packages/opencode/src/kilocode/session/routed-model.ts +++ b/packages/opencode/src/kilocode/session/routed-model.ts @@ -47,15 +47,10 @@ export namespace KiloRoutedModel { input: { providerID: ProviderID; modelID: string; selected?: string }, ) { if (input.providerID !== ProviderID.kilo) return undefined - if (!routedSelection(input.modelID)) return undefined + if (!input.modelID.startsWith("kilo-auto/") && !input.modelID.includes("fable")) return undefined const model = read(meta, input.providerID) if (!model) return undefined if (model.modelID === input.modelID || model.modelID === input.selected) return undefined return model } - - export function routedSelection(modelID: string) { - const id = modelID.trim() - return id.startsWith("kilo-auto/") || /(?:^|[/.])claude-fable(?:[-.:/]|$)/.test(id) - } } diff --git a/packages/opencode/test/kilocode/cli/cmd/tui/prompt/vim.test.ts b/packages/opencode/test/kilocode/cli/cmd/tui/prompt/vim.test.ts index 878ffed953..43a2330d61 100644 --- a/packages/opencode/test/kilocode/cli/cmd/tui/prompt/vim.test.ts +++ b/packages/opencode/test/kilocode/cli/cmd/tui/prompt/vim.test.ts @@ -199,28 +199,6 @@ describe("vim edits", () => { expect(doc.text).toBe("bar baz") }) - test("d$ deletes line content without consuming the newline", () => { - const doc = new MockDoc("abc\ndef", 0) - const state = createVimState("normal") - feed(doc, state, "d$") - expect(doc.text).toBe("\ndef") - }) - - test("c$ changes line content without consuming the newline", () => { - const doc = new MockDoc("abc\ndef", 0) - const state = createVimState("normal") - feed(doc, state, "c$") - expect(doc.text).toBe("\ndef") - expect(state.mode).toBe("insert") - }) - - test("y$ yanks line content without the newline", () => { - const doc = new MockDoc("abc\ndef", 0) - const state = createVimState("normal") - feed(doc, state, "y$") - expect(state.register).toEqual({ text: "abc", linewise: false }) - }) - test("dd deletes the current line", () => { const doc = new MockDoc("one\ntwo\nthree", 4) const state = createVimState("normal") diff --git a/packages/opencode/test/kilocode/config/config.test.ts b/packages/opencode/test/kilocode/config/config.test.ts index b8a41edcc6..467282a346 100644 --- a/packages/opencode/test/kilocode/config/config.test.ts +++ b/packages/opencode/test/kilocode/config/config.test.ts @@ -243,51 +243,6 @@ describe("kilocode indexing config", () => { }) describe("kilocode sandbox writable paths config", () => { - test("honors sandbox_writable_paths from global config", async () => { - await using globalTmp = await tmpdir() - await using tmp = await tmpdir({ git: true }) - - const prev = Global.Path.config - ;(Global.Path as { config: string }).config = globalTmp.path - await clear() - await disposeAllInstances() - - try { - await writeConfig(globalTmp.path, { - $schema: "https://app.kilo.ai/config.json", - experimental: { sandbox_writable_paths: ["/tmp/global"] }, - }) - - await provideTestInstance({ - directory: tmp.path, - fn: async () => { - const config = await load() - expect(config.experimental?.sandbox_writable_paths).toEqual(["/tmp/global"]) - }, - }) - } finally { - ;(Global.Path as { config: string }).config = prev - await clear() - await disposeAllInstances() - } - }) - - test("drops sandbox_writable_paths from project config", async () => { - await using tmp = await tmpdir({ git: true }) - - await writeConfig(tmp.path, { - experimental: { sandbox_writable_paths: ["/tmp/project"] }, - }) - - await provideTestInstance({ - directory: tmp.path, - fn: async () => { - const config = await load() - expect(config.experimental?.sandbox_writable_paths).toBeUndefined() - }, - }) - }) - test("honors sandbox_writable_paths from global config only, ignoring project config", async () => { await using globalTmp = await tmpdir() await using tmp = await tmpdir({ git: true }) diff --git a/packages/opencode/test/kilocode/session-routed-model.test.ts b/packages/opencode/test/kilocode/session-routed-model.test.ts index de86c5af4b..64981bad90 100644 --- a/packages/opencode/test/kilocode/session-routed-model.test.ts +++ b/packages/opencode/test/kilocode/session-routed-model.test.ts @@ -103,7 +103,7 @@ describe("session routed model", () => { expect(KiloRoutedModel.displayName("o3")).toBe("o3") }) - test("shows compact labels only for routed Kilo selections", () => { + test("shows compact labels only for Kilo auto selections", () => { const model = { providerID: "openai", modelID: "gpt-5.5" } const parts = [reason, finish(model)] @@ -115,13 +115,6 @@ describe("session routed model", () => { expect(routed.footer).toBe("gpt-5.5") expect(routed.consumed.has("finish")).toBe(true) - const fable = RoutedModelMeta.info(undefined, parts, false, { - providerID: "kilo", - modelID: "anthropic/claude-fable-5", - }) - expect(fable.labels.get("reasoning")).toBe("gpt-5.5") - expect(fable.footer).toBe("gpt-5.5") - const explicit = RoutedModelMeta.info(undefined, parts, false, { providerID: "openai", modelID: "gpt-5.5", @@ -176,7 +169,7 @@ describe("session routed model", () => { expect(routed.consumed.has("last")).toBe(false) }) - test("reads routed model only for selected Kilo routed models", () => { + test("reads routed model only for selected Kilo auto models", () => { const meta = { kilocode: { routedModelID: "openai/gpt-5.5-20260423" } } expect( @@ -189,22 +182,6 @@ describe("session routed model", () => { modelID: ModelID.make("openai/gpt-5.5-20260423"), }) - expect( - KiloRoutedModel.readAuto(meta, { - providerID: ProviderID.kilo, - modelID: "anthropic.claude-fable-5", - }), - ).toEqual({ - providerID: ProviderID.kilo, - modelID: ModelID.make("openai/gpt-5.5-20260423"), - }) - - expect( - KiloRoutedModel.readAuto(meta, { - providerID: ProviderID.kilo, - modelID: "openai/affable-model", - }), - ).toBeUndefined() expect( KiloRoutedModel.readAuto(meta, { providerID: ProviderID.kilo, From 23bba412ff3623a2b69e69cf2af62d8e65451e7f Mon Sep 17 00:00:00 2001 From: kirillk Date: Tue, 7 Jul 2026 14:29:37 -0400 Subject: [PATCH 66/68] chore: drop speech capture change --- packages/kilo-vscode/src/speech-to-text/capture.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/kilo-vscode/src/speech-to-text/capture.ts b/packages/kilo-vscode/src/speech-to-text/capture.ts index 761d01dc59..6250c0822c 100644 --- a/packages/kilo-vscode/src/speech-to-text/capture.ts +++ b/packages/kilo-vscode/src/speech-to-text/capture.ts @@ -239,7 +239,6 @@ async function resolveFFmpeg(): Promise { if (cached) { const bin = await cached if (!path.isAbsolute(bin) || existsSync(bin)) return bin - if (ffmpeg === cached) ffmpeg = undefined } const task = findFFmpeg() From 0aa3574b44bf4a030f14adcc12df66cf0b13b894 Mon Sep 17 00:00:00 2001 From: "kilo-maintainer[bot]" Date: Tue, 7 Jul 2026 18:58:54 +0000 Subject: [PATCH 67/68] release(jetbrains): v7.0.2-rc.2 --- packages/kilo-jetbrains/CHANGELOG.md | 17 +++++++++++++++++ packages/kilo-jetbrains/gradle.properties | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/kilo-jetbrains/CHANGELOG.md b/packages/kilo-jetbrains/CHANGELOG.md index dd47ea6971..023076fae9 100644 --- a/packages/kilo-jetbrains/CHANGELOG.md +++ b/packages/kilo-jetbrains/CHANGELOG.md @@ -152,6 +152,23 @@ ## [Unreleased] +## [7.0.2-rc.2] - 2026-07-07 + +### Added +- feat(sandbox): widen writable-paths settings input and add coverage by @marius-kilocode in https://github.com/Kilo-Org/kilocode/pull/12008 +- feat(jetbrains): polish chat UI and bundled CLI runtime flow by @kirillk in https://github.com/Kilo-Org/kilocode/pull/11978 + +### Fixed +- fix(vscode): show sandbox tooltip immediately on hover by @marius-kilocode in https://github.com/Kilo-Org/kilocode/pull/12012 +- fix(vscode): preserve mode on first send by @marius-kilocode in https://github.com/Kilo-Org/kilocode/pull/12009 +- fix(vscode): allow shrinking prompt mention selections by @mjnaderi in https://github.com/Kilo-Org/kilocode/pull/11936 +- fix(vscode): handle multiline bidi prompt input by @mjnaderi in https://github.com/Kilo-Org/kilocode/pull/12006 +- fix: honor Kilo cloud account preferences by @markijbema in https://github.com/Kilo-Org/kilocode/pull/11999 + +### Changed +- release(jetbrains): v7.0.2-rc.1 by @kilo-maintainer[bot] in https://github.com/Kilo-Org/kilocode/pull/12011 + + ## [7.0.2-rc.1] - 2026-07-07 ### Added diff --git a/packages/kilo-jetbrains/gradle.properties b/packages/kilo-jetbrains/gradle.properties index 23aa553936..c93a93d6ea 100644 --- a/packages/kilo-jetbrains/gradle.properties +++ b/packages/kilo-jetbrains/gradle.properties @@ -1,5 +1,5 @@ kotlin.stdlib.default.dependency=false -kilo.jetbrains.version=7.0.2-rc.1 +kilo.jetbrains.version=7.0.2-rc.2 org.gradle.configuration-cache=true org.gradle.caching=true org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m From af9f15c98c01c3bc591979eea0d729c1946a309b Mon Sep 17 00:00:00 2001 From: Kirill Kalishev Date: Tue, 7 Jul 2026 15:17:28 -0400 Subject: [PATCH 68/68] docs(jetbrains): edit changelog for v7.0.2-rc.2 --- packages/kilo-jetbrains/CHANGELOG.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/kilo-jetbrains/CHANGELOG.md b/packages/kilo-jetbrains/CHANGELOG.md index 023076fae9..1a48e5dda1 100644 --- a/packages/kilo-jetbrains/CHANGELOG.md +++ b/packages/kilo-jetbrains/CHANGELOG.md @@ -155,19 +155,17 @@ ## [7.0.2-rc.2] - 2026-07-07 ### Added -- feat(sandbox): widen writable-paths settings input and add coverage by @marius-kilocode in https://github.com/Kilo-Org/kilocode/pull/12008 -- feat(jetbrains): polish chat UI and bundled CLI runtime flow by @kirillk in https://github.com/Kilo-Org/kilocode/pull/11978 +- Show compact previews for collapsed reasoning blocks so long assistant reasoning stays readable without taking over the transcript. +- Add clearer Kilo Core runtime information and diagnostics for release download failures. ### Fixed -- fix(vscode): show sandbox tooltip immediately on hover by @marius-kilocode in https://github.com/Kilo-Org/kilocode/pull/12012 -- fix(vscode): preserve mode on first send by @marius-kilocode in https://github.com/Kilo-Org/kilocode/pull/12009 -- fix(vscode): allow shrinking prompt mention selections by @mjnaderi in https://github.com/Kilo-Org/kilocode/pull/11936 -- fix(vscode): handle multiline bidi prompt input by @mjnaderi in https://github.com/Kilo-Org/kilocode/pull/12006 -- fix: honor Kilo cloud account preferences by @markijbema in https://github.com/Kilo-Org/kilocode/pull/11999 +- Resolve the CLI executable using the user's shell environment so custom PATH setups work when sessions start from JetBrains. +- Keep retry and offline status visible in the session footer while preserving transcript context. +- Prevent oversized header popups by capping preview content. ### Changed -- release(jetbrains): v7.0.2-rc.1 by @kilo-maintainer[bot] in https://github.com/Kilo-Org/kilocode/pull/12011 - +- Download the required Kilo Core release at runtime and prune stale cached runtime binaries automatically. +- Polish JetBrains chat spacing, prompt input behavior, question/todo layout, history scrolling, code block padding, and session background colors. ## [7.0.2-rc.1] - 2026-07-07