From 4de82931ed3d5315e6717827a9b53b11c1162e7f Mon Sep 17 00:00:00 2001 From: Mohammad Javad Naderi Date: Fri, 5 Jun 2026 11:31:32 +0330 Subject: [PATCH 01/30] fix(vscode): tolerate duplicate panel command registration --- .changeset/quiet-terminal-panel.md | 5 +++++ .../src/agent-manager/SessionTerminalManager.ts | 17 +++++++++++++++-- .../tests/unit/session-terminal-manager.test.ts | 7 +++++++ 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 .changeset/quiet-terminal-panel.md diff --git a/.changeset/quiet-terminal-panel.md b/.changeset/quiet-terminal-panel.md new file mode 100644 index 0000000000..794b02b799 --- /dev/null +++ b/.changeset/quiet-terminal-panel.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Avoid failing Agent Manager startup when another extension already registered VS Code panel commands. diff --git a/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts b/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts index 189045d5a7..0af740f994 100644 --- a/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts +++ b/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts @@ -203,8 +203,10 @@ export class SessionTerminalManager { return result } + const disposable = this.tryRegisterCommand(id, handler) + if (!disposable) return this.commandHandlers.set(id, handler) - this.commandDisposables.set(id, this.host.registerCommand(id, handler)) + this.commandDisposables.set(id, disposable) } private async runOriginalCommand(id: string, args: unknown[]): Promise { @@ -219,11 +221,22 @@ export class SessionTerminalManager { } finally { const handler = this.commandHandlers.get(id) if (!handler) return - const replacement = this.host.registerCommand(id, handler) + const replacement = this.tryRegisterCommand(id, handler) + if (!replacement) return this.commandDisposables.set(id, replacement) } } + private tryRegisterCommand(id: string, handler: (...args: unknown[]) => Promise): Disposable | undefined { + try { + return this.host.registerCommand(id, handler) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + this.log(`panel command registration skipped for ${id}: ${msg}`) + return undefined + } + } + private isManaged(terminal: TerminalHandle): boolean { for (const entry of this.terminals.values()) { if (entry.terminal === terminal) return true diff --git a/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts b/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts index bb08e44b82..c7a7f29197 100644 --- a/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts +++ b/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts @@ -88,6 +88,13 @@ describe("SessionTerminalManager structure", () => { expect(text).toContain("this.showExistingLocal()") }) + it("panel command registration is best effort", () => { + const text = body("tryRegisterCommand") + expect(text).toContain("this.host.registerCommand") + expect(text).toContain("catch (err)") + expect(text).toContain("panel command registration skipped") + }) + it("exposes active terminal state for terminal context routing", () => { const text = body("hasActiveTerminal") expect(text).toContain("this.host.activeTerminal()") From 2c9e72c14a87387199fd42546746bbea30aa1570 Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Thu, 18 Jun 2026 10:34:30 +0000 Subject: [PATCH 02/30] fix: deny Kilo Gateway data collection Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .changeset/deny-gateway-data-collection.md | 6 +++++ packages/kilo-gateway/src/provider.ts | 17 ++++++++++++- packages/kilo-gateway/src/types.ts | 5 ++++ packages/kilo-gateway/test/provider.test.ts | 25 ++++++++++++++++++- .../src/kilocode/provider/provider.ts | 11 +++++++- packages/opencode/src/provider/provider.ts | 2 ++ .../test/kilocode/kilo-loader-auth.test.ts | 17 ++++++++++++- 7 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 .changeset/deny-gateway-data-collection.md diff --git a/.changeset/deny-gateway-data-collection.md b/.changeset/deny-gateway-data-collection.md new file mode 100644 index 0000000000..48286252fa --- /dev/null +++ b/.changeset/deny-gateway-data-collection.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": patch +"@kilocode/kilo-gateway": patch +--- + +Deny provider data collection for Kilo Gateway requests when prompt-training models are hidden. diff --git a/packages/kilo-gateway/src/provider.ts b/packages/kilo-gateway/src/provider.ts index b0a54bded8..ddf079c28a 100644 --- a/packages/kilo-gateway/src/provider.ts +++ b/packages/kilo-gateway/src/provider.ts @@ -19,6 +19,21 @@ export function buildRequestHeaders(defaultHeaders: Record, requ return headers } +export function addDataCollection(body: BodyInit | null | undefined, value?: "allow" | "deny") { + if (!value || typeof body !== "string") return body + const data = (() => { + try { + return JSON.parse(body) as unknown + } catch { + return undefined + } + })() + if (typeof data !== "object" || data === null || Array.isArray(data)) return body + const current = "provider" in data ? data.provider : undefined + const provider = typeof current === "object" && current !== null && !Array.isArray(current) ? current : {} + return JSON.stringify({ ...data, provider: { ...provider, data_collection: value } }) +} + /** * Create a KiloCode provider instance * @@ -55,7 +70,7 @@ export function createKilo(options: KiloProviderOptions = {}): KiloProvider { const originalFetch = options.fetch ?? fetch const wrappedFetch = async (input: string | URL | Request, init?: RequestInit) => { const headers = buildRequestHeaders(customHeaders, init?.headers) - const body = sanitizeResponsesBody(input, init?.body) + const body = addDataCollection(sanitizeResponsesBody(input, init?.body), options.dataCollection) // Add authorization if API key exists if (apiKey) { diff --git a/packages/kilo-gateway/src/types.ts b/packages/kilo-gateway/src/types.ts index e9e8080f02..ab214d3274 100644 --- a/packages/kilo-gateway/src/types.ts +++ b/packages/kilo-gateway/src/types.ts @@ -95,6 +95,11 @@ export interface KiloProviderOptions { */ name?: string + /** + * Data collection preference for upstream provider routing + */ + dataCollection?: "allow" | "deny" + /** * Custom fetch function */ diff --git a/packages/kilo-gateway/test/provider.test.ts b/packages/kilo-gateway/test/provider.test.ts index 34f468a80b..80c8ef34af 100644 --- a/packages/kilo-gateway/test/provider.test.ts +++ b/packages/kilo-gateway/test/provider.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { buildRequestHeaders } from "../src/provider" +import { addDataCollection, buildRequestHeaders } from "../src/provider" describe("Kilo provider request headers", () => { test("request headers override provider defaults", () => { @@ -21,3 +21,26 @@ describe("Kilo provider request headers", () => { expect(headers.get("x-request-only")).toBe("kept-too") }) }) + +describe("Kilo provider request body", () => { + test("denies data collection while preserving provider routing", () => { + const body = JSON.stringify({ + model: "anthropic/claude-sonnet-4", + provider: { order: ["anthropic"] }, + }) + const result = addDataCollection(body, "deny") + + expect(JSON.parse(result as string)).toEqual({ + model: "anthropic/claude-sonnet-4", + provider: { + order: ["anthropic"], + data_collection: "deny", + }, + }) + }) + + test("leaves the request body unchanged without a preference", () => { + const body = JSON.stringify({ model: "anthropic/claude-sonnet-4" }) + expect(addDataCollection(body)).toBe(body) + }) +}) diff --git a/packages/opencode/src/kilocode/provider/provider.ts b/packages/opencode/src/kilocode/provider/provider.ts index cb35d737ac..6bd6d0e9bf 100644 --- a/packages/opencode/src/kilocode/provider/provider.ts +++ b/packages/opencode/src/kilocode/provider/provider.ts @@ -115,6 +115,11 @@ function useLanguageModel(sdk: any) { return sdk.responses === undefined && sdk.chat === undefined } +export function patchKiloProviderPrivacy(provider: { options?: Record } | undefined, config: any) { + if (!provider || config.hide_prompt_training_models !== true) return + provider.options = { ...provider.options, dataCollection: "deny" } +} + export function kiloCustomLoaders(dep: CustomDep): Record { return { "github-copilot-enterprise": () => @@ -129,10 +134,11 @@ export function kiloCustomLoaders(dep: CustomDep): Record kilo: Effect.fnUntraced(function* (input: any) { const env = yield* dep.env() + const config = yield* dep.config() const hasKey = yield* Effect.gen(function* () { if (input.env.some((item: string) => env[item])) return true if (yield* dep.auth(input.id)) return true - if ((yield* dep.config()).provider?.["kilo"]?.options?.apiKey) return true + if (config.provider?.["kilo"]?.options?.apiKey) return true return false }) @@ -140,6 +146,9 @@ export function kiloCustomLoaders(dep: CustomDep): Record if (env.KILO_ORG_ID) { options.kilocodeOrganizationId = env.KILO_ORG_ID } + if (config.hide_prompt_training_models === true) { + options.dataCollection = "deny" + } if (!hasKey) { options.apiKey = "anonymous" } diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 6d3962cfa5..38ec0465d5 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -35,6 +35,7 @@ import { patchModelsDevModel as patchKiloModel, patchConfigModel as patchKiloConfigModel, patchCustomLoaderResult, + patchKiloProviderPrivacy, kiloSmallModelPriority, buildTimeoutSignal, REQUEST_TIMEOUT_MS, @@ -1489,6 +1490,7 @@ export const layer = Layer.effect( if (provider.options) partial.options = provider.options mergeProvider(providerID, partial) } + patchKiloProviderPrivacy(providers[ProviderID.make("kilo")], cfg) // kilocode_change const gitlab = ProviderID.make("gitlab") if (discoveryLoaders[gitlab] && providers[gitlab] && isProviderAllowed(gitlab)) { diff --git a/packages/opencode/test/kilocode/kilo-loader-auth.test.ts b/packages/opencode/test/kilocode/kilo-loader-auth.test.ts index 26567407aa..8060dc4462 100644 --- a/packages/opencode/test/kilocode/kilo-loader-auth.test.ts +++ b/packages/opencode/test/kilocode/kilo-loader-auth.test.ts @@ -7,7 +7,7 @@ import { ModelsDev } from "../../src/provider/models" import * as CoreModels from "@opencode-ai/core/models" import { Effect, Layer } from "effect" import { FetchHttpClient } from "effect/unstable/http" -import { kiloCustomLoaders } from "../../src/kilocode/provider/provider" +import { kiloCustomLoaders, patchKiloProviderPrivacy } from "../../src/kilocode/provider/provider" import { Auth } from "../../src/auth" import { ModelCache } from "../../src/provider/model-cache" import { Provider } from "../../src/provider/provider" @@ -165,6 +165,21 @@ it.effect("enables a paid catalog when config apiKey is present", () => }), ) +it.effect("denies provider data collection when prompt-training models are hidden", () => + Effect.gen(function* () { + const result = yield* load({ config: { hide_prompt_training_models: true } }) + expect(result.options).toEqual({ apiKey: "anonymous", dataCollection: "deny" }) + }), +) + +it.effect("keeps data collection denied after configured options are applied", () => + Effect.sync(() => { + const provider = { options: { dataCollection: "allow", baseURL: "https://api.kilo.ai" } } + patchKiloProviderPrivacy(provider, { hide_prompt_training_models: true }) + expect(provider.options).toEqual({ dataCollection: "deny", baseURL: "https://api.kilo.ai" }) + }), +) + it.effect("enables a paid catalog when auth exists", () => Effect.gen(function* () { const result = yield* load({ auth: { type: "api", key: "test-key" } }) From 3bb2b20268479d7f964afcce5568670e27e9eeb3 Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Thu, 18 Jun 2026 10:45:30 +0000 Subject: [PATCH 03/30] refactor(gateway): transform request bodies once Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- packages/kilo-gateway/src/provider.ts | 19 ++--------- packages/kilo-gateway/src/responses.ts | 23 +++++++++---- packages/kilo-gateway/test/provider.test.ts | 25 +------------- packages/kilo-gateway/test/responses.test.ts | 35 ++++++++++++++++---- 4 files changed, 48 insertions(+), 54 deletions(-) diff --git a/packages/kilo-gateway/src/provider.ts b/packages/kilo-gateway/src/provider.ts index ddf079c28a..1f52b09c2d 100644 --- a/packages/kilo-gateway/src/provider.ts +++ b/packages/kilo-gateway/src/provider.ts @@ -9,7 +9,7 @@ import { getApiKey } from "./auth/token.js" import { buildKiloHeaders, getDefaultHeaders } from "./headers.js" import { ANONYMOUS_API_KEY } from "./api/constants.js" import { resolveKiloOpenRouterBaseUrl } from "./api/url.js" -import { sanitizeResponsesBody } from "./responses.js" +import { transformRequestBody } from "./responses.js" export function buildRequestHeaders(defaultHeaders: Record, requestHeaders?: HeadersInit): Headers { const headers = new Headers(defaultHeaders) @@ -19,21 +19,6 @@ export function buildRequestHeaders(defaultHeaders: Record, requ return headers } -export function addDataCollection(body: BodyInit | null | undefined, value?: "allow" | "deny") { - if (!value || typeof body !== "string") return body - const data = (() => { - try { - return JSON.parse(body) as unknown - } catch { - return undefined - } - })() - if (typeof data !== "object" || data === null || Array.isArray(data)) return body - const current = "provider" in data ? data.provider : undefined - const provider = typeof current === "object" && current !== null && !Array.isArray(current) ? current : {} - return JSON.stringify({ ...data, provider: { ...provider, data_collection: value } }) -} - /** * Create a KiloCode provider instance * @@ -70,7 +55,7 @@ export function createKilo(options: KiloProviderOptions = {}): KiloProvider { const originalFetch = options.fetch ?? fetch const wrappedFetch = async (input: string | URL | Request, init?: RequestInit) => { const headers = buildRequestHeaders(customHeaders, init?.headers) - const body = addDataCollection(sanitizeResponsesBody(input, init?.body), options.dataCollection) + const body = transformRequestBody(input, init?.body, options.dataCollection) // Add authorization if API key exists if (apiKey) { diff --git a/packages/kilo-gateway/src/responses.ts b/packages/kilo-gateway/src/responses.ts index 3697c5cf77..5ba68266ed 100644 --- a/packages/kilo-gateway/src/responses.ts +++ b/packages/kilo-gateway/src/responses.ts @@ -28,8 +28,13 @@ function strip(input: unknown[]) { return { kept, changed } } -export function sanitizeResponsesBody(input: string | URL | Request, body: BodyInit | null | undefined) { - if (!endpoint(input)) return body +export function transformRequestBody( + input: string | URL | Request, + body: BodyInit | null | undefined, + value?: "allow" | "deny", +) { + const responses = endpoint(input) + if (!responses && !value) return body if (typeof body !== "string") return body const data = (() => { @@ -40,10 +45,14 @@ export function sanitizeResponsesBody(input: string | URL | Request, body: BodyI } })() if (!record(data)) return body - if (data.store === true) return body - if (!Array.isArray(data.input)) return body - const result = strip(data.input) - if (!result.changed) return body - return JSON.stringify({ ...data, input: result.kept }) + const result = responses && data.store !== true && Array.isArray(data.input) ? strip(data.input) : undefined + if (!result?.changed && !value) return body + + const provider = record(data.provider) ? data.provider : {} + return JSON.stringify({ + ...data, + ...(result?.changed ? { input: result.kept } : {}), + ...(value ? { provider: { ...provider, data_collection: value } } : {}), + }) } diff --git a/packages/kilo-gateway/test/provider.test.ts b/packages/kilo-gateway/test/provider.test.ts index 80c8ef34af..34f468a80b 100644 --- a/packages/kilo-gateway/test/provider.test.ts +++ b/packages/kilo-gateway/test/provider.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { addDataCollection, buildRequestHeaders } from "../src/provider" +import { buildRequestHeaders } from "../src/provider" describe("Kilo provider request headers", () => { test("request headers override provider defaults", () => { @@ -21,26 +21,3 @@ describe("Kilo provider request headers", () => { expect(headers.get("x-request-only")).toBe("kept-too") }) }) - -describe("Kilo provider request body", () => { - test("denies data collection while preserving provider routing", () => { - const body = JSON.stringify({ - model: "anthropic/claude-sonnet-4", - provider: { order: ["anthropic"] }, - }) - const result = addDataCollection(body, "deny") - - expect(JSON.parse(result as string)).toEqual({ - model: "anthropic/claude-sonnet-4", - provider: { - order: ["anthropic"], - data_collection: "deny", - }, - }) - }) - - test("leaves the request body unchanged without a preference", () => { - const body = JSON.stringify({ model: "anthropic/claude-sonnet-4" }) - expect(addDataCollection(body)).toBe(body) - }) -}) diff --git a/packages/kilo-gateway/test/responses.test.ts b/packages/kilo-gateway/test/responses.test.ts index 49520300f5..9d73d65c78 100644 --- a/packages/kilo-gateway/test/responses.test.ts +++ b/packages/kilo-gateway/test/responses.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { sanitizeResponsesBody } from "../src/responses" +import { transformRequestBody } from "../src/responses" describe("Responses request sanitization", () => { test("strips item ids when storage is disabled", () => { @@ -30,7 +30,7 @@ describe("Responses request sanitization", () => { ], }) - const result = sanitizeResponsesBody("https://api.kilo.ai/api/openrouter/responses", body) + const result = transformRequestBody("https://api.kilo.ai/api/openrouter/responses", body) const data = JSON.parse(result as string) expect(data.input).toHaveLength(3) @@ -60,19 +60,19 @@ describe("Responses request sanitization", () => { ], }) - expect(sanitizeResponsesBody("https://api.kilo.ai/api/openrouter/responses", body)).toBe(body) + expect(transformRequestBody("https://api.kilo.ai/api/openrouter/responses", body)).toBe(body) }) test("leaves non-responses requests unchanged", () => { const body = "not json" - expect(sanitizeResponsesBody("https://api.kilo.ai/api/openrouter/chat/completions", body)).toBe(body) + expect(transformRequestBody("https://api.kilo.ai/api/openrouter/chat/completions", body)).toBe(body) }) test("leaves invalid responses JSON unchanged", () => { const body = "not json" - expect(sanitizeResponsesBody("https://api.kilo.ai/api/openrouter/responses", body)).toBe(body) + expect(transformRequestBody("https://api.kilo.ai/api/openrouter/responses", body)).toBe(body) }) test("matches relative responses paths without a placeholder host", () => { @@ -86,9 +86,32 @@ describe("Responses request sanitization", () => { }, ], }) - const result = sanitizeResponsesBody("/api/openrouter/responses?stream=true", body) + const result = transformRequestBody("/api/openrouter/responses?stream=true", body) const data = JSON.parse(result as string) expect(data.input[0].id).toBeUndefined() }) + + test("sanitizes responses and denies data collection in one transform", () => { + const body = JSON.stringify({ + input: [{ type: "message", role: "assistant", id: "msg_tmp_123" }], + provider: { order: ["anthropic"] }, + }) + const result = transformRequestBody("https://api.kilo.ai/api/openrouter/responses", body, "deny") + + expect(JSON.parse(result as string)).toEqual({ + input: [{ type: "message", role: "assistant" }], + provider: { order: ["anthropic"], data_collection: "deny" }, + }) + }) + + test("denies data collection for non-responses requests", () => { + const body = JSON.stringify({ model: "anthropic/claude-sonnet-4" }) + const result = transformRequestBody("https://api.kilo.ai/api/openrouter/chat/completions", body, "deny") + + expect(JSON.parse(result as string)).toEqual({ + model: "anthropic/claude-sonnet-4", + provider: { data_collection: "deny" }, + }) + }) }) From c8a05db14027b92f90920b79f0a604f4a82888ad Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Thu, 18 Jun 2026 12:00:13 +0000 Subject: [PATCH 04/30] docs: document prompt-training model visibility Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .../pages/getting-started/settings/index.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/kilo-docs/pages/getting-started/settings/index.md b/packages/kilo-docs/pages/getting-started/settings/index.md index a3e9ddd536..9c2a9be2be 100644 --- a/packages/kilo-docs/pages/getting-started/settings/index.md +++ b/packages/kilo-docs/pages/getting-started/settings/index.md @@ -59,6 +59,18 @@ When the Kilo provider is enabled and you are signed in, choose the transcriptio } ``` +### Prompt-Training Model Visibility + +Enable **Hide Prompt-Training Models** under **Models** to remove Kilo Gateway models whose providers may use your prompts for training from model lists. Models from other providers and models without explicit prompt-training metadata remain visible. The setting is disabled by default. + +You can also enable it in `kilo.jsonc`: + +```json +{ + "hide_prompt_training_models": true +} +``` + ### Reasoning Blocks Reasoning blocks stay expanded by default in the VS Code chat UI. Enable **Auto-Collapse Reasoning** in the Display tab, or set `auto_collapse_reasoning` in `kilo.jsonc`, to collapse them after the agent finishes writing them: From b134c1c18f949d0011dcbd684f0c3da4922387f7 Mon Sep 17 00:00:00 2001 From: "kilo-maintainer[bot]" Date: Thu, 18 Jun 2026 12:00:41 +0000 Subject: [PATCH 05/30] release: v7.3.49 --- .changeset/roo-code-session-import.md | 5 --- bun.lock | 42 ++++++++++----------- package.json | 2 +- packages/core/package.json | 2 +- packages/effect-drizzle-sqlite/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++--- packages/http-recorder/package.json | 2 +- packages/kilo-console/package.json | 2 +- packages/kilo-docs/package.json | 2 +- packages/kilo-gateway/package.json | 2 +- packages/kilo-i18n/package.json | 2 +- packages/kilo-indexing/package.json | 2 +- packages/kilo-jetbrains/package.json | 2 +- packages/kilo-telemetry/package.json | 2 +- packages/kilo-ui/package.json | 2 +- packages/kilo-vscode/CHANGELOG.md | 6 +++ packages/kilo-vscode/package.json | 2 +- packages/kilo-vscode/tests/package.json | 2 +- packages/kilo-web-ui/package.json | 2 +- packages/llm/package.json | 2 +- packages/opencode/CHANGELOG.md | 2 + packages/opencode/package.json | 2 +- packages/plugin-atomic-chat/package.json | 2 +- packages/plugin/package.json | 2 +- packages/script/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/storybook/package.json | 2 +- packages/ui/package.json | 2 +- script/upstream/package.json | 2 +- 29 files changed, 59 insertions(+), 56 deletions(-) delete mode 100644 .changeset/roo-code-session-import.md diff --git a/.changeset/roo-code-session-import.md b/.changeset/roo-code-session-import.md deleted file mode 100644 index 4bbdc50e57..0000000000 --- a/.changeset/roo-code-session-import.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"kilo-code": minor ---- - -Add an "Import Sessions from Roo Code" button in the About settings tab that discovers and imports conversation history from an existing Roo Code installation, reusing the existing migration wizard. diff --git a/bun.lock b/bun.lock index b16cd5d81b..9695753def 100644 --- a/bun.lock +++ b/bun.lock @@ -28,7 +28,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "7.3.48", + "version": "7.3.49", "bin": { "opencode": "./bin/opencode", }, @@ -90,7 +90,7 @@ }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -104,7 +104,7 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "@effect/platform-node": "catalog:", "effect": "catalog:", @@ -117,7 +117,7 @@ }, "packages/kilo-console": { "name": "@kilocode/kilo-console", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "@kilocode/kilo-web-ui": "workspace:*", "@kilocode/sdk": "workspace:*", @@ -139,7 +139,7 @@ }, "packages/kilo-docs": { "name": "@kilocode/kilo-docs", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "@docsearch/css": "^4", "@docsearch/js": "^4", @@ -169,7 +169,7 @@ }, "packages/kilo-gateway": { "name": "@kilocode/kilo-gateway", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "@ai-sdk/alibaba": "1.0.17", "@ai-sdk/anthropic": "3.0.71", @@ -205,7 +205,7 @@ }, "packages/kilo-i18n": { "name": "@kilocode/kilo-i18n", - "version": "7.3.48", + "version": "7.3.49", "devDependencies": { "@tsconfig/node22": "catalog:", "@types/bun": "catalog:", @@ -215,7 +215,7 @@ }, "packages/kilo-indexing": { "name": "@kilocode/kilo-indexing", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "@aws-sdk/client-bedrock-runtime": "3.1005.0", "@aws-sdk/credential-provider-ini": "3.972.31", @@ -247,11 +247,11 @@ }, "packages/kilo-jetbrains": { "name": "@kilocode/kilo-jetbrains", - "version": "7.3.48", + "version": "7.3.49", }, "packages/kilo-telemetry": { "name": "@kilocode/kilo-telemetry", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "@kilocode/kilo-gateway": "workspace:*", "posthog-node": "4.4.0", @@ -265,7 +265,7 @@ }, "packages/kilo-ui": { "name": "@kilocode/kilo-ui", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "@kilocode/sdk": "workspace:*", "@kobalte/core": "0.13.11", @@ -302,7 +302,7 @@ }, "packages/kilo-vscode": { "name": "kilo-code", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "@anthropic-ai/sdk": "^0.39.0", "@kilocode/kilo-gateway": "workspace:*", @@ -368,7 +368,7 @@ }, "packages/kilo-web-ui": { "name": "@kilocode/kilo-web-ui", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "@kilocode/kilo-ui": "workspace:*", "@kobalte/core": "catalog:", @@ -385,7 +385,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", @@ -403,7 +403,7 @@ }, "packages/opencode": { "name": "@kilocode/cli", - "version": "7.3.48", + "version": "7.3.49", "bin": { "kilo": "./bin/kilo", "kilocode": "./bin/kilo", @@ -550,7 +550,7 @@ }, "packages/plugin": { "name": "@kilocode/plugin", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "@kilocode/sdk": "workspace:*", "effect": "catalog:", @@ -578,7 +578,7 @@ }, "packages/plugin-atomic-chat": { "name": "@kilocode/plugin-atomic-chat", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "@kilocode/plugin": "workspace:*", }, @@ -592,7 +592,7 @@ }, "packages/script": { "name": "@opencode-ai/script", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "semver": "^7.6.3", }, @@ -603,7 +603,7 @@ }, "packages/sdk/js": { "name": "@kilocode/sdk", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "cross-spawn": "catalog:", }, @@ -618,7 +618,7 @@ }, "packages/storybook": { "name": "@opencode-ai/storybook", - "version": "7.3.48", + "version": "7.3.49", "devDependencies": { "@opencode-ai/ui": "workspace:*", "@solidjs/meta": "catalog:", @@ -641,7 +641,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "7.3.48", + "version": "7.3.49", "dependencies": { "@kilocode/sdk": "workspace:*", "@kobalte/core": "catalog:", diff --git a/package.json b/package.json index cdc98292b4..a45edfc06f 100644 --- a/package.json +++ b/package.json @@ -148,6 +148,6 @@ "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", "mammoth@1.12.0": "patches/mammoth@1.12.0.patch" }, - "version": "7.3.48", + "version": "7.3.49", "peerDependencies": {} } diff --git a/packages/core/package.json b/packages/core/package.json index 8c78fc142b..6a9be3c502 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.3.48", + "version": "7.3.49", "name": "@opencode-ai/core", "type": "module", "license": "MIT", diff --git a/packages/effect-drizzle-sqlite/package.json b/packages/effect-drizzle-sqlite/package.json index b7628cd6b1..7c1a5ef67b 100644 --- a/packages/effect-drizzle-sqlite/package.json +++ b/packages/effect-drizzle-sqlite/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.3.48", + "version": "7.3.49", "name": "@opencode-ai/effect-drizzle-sqlite", "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index e105884ba4..2def83eb98 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "kilo" name = "Kilo" description = "The open source coding agent." -version = "7.3.48" +version = "7.3.49" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/Kilo-Org/kilocode" @@ -11,26 +11,26 @@ name = "Kilo" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.48/opencode-darwin-arm64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.49/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.48/opencode-darwin-x64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.49/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.48/opencode-linux-arm64.tar.gz" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.49/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.48/opencode-linux-x64.tar.gz" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.49/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.48/opencode-windows-x64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.3.49/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/http-recorder/package.json b/packages/http-recorder/package.json index 58f9e5215b..196d5d7e47 100644 --- a/packages/http-recorder/package.json +++ b/packages/http-recorder/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.3.48", + "version": "7.3.49", "name": "@opencode-ai/http-recorder", "type": "module", "license": "MIT", diff --git a/packages/kilo-console/package.json b/packages/kilo-console/package.json index 2a98358b41..6903d0e35c 100755 --- a/packages/kilo-console/package.json +++ b/packages/kilo-console/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-console", - "version": "7.3.48", + "version": "7.3.49", "private": true, "type": "module", "scripts": { diff --git a/packages/kilo-docs/package.json b/packages/kilo-docs/package.json index 59afaa05d6..7ba653f7c0 100644 --- a/packages/kilo-docs/package.json +++ b/packages/kilo-docs/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-docs", - "version": "7.3.48", + "version": "7.3.49", "private": true, "scripts": { "dev": "next dev --webpack --port 3002", diff --git a/packages/kilo-gateway/package.json b/packages/kilo-gateway/package.json index 8cb6c4bab7..26418f3adc 100644 --- a/packages/kilo-gateway/package.json +++ b/packages/kilo-gateway/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/kilo-gateway", - "version": "7.3.48", + "version": "7.3.49", "type": "module", "license": "MIT", "description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration", diff --git a/packages/kilo-i18n/package.json b/packages/kilo-i18n/package.json index 9688347cbf..77c43e2a42 100644 --- a/packages/kilo-i18n/package.json +++ b/packages/kilo-i18n/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/kilo-i18n", - "version": "7.3.48", + "version": "7.3.49", "type": "module", "license": "MIT", "description": "Kilo-specific i18n translations and overrides", diff --git a/packages/kilo-indexing/package.json b/packages/kilo-indexing/package.json index 828fc54dcd..6df3d29341 100644 --- a/packages/kilo-indexing/package.json +++ b/packages/kilo-indexing/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/kilo-indexing", - "version": "7.3.48", + "version": "7.3.49", "type": "module", "license": "MIT", "description": "Standalone indexing engine and host helpers for Kilo Code", diff --git a/packages/kilo-jetbrains/package.json b/packages/kilo-jetbrains/package.json index 21bb01694c..c5cb2716c6 100644 --- a/packages/kilo-jetbrains/package.json +++ b/packages/kilo-jetbrains/package.json @@ -8,7 +8,7 @@ "test": "./gradlew test", "test:ci": "bun script/test-ci.ts" }, - "version": "7.3.48", + "version": "7.3.49", "dependencies": {}, "devDependencies": {}, "peerDependencies": {} diff --git a/packages/kilo-telemetry/package.json b/packages/kilo-telemetry/package.json index 48bbaaaa0c..ed4e1b0cc1 100644 --- a/packages/kilo-telemetry/package.json +++ b/packages/kilo-telemetry/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/kilo-telemetry", - "version": "7.3.48", + "version": "7.3.49", "type": "module", "license": "MIT", "description": "Telemetry for Kilo CLI - PostHog analytics integration", diff --git a/packages/kilo-ui/package.json b/packages/kilo-ui/package.json index 0b3a6b030d..f167b71a87 100644 --- a/packages/kilo-ui/package.json +++ b/packages/kilo-ui/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-ui", - "version": "7.3.48", + "version": "7.3.49", "type": "module", "license": "MIT", "exports": { diff --git a/packages/kilo-vscode/CHANGELOG.md b/packages/kilo-vscode/CHANGELOG.md index d353fb7cf8..41afeb6a1c 100644 --- a/packages/kilo-vscode/CHANGELOG.md +++ b/packages/kilo-vscode/CHANGELOG.md @@ -1,5 +1,11 @@ # kilo-code +## 7.3.49 + +### Minor Changes + +- [#11303](https://github.com/Kilo-Org/kilocode/pull/11303) [`6faa3f1`](https://github.com/Kilo-Org/kilocode/commit/6faa3f14b619144a1353f169a0a2855dcbf69bbb) - Add an "Import Sessions from Roo Code" button in the About settings tab that discovers and imports conversation history from an existing Roo Code installation, reusing the existing migration wizard. + ## 7.3.48 ### Minor Changes diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json index fce6628045..b934755d50 100644 --- a/packages/kilo-vscode/package.json +++ b/packages/kilo-vscode/package.json @@ -2,7 +2,7 @@ "name": "kilo-code", "displayName": "Kilo Code: AI Coding Agent, Copilot, and Autocomplete", "description": "Open Source AI coding agent that generates code from natural language, automates tasks, and runs terminal commands. Features inline autocomplete, browser automation, automated refactoring, and custom modes for planning, coding, and debugging. Supports 500+ AI models including Claude (Anthropic), Gemini, Grok, GPT, Codex and GLM.", - "version": "7.3.48", + "version": "7.3.49", "icon": "assets/icons/logo-outline-black.png", "galleryBanner": { "color": "#FFFFFF", diff --git a/packages/kilo-vscode/tests/package.json b/packages/kilo-vscode/tests/package.json index 68f994abfe..25778d6ec7 100644 --- a/packages/kilo-vscode/tests/package.json +++ b/packages/kilo-vscode/tests/package.json @@ -1,6 +1,6 @@ { "type": "module", - "version": "7.3.48", + "version": "7.3.49", "dependencies": {}, "devDependencies": {}, "peerDependencies": {} diff --git a/packages/kilo-web-ui/package.json b/packages/kilo-web-ui/package.json index 2c3a0e9a68..9a0e491bfb 100644 --- a/packages/kilo-web-ui/package.json +++ b/packages/kilo-web-ui/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-web-ui", - "version": "7.3.48", + "version": "7.3.49", "type": "module", "license": "MIT", "exports": { diff --git a/packages/llm/package.json b/packages/llm/package.json index 4fbb7aa8c9..6f7a4b58d8 100644 --- a/packages/llm/package.json +++ b/packages/llm/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.3.48", + "version": "7.3.49", "name": "@opencode-ai/llm", "type": "module", "license": "MIT", diff --git a/packages/opencode/CHANGELOG.md b/packages/opencode/CHANGELOG.md index 7335b645ac..1399f55759 100644 --- a/packages/opencode/CHANGELOG.md +++ b/packages/opencode/CHANGELOG.md @@ -1,5 +1,7 @@ # @kilocode/cli +## 7.3.49 + ## 7.3.48 ### Minor Changes diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 0d35b24c9c..3db70fbaf7 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.3.48", + "version": "7.3.49", "name": "@kilocode/cli", "type": "module", "license": "MIT", diff --git a/packages/plugin-atomic-chat/package.json b/packages/plugin-atomic-chat/package.json index 88bcfafb44..e93b407dc3 100644 --- a/packages/plugin-atomic-chat/package.json +++ b/packages/plugin-atomic-chat/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/plugin-atomic-chat", - "version": "7.3.48", + "version": "7.3.49", "description": "Kilo Code plugin for Atomic Chat: auto-detection and dynamic model discovery (OpenAI-compatible local API)", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 65d77c49b3..ad4dd17f18 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/plugin", - "version": "7.3.48", + "version": "7.3.49", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/script/package.json b/packages/script/package.json index 77a7f59988..ad86d33e46 100644 --- a/packages/script/package.json +++ b/packages/script/package.json @@ -12,6 +12,6 @@ "exports": { ".": "./src/index.ts" }, - "version": "7.3.48", + "version": "7.3.49", "peerDependencies": {} } diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 76158f4d12..fc6a0b1e6e 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/sdk", - "version": "7.3.48", + "version": "7.3.49", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/storybook/package.json b/packages/storybook/package.json index ce24afe4c7..7ece74c2c8 100644 --- a/packages/storybook/package.json +++ b/packages/storybook/package.json @@ -26,7 +26,7 @@ "typescript": "catalog:", "vite": "catalog:" }, - "version": "7.3.48", + "version": "7.3.49", "dependencies": {}, "peerDependencies": {} } diff --git a/packages/ui/package.json b/packages/ui/package.json index 91a96f8842..6c39942f15 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "7.3.48", + "version": "7.3.49", "type": "module", "license": "MIT", "exports": { diff --git a/script/upstream/package.json b/script/upstream/package.json index 6520100029..7435f5721d 100644 --- a/script/upstream/package.json +++ b/script/upstream/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/upstream-merge", - "version": "7.3.48", + "version": "7.3.49", "private": true, "type": "module", "description": "Scripts for automating upstream opencode merges into Kilo", From 6190c704c8b08cfa8b11d9b0df51619a4feac8fd Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Thu, 18 Jun 2026 14:10:07 +0200 Subject: [PATCH 06/30] Revert "Add top-level announcement notification" --- packages/kilo-docs/source-links.md | 2 -- .../webview-ui/src/context/notifications.tsx | 13 +------------ 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/packages/kilo-docs/source-links.md b/packages/kilo-docs/source-links.md index 5a30e68d6e..49c8e1c151 100644 --- a/packages/kilo-docs/source-links.md +++ b/packages/kilo-docs/source-links.md @@ -81,8 +81,6 @@ - -- - - - diff --git a/packages/kilo-vscode/webview-ui/src/context/notifications.tsx b/packages/kilo-vscode/webview-ui/src/context/notifications.tsx index 67deb6f99c..8c722585e6 100644 --- a/packages/kilo-vscode/webview-ui/src/context/notifications.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/notifications.tsx @@ -11,16 +11,6 @@ import { import { useVSCode } from "./vscode" import type { KilocodeNotification, ExtensionMessage } from "../types/messages" -// Static notifications always shown unconditionally (not fetched from API) -const STATIC_NOTIFICATIONS: KilocodeNotification[] = [ - { - id: "star-giveaway-june-2026", - title: "GitHub Star Giveaway", - message: "We're giving away $500 of AI Credits when we reach 25,000 stars on GitHub. Support us:", - action: { actionText: "github.com/Kilo-Org/kilocode", actionURL: "https://github.com/Kilo-Org/kilocode/" }, - }, -] - interface NotificationsContextValue { notifications: Accessor filteredNotifications: Accessor @@ -63,8 +53,7 @@ export const NotificationsProvider: ParentComponent = (props) => { const filteredNotifications = createMemo(() => { const dismissed = dismissedIds() - const all = [...STATIC_NOTIFICATIONS, ...notifications()] - return all.filter((n) => !dismissed.includes(n.id)) + return notifications().filter((n) => !dismissed.includes(n.id)) }) const dismiss = (id: string) => { From ccec2162383a6f378ed5e62d630720607d185209 Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Thu, 18 Jun 2026 12:42:54 +0000 Subject: [PATCH 07/30] feat: show BYOK availability in model pickers Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .changeset/bright-badgers-bring-keys.md | 7 +++++++ packages/kilo-docs/pages/getting-started/byok.md | 3 ++- packages/kilo-gateway/src/api/models.ts | 2 ++ packages/kilo-gateway/test/api/models.test.ts | 2 ++ .../ai/kilocode/backend/cli/KiloCliDataParser.kt | 2 ++ .../backend/rpc/KiloWorkspaceDtoMapper.kt | 1 + .../backend/workspace/KiloWorkspaceState.kt | 1 + .../cli/GeneratedApiModelSerializationTest.kt | 4 +++- .../kilocode/backend/cli/KiloCliDataParserTest.kt | 4 ++++ .../backend/cli/ProjectModelSerializationTest.kt | 4 +++- .../ai/kilocode/client/session/SessionUi.kt | 1 + .../session/controller/SessionController.kt | 1 + .../kilocode/client/session/model/SessionModel.kt | 1 + .../client/session/ui/model/ModelPicker.kt | 1 + .../session/ui/model/ModelPickerRenderer.kt | 13 +++++++++++++ .../client/settings/models/ModelsSettingsUi.kt | 11 ++++++++++- .../client/session/ui/model/ModelPickerTest.kt | 13 +++++++++++++ .../kotlin/ai/kilocode/rpc/dto/ProviderDto.kt | 1 + .../tests/unit/model-preview-data-line.test.ts | 9 +++++++++ .../tests/unit/model-selector-utils.test.ts | 9 +++++++++ .../agent-manager/MultiModelSelector.tsx | 10 ++++++++-- .../webview-ui/agent-manager/agent-manager.css | 15 ++++++++++++--- .../src/components/shared/ModelPreview.tsx | 7 +++++-- .../src/components/shared/ModelSelector.tsx | 8 +++++++- .../src/components/shared/model-selector-utils.ts | 4 ++++ .../webview-ui/src/types/messages/providers.ts | 1 + .../src/cli/cmd/tui/component/dialog-model.tsx | 9 ++++++--- .../kilocode/components/free-model-disclosure.ts | 4 ++++ .../src/kilocode/components/model-info-panel.tsx | 5 +++++ .../opencode/src/kilocode/provider/provider.ts | 3 +++ .../test/kilocode/free-model-disclosure.test.ts | 7 +++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 1 + packages/sdk/openapi.json | 3 +++ 33 files changed, 152 insertions(+), 15 deletions(-) create mode 100644 .changeset/bright-badgers-bring-keys.md diff --git a/.changeset/bright-badgers-bring-keys.md b/.changeset/bright-badgers-bring-keys.md new file mode 100644 index 0000000000..6da66c8b42 --- /dev/null +++ b/.changeset/bright-badgers-bring-keys.md @@ -0,0 +1,7 @@ +--- +"@kilocode/cli": minor +"@kilocode/kilo-gateway": minor +"kilo-code": minor +--- + +Show a BYOK badge for models that can use an enabled personal or organization provider key. diff --git a/packages/kilo-docs/pages/getting-started/byok.md b/packages/kilo-docs/pages/getting-started/byok.md index 1243bbb454..47145c6e11 100644 --- a/packages/kilo-docs/pages/getting-started/byok.md +++ b/packages/kilo-docs/pages/getting-started/byok.md @@ -97,5 +97,6 @@ Your IAM user or role must have the following permissions: ## Using BYOK in the Extensions and CLI - BYOK works with the Kilo Gateway provider. Users should ensure that is set as the active [provider](/docs/ai-providers). -- Select a model from a provider configured for BYOK, for example Claude Sonnet 4.5 if you configured BYOK for Anthropic, or GLM-4.7 if you configured the Z.ai Coding Plan. +- Models that can use one of your enabled personal or organization BYOK providers display a `BYOK` badge in the model picker. +- Select a model with the `BYOK` badge, for example Claude Sonnet 4.5 if you configured BYOK for Anthropic, or GLM-4.7 if you configured the Z.ai Coding Plan. - (Optional) Validate with the provider that traffic is being served by that key. diff --git a/packages/kilo-gateway/src/api/models.ts b/packages/kilo-gateway/src/api/models.ts index d59fb219fc..598255e66a 100644 --- a/packages/kilo-gateway/src/api/models.ts +++ b/packages/kilo-gateway/src/api/models.ts @@ -37,6 +37,7 @@ const openRouterModelSchema = z.object({ preferredIndex: z.number().optional(), isFree: z.boolean().optional(), mayTrainOnYourPrompts: z.boolean().optional(), + hasUserByokAvailable: z.boolean().optional(), terminalBench: z .object({ overallScore: z.number(), @@ -192,6 +193,7 @@ function transformToModelDevFormat(model: OpenRouterModel): any { tool_call: supportsTools, isFree: model.isFree, mayTrainOnYourPrompts: model.mayTrainOnYourPrompts, + hasUserByokAvailable: model.hasUserByokAvailable, ...(model.terminalBench && { terminalBench: model.terminalBench }), ...(inputPrice !== undefined && outputPrice !== undefined && { diff --git a/packages/kilo-gateway/test/api/models.test.ts b/packages/kilo-gateway/test/api/models.test.ts index 10ce9ecedb..56a589af0e 100644 --- a/packages/kilo-gateway/test/api/models.test.ts +++ b/packages/kilo-gateway/test/api/models.test.ts @@ -17,6 +17,7 @@ const VALID_RESPONSE = JSON.stringify({ supported_parameters: ["tools", "temperature"], isFree: false, mayTrainOnYourPrompts: true, + hasUserByokAvailable: true, }, ], }) @@ -148,6 +149,7 @@ test("returns models without error on success", async () => { expect(result.models["test/model-a"]).toMatchObject({ isFree: false, mayTrainOnYourPrompts: true, + hasUserByokAvailable: true, }) }) diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt index 02a63765b3..4fa7e4b608 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt @@ -988,6 +988,7 @@ object KiloCliDataParser { temperature = model.temperature, toolCall = model.toolCall, free = model.free, + byok = model.byok, status = model.status, recommendedIndex = model.recommendedIndex, variants = model.variants, @@ -1006,6 +1007,7 @@ object KiloCliDataParser { temperature = cap.bool("temperature"), toolCall = cap.bool("toolcall"), free = obj.bool("isFree"), + byok = obj.bool("hasUserByokAvailable"), status = obj.str("status"), recommendedIndex = obj.num("recommendedIndex"), variants = parseVariants(obj), diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt index fc6c4246b9..c1c604285c 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt @@ -74,6 +74,7 @@ internal object KiloWorkspaceDtoMapper { temperature = m.temperature, toolCall = m.toolCall, free = m.free, + byok = m.byok, status = m.status, recommendedIndex = m.recommendedIndex, variants = m.variants, diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloWorkspaceState.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloWorkspaceState.kt index b21b4bf707..20256e96f5 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloWorkspaceState.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloWorkspaceState.kt @@ -54,6 +54,7 @@ data class ModelInfo( val temperature: Boolean, val toolCall: Boolean, val free: Boolean, + val byok: Boolean = false, val status: String?, val recommendedIndex: Double?, val variants: List, diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/GeneratedApiModelSerializationTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/GeneratedApiModelSerializationTest.kt index b19ed6072d..a457d30883 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/GeneratedApiModelSerializationTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/GeneratedApiModelSerializationTest.kt @@ -128,7 +128,8 @@ class GeneratedApiModelSerializationTest { "options": {}, "headers": {}, "release_date": "2025-01-01", - "isFree": true + "isFree": true, + "hasUserByokAvailable": true } } }], @@ -139,6 +140,7 @@ class GeneratedApiModelSerializationTest { val obj = json.decodeFromString(src) val model = obj.all[0].models["free-model"]!! assertEquals(true, model.isFree) + assertEquals(true, model.hasUserByokAvailable) assertEquals( ai.kilocode.jetbrains.api.model.Model.Status.BETA, model.status, diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt index 5c824749af..ce84b208ee 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt @@ -1254,6 +1254,8 @@ class KiloCliDataParserTest { }, "limit": {"context": 200000, "input": 100000, "output": 16000}, "status": "active", + "isFree": false, + "hasUserByokAvailable": true, "recommendedIndex": 2, "variants": {"high": {}, "low": {}, "medium": {}}, "options": {}, "headers": {} @@ -1273,6 +1275,8 @@ class KiloCliDataParserTest { assertTrue(model.temperature) assertTrue(model.toolCall) assertEquals("active", model.status) + assertFalse(model.free) + assertTrue(model.byok) assertEquals(2.0, model.recommendedIndex) assertEquals(200000L, model.limit?.context) assertEquals(100000L, model.limit?.input) diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/ProjectModelSerializationTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/ProjectModelSerializationTest.kt index 35ba6f52e3..d9ad9a7c96 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/ProjectModelSerializationTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/ProjectModelSerializationTest.kt @@ -111,7 +111,8 @@ class ProjectModelSerializationTest { "options": {}, "headers": {}, "release_date": "2025-01-01", - "isFree": true + "isFree": true, + "hasUserByokAvailable": true } } }], @@ -122,6 +123,7 @@ class ProjectModelSerializationTest { val obj = json.decodeFromString(src) val model = obj.all[0].models["free-model"]!! assertEquals(true, model.isFree) + assertEquals(true, model.hasUserByokAvailable) assertEquals( ai.kilocode.jetbrains.api.model.Model.Status.BETA, model.status, diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt index 8a47e19af4..1865e11e43 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt @@ -404,6 +404,7 @@ class SessionUi( it.providerName, it.recommendedIndex, it.free, + it.byok, it.variants, it.attachment, ) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt index 7048f8e9aa..27eea93327 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt @@ -663,6 +663,7 @@ class SessionController( provider.name, info.recommendedIndex, info.free, + info.byok, info.variants, info.limit?.let { ModelLimitItem(it.context, it.input, it.output) }, info.attachment, diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt index 79eeb432f8..f1631540bd 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/SessionModel.kt @@ -583,6 +583,7 @@ data class ModelItem( val providerName: String, val recommendedIndex: Double?, val free: Boolean, + val byok: Boolean = false, val variants: List, val limit: ModelLimitItem?, val attachment: Boolean = false, diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt index a7b8efe4dd..86e767afd7 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPicker.kt @@ -56,6 +56,7 @@ class ModelPicker : PickerButton() { val providerName: String, val recommendedIndex: Double? = null, val free: Boolean = false, + val byok: Boolean = false, val variants: List = emptyList(), val attachment: Boolean = false, ) { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt index 4ce73c10a4..8342eb5d6c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt @@ -74,6 +74,14 @@ internal class ModelPickerRenderer( private val badgeLabel = BadgeLabel(badge).apply { border = JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap()) } + private val byok = FilledBadgeIcon( + "BYOK", + ModelText.freeBg(), + JBColor.namedColor("Kilo.ModelPicker.freeBadgeForeground", JBColor.WHITE), + ) + private val byokLabel = BadgeLabel(byok).apply { + border = JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap()) + } private val warn = JBLabel(DATA_COLLECTED).apply { toolTipText = ModelText.dataCollected() border = JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap()) @@ -83,6 +91,7 @@ internal class ModelPickerRenderer( add(title) add(warn) add(badgeLabel) + add(byokLabel) add(provider) } private val star = JBLabel().apply { @@ -136,6 +145,7 @@ internal class ModelPickerRenderer( if (item == null) { title.append(value.emptyText, SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, fg)) badgeLabel.isVisible = false + byokLabel.isVisible = false warn.isVisible = false provider.isVisible = false star.icon = EmptyIcon.ICON_16 @@ -151,6 +161,7 @@ internal class ModelPickerRenderer( warn.isVisible = ModelText.collectsData(item) badgeLabel.isVisible = item.free + byokLabel.isVisible = item.byok provider.isVisible = value.favorite provider.text = item.providerName provider.foreground = weak @@ -174,6 +185,8 @@ internal class ModelPickerRenderer( internal fun badgeText(): String = badge.text + internal fun byokVisible(): Boolean = byokLabel.isVisible + internal fun warningVisible(): Boolean = warn.isVisible internal fun warningTooltip(): String? = warn.toolTipText diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUi.kt index 322e5fcfcb..3a67129f5e 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/models/ModelsSettingsUi.kt @@ -174,7 +174,16 @@ internal class ModelsSettingsUi( .filter { it.id == KILO_PROVIDER || it.id in cfg.connected } .flatMap { provider -> provider.models.mapNotNull { (id, model) -> - val item = ModelPicker.Item(id, model.name, provider.id, provider.name, model.recommendedIndex, model.free, model.variants) + val item = ModelPicker.Item( + id, + model.name, + provider.id, + provider.name, + model.recommendedIndex, + model.free, + model.byok, + model.variants, + ) if (!includeSmall && ModelText.small(item)) return@mapNotNull null item } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt index 056dcc7ba0..d4ca9801be 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt @@ -397,6 +397,18 @@ class ModelPickerTest : BasePlatformTestCase() { assertEquals("Data may be used for training", renderer.warningTooltip()) } + fun `test renderer shows BYOK badge when available`() { + val row = ModelPickerRow(ModelPicker.Item("claude", "Claude", "kilo", "Kilo", byok = true), "Kilo", false) + val model = CollectionListModel(listOf(row)) + val renderer = ModelPickerRenderer(model, { null }, { emptySet() }) + val list = JBList(model) + + renderer.getListCellRendererComponent(list, row, 0, false, false) + + assertTrue(renderer.byokVisible()) + assertFalse(renderer.badgeVisible()) + } + fun `test renderer hides data collection warning for non-kilo free model`() { val row = ModelPickerRow(ModelPicker.Item("free", "Free", "openrouter", "OpenRouter", free = true), "OpenRouter", false) val model = CollectionListModel(listOf(row)) @@ -420,6 +432,7 @@ class ModelPickerTest : BasePlatformTestCase() { assertSame(EmptyIcon.ICON_16, renderer.starIcon()) assertFalse(renderer.badgeVisible()) + assertFalse(renderer.byokVisible()) } private fun item( diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ProviderDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ProviderDto.kt index 6045f5c130..918b2dae51 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ProviderDto.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ProviderDto.kt @@ -11,6 +11,7 @@ data class ModelDto( val temperature: Boolean = false, val toolCall: Boolean = false, val free: Boolean = false, + val byok: Boolean = false, val status: String? = null, val recommendedIndex: Double? = null, val variants: List = emptyList(), diff --git a/packages/kilo-vscode/tests/unit/model-preview-data-line.test.ts b/packages/kilo-vscode/tests/unit/model-preview-data-line.test.ts index 788a70b61e..22e370ffc5 100644 --- a/packages/kilo-vscode/tests/unit/model-preview-data-line.test.ts +++ b/packages/kilo-vscode/tests/unit/model-preview-data-line.test.ts @@ -31,6 +31,15 @@ describe("model preview data collection line", () => { ) }) + it("renders BYOK availability independently from free and training metadata", () => { + expect(selector).toContain("hasByok(model)") + expect(preview).toContain("hasByok(model())") + expect(agent).toContain("hasByok(model)") + expect(selector).toContain(">BYOK") + expect(preview).toContain(">BYOK") + expect(agent).toContain(">BYOK") + }) + it("uses the book open check icon for all webview model data disclosures", () => { expect(selector).toContain('Icon name="book-open-check"') expect(selector).not.toContain('Icon name="warning"') diff --git a/packages/kilo-vscode/tests/unit/model-selector-utils.test.ts b/packages/kilo-vscode/tests/unit/model-selector-utils.test.ts index 4483a55488..8fbbd5e51c 100644 --- a/packages/kilo-vscode/tests/unit/model-selector-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/model-selector-utils.test.ts @@ -8,6 +8,7 @@ import { PROVIDER_ORDER, freeDataLabel, isDataCollectedModel, + hasByok, isFree, } from "../../webview-ui/src/components/shared/model-selector-utils" @@ -119,6 +120,14 @@ describe("isDataCollectedModel", () => { }) }) +describe("hasByok", () => { + it("uses only explicit user BYOK metadata", () => { + expect(hasByok({ hasUserByokAvailable: true })).toBe(true) + expect(hasByok({ hasUserByokAvailable: false })).toBe(false) + expect(hasByok({})).toBe(false) + }) +}) + describe("buildTriggerLabel", () => { it("returns resolved model name for non-kilo provider unchanged", () => { expect(buildTriggerLabel("GPT-4o", "openai", undefined, null, false, "", true, labels)).toBe("GPT-4o") diff --git a/packages/kilo-vscode/webview-ui/agent-manager/MultiModelSelector.tsx b/packages/kilo-vscode/webview-ui/agent-manager/MultiModelSelector.tsx index b9d3d0aa86..879adfc8b8 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/MultiModelSelector.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/MultiModelSelector.tsx @@ -7,6 +7,7 @@ import { useLanguage } from "../src/context/language" import { KILO_GATEWAY_ID, freeDataLabel, + hasByok, isDataCollectedModel, providerSortKey, } from "../src/components/shared/model-selector-utils" @@ -115,9 +116,14 @@ export const MultiModelSelector: Component<{ } /> {model.name} - + - {freeLabel()} + + {freeLabel()} + + + BYOK + diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index 995e0443cf..615a2c4b82 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -2891,18 +2891,27 @@ body.am-wt-dragging-active * { white-space: nowrap; } -.am-mm-free-badge { +.am-mm-free-badge, +.am-mm-byok-badge { display: inline-flex; align-items: center; padding: 1px 4px; border-radius: var(--radius-sm); - color: #fff; - background: var(--vscode-testing-iconPassed, #4caf50); font-size: var(--kilo-font-size-10); overflow: hidden; text-overflow: ellipsis; } +.am-mm-free-badge { + color: #fff; + background: var(--vscode-testing-iconPassed, #4caf50); +} + +.am-mm-byok-badge { + color: var(--vscode-badge-foreground); + background: var(--vscode-badge-background); +} + .am-mm-free-data-icon { display: inline-flex; align-items: center; diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/ModelPreview.tsx b/packages/kilo-vscode/webview-ui/src/components/shared/ModelPreview.tsx index ae3589f86d..cd3a5580ef 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/ModelPreview.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/shared/ModelPreview.tsx @@ -5,7 +5,7 @@ import { Markdown } from "@kilocode/kilo-ui/markdown" import { Icon } from "@kilocode/kilo-ui/icon" import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { useLanguage } from "../../context/language" -import { freeDataLabel, isDataCollectedModel, sanitizeName } from "./model-selector-utils" +import { freeDataLabel, hasByok, isDataCollectedModel, sanitizeName } from "./model-selector-utils" import { avgPrice, fmtAttemptCost, fmtCachedPrice, fmtPrice, fmtTerminalBenchScore } from "./model-preview-utils" interface Props { @@ -91,11 +91,14 @@ export const ModelPreview: Component = (props) => { })()} - + {freeLabel()} + + BYOK + diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx b/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx index df7ff77f08..2cd3059662 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx @@ -38,6 +38,7 @@ import { providerSortKey, isFree, isDataCollectedModel, + hasByok, freeDataLabel, buildTriggerLabel, sanitizeName, @@ -936,13 +937,18 @@ export const ModelSelectorBase: Component = (props) => { ) })()} - + {freeLabel()} + + + BYOK + + diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts b/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts index f5a183f184..5c75b79782 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/components/shared/model-selector-utils.ts @@ -26,6 +26,10 @@ export function isDataCollectedModel(model: Pick): boolean { + return model.hasUserByokAvailable === true +} + export function freeDataLabel(_free: string, data: string): string { return data } diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/providers.ts b/packages/kilo-vscode/webview-ui/src/types/messages/providers.ts index 67c865be0b..9726f263f3 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/providers.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/providers.ts @@ -19,6 +19,7 @@ export interface ProviderModel { recommendedIndex?: number isFree?: boolean mayTrainOnYourPrompts?: boolean + hasUserByokAvailable?: boolean terminalBench?: { overallScore: number avgAttemptCostUsd: number diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx index 2a3378ed96..b64c8008be 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx @@ -64,9 +64,12 @@ export function DialogModel(props: { providerID?: string }) { }) const footer = (providerID: string, model: Model) => { - if (providerID === "kilo" && FreeModelDisclosure.collectsData(model)) return FreeModelDisclosure.label - if (model.cost?.input === 0 && providerID === "opencode") return "Free" - return undefined + const labels = [ + providerID === "kilo" && FreeModelDisclosure.hasByok(model) ? FreeModelDisclosure.byok : undefined, + providerID === "kilo" && FreeModelDisclosure.collectsData(model) ? FreeModelDisclosure.label : undefined, + model.cost?.input === 0 && providerID === "opencode" ? "Free" : undefined, + ].filter((label) => label !== undefined) + return labels.length > 0 ? labels.join(" · ") : undefined } // kilocode_change end diff --git a/packages/opencode/src/kilocode/components/free-model-disclosure.ts b/packages/opencode/src/kilocode/components/free-model-disclosure.ts index f28ce09934..b9cb56ccb4 100644 --- a/packages/opencode/src/kilocode/components/free-model-disclosure.ts +++ b/packages/opencode/src/kilocode/components/free-model-disclosure.ts @@ -1,7 +1,11 @@ export const FreeModelDisclosure = { label: "May train", panel: "Data may be used for training", + byok: "BYOK", collectsData(model: { mayTrainOnYourPrompts?: boolean }): boolean { return model.mayTrainOnYourPrompts === true }, + hasByok(model: { hasUserByokAvailable?: boolean }): boolean { + return model.hasUserByokAvailable === true + }, } as const diff --git a/packages/opencode/src/kilocode/components/model-info-panel.tsx b/packages/opencode/src/kilocode/components/model-info-panel.tsx index e726d717ad..e7fa189349 100644 --- a/packages/opencode/src/kilocode/components/model-info-panel.tsx +++ b/packages/opencode/src/kilocode/components/model-info-panel.tsx @@ -75,6 +75,11 @@ export function ModelInfoPanel(props: Props) { {props.provider ?? m().providerID ?? ""} + + + {FreeModelDisclosure.byok} + + {FreeModelDisclosure.panel} diff --git a/packages/opencode/src/kilocode/provider/provider.ts b/packages/opencode/src/kilocode/provider/provider.ts index cb35d737ac..aeb8b6d449 100644 --- a/packages/opencode/src/kilocode/provider/provider.ts +++ b/packages/opencode/src/kilocode/provider/provider.ts @@ -36,6 +36,7 @@ export const KILO_MODEL_SCHEMA_EXTENSIONS = { prompt: Schema.optional(Schema.Literals(PROMPTS)), isFree: Schema.optional(Schema.Boolean), mayTrainOnYourPrompts: Schema.optional(Schema.Boolean), + hasUserByokAvailable: Schema.optional(Schema.Boolean), terminalBench: optionalOmitUndefined( Schema.Struct({ overallScore: Schema.Finite, @@ -56,6 +57,7 @@ export function patchModelsDevModel(providerID: string, source: any) { prompt: source.prompt, isFree: source.isFree, mayTrainOnYourPrompts: source.mayTrainOnYourPrompts, + hasUserByokAvailable: source.hasUserByokAvailable, terminalBench: source.terminalBench, ai_sdk_provider: source.ai_sdk_provider, options: source.options ?? {}, @@ -72,6 +74,7 @@ export function patchConfigModel(cfg: any, existing: any) { prompt: cfg.prompt ?? existing?.prompt, isFree: cfg.isFree ?? existing?.isFree, mayTrainOnYourPrompts: cfg.mayTrainOnYourPrompts ?? existing?.mayTrainOnYourPrompts, + hasUserByokAvailable: cfg.hasUserByokAvailable ?? existing?.hasUserByokAvailable, terminalBench: existing?.terminalBench, ai_sdk_provider: cfg.ai_sdk_provider ?? existing?.ai_sdk_provider, variants: cfg.variants diff --git a/packages/opencode/test/kilocode/free-model-disclosure.test.ts b/packages/opencode/test/kilocode/free-model-disclosure.test.ts index f1825c4148..7105ccdb6e 100644 --- a/packages/opencode/test/kilocode/free-model-disclosure.test.ts +++ b/packages/opencode/test/kilocode/free-model-disclosure.test.ts @@ -12,4 +12,11 @@ describe("FreeModelDisclosure", () => { expect(FreeModelDisclosure.collectsData({ mayTrainOnYourPrompts: false })).toBe(false) expect(FreeModelDisclosure.collectsData({})).toBe(false) }) + + test("uses only explicit user BYOK metadata", () => { + expect(FreeModelDisclosure.byok).toBe("BYOK") + expect(FreeModelDisclosure.hasByok({ hasUserByokAvailable: true })).toBe(true) + expect(FreeModelDisclosure.hasByok({ hasUserByokAvailable: false })).toBe(false) + expect(FreeModelDisclosure.hasByok({})).toBe(false) + }) }) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 9c6f7f898c..a28835533c 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1620,6 +1620,7 @@ export type Model = { prompt?: "codex" | "gemini" | "beast" | "anthropic" | "trinity" | "anthropic_without_todo" | "ling" | "gpt55" isFree?: boolean mayTrainOnYourPrompts?: boolean + hasUserByokAvailable?: boolean terminalBench?: { overallScore: number avgAttemptCostUsd: number diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 915815243b..19d6f49f1a 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -21282,6 +21282,9 @@ "mayTrainOnYourPrompts": { "type": "boolean" }, + "hasUserByokAvailable": { + "type": "boolean" + }, "terminalBench": { "type": "object", "properties": { From aa17a8a4191a04604b3a402e75d5c7e7b8149da4 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 18 Jun 2026 15:01:12 +0200 Subject: [PATCH 08/30] fix(vscode): keep diff identifiers intact --- .changeset/keep-diff-identifiers-intact.md | 5 +++++ packages/kilo-ui/src/pierre/index.test.ts | 6 +++--- packages/kilo-ui/src/pierre/index.ts | 4 +++- 3 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 .changeset/keep-diff-identifiers-intact.md diff --git a/.changeset/keep-diff-identifiers-intact.md b/.changeset/keep-diff-identifiers-intact.md new file mode 100644 index 0000000000..57c03dbff5 --- /dev/null +++ b/.changeset/keep-diff-identifiers-intact.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Keep changed identifiers intact when highlighting edits within diff lines. diff --git a/packages/kilo-ui/src/pierre/index.test.ts b/packages/kilo-ui/src/pierre/index.test.ts index 5f2df0e951..7bc73fd3a2 100644 --- a/packages/kilo-ui/src/pierre/index.test.ts +++ b/packages/kilo-ui/src/pierre/index.test.ts @@ -2,8 +2,8 @@ import { describe, expect, test } from "bun:test" import { createDefaultOptions } from "./index" describe("Pierre diff options", () => { - test("highlights changed characters in unified and split diffs", () => { - expect(createDefaultOptions("unified").lineDiffType).toBe("char") - expect(createDefaultOptions("split").lineDiffType).toBe("char") + test("keeps changed identifiers intact in unified and split diffs", () => { + expect(createDefaultOptions("unified").lineDiffType).toBe("word-alt") + expect(createDefaultOptions("split").lineDiffType).toBe("word-alt") }) }) diff --git a/packages/kilo-ui/src/pierre/index.ts b/packages/kilo-ui/src/pierre/index.ts index 1df03fb88d..7b4b13aabe 100644 --- a/packages/kilo-ui/src/pierre/index.ts +++ b/packages/kilo-ui/src/pierre/index.ts @@ -10,7 +10,9 @@ import { createDefaultOptions as defaults, styleVariables } from "@opencode-ai/u export { styleVariables } -export const LINE_DIFF_TYPE = "char" as const +// Character matching fragments inserted identifiers when they share letters with +// existing symbols. Word-alt keeps those logical additions visually intact. +export const LINE_DIFF_TYPE = "word-alt" as const // Pierre 1.1 treats its changed-line override properties as tint targets. Apply // Kilo semantic surfaces at the computed row level so host diff colors stay final. From f6a05fb3d41a257850c24f950c52af6485f743bf Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Thu, 18 Jun 2026 13:17:00 +0000 Subject: [PATCH 09/30] fix(jetbrains): distinguish BYOK model badges Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .../kilocode/client/session/ui/model/ModelPickerRenderer.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt index 8342eb5d6c..f670ae0ec7 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt @@ -76,8 +76,8 @@ internal class ModelPickerRenderer( } private val byok = FilledBadgeIcon( "BYOK", - ModelText.freeBg(), - JBColor.namedColor("Kilo.ModelPicker.freeBadgeForeground", JBColor.WHITE), + UiStyle.Colors.badgeBg(), + UiStyle.Colors.badgeFg(), ) private val byokLabel = BadgeLabel(byok).apply { border = JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap()) From f0b432e1dc2e5fae4900c9795e62b6ab61b794e1 Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:04:40 +0000 Subject: [PATCH 10/30] fix: prioritize BYOK model badges Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .../session/ui/model/ModelPickerRenderer.kt | 2 +- .../session/ui/model/ModelPickerTest.kt | 8 ++++-- .../unit/model-preview-data-line.test.ts | 26 +++++++++++++------ .../agent-manager/MultiModelSelector.tsx | 2 +- .../src/components/shared/ModelPreview.tsx | 2 +- .../src/components/shared/ModelSelector.tsx | 4 +-- .../webview-ui/src/styles/model-selector.css | 5 ++++ 7 files changed, 34 insertions(+), 15 deletions(-) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt index f670ae0ec7..e3e9797dd2 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/model/ModelPickerRenderer.kt @@ -160,7 +160,7 @@ internal class ModelPickerRenderer( title.append(name.model, SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, fg)) warn.isVisible = ModelText.collectsData(item) - badgeLabel.isVisible = item.free + badgeLabel.isVisible = item.free && !item.byok byokLabel.isVisible = item.byok provider.isVisible = value.favorite provider.text = item.providerName diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt index d4ca9801be..02d0d2fb3b 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/model/ModelPickerTest.kt @@ -397,8 +397,12 @@ class ModelPickerTest : BasePlatformTestCase() { assertEquals("Data may be used for training", renderer.warningTooltip()) } - fun `test renderer shows BYOK badge when available`() { - val row = ModelPickerRow(ModelPicker.Item("claude", "Claude", "kilo", "Kilo", byok = true), "Kilo", false) + fun `test renderer shows BYOK instead of free when both are available`() { + val row = ModelPickerRow( + ModelPicker.Item("claude", "Claude", "kilo", "Kilo", free = true, byok = true), + "Kilo", + false, + ) val model = CollectionListModel(listOf(row)) val renderer = ModelPickerRenderer(model, { null }, { emptySet() }) val list = JBList(model) diff --git a/packages/kilo-vscode/tests/unit/model-preview-data-line.test.ts b/packages/kilo-vscode/tests/unit/model-preview-data-line.test.ts index 22e370ffc5..529e8b5565 100644 --- a/packages/kilo-vscode/tests/unit/model-preview-data-line.test.ts +++ b/packages/kilo-vscode/tests/unit/model-preview-data-line.test.ts @@ -22,16 +22,13 @@ describe("model preview data collection line", () => { expect(styles).toContain(".model-preview-data-line") }) - it("renders prompt training independently from the free badge", () => { - expect(selector).toMatch( - /[\s\S]*?<\/Show>\s*/, - ) - expect(preview).toMatch( - /[\s\S]*?<\/Show>\s*/, - ) + it("renders prompt training independently from the model badges", () => { + expect(selector).toContain("isDataCollectedModel(model)") + expect(preview).toContain("isDataCollectedModel(model())") + expect(agent).toContain("isDataCollectedModel(model)") }) - it("renders BYOK availability independently from free and training metadata", () => { + it("renders BYOK availability independently from training metadata", () => { expect(selector).toContain("hasByok(model)") expect(preview).toContain("hasByok(model())") expect(agent).toContain("hasByok(model)") @@ -40,6 +37,19 @@ describe("model preview data collection line", () => { expect(agent).toContain(">BYOK") }) + it("shows BYOK instead of Free when both metadata fields are set", () => { + expect(selector).toContain("isFree(model) && !hasByok(model)") + expect(preview).toContain("model().isFree && !hasByok(model())") + expect(agent).toContain("model.isFree && !hasByok(model)") + }) + + it("uses neutral colors for the main model picker BYOK badge", () => { + expect(selector).toContain("model-selector-data-badge--byok") + expect(styles).toContain(".model-selector-data-badge--byok") + expect(styles).toContain("background: var(--vscode-badge-background) !important") + expect(styles).toContain("color: var(--vscode-badge-foreground) !important") + }) + it("uses the book open check icon for all webview model data disclosures", () => { expect(selector).toContain('Icon name="book-open-check"') expect(selector).not.toContain('Icon name="warning"') diff --git a/packages/kilo-vscode/webview-ui/agent-manager/MultiModelSelector.tsx b/packages/kilo-vscode/webview-ui/agent-manager/MultiModelSelector.tsx index 879adfc8b8..71b39fed3c 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/MultiModelSelector.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/MultiModelSelector.tsx @@ -118,7 +118,7 @@ export const MultiModelSelector: Component<{ {model.name} - + {freeLabel()} diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/ModelPreview.tsx b/packages/kilo-vscode/webview-ui/src/components/shared/ModelPreview.tsx index cd3a5580ef..485d3b8003 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/ModelPreview.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/shared/ModelPreview.tsx @@ -93,7 +93,7 @@ export const ModelPreview: Component = (props) => { - + {freeLabel()} diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx b/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx index 2cd3059662..e856d5c1f6 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx @@ -939,13 +939,13 @@ export const ModelSelectorBase: Component = (props) => { - + {freeLabel()} - + BYOK diff --git a/packages/kilo-vscode/webview-ui/src/styles/model-selector.css b/packages/kilo-vscode/webview-ui/src/styles/model-selector.css index 7140c77dd8..387d1bd395 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/model-selector.css +++ b/packages/kilo-vscode/webview-ui/src/styles/model-selector.css @@ -437,6 +437,11 @@ opacity: 0.8; } +.model-selector-data-badge--byok [data-component="tag"] { + background: var(--vscode-badge-background) !important; + color: var(--vscode-badge-foreground) !important; +} + .model-selector-trigger-free-data { display: inline-flex; align-items: center; From 4ff799f4ac479c6f81738212fb9c1b2a3780b7b0 Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:39:48 +0000 Subject: [PATCH 11/30] docs: clarify BYOK badge applicability in documentation Update documentation and changesets to specify that BYOK badges apply specifically to Kilo Gateway models and do not apply to models selected through other providers. Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .changeset/bright-badgers-bring-keys.md | 2 +- packages/kilo-docs/pages/getting-started/byok.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/bright-badgers-bring-keys.md b/.changeset/bright-badgers-bring-keys.md index 6da66c8b42..f7dd403a71 100644 --- a/.changeset/bright-badgers-bring-keys.md +++ b/.changeset/bright-badgers-bring-keys.md @@ -4,4 +4,4 @@ "kilo-code": minor --- -Show a BYOK badge for models that can use an enabled personal or organization provider key. +Show a BYOK badge for Kilo Gateway models that can use an enabled personal or organization provider key. diff --git a/packages/kilo-docs/pages/getting-started/byok.md b/packages/kilo-docs/pages/getting-started/byok.md index 47145c6e11..adb426172d 100644 --- a/packages/kilo-docs/pages/getting-started/byok.md +++ b/packages/kilo-docs/pages/getting-started/byok.md @@ -97,6 +97,6 @@ Your IAM user or role must have the following permissions: ## Using BYOK in the Extensions and CLI - BYOK works with the Kilo Gateway provider. Users should ensure that is set as the active [provider](/docs/ai-providers). -- Models that can use one of your enabled personal or organization BYOK providers display a `BYOK` badge in the model picker. +- Kilo Gateway models that can use one of your enabled personal or organization BYOK providers display a `BYOK` badge in the model picker. The badge does not apply to models selected through other providers. - Select a model with the `BYOK` badge, for example Claude Sonnet 4.5 if you configured BYOK for Anthropic, or GLM-4.7 if you configured the Z.ai Coding Plan. - (Optional) Validate with the provider that traffic is being served by that key. From c18baae9ca35a640cf58e8be75418188dc63e066 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:11:38 +0000 Subject: [PATCH 12/30] docs: add Auto Efficient tier to Auto Model documentation --- .../pages/code-with-ai/agents/auto-model.md | 12 +++++++++++- .../pages/code-with-ai/agents/model-selection.md | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/kilo-docs/pages/code-with-ai/agents/auto-model.md b/packages/kilo-docs/pages/code-with-ai/agents/auto-model.md index 189db05657..5d9fc1fcfd 100644 --- a/packages/kilo-docs/pages/code-with-ai/agents/auto-model.md +++ b/packages/kilo-docs/pages/code-with-ai/agents/auto-model.md @@ -11,6 +11,7 @@ Auto Model is a smart routing system that selects an underlying model for each r |---|---|---| | `kilo-auto/frontier` | Maximum capability with the best available models | Paid | | `kilo-auto/balanced` | Strong performance at a lower cost | Paid | +| `kilo-auto/efficient` | Lowest cost per task, with capability matched to difficulty | Paid | | `kilo-auto/free` | The best free models available | Free | ## How It Works @@ -31,8 +32,17 @@ The underlying models behind each Auto Model tier are updated server-side as bet - **Frontier** — Routes to the latest and most capable paid models. Uses different models for reasoning-heavy tasks (planning, architecture, debugging) versus implementation tasks (coding, building, exploring), pairing the right capability to each type of work. - **Balanced** — Routes to a cost-effective model for all modes. The specific model is selected based on the API interface in use, but does not vary by mode. A good default for most developers who want strong AI assistance without paying frontier prices. +- **Efficient** — Session-aware routing that classifies the difficulty of each request in real time and routes it to the cheapest model proven accurate enough for that task, based on Kilo's continuously-run benchmarks. Routine work stays lean while harder tasks get a more capable model. Because it watches your session in context, it keeps using a model across related turns and only switches when a cheaper option is clearly worth it. If a routing decision can't be made, it falls back to the Balanced tier, so quality never drops below Balanced. - **Free** — Routes to the best available free models on OpenRouter, splitting traffic across them. Because free model availability shifts over time as providers change promotional periods, the mapping is updated server-side — you always get the best free option without having to track what's currently available. Quality will be lower than paid tiers, and the models may change over time. +### How Auto Efficient routing works + +1. Kilo observes your coding session in context +2. It classifies each task by complexity +3. It routes to the benchmark-proven best model for that task automatically + +You get lean costs on routine work and stronger models when the work demands it — with no manual switching. + {% callout type="warning" title="Data handling for Auto Free" %} Auto Free may route your requests to providers that log prompts and outputs and use them to improve their services. Do not submit personal or confidential data when using Auto Free. In particular, it may route to NVIDIA's free endpoints. @@ -51,7 +61,7 @@ No need to manually switch models when changing modes. Auto Model handles routin ### Flexible Cost Control -Pick the tier that fits your budget. Frontier gives you the best models for demanding work; Balanced offers capable models at a fraction of the cost; Free costs nothing. +Pick the tier that fits your budget. Frontier gives you the best models for demanding work; Balanced offers capable models at a fraction of the cost; Efficient minimizes cost per task by matching model capability to task difficulty; Free costs nothing. ## Requirements diff --git a/packages/kilo-docs/pages/code-with-ai/agents/model-selection.md b/packages/kilo-docs/pages/code-with-ai/agents/model-selection.md index 710f38bfee..b187da64dc 100644 --- a/packages/kilo-docs/pages/code-with-ai/agents/model-selection.md +++ b/packages/kilo-docs/pages/code-with-ai/agents/model-selection.md @@ -60,7 +60,7 @@ While the specifics change constantly, some principles stay consistent: ## Free and Budget Model Picks -You don't need a paid API key to use Kilo Code productively. The fastest way to start for free is [Auto Model Free](/docs/code-with-ai/agents/auto-model) (`kilo-auto/free`), which routes to the best available free models automatically. See [Using Kilo for Free](/docs/getting-started/using-kilo-for-free) for the full zero-cost setup. +You don't need a paid API key to use Kilo Code productively. For the lowest cost on paid work, [Auto Efficient](/docs/code-with-ai/agents/auto-model#tiers) (`kilo-auto/efficient`) routes each request to the cheapest model proven accurate enough for that task. The fastest way to start for free is [Auto Model Free](/docs/code-with-ai/agents/auto-model) (`kilo-auto/free`), which routes to the best available free models automatically. See [Using Kilo for Free](/docs/getting-started/using-kilo-for-free) for the full zero-cost setup. If you prefer to pick models yourself, type `free` in the model picker to filter by free models, or browse the full list at [kilo.ai/models](https://kilo.ai/models). From 867beac3a19881861536d6c22a9efcb5ae379cc4 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 18 Jun 2026 17:30:39 +0200 Subject: [PATCH 13/30] fix(agent-manager): stabilize concurrent subagent rendering --- .../stabilize-agent-manager-transcripts.md | 5 ++ .../kilo-vscode/tests/accessibility.spec.ts | 60 +++++++++++++++++++ .../tests/unit/task-tool-identity.test.ts | 58 ++++++++++++++++++ .../agent-manager/agent-manager.css | 41 +++++++++++++ .../src/components/chat/ChatView.tsx | 1 + .../src/components/chat/MessageList.tsx | 29 ++++++++- .../src/components/chat/TaskToolExpanded.tsx | 14 ++--- .../webview-ui/src/context/session.tsx | 13 ++-- 8 files changed, 208 insertions(+), 13 deletions(-) create mode 100644 .changeset/stabilize-agent-manager-transcripts.md create mode 100644 packages/kilo-vscode/tests/unit/task-tool-identity.test.ts diff --git a/.changeset/stabilize-agent-manager-transcripts.md b/.changeset/stabilize-agent-manager-transcripts.md new file mode 100644 index 0000000000..be190dce0c --- /dev/null +++ b/.changeset/stabilize-agent-manager-transcripts.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Prevent concurrent subagent updates from blanking the Agent Manager webview. diff --git a/packages/kilo-vscode/tests/accessibility.spec.ts b/packages/kilo-vscode/tests/accessibility.spec.ts index 2f1565ab4d..fead3c81f3 100644 --- a/packages/kilo-vscode/tests/accessibility.spec.ts +++ b/packages/kilo-vscode/tests/accessibility.spec.ts @@ -49,6 +49,66 @@ test.describe("webview accessibility ratchet", () => { }) } + test("Agent Manager keeps virtualized transcript fragments laid out", async ({ page }) => { + await open(page, "agentmanager--sidebar-search-open") + + const visibility = await page.locator("#storybook-root").evaluate((root) => { + const layout = document.createElement("div") + layout.className = "am-layout" + root.append(layout) + const names = ["assistant-message", "tool-trigger", "file", "code", "diff"] + const values = names.map((name) => { + const node = document.createElement("div") + node.dataset.component = name + layout.append(node) + return getComputedStyle(node).contentVisibility + }) + layout.remove() + return values + }) + + expect(visibility).toEqual(["visible", "visible", "visible", "visible", "visible"]) + }) + + test("Agent Manager avoids generated separator text in updating tool rows", async ({ page }) => { + await open(page, "agentmanager--sidebar-search-open") + + const content = await page.locator("#storybook-root").evaluate((root) => { + const layout = document.createElement("div") + layout.className = "am-layout" + const wrapper = document.createElement("div") + wrapper.dataset.component = "tool-part-wrapper" + wrapper.dataset.partType = "tool" + const collapsible = document.createElement("div") + collapsible.className = "tool-collapsible" + collapsible.dataset.component = "collapsible" + const title = document.createElement("span") + title.dataset.slot = "basic-tool-tool-title" + const subtitle = document.createElement("span") + subtitle.dataset.slot = "basic-tool-tool-subtitle" + collapsible.append(title, subtitle) + wrapper.append(collapsible) + layout.append(wrapper) + root.append(layout) + const value = getComputedStyle(subtitle, "::before").content + layout.remove() + return value + }) + + expect(content).toBe("none") + }) + + test("sidebar keeps transcript announcements while Agent Manager bounds them", async ({ page }) => { + await open(page, "chat--chat-view-with-messages") + await expect(page.locator(".message-list")).toHaveAttribute("role", "log") + await expect(page.locator(".message-list")).toHaveAttribute("aria-live", "polite") + + await open(page, "chat--chat-view-agent-manager-completed") + await expect(page.locator(".message-list")).not.toHaveAttribute("role") + await expect(page.locator(".message-list")).not.toHaveAttribute("aria-live") + await expect(page.locator('.sr-only[role="status"]')).toHaveAttribute("aria-live", "polite") + }) + test("Profile login exposes a keyboard-operable named control", async ({ page }) => { await open(page, "profile--not-logged-in") diff --git a/packages/kilo-vscode/tests/unit/task-tool-identity.test.ts b/packages/kilo-vscode/tests/unit/task-tool-identity.test.ts new file mode 100644 index 0000000000..ae516186e7 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/task-tool-identity.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "bun:test" +import fs from "node:fs" +import path from "node:path" + +const ROOT = path.resolve(import.meta.dir, "../..") +const SESSION = path.join(ROOT, "webview-ui/src/context/session.tsx") +const TASK = path.join(ROOT, "webview-ui/src/components/chat/TaskToolExpanded.tsx") + +describe("task tool index identity", () => { + it("connects keyed reconciliation to indexed child rows", () => { + const session = fs.readFileSync(SESSION, "utf8") + const task = fs.readFileSync(TASK, "utf8") + expect(session).toContain('setStore("toolParts", sessionID, reconcile(tools, { key: "id" }))') + expect(task).toContain("") + }) + + it("preserves a streamed child tool proxy across status updates", () => { + const result = Bun.spawnSync( + [ + "bun", + "--conditions=browser", + "-e", + ` + import { createRoot } from "solid-js" + import { createStore, reconcile } from "solid-js/store" + import { upsertSessionToolPart } from "./webview-ui/src/context/session-utils.ts" + + const running = { + id: "tool-1", + type: "tool", + tool: "read", + state: { status: "running", input: { filePath: "src/app.ts" }, title: "Reading" }, + } + const completed = { + id: "tool-1", + type: "tool", + tool: "read", + state: { status: "completed", input: { filePath: "src/app.ts" }, title: "Read", output: "done" }, + } + + createRoot((dispose) => { + const [store, setStore] = createStore({ tools: [running] }) + const row = store.tools[0] + const tools = upsertSessionToolPart(store.tools, completed, { id: "message-1", sessionID: "child-1" }) + setStore("tools", reconcile(tools, { key: "id" })) + if (store.tools[0] !== row) throw new Error("tool proxy was replaced") + if (store.tools[0].state.status !== "completed") throw new Error("tool proxy was not updated") + dispose() + }) + `, + ], + { cwd: ROOT, stdout: "pipe", stderr: "pipe" }, + ) + + const output = result.stdout.toString() + result.stderr.toString() + expect(result.exitCode, output).toBe(0) + }) +}) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index 995e0443cf..0bdd63ddee 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -7,6 +7,47 @@ overflow: hidden; } +/* Virtua already removes off-screen transcript rows. Avoid a second layer of + layout skipping, which can leave Chromium accessibility traversing fragments + whose layout items were discarded while Agent Manager scrolls. */ +.am-layout + :is( + [data-component="assistant-message"], + [data-component="tool-trigger"], + [data-component="file"], + [data-component="code"], + [data-component="diff"] + ) { + content-visibility: visible; +} + +/* Generated separators become accessibility text nodes. Keep them out of + concurrently updating Agent Manager tool rows while preserving sidebar UI. */ +html[data-theme="kilo-vscode"] + .am-layout + [data-component="tool-part-wrapper"][data-part-type="tool"] + [data-component="collapsible"] + [data-slot="basic-tool-tool-title"] + + :is( + [data-slot="basic-tool-tool-subtitle"], + [data-slot="basic-tool-tool-arg"], + [data-slot="message-part-meta-line"], + [data-slot="webfetch-meta"], + [data-component="shell-submessage"] + )::before, +html[data-theme="kilo-vscode"] + .am-layout + [data-component="tool-part-wrapper"][data-part-type="tool"] + [data-component="collapsible"] + [data-slot="message-part-title-text"] + + :is([data-slot="message-part-meta-line"], [data-slot="basic-tool-tool-subtitle"])::before, +html[data-theme="kilo-vscode"] + .am-layout + [data-component="tool-part-wrapper"][data-part-type="tool"] + [data-slot="context-tool-group-summary"]::before { + content: none; +} + .am-sidebar { position: relative; min-width: 200px; diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx index ed2ce65d7b..d9e69e6528 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx @@ -332,6 +332,7 @@ export const ChatView: Component = (props) => { suggestions={standaloneSuggestions} readonly={props.readonly} emptyState={props.emptyState} + announce={isSidebar()} /> diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx index 19e8f81b96..6a80c9454a 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx @@ -63,6 +63,8 @@ interface MessageListProps { readonly?: boolean /** Optionally replace the standard welcome content while the conversation is empty. */ emptyState?: () => JSX.Element + /** Announce transcript changes as a live log. Disable for multi-session surfaces with concurrent streams. */ + announce?: boolean } export const MessageList: Component = (props) => { @@ -73,6 +75,19 @@ export const MessageList: Component = (props) => { const autoScroll = createAutoScroll({ working: () => session.status() !== "idle", }) + const [announcement, setAnnouncement] = createSignal("") + createEffect( + (prev: { sid?: string; working: boolean }) => { + const sid = session.currentSessionID() + const working = session.status() !== "idle" + if (working && (!prev.working || prev.sid !== sid)) setAnnouncement(language.t("session.status.working")) + if (!working && prev.working && prev.sid === sid) { + setAnnouncement(language.t("settings.agentBehaviour.editMode.save")) + } + return { sid, working } + }, + { sid: undefined, working: false }, + ) // Explicit output-producing actions resume auto-scroll before appending. const onResumeAutoScroll = () => autoScroll.resume() @@ -260,13 +275,25 @@ export const MessageList: Component = (props) => { return (
+ +
+ {announcement()} +
+
-
+
diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/TaskToolExpanded.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/TaskToolExpanded.tsx index 62162fda12..a05380b245 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/TaskToolExpanded.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/TaskToolExpanded.tsx @@ -7,7 +7,7 @@ * Call registerExpandedTaskTool() once at app startup to activate. */ -import { Component, createEffect, createMemo, createSignal, For, Show, onCleanup } from "solid-js" +import { Component, createEffect, createMemo, createSignal, Index, Show, onCleanup } from "solid-js" import { ToolRegistry, ToolProps, getToolInfo } from "@kilocode/kilo-ui/message-part" import { BasicTool, initialOpen } from "@kilocode/kilo-ui/basic-tool" import { Icon } from "@kilocode/kilo-ui/icon" @@ -141,15 +141,13 @@ const TaskToolRenderer: Component = (props) => {
{(text) => } - + {(item) => { - const info = createMemo(() => getToolInfo(item.tool, item.state?.input)) + const info = createMemo(() => getToolInfo(item().tool, item().state?.input)) const subtitle = createMemo(() => { if (info().subtitle) return info().subtitle - const state = item.state as { status: string; title?: string } - if (state.status === "completed" || state.status === "running") { - return state.title - } + const state = item().state as { status: string; title?: string } + if (state.status === "completed" || state.status === "running") return state.title return undefined }) return ( @@ -162,7 +160,7 @@ const TaskToolRenderer: Component = (props) => {
) }} - +
diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index 01788f5d66..b4e6de2a8f 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -1290,12 +1290,16 @@ export const SessionProvider: ParentComponent = (props) => { return true } + function setTools(sessionID: string, tools: ToolPart[]) { + setStore("toolParts", sessionID, reconcile(tools, { key: "id" })) + } + function rebuildToolParts(sessionID: string, messages: Message[], parts?: Record) { const tools = buildSessionToolParts( messages, (msg) => parts?.[msg.id] ?? store.parts[msg.id] ?? stash.peek(msg.id) ?? msg.parts, ) - setStore("toolParts", sessionID, tools) + setTools(sessionID, tools) } function messageParts(messages: Message[]): Record { @@ -1310,16 +1314,17 @@ export const SessionProvider: ParentComponent = (props) => { const sid = sessionID ?? part.sessionID if (!sid) return if (part.type !== "tool") return - setStore("toolParts", sid, (tools = []) => upsertSessionToolPart(tools, part, { id: messageID, sessionID: sid })) + const tools = upsertSessionToolPart(store.toolParts[sid] ?? [], part, { id: messageID, sessionID: sid }) + setTools(sid, tools) } function dropToolPart(sessionID: string | undefined, partID: string) { if (!sessionID) return - setStore("toolParts", sessionID, (tools = []) => removeSessionToolPart(tools, partID)) + setTools(sessionID, removeSessionToolPart(store.toolParts[sessionID] ?? [], partID)) } function dropMessageTools(sessionID: string, messageID: string) { - setStore("toolParts", sessionID, (tools = []) => removeSessionToolPartsForMessage(tools, messageID)) + setTools(sessionID, removeSessionToolPartsForMessage(store.toolParts[sessionID] ?? [], messageID)) } function handleMessagesLoaded( From 8e622255d5428030842857abd7d723ef47340a62 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 18 Jun 2026 18:31:47 +0200 Subject: [PATCH 14/30] test(agent-manager): verify rendered tool identity --- .../tests/unit/task-tool-identity.test.ts | 62 ++++++++++++------- .../webview-ui/src/context/session-utils.ts | 5 ++ .../webview-ui/src/context/session.tsx | 3 +- 3 files changed, 45 insertions(+), 25 deletions(-) diff --git a/packages/kilo-vscode/tests/unit/task-tool-identity.test.ts b/packages/kilo-vscode/tests/unit/task-tool-identity.test.ts index ae516186e7..acb6ccafe6 100644 --- a/packages/kilo-vscode/tests/unit/task-tool-identity.test.ts +++ b/packages/kilo-vscode/tests/unit/task-tool-identity.test.ts @@ -1,29 +1,29 @@ import { describe, expect, it } from "bun:test" -import fs from "node:fs" import path from "node:path" const ROOT = path.resolve(import.meta.dir, "../..") -const SESSION = path.join(ROOT, "webview-ui/src/context/session.tsx") -const TASK = path.join(ROOT, "webview-ui/src/components/chat/TaskToolExpanded.tsx") describe("task tool index identity", () => { - it("connects keyed reconciliation to indexed child rows", () => { - const session = fs.readFileSync(SESSION, "utf8") - const task = fs.readFileSync(TASK, "utf8") - expect(session).toContain('setStore("toolParts", sessionID, reconcile(tools, { key: "id" }))') - expect(task).toContain("") - }) - - it("preserves a streamed child tool proxy across status updates", () => { + it("preserves a rendered child tool row across status updates", () => { const result = Bun.spawnSync( [ "bun", "--conditions=browser", "-e", ` - import { createRoot } from "solid-js" - import { createStore, reconcile } from "solid-js/store" - import { upsertSessionToolPart } from "./webview-ui/src/context/session-utils.ts" + import { Window } from "happy-dom" + + const window = new Window() + globalThis.window = window + globalThis.document = window.document + globalThis.Node = window.Node + + const { Index, createComponent, createRenderEffect } = await import("solid-js") + const { createStore } = await import("solid-js/store") + const { render } = await import("solid-js/web") + const { reconcileSessionToolParts, upsertSessionToolPart } = await import( + "./webview-ui/src/context/session-utils.ts" + ) const running = { id: "tool-1", @@ -37,16 +37,30 @@ describe("task tool index identity", () => { tool: "read", state: { status: "completed", input: { filePath: "src/app.ts" }, title: "Read", output: "done" }, } - - createRoot((dispose) => { - const [store, setStore] = createStore({ tools: [running] }) - const row = store.tools[0] - const tools = upsertSessionToolPart(store.tools, completed, { id: "message-1", sessionID: "child-1" }) - setStore("tools", reconcile(tools, { key: "id" })) - if (store.tools[0] !== row) throw new Error("tool proxy was replaced") - if (store.tools[0].state.status !== "completed") throw new Error("tool proxy was not updated") - dispose() - }) + const root = document.createElement("div") + const [store, setStore] = createStore({ tools: [running] }) + const dispose = render( + () => + createComponent(Index, { + get each() { + return store.tools + }, + children: (item) => { + const row = document.createElement("div") + createRenderEffect(() => { + row.textContent = item().state.status + }) + return row + }, + }), + root, + ) + const row = root.firstElementChild + const tools = upsertSessionToolPart(store.tools, completed, { id: "message-1", sessionID: "child-1" }) + setStore("tools", reconcileSessionToolParts(tools)) + if (root.firstElementChild !== row) throw new Error("rendered tool row was replaced") + if (row?.textContent !== "completed") throw new Error("rendered tool row was not updated") + dispose() `, ], { cwd: ROOT, stdout: "pipe", stderr: "pipe" }, diff --git a/packages/kilo-vscode/webview-ui/src/context/session-utils.ts b/packages/kilo-vscode/webview-ui/src/context/session-utils.ts index 9e7798119a..ae448a3261 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/context/session-utils.ts @@ -1,3 +1,4 @@ +import { reconcile } from "solid-js/store" import type { Message, Part, ToolPart } from "../types/messages" export const SNAPSHOT_PROGRESS_TEXT = "Initializing snapshot..." @@ -85,6 +86,10 @@ export function buildSessionToolParts( return tools } +export function reconcileSessionToolParts(tools: ToolPart[]) { + return reconcile(tools, { key: "id" }) +} + export function upsertSessionToolPart( current: ToolPart[], part: Part, diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index b4e6de2a8f..6360afb01a 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -57,6 +57,7 @@ import { buildCostBreakdown, buildSessionToolParts, childID, + reconcileSessionToolParts, removeSessionToolPart, removeSessionToolPartsForMessage, upsertSessionToolPart, @@ -1291,7 +1292,7 @@ export const SessionProvider: ParentComponent = (props) => { } function setTools(sessionID: string, tools: ToolPart[]) { - setStore("toolParts", sessionID, reconcile(tools, { key: "id" })) + setStore("toolParts", sessionID, reconcileSessionToolParts(tools)) } function rebuildToolParts(sessionID: string, messages: Message[], parts?: Record) { From 9d2c8965ec2528a0f576473a30789b66cbf9c075 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 18 Jun 2026 18:37:59 +0200 Subject: [PATCH 15/30] test(agent-manager): remove implicit DOM dependency --- .../tests/unit/task-tool-identity.test.ts | 55 ++++++------------- 1 file changed, 17 insertions(+), 38 deletions(-) diff --git a/packages/kilo-vscode/tests/unit/task-tool-identity.test.ts b/packages/kilo-vscode/tests/unit/task-tool-identity.test.ts index acb6ccafe6..2526322cd5 100644 --- a/packages/kilo-vscode/tests/unit/task-tool-identity.test.ts +++ b/packages/kilo-vscode/tests/unit/task-tool-identity.test.ts @@ -4,26 +4,19 @@ import path from "node:path" const ROOT = path.resolve(import.meta.dir, "../..") describe("task tool index identity", () => { - it("preserves a rendered child tool row across status updates", () => { + it("preserves a child tool proxy across status updates", () => { const result = Bun.spawnSync( [ "bun", "--conditions=browser", "-e", ` - import { Window } from "happy-dom" - - const window = new Window() - globalThis.window = window - globalThis.document = window.document - globalThis.Node = window.Node - - const { Index, createComponent, createRenderEffect } = await import("solid-js") - const { createStore } = await import("solid-js/store") - const { render } = await import("solid-js/web") - const { reconcileSessionToolParts, upsertSessionToolPart } = await import( - "./webview-ui/src/context/session-utils.ts" - ) + import { createRoot } from "solid-js" + import { createStore } from "solid-js/store" + import { + reconcileSessionToolParts, + upsertSessionToolPart, + } from "./webview-ui/src/context/session-utils.ts" const running = { id: "tool-1", @@ -37,30 +30,16 @@ describe("task tool index identity", () => { tool: "read", state: { status: "completed", input: { filePath: "src/app.ts" }, title: "Read", output: "done" }, } - const root = document.createElement("div") - const [store, setStore] = createStore({ tools: [running] }) - const dispose = render( - () => - createComponent(Index, { - get each() { - return store.tools - }, - children: (item) => { - const row = document.createElement("div") - createRenderEffect(() => { - row.textContent = item().state.status - }) - return row - }, - }), - root, - ) - const row = root.firstElementChild - const tools = upsertSessionToolPart(store.tools, completed, { id: "message-1", sessionID: "child-1" }) - setStore("tools", reconcileSessionToolParts(tools)) - if (root.firstElementChild !== row) throw new Error("rendered tool row was replaced") - if (row?.textContent !== "completed") throw new Error("rendered tool row was not updated") - dispose() + + createRoot((dispose) => { + const [store, setStore] = createStore({ tools: [running] }) + const row = store.tools[0] + const tools = upsertSessionToolPart(store.tools, completed, { id: "message-1", sessionID: "child-1" }) + setStore("tools", reconcileSessionToolParts(tools)) + if (store.tools[0] !== row) throw new Error("tool proxy was replaced") + if (row.state.status !== "completed") throw new Error("tool proxy was not updated") + dispose() + }) `, ], { cwd: ROOT, stdout: "pipe", stderr: "pipe" }, From 2dcd90e1769d7823e3761e583746f904aa3b29e0 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 18 Jun 2026 18:47:04 +0200 Subject: [PATCH 16/30] test(agent-manager): cover rendered tool identity --- bun.lock | 11 +++- packages/kilo-vscode/package.json | 1 + .../tests/unit/task-tool-identity.test.ts | 55 +++++++++++++------ 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/bun.lock b/bun.lock index 9695753def..78bafbc5e6 100644 --- a/bun.lock +++ b/bun.lock @@ -354,6 +354,7 @@ "esbuild-plugin-solid": "^0.6.0", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", + "happy-dom": "20.8.9", "knip": "5.85.0", "prettier": "3.6.2", "qrcode": "^1.5.4", @@ -2204,6 +2205,8 @@ "@types/vscode": ["@types/vscode@1.120.0", "", {}, "sha512-feaT4Rst+FkTch5zz/ZbNCxoIvo55YU80Be2kiL7OJcod4+CUYf2lUBPdIJzozNnSEMq1VRTGrWEcCGFB3fBmA=="], + "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], + "@types/which": ["@types/which@3.0.4", "", {}, "sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], @@ -2486,6 +2489,8 @@ "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "buffer-image-size": ["buffer-image-size@0.6.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="], + "bun-ffi-structs": ["bun-ffi-structs@0.2.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-N/ZWtyN0piZlrXQT7TO0V+q952orYqkfhXRXM1Hcbb+R3QSiBH4vLnib187Mrs1H7pWIYECAmPeapGYDOMCl+w=="], "bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="], @@ -3076,6 +3081,8 @@ "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="], + "happy-dom": ["happy-dom@20.10.3", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-Hjdiy8RziuCcn5z04QI/rlsNuQoG8P0xxjgvsSMpi89cvIXIOcucQtiHS1yHSShxoBcSCeYqAskINmTiy/mlfw=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], @@ -4270,7 +4277,7 @@ "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], - "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], @@ -4630,6 +4637,8 @@ "cheerio/undici": ["undici@7.27.2", "", {}, "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA=="], + "cheerio/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + "clean-stack/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "cli-truncate/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json index b934755d50..602dd43c24 100644 --- a/packages/kilo-vscode/package.json +++ b/packages/kilo-vscode/package.json @@ -1100,6 +1100,7 @@ "esbuild-plugin-solid": "^0.6.0", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", + "happy-dom": "20.8.9", "knip": "5.85.0", "prettier": "3.6.2", "qrcode": "^1.5.4", diff --git a/packages/kilo-vscode/tests/unit/task-tool-identity.test.ts b/packages/kilo-vscode/tests/unit/task-tool-identity.test.ts index 2526322cd5..acb6ccafe6 100644 --- a/packages/kilo-vscode/tests/unit/task-tool-identity.test.ts +++ b/packages/kilo-vscode/tests/unit/task-tool-identity.test.ts @@ -4,19 +4,26 @@ import path from "node:path" const ROOT = path.resolve(import.meta.dir, "../..") describe("task tool index identity", () => { - it("preserves a child tool proxy across status updates", () => { + it("preserves a rendered child tool row across status updates", () => { const result = Bun.spawnSync( [ "bun", "--conditions=browser", "-e", ` - import { createRoot } from "solid-js" - import { createStore } from "solid-js/store" - import { - reconcileSessionToolParts, - upsertSessionToolPart, - } from "./webview-ui/src/context/session-utils.ts" + import { Window } from "happy-dom" + + const window = new Window() + globalThis.window = window + globalThis.document = window.document + globalThis.Node = window.Node + + const { Index, createComponent, createRenderEffect } = await import("solid-js") + const { createStore } = await import("solid-js/store") + const { render } = await import("solid-js/web") + const { reconcileSessionToolParts, upsertSessionToolPart } = await import( + "./webview-ui/src/context/session-utils.ts" + ) const running = { id: "tool-1", @@ -30,16 +37,30 @@ describe("task tool index identity", () => { tool: "read", state: { status: "completed", input: { filePath: "src/app.ts" }, title: "Read", output: "done" }, } - - createRoot((dispose) => { - const [store, setStore] = createStore({ tools: [running] }) - const row = store.tools[0] - const tools = upsertSessionToolPart(store.tools, completed, { id: "message-1", sessionID: "child-1" }) - setStore("tools", reconcileSessionToolParts(tools)) - if (store.tools[0] !== row) throw new Error("tool proxy was replaced") - if (row.state.status !== "completed") throw new Error("tool proxy was not updated") - dispose() - }) + const root = document.createElement("div") + const [store, setStore] = createStore({ tools: [running] }) + const dispose = render( + () => + createComponent(Index, { + get each() { + return store.tools + }, + children: (item) => { + const row = document.createElement("div") + createRenderEffect(() => { + row.textContent = item().state.status + }) + return row + }, + }), + root, + ) + const row = root.firstElementChild + const tools = upsertSessionToolPart(store.tools, completed, { id: "message-1", sessionID: "child-1" }) + setStore("tools", reconcileSessionToolParts(tools)) + if (root.firstElementChild !== row) throw new Error("rendered tool row was replaced") + if (row?.textContent !== "completed") throw new Error("rendered tool row was not updated") + dispose() `, ], { cwd: ROOT, stdout: "pipe", stderr: "pipe" }, From b45dd9b3039dff0957817f22ec77d67ecba93ae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catriel=20M=C3=BCller?= Date: Thu, 18 Jun 2026 14:07:47 -0300 Subject: [PATCH 17/30] feat: add account profile to Kilo Console --- packages/kilo-console/src/client.ts | 44 ++ packages/kilo-console/src/index.tsx | 2 + .../src/routes/profile/LoginRoute.tsx | 244 +++++++++ .../src/routes/profile/ProfileRoute.tsx | 314 +++++++++++- .../src/routes/profile/profile-utils.test.ts | 44 ++ .../src/routes/profile/profile-utils.ts | 80 +++ .../kilo-console/src/routes/profile/server.ts | 71 +++ .../src/shared/navigation.test.ts | 1 + .../kilo-console/src/shared/navigation.ts | 2 +- packages/kilo-console/src/styles.css | 1 + packages/kilo-console/src/styles/profile.css | 480 ++++++++++++++++++ 11 files changed, 1278 insertions(+), 5 deletions(-) create mode 100644 packages/kilo-console/src/routes/profile/LoginRoute.tsx create mode 100644 packages/kilo-console/src/routes/profile/profile-utils.test.ts create mode 100644 packages/kilo-console/src/routes/profile/profile-utils.ts create mode 100644 packages/kilo-console/src/routes/profile/server.ts create mode 100644 packages/kilo-console/src/styles/profile.css diff --git a/packages/kilo-console/src/client.ts b/packages/kilo-console/src/client.ts index 076b5f4a88..b1e74cadc1 100644 --- a/packages/kilo-console/src/client.ts +++ b/packages/kilo-console/src/client.ts @@ -11,6 +11,7 @@ import type { GlobalHealthResponse, GlobalEvent, KiloEmbeddingModelCatalog, + KiloProfileResponse, LspStatusResponse, McpStatusResponse, Pty as PtyInfo, @@ -42,6 +43,8 @@ export type Query = { export type ProjectQuery = Pick +export type KiloProfileData = KiloProfileResponse + export type ProjectItem = KiloProject export type RecentProjectItem = ProjectItem & { sessions: number @@ -210,6 +213,11 @@ function model(input: unknown) { return { provider: input.slice(0, index), model: input.slice(index + 1) } } +function directory(input: ProjectQuery) { + const dir = value(input.dir) + return dir ? { directory: dir } : undefined +} + function title(input: string) { return input .split("_") @@ -407,6 +415,42 @@ export async function loadEmbeddingModels(input: Query): Promise { + const sdk = client(input) + const result = await sdk.kilo.profile(directory(input)) + return demand("Kilo profile", result) +} + +export async function setKiloOrganization(input: ProjectQuery, organizationId: string | null) { + const sdk = client(input) + const result = await sdk.kilo.organization.set({ ...directory(input), organizationId }) + demand("Switch Kilo account", result) + await sdk.global.dispose() +} + +export async function logoutKilo(input: ProjectQuery) { + const sdk = client(input) + const result = await sdk.auth.remove({ providerID: "kilo" }) + demand("Log out of Kilo", result) + await sdk.global.dispose() +} + +export async function startKiloLogin(input: ProjectQuery): Promise { + const sdk = client(input) + const result = await sdk.provider.oauth.authorize({ ...directory(input), providerID: "kilo", method: 0 }) + return demand("Start Kilo login", result) +} + +export async function completeKiloLogin(input: ProjectQuery, signal?: AbortSignal) { + const sdk = client(input) + const result = await sdk.provider.oauth.callback( + { ...directory(input), providerID: "kilo", method: 0 }, + signal ? { signal } : undefined, + ) + demand("Complete Kilo login", result) + await sdk.global.dispose() +} + export async function loadProjects(input: ProjectQuery): Promise { const sdk = client(input) const dir = value(input.dir) diff --git a/packages/kilo-console/src/index.tsx b/packages/kilo-console/src/index.tsx index 786f9c9cad..13036c76ff 100644 --- a/packages/kilo-console/src/index.tsx +++ b/packages/kilo-console/src/index.tsx @@ -6,6 +6,7 @@ import "./styles.css" import { ProjectConsoleRoute } from "./routes/projects/ProjectConsoleRoute" import { ProjectsRoute } from "./routes/projects/ProjectsRoute" import { ProfileRoute } from "./routes/profile/ProfileRoute" +import { LoginRoute } from "./routes/profile/LoginRoute" import { ConfigLayout } from "./layouts/ConfigLayout" import { configSections } from "./routes/config/sections" @@ -27,6 +28,7 @@ render( {routes()} + {routes()} diff --git a/packages/kilo-console/src/routes/profile/LoginRoute.tsx b/packages/kilo-console/src/routes/profile/LoginRoute.tsx new file mode 100644 index 0000000000..3fde3f6567 --- /dev/null +++ b/packages/kilo-console/src/routes/profile/LoginRoute.tsx @@ -0,0 +1,244 @@ +import { useLocation, useNavigate } from "@solidjs/router" +import { Button } from "@kilocode/kilo-web-ui/button" +import { Card } from "@kilocode/kilo-web-ui/card" +import { createEffect, createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js" +import { LoadingScreen } from "../../components/LoadingScreen" +import { completeKiloLogin, loadKiloProfile, startKiloLogin, type ProjectQuery } from "../../client" +import { errMsg } from "../../shared/utils" +import { markDisconnected, page, parseDeviceCode, safeReturn } from "./profile-utils" +import { useProfileServer } from "./server" + +type Status = "idle" | "initiating" | "pending" | "success" | "error" | "cancelled" + +type State = { + status: Status + code?: string + url?: string + expiresIn?: number + error?: string +} + +function time(input: number) { + const min = Math.floor(input / 60) + const sec = input % 60 + return `${min}:${sec.toString().padStart(2, "0")}` +} + +export function LoginRoute() { + const loc = useLocation() + const nav = useNavigate() + const params = createMemo(() => new URLSearchParams(loc.search)) + const server = useProfileServer(params) + const [auth, setAuth] = createSignal({ status: "idle" }) + const [left, setLeft] = createSignal(900) + const [attempt, setAttempt] = createSignal(0) + const [active, setActive] = createSignal() + const [copied, setCopied] = createSignal("") + const ret = () => safeReturn(params().get("return")) + const profile = () => page(params(), "/profile") + + function abort() { + active()?.abort() + setActive(undefined) + } + + function done() { + window.setTimeout(() => nav(ret(), { replace: true }), 650) + } + + function start(input: ProjectQuery | undefined = server.query()) { + if (!input) return + abort() + const ctl = new AbortController() + const rev = attempt() + 1 + setAttempt(rev) + setActive(ctl) + setAuth({ status: "initiating" }) + void startKiloLogin(input) + .then((info) => { + if (attempt() !== rev) return false + setAuth({ + status: "pending", + code: parseDeviceCode(info.instructions), + url: info.url, + expiresIn: 900, + }) + return completeKiloLogin(input, ctl.signal).then(() => true) + }) + .then((ok) => { + if (!ok || attempt() !== rev) return + setAuth({ status: "success" }) + void loadKiloProfile(input) + .then(() => markDisconnected(false)) + .catch(() => markDisconnected(false)) + .finally(done) + }) + .catch((err) => { + if (attempt() !== rev) return + setAuth({ status: "error", error: errMsg(err) }) + }) + .finally(() => { + if (active() === ctl) setActive(undefined) + }) + } + + function cancel() { + abort() + setAttempt((value) => value + 1) + setAuth({ status: "cancelled" }) + } + + function copy(label: string, value: string | undefined) { + if (!value) return + void navigator.clipboard + .writeText(value) + .then(() => { + setCopied(label) + window.setTimeout(() => { + if (copied() === label) setCopied("") + }, 1400) + }) + .catch((err) => setAuth({ status: "error", error: errMsg(err) })) + } + + function open() { + const url = auth().url + if (!url) return + window.open(url, "_blank", "noopener,noreferrer") + } + + createEffect(() => { + const current = server.query() + if (!current || auth().status !== "idle") return + start(current) + }) + + createEffect(() => { + const state = auth() + if (state.status !== "pending") return + setLeft(state.expiresIn ?? 900) + const timer = window.setInterval(() => setLeft((value) => Math.max(0, value - 1)), 1000) + onCleanup(() => window.clearInterval(timer)) + }) + + createEffect(() => { + if (auth().status === "success") server.remember() + }) + + onCleanup(() => { + abort() + setAttempt((value) => value + 1) + }) + + return ( +
+ +
+ ) +} diff --git a/packages/kilo-console/src/routes/profile/ProfileRoute.tsx b/packages/kilo-console/src/routes/profile/ProfileRoute.tsx index 90d20f8a16..71a74ac6aa 100644 --- a/packages/kilo-console/src/routes/profile/ProfileRoute.tsx +++ b/packages/kilo-console/src/routes/profile/ProfileRoute.tsx @@ -1,9 +1,315 @@ +import { A, useLocation, useNavigate } from "@solidjs/router" +import { Button } from "@kilocode/kilo-web-ui/button" +import { Card } from "@kilocode/kilo-web-ui/card" +import { createEffect, createMemo, createResource, createSignal, For, Show } from "solid-js" +import { LoadingScreen } from "../../components/LoadingScreen" +import { loadKiloProfile, logoutKilo, setKiloOrganization, type KiloProfileData, type ProjectQuery } from "../../client" +import { errMsg } from "../../shared/utils" +import { + authError, + cloud, + credits, + initials, + markDisconnected, + money, + page, + personal, + usage, + wasDisconnected, +} from "./profile-utils" +import { useProfileServer } from "./server" + +type Org = NonNullable[number] +type State = { kind: "connected"; data: KiloProfileData } | { kind: "disconnected" } + +async function load(input: ProjectQuery): Promise { + if (wasDisconnected()) return { kind: "disconnected" } + try { + const data = await loadKiloProfile(input) + markDisconnected(false) + return { kind: "connected", data } + } catch (err) { + if (authError(err)) { + markDisconnected(true) + return { kind: "disconnected" } + } + throw err + } +} + +function org(data: KiloProfileData | undefined) { + if (!data?.currentOrgId) return undefined + return data.profile.organizations?.find((item) => item.id === data.currentOrgId) +} + +function account(data: KiloProfileData | undefined) { + return org(data)?.name ?? "Personal Account" +} + +function role(input: Org) { + if (input.role === "owner") return "Owner" + if (input.role === "admin") return "Admin" + if (input.role === "billing_manager") return "Billing" + return "Member" +} + export function ProfileRoute() { + const loc = useLocation() + const nav = useNavigate() + const params = createMemo(() => new URLSearchParams(loc.search)) + const server = useProfileServer(params) + const [data, actions] = createResource(server.query, load) + const [saving, setSaving] = createSignal() + const [error, setError] = createSignal("") + const profile = createMemo(() => { + const state = data() + if (state?.kind === "connected") return state.data + return undefined + }) + const orgs = createMemo(() => profile()?.profile.organizations ?? []) + const active = createMemo(() => org(profile())) + const disconnected = createMemo(() => data()?.kind === "disconnected") + const scope = createMemo(() => { + if (profile()) return account(profile()) + if (data.loading) return "Loading..." + return "Not connected" + }) + const login = () => page(params(), "/kilo/login") + const overview = () => page(params(), "/profile") + const usageUrl = createMemo(() => usage(profile()?.currentOrgId)) + const creditsUrl = createMemo(() => credits(profile()?.currentOrgId)) + + createEffect(() => { + if (profile()) server.remember() + }) + + createEffect(() => { + const err = data.error + if (!err || authError(err)) return + server.recover() + }) + + function refresh() { + setError("") + void actions.refetch() + } + + function choose(id: string | null) { + const current = server.query() + if (!current) return + const next = id ?? personal + const selected = profile()?.currentOrgId ?? null + if (selected === id) return + setSaving(next) + setError("") + void setKiloOrganization(current, id) + .then(() => actions.refetch()) + .catch((err) => setError(errMsg(err))) + .finally(() => setSaving(undefined)) + } + + function logout() { + const current = server.query() + if (!current) return + setSaving("logout") + setError("") + void logoutKilo(current) + .then(() => { + markDisconnected(true) + actions.mutate({ kind: "disconnected" }) + nav(overview(), { replace: true }) + }) + .catch((err) => setError(errMsg(err))) + .finally(() => setSaving(undefined)) + } + return ( -
-

Profile

-

Profile Settings

-

Profile identity, account preferences, and workspace defaults will live here.

+
+ + + +
+
+ +
+
+

Kilo Account

+

Your Profile

+

Manage your Kilo identity, account context, credits, and billing shortcuts.

+
+
+ + + Open Dashboard + +
+
+
+ + + + + + + + + + + + +
+

Kilo Account

+

Connect your Kilo account

+

Sign in to view your credits, organizations, and account details in Kilo Console.

+
+ + Connect + +
+
+ + + + Kilo server not found + Start Kilo Console from a running Kilo server or pass a server URL with ?server=. + + + + + + Profile request failed + {errMsg(data.error)} + + + + + + Account update failed + {error()} + + + + + {(info) => ( +
+ + + + Remaining Credits + {money(info().balance?.balance)} +

+ Credits shown for {account(info())}. Switch accounts below to see organization balances when + available. +

+
+ + +
+
+ Organizations +

Your Accounts

+
+ {orgs().length === 1 ? "1 org" : `${orgs().length} orgs`} +
+
+ + + {(item) => ( + + )} + +
+
+
+ )} +
+
+
) } diff --git a/packages/kilo-console/src/routes/profile/profile-utils.test.ts b/packages/kilo-console/src/routes/profile/profile-utils.test.ts new file mode 100644 index 0000000000..95ed86a0e9 --- /dev/null +++ b/packages/kilo-console/src/routes/profile/profile-utils.test.ts @@ -0,0 +1,44 @@ +import { expect, test } from "bun:test" +import { authError, credits, initials, money, page, parseDeviceCode, safeReturn, usage } from "./profile-utils" + +test("parses device auth codes from instructions", () => { + expect(parseDeviceCode("Open https://app.kilo.ai/device-auth and enter code: ABCD-2345")).toBe("ABCD-2345") + expect(parseDeviceCode("Use ABCD-2345 to continue")).toBe("ABCD-2345") + expect(parseDeviceCode(undefined)).toBeUndefined() +}) + +test("detects Kilo auth failures", () => { + expect(authError(new Error("Kilo profile: Unauthorized"))).toBe(true) + expect(authError({ status: 401 })).toBe(true) + expect(authError(new Error("Kilo profile: temporary network failure"))).toBe(false) +}) + +test("formats account display helpers", () => { + expect(money(12.5)).toBe("$12.50") + expect(money(null)).toBe("Unknown") + expect(initials("Jane Developer", "jane@example.com")).toBe("JD") + expect(initials("", "solo@example.com")).toBe("SE") +}) + +test("keeps login returns internal", () => { + expect(safeReturn("/profile?server=http%3A%2F%2F127.0.0.1%3A4097")).toBe( + "/profile?server=http%3A%2F%2F127.0.0.1%3A4097", + ) + expect(safeReturn("https://app.kilo.ai/profile")).toBe("/profile") + expect(safeReturn("//app.kilo.ai/profile")).toBe("/profile") +}) + +test("builds local links while preserving server", () => { + const params = new URLSearchParams({ server: "http://127.0.0.1:4097", ignored: "1" }) + expect(page(params, "/kilo/login", { return: "/profile" })).toBe( + "/kilo/login?server=http%3A%2F%2F127.0.0.1%3A4097&return=%2Fprofile", + ) +}) + +test("builds account-aware cloud links", () => { + const id = "9d4b144c-0a2b-477b-973d-24fa02bebf13" + expect(usage(null)).toBe("https://app.kilo.ai/usage") + expect(usage(id)).toBe(`https://app.kilo.ai/organizations/${id}/usage-details`) + expect(credits(undefined)).toBe("https://app.kilo.ai/profile") + expect(credits(id)).toBe(`https://app.kilo.ai/organizations/${id}`) +}) diff --git a/packages/kilo-console/src/routes/profile/profile-utils.ts b/packages/kilo-console/src/routes/profile/profile-utils.ts new file mode 100644 index 0000000000..b61e6e0347 --- /dev/null +++ b/packages/kilo-console/src/routes/profile/profile-utils.ts @@ -0,0 +1,80 @@ +export const personal = "personal" + +let marked = false + +export function markDisconnected(input: boolean) { + marked = input +} + +export function wasDisconnected() { + return marked +} + +function text(input: unknown) { + if (input instanceof Error) return input.message + if (typeof input === "string") return input + if (input === undefined || input === null) return "" + return JSON.stringify(input) +} + +export function parseDeviceCode(input: string | undefined) { + if (!input) return undefined + const code = input.match(/code:\s*([A-Z0-9-]+)/i)?.[1] + if (code) return code.toUpperCase() + return input.match(/\b([A-Z0-9]{4}-[A-Z0-9]{4})\b/i)?.[1]?.toUpperCase() +} + +export function authError(input: unknown) { + const value = text(input).toLowerCase() + return value.includes("unauthorized") || value.includes("invalid token") || value.includes('status":401') +} + +export function money(input: number | null | undefined) { + if (typeof input !== "number" || !Number.isFinite(input)) return "Unknown" + return `$${input.toFixed(2)}` +} + +export function initials(name: string | undefined, email: string) { + const parts = (name?.trim() || email) + .split(/[\s._@-]+/) + .filter(Boolean) + .slice(0, 2) + const value = parts.map((part) => part[0] ?? "").join("") + return value.toUpperCase() || "KG" +} + +export function safeReturn(input: string | null | undefined) { + if (!input || !input.startsWith("/") || input.startsWith("//")) return "/profile" + try { + const url = new URL(input, "http://localhost") + if (url.origin !== "http://localhost") return "/profile" + return `${url.pathname}${url.search}${url.hash}` + } catch { + return "/profile" + } +} + +export function page(params: URLSearchParams, path: string, extra?: Record) { + const next = new URLSearchParams() + const server = params.get("server") + if (server) next.set("server", server) + for (const [key, value] of Object.entries(extra ?? {})) { + if (value) next.set(key, value) + } + const query = next.toString() + return `${path}${query ? `?${query}` : ""}` +} + +export function cloud(path = "/profile") { + return `https://app.kilo.ai${path}` +} + +export function usage(id: string | null | undefined) { + if (!id) return cloud("/usage") + return cloud(`/organizations/${encodeURIComponent(id)}/usage-details`) +} + +export function credits(id: string | null | undefined) { + if (!id) return cloud("/profile") + return cloud(`/organizations/${encodeURIComponent(id)}`) +} diff --git a/packages/kilo-console/src/routes/profile/server.ts b/packages/kilo-console/src/routes/profile/server.ts new file mode 100644 index 0000000000..82c748a618 --- /dev/null +++ b/packages/kilo-console/src/routes/profile/server.ts @@ -0,0 +1,71 @@ +import { createEffect, createMemo, createSignal } from "solid-js" +import { + discover, + forgetCached, + loadCached, + resolveServer, + saveCached, + type ProjectQuery, +} from "../../client" +import { clean } from "../../shared/utils" + +const ports = new Set(["3017", "3018"]) + +function shouldDiscover(input: URLSearchParams) { + if (input.get("server")) return false + return ports.has(window.location.port) +} + +function base(input: URLSearchParams) { + const param = input.get("server") + if (param) return param + const cached = shouldDiscover(input) ? loadCached() : "" + if (cached) return cached + if (shouldDiscover(input)) return "" + return window.location.origin +} + +export function useProfileServer(params: () => URLSearchParams) { + const [url, setUrl] = createSignal(base(params())) + const discoverable = () => shouldDiscover(params()) + const query = createMemo(() => { + const target = clean(url()) || base(params()) + if (!target) return undefined + return { url: target, dir: "" } + }) + + createEffect(() => { + const next = params().get("server") + if (next && next !== url()) setUrl(next) + }) + + createEffect(() => { + if (!discoverable()) return + void resolveServer().then((value) => { + if (!value) return + saveCached(value) + setUrl(value) + }) + }) + + function remember() { + const current = query() + if (!current || !discoverable()) return + saveCached(current.url) + } + + function recover() { + if (!discoverable()) return + const cached = loadCached() + if (!cached || cached !== url()) return + forgetCached() + setUrl("") + void discover().then((value) => { + if (!value) return + saveCached(value) + setUrl(value) + }) + } + + return { query, discoverable, remember, recover } +} diff --git a/packages/kilo-console/src/shared/navigation.test.ts b/packages/kilo-console/src/shared/navigation.test.ts index 01f36705c1..53f6682cea 100644 --- a/packages/kilo-console/src/shared/navigation.test.ts +++ b/packages/kilo-console/src/shared/navigation.test.ts @@ -11,6 +11,7 @@ test("classifies routes after stripping the console base", () => { expect(path("/console/projects/demo/settings/agents", "/console")).toBe("/project") expect(path("/console/settings/agents", "/console")).toBe("/settings") expect(path("/console/profile", "/console")).toBe("/profile") + expect(path("/console/kilo/login", "/console")).toBe("/profile") }) test("builds settings roots without preserving the console base", () => { diff --git a/packages/kilo-console/src/shared/navigation.ts b/packages/kilo-console/src/shared/navigation.ts index 3d1476eebe..195f491a0a 100644 --- a/packages/kilo-console/src/shared/navigation.ts +++ b/packages/kilo-console/src/shared/navigation.ts @@ -21,7 +21,7 @@ export function settings(input: string, prefix = base()) { export function path(input: string, prefix = base()): Path { const route = strip(input, prefix) - if (route === "/profile") return "/profile" + if (route === "/profile" || route.startsWith("/kilo/login")) return "/profile" if (route.startsWith("/settings") || route.startsWith("/config")) return "/settings" if (route.startsWith("/projects/")) return "/project" return "/projects" diff --git a/packages/kilo-console/src/styles.css b/packages/kilo-console/src/styles.css index 5ab4a61da3..bad09eb3ec 100644 --- a/packages/kilo-console/src/styles.css +++ b/packages/kilo-console/src/styles.css @@ -14,6 +14,7 @@ @import "./styles/agents-tools.css"; @import "./styles/projects.css"; @import "./styles/project-console.css"; +@import "./styles/profile.css"; @import "./styles/servers.css"; @import "./styles/sources.css"; @import "./styles/responsive.css"; diff --git a/packages/kilo-console/src/styles/profile.css b/packages/kilo-console/src/styles/profile.css new file mode 100644 index 0000000000..c5630cfa28 --- /dev/null +++ b/packages/kilo-console/src/styles/profile.css @@ -0,0 +1,480 @@ +.kilo-console .profile-page, +.kilo-console .profile-login-page { + display: grid; + gap: 1rem; + max-width: 64rem; + margin: 0 auto; +} + +.kilo-console .profile-login-page { + max-width: 34rem; +} + +.kilo-console .profile-shell.disconnected { + grid-template-columns: minmax(0, 1fr); +} + +.kilo-console .profile-header, +.kilo-console .profile-login-header { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 1rem; + align-items: start; + padding-bottom: 0.5rem; +} + +.kilo-console .profile-login-header { + grid-template-columns: 1fr; + text-align: center; +} + +.kilo-console .profile-header h1, +.kilo-console .profile-login-header h1 { + margin: 0; + color: var(--foreground); + font-size: 1.25rem; + font-weight: 600; + letter-spacing: -0.02em; +} + +.kilo-console .profile-header p:not(.eyebrow), +.kilo-console .profile-login-header p:not(.eyebrow) { + max-width: 42rem; + margin: 0.625rem 0 0; + color: var(--muted-foreground); + font-size: 0.8125rem; + line-height: 1.45; +} + +.kilo-console .profile-actions, +.kilo-console .profile-card-actions, +.kilo-console .profile-login-actions { + display: flex; + gap: 0.5rem; + align-items: center; + justify-content: flex-end; + flex-wrap: wrap; +} + +.kilo-console .profile-primary-link { + text-decoration: none; +} + +.kilo-console .profile-connect-card { + display: grid; + min-height: 18rem; + place-items: center; + align-content: center; + gap: 1.25rem; + text-align: center; +} + +.kilo-console .profile-connect-mark { + display: grid; + width: 3.5rem; + height: 3.5rem; + place-items: center; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: + linear-gradient(135deg, color-mix(in oklab, var(--primary) 32%, transparent), transparent), + color-mix(in oklab, var(--muted) 70%, transparent); + color: var(--foreground); + font-size: 1rem; + font-weight: 700; + letter-spacing: 0.08em; +} + +.kilo-console .profile-connect-copy { + display: grid; + max-width: 28rem; + gap: 0.625rem; +} + +.kilo-console .profile-connect-copy h1, +.kilo-console .profile-connect-copy p { + margin: 0; +} + +.kilo-console .profile-connect-copy h1 { + color: var(--foreground); + font-size: 1.125rem; + font-weight: 600; + letter-spacing: -0.02em; +} + +.kilo-console .profile-connect-copy p:not(.eyebrow) { + color: var(--muted-foreground); + font-size: 0.8125rem; + line-height: 1.5; +} + +.kilo-console .profile-link-button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 1.75rem; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--input-base); + color: var(--foreground); + font-size: 0.75rem; + font-weight: 500; + line-height: 1.45; + padding: 0.25rem 0.625rem; + text-decoration: none; + transition: + background 120ms ease, + border-color 120ms ease, + color 120ms ease; +} + +.kilo-console .profile-link-button:hover, +.kilo-console .profile-link-button:focus-visible { + border-color: var(--ring); + background: var(--muted); + color: var(--foreground); + outline: 0; +} + +.kilo-console .profile-banner { + margin-bottom: 0; +} + +.kilo-console .profile-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} + +.kilo-console .profile-card { + gap: 0.875rem; +} + +.kilo-console .profile-wide-card { + grid-column: 1 / -1; +} + +.kilo-console .profile-account-card, +.kilo-console .profile-balance-card { + min-height: 10rem; +} + +.kilo-console .profile-account-head { + display: flex; + gap: 0.875rem; + align-items: center; + min-width: 0; +} + +.kilo-console .profile-avatar, +.kilo-console .profile-org-icon { + display: grid; + flex: 0 0 auto; + place-items: center; + border: 1px solid var(--border); + background: + linear-gradient(135deg, color-mix(in oklab, var(--primary) 28%, transparent), transparent), + color-mix(in oklab, var(--muted) 70%, transparent); + color: var(--foreground); + font-weight: 700; + letter-spacing: 0.06em; +} + +.kilo-console .profile-avatar { + width: 3.25rem; + height: 3.25rem; + border-radius: var(--radius-lg); + font-size: 1rem; +} + +.kilo-console .profile-org-icon { + width: 2rem; + height: 2rem; + border-radius: var(--radius-sm); + font-size: 0.6875rem; +} + +.kilo-console .profile-account-text { + display: grid; + min-width: 0; + gap: 0.125rem; +} + +.kilo-console .profile-account-text strong, +.kilo-console .profile-card-head h2, +.kilo-console .profile-login-state strong, +.kilo-console .profile-login-step strong { + margin: 0; + color: var(--foreground); + font-size: 0.9375rem; + font-weight: 600; + line-height: 1.25; +} + +.kilo-console .profile-account-text span, +.kilo-console .profile-login-state p, +.kilo-console .profile-login-step > span, +.kilo-console .profile-login-waiting { + color: var(--muted-foreground); + font-size: 0.75rem; + line-height: 1.45; +} + +.kilo-console .profile-account-text span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.kilo-console .profile-meta-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 0.75rem; + align-items: center; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: color-mix(in oklab, var(--muted) 35%, transparent); + padding: 0.75rem; +} + +.kilo-console .profile-meta-row span, +.kilo-console .profile-card-label { + color: var(--muted-foreground); + font-size: 0.6875rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.kilo-console .profile-meta-row strong { + color: var(--foreground); + font-size: 0.75rem; + font-weight: 600; +} + +.kilo-console .profile-balance-card strong { + color: var(--foreground); + font-size: clamp(2rem, 6vw, 3.25rem); + font-weight: 700; + letter-spacing: -0.05em; + line-height: 0.95; +} + +.kilo-console .profile-balance-card p, +.kilo-console .profile-login-state p { + margin: 0; +} + +.kilo-console .profile-card-head { + display: flex; + gap: 1rem; + align-items: flex-start; + justify-content: space-between; +} + +.kilo-console .profile-pill { + display: inline-flex; + align-items: center; + min-height: 1.5rem; + border: 1px solid var(--border); + border-radius: 999px; + color: var(--muted-foreground); + font-size: 0.6875rem; + font-weight: 600; + padding: 0 0.625rem; + text-decoration: none; + text-transform: uppercase; +} + +.kilo-console .profile-org-list { + display: grid; + gap: 0.5rem; +} + +.kilo-console .profile-org-row { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + gap: 0.75rem; + align-items: center; + width: 100%; + min-height: 3.5rem; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--card); + color: var(--foreground); + cursor: pointer; + font: inherit; + padding: 0.75rem; + text-align: left; + transition: + background 120ms ease, + border-color 120ms ease; +} + +.kilo-console .profile-org-row:hover, +.kilo-console .profile-org-row:focus-visible, +.kilo-console .profile-org-row.active { + border-color: var(--ring); + background: color-mix(in oklab, var(--muted) 45%, transparent); + outline: 0; +} + +.kilo-console .profile-org-row:disabled { + cursor: not-allowed; + opacity: 0.65; +} + +.kilo-console .profile-org-body { + display: grid; + min-width: 0; + gap: 0.125rem; +} + +.kilo-console .profile-org-body strong { + overflow: hidden; + color: var(--foreground); + font-size: 0.8125rem; + font-weight: 600; + text-overflow: ellipsis; + white-space: nowrap; +} + +.kilo-console .profile-org-body span, +.kilo-console .profile-org-state { + color: var(--muted-foreground); + font-size: 0.6875rem; +} + +.kilo-console .profile-org-state { + font-weight: 600; +} + +.kilo-console .profile-login-card { + min-height: 18rem; +} + +.kilo-console .profile-login-state, +.kilo-console .profile-login-flow, +.kilo-console .profile-login-step { + display: grid; + gap: 0.875rem; +} + +.kilo-console .profile-login-state { + place-items: center; + min-height: 14rem; + text-align: center; +} + +.kilo-console .profile-login-state.error strong { + color: var(--destructive); +} + +.kilo-console .profile-login-state.success strong { + color: var(--success, var(--primary)); +} + +.kilo-console .profile-login-step { + border: 1px solid var(--border); + border-radius: var(--radius-lg); + background: color-mix(in oklab, var(--muted) 25%, transparent); + padding: 1rem; +} + +.kilo-console .profile-login-url { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 0.5rem; + align-items: center; +} + +.kilo-console .profile-login-url span { + overflow: hidden; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--input-base); + color: var(--foreground); + font-family: var(--font-family-mono, monospace); + font-size: 0.75rem; + padding: 0.5rem 0.625rem; + text-overflow: ellipsis; + white-space: nowrap; +} + +.kilo-console .profile-login-code { + display: grid; + gap: 0.25rem; + place-items: center; + border: 1px solid var(--ring); + border-radius: var(--radius-lg); + background: color-mix(in oklab, var(--primary) 12%, var(--input-base)); + color: var(--foreground); + cursor: pointer; + font-family: var(--font-family-mono, monospace); + font-size: 2rem; + font-weight: 700; + letter-spacing: 0.16em; + padding: 1rem; +} + +.kilo-console .profile-login-code small { + color: var(--muted-foreground); + font-family: var(--font-family-sans, system-ui, sans-serif); + font-size: 0.6875rem; + font-weight: 600; + letter-spacing: 0; +} + +.kilo-console .profile-login-waiting { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; +} + +.kilo-console .profile-spinner { + width: 1rem; + height: 1rem; + border: 2px solid color-mix(in oklab, var(--muted-foreground) 30%, transparent); + border-top-color: var(--primary); + border-radius: 999px; + animation: profile-spin 900ms linear infinite; +} + +@keyframes profile-spin { + to { + transform: rotate(360deg); + } +} + +@media (max-width: 760px) { + .kilo-console .profile-header, + .kilo-console .profile-grid { + grid-template-columns: 1fr; + } + + .kilo-console .profile-actions, + .kilo-console .profile-card-actions, + .kilo-console .profile-login-actions { + justify-content: flex-start; + } + + .kilo-console .profile-login-url { + grid-template-columns: 1fr; + } +} + +@media (max-width: 520px) { + .kilo-console .profile-org-row { + grid-template-columns: auto minmax(0, 1fr); + } + + .kilo-console .profile-org-state { + grid-column: 2; + } + + .kilo-console .profile-login-code { + font-size: 1.5rem; + } +} From d1bfd7dbee640c09ce67447bcfd9b5b63c8036af Mon Sep 17 00:00:00 2001 From: "kilo-maintainer[bot]" Date: Thu, 18 Jun 2026 17:50:34 +0000 Subject: [PATCH 18/30] release(jetbrains): v7.0.1-rc.12 --- packages/kilo-jetbrains/CHANGELOG.md | 25 +++++++++++++++++++++++ packages/kilo-jetbrains/gradle.properties | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/kilo-jetbrains/CHANGELOG.md b/packages/kilo-jetbrains/CHANGELOG.md index 777f414b95..008c225cc4 100644 --- a/packages/kilo-jetbrains/CHANGELOG.md +++ b/packages/kilo-jetbrains/CHANGELOG.md @@ -88,6 +88,31 @@ ## [Unreleased] +## [7.0.1-rc.12] - 2026-06-18 + +### Added +- feat(vscode): import sessions from Roo Code by @markijbema in https://github.com/Kilo-Org/kilocode/pull/11303 + +### Fixed +- fix(cli): handle string permission migration by @noobezlol in https://github.com/Kilo-Org/kilocode/pull/10788 +- fix(cli): restore source file when reverting a move across folders by @lambertjosh in https://github.com/Kilo-Org/kilocode/pull/9758 +- fix(cli): restore Kilo Console release build by @lambertjosh in https://github.com/Kilo-Org/kilocode/pull/11387 +- fix(vscode): prevent shell output from blanking webview by @marius-kilocode in https://github.com/Kilo-Org/kilocode/pull/11402 +- fix(cli): share session runtime with HTTP listeners by @marius-kilocode in https://github.com/Kilo-Org/kilocode/pull/11410 +- fix(vscode): stop moved active sessions by @marius-kilocode in https://github.com/Kilo-Org/kilocode/pull/11404 +- fix: deny Kilo Gateway data collection by @chrarnoldus in https://github.com/Kilo-Org/kilocode/pull/11412 + +### Changed +- release(jetbrains): v7.0.1-rc.11 by @kilo-maintainer[bot] in https://github.com/Kilo-Org/kilocode/pull/11382 +- Add top-level announcement notification by @kilo-code-bot[bot] in https://github.com/Kilo-Org/kilocode/pull/11379 +- ci(jetbrains): run inside pre-built container, drop manual setup steps by @markijbema in https://github.com/Kilo-Org/kilocode/pull/11342 +- test(vscode): stabilize visual regression snapshots by @markijbema in https://github.com/Kilo-Org/kilocode/pull/11401 +- docs(kilo-docs): remove Managed Indexing documentation by @marius-kilocode in https://github.com/Kilo-Org/kilocode/pull/11400 +- OpenCode v1.15.9 by @imanolmzd-svg in https://github.com/Kilo-Org/kilocode/pull/11356 +- Revert "Add top-level announcement notification" by @markijbema in https://github.com/Kilo-Org/kilocode/pull/11419 +- docs: add Auto Efficient tier to Auto Model documentation by @kilo-code-bot[bot] in https://github.com/Kilo-Org/kilocode/pull/11431 + + ## [7.0.1-rc.11] - 2026-06-17 ### Added diff --git a/packages/kilo-jetbrains/gradle.properties b/packages/kilo-jetbrains/gradle.properties index a1cdd86dc7..21af8c36d3 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-rc.11 +kilo.jetbrains.version=7.0.1-rc.12 org.gradle.configuration-cache=true org.gradle.caching=true org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=512m From 33fb3a2f284696d71436db151e35108c76d25045 Mon Sep 17 00:00:00 2001 From: Kirill Kalishev Date: Thu, 18 Jun 2026 14:04:26 -0400 Subject: [PATCH 19/30] docs(jetbrains): edit changelog for v7.0.1-rc.12 --- packages/kilo-jetbrains/CHANGELOG.md | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/packages/kilo-jetbrains/CHANGELOG.md b/packages/kilo-jetbrains/CHANGELOG.md index 008c225cc4..bad3c6fd29 100644 --- a/packages/kilo-jetbrains/CHANGELOG.md +++ b/packages/kilo-jetbrains/CHANGELOG.md @@ -91,27 +91,19 @@ ## [7.0.1-rc.12] - 2026-06-18 ### Added -- feat(vscode): import sessions from Roo Code by @markijbema in https://github.com/Kilo-Org/kilocode/pull/11303 + +- Provider settings management, including searchable provider lists, API-key configuration, OAuth provider login, provider enable/disable controls, disconnect actions, and shared provider metadata. +- Add copy controls to session messages so prompts and assistant responses can be copied directly from the transcript. +- Share codebase indexes across worktrees so Agent Manager and worktree sessions can use semantic search without duplicating the full index. ### Fixed -- fix(cli): handle string permission migration by @noobezlol in https://github.com/Kilo-Org/kilocode/pull/10788 -- fix(cli): restore source file when reverting a move across folders by @lambertjosh in https://github.com/Kilo-Org/kilocode/pull/9758 -- fix(cli): restore Kilo Console release build by @lambertjosh in https://github.com/Kilo-Org/kilocode/pull/11387 -- fix(vscode): prevent shell output from blanking webview by @marius-kilocode in https://github.com/Kilo-Org/kilocode/pull/11402 -- fix(cli): share session runtime with HTTP listeners by @marius-kilocode in https://github.com/Kilo-Org/kilocode/pull/11410 -- fix(vscode): stop moved active sessions by @marius-kilocode in https://github.com/Kilo-Org/kilocode/pull/11404 -- fix: deny Kilo Gateway data collection by @chrarnoldus in https://github.com/Kilo-Org/kilocode/pull/11412 + +- Keep long JetBrains prompt input usable by capping growth, preserving scrolling, and hiding soft-wrap glyphs. +- Copy actions correctly in session. ### Changed -- release(jetbrains): v7.0.1-rc.11 by @kilo-maintainer[bot] in https://github.com/Kilo-Org/kilocode/pull/11382 -- Add top-level announcement notification by @kilo-code-bot[bot] in https://github.com/Kilo-Org/kilocode/pull/11379 -- ci(jetbrains): run inside pre-built container, drop manual setup steps by @markijbema in https://github.com/Kilo-Org/kilocode/pull/11342 -- test(vscode): stabilize visual regression snapshots by @markijbema in https://github.com/Kilo-Org/kilocode/pull/11401 -- docs(kilo-docs): remove Managed Indexing documentation by @marius-kilocode in https://github.com/Kilo-Org/kilocode/pull/11400 -- OpenCode v1.15.9 by @imanolmzd-svg in https://github.com/Kilo-Org/kilocode/pull/11356 -- Revert "Add top-level announcement notification" by @markijbema in https://github.com/Kilo-Org/kilocode/pull/11419 -- docs: add Auto Efficient tier to Auto Model documentation by @kilo-code-bot[bot] in https://github.com/Kilo-Org/kilocode/pull/11431 +- Update the bundled CLI runtime to OpenCode 1.15.9 ## [7.0.1-rc.11] - 2026-06-17 From 5d7b1bf4a392cdbd37177fe11590ab7e54b93bb3 Mon Sep 17 00:00:00 2001 From: nyxst4ck <289980115+nyxst4ck@users.noreply.github.com> Date: Fri, 19 Jun 2026 02:43:43 -0300 Subject: [PATCH 20/30] fix(console): add context to settings fields --- packages/kilo-console/src/routes/config/AgentsRoute.tsx | 6 +++++- packages/kilo-console/src/routes/config/ModelsRoute.tsx | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/kilo-console/src/routes/config/AgentsRoute.tsx b/packages/kilo-console/src/routes/config/AgentsRoute.tsx index 36d740df0d..25299e7b10 100644 --- a/packages/kilo-console/src/routes/config/AgentsRoute.tsx +++ b/packages/kilo-console/src/routes/config/AgentsRoute.tsx @@ -265,7 +265,10 @@ export function AgentBuilderRoute() { onInput={(event) => state.setDesc(event.currentTarget.value)} /> - + diff --git a/packages/kilo-console/src/routes/config/ModelsRoute.tsx b/packages/kilo-console/src/routes/config/ModelsRoute.tsx index d87e93e914..2d5ed1230b 100644 --- a/packages/kilo-console/src/routes/config/ModelsRoute.tsx +++ b/packages/kilo-console/src/routes/config/ModelsRoute.tsx @@ -40,6 +40,7 @@ function chips(model: Model) { const list: string[] = [] if (model.capabilities.toolcall) list.push("toolcall") if (model.capabilities.attachment) list.push("attachment") + if (model.capabilities.temperature) list.push("temperature") if (model.capabilities.input.image) list.push("vision") if (model.capabilities.input.audio || model.capabilities.output.audio) list.push("audio") return list @@ -87,6 +88,7 @@ function Stat(props: { label: string; value: string; sub?: string; mono?: boolea const caps = [ { key: "toolcall", label: "toolcall" }, { key: "attachment", label: "attachment" }, + { key: "temperature", label: "temperature" }, { key: "input:image", label: "vision" }, { key: "input:audio", label: "audio" }, ] satisfies { key: Capability; label: string }[] @@ -476,6 +478,7 @@ export function ModelsAvailableRoute() { +
From 732fbc30dc3ee969caabc96282ad1a0ff5c652ce Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 19 Jun 2026 11:03:50 +0200 Subject: [PATCH 21/30] fix(vscode): widen chat readable lane --- .changeset/widen-chat-lane-further.md | 5 +++++ packages/kilo-vscode/webview-ui/src/styles/chat-layout.css | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/widen-chat-lane-further.md diff --git a/.changeset/widen-chat-lane-further.md b/.changeset/widen-chat-lane-further.md new file mode 100644 index 0000000000..8e74036f20 --- /dev/null +++ b/.changeset/widen-chat-lane-further.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Widen the chat readable lane from 88ch to 98ch so conversations, tools, and diffs can use more editor space. 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 fbea166aff..dcb17a5a6a 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css +++ b/packages/kilo-vscode/webview-ui/src/styles/chat-layout.css @@ -18,7 +18,7 @@ min-height: 0; overflow: hidden; container: chat / inline-size; - --chat-readable-width: 88ch; + --chat-readable-width: 98ch; --chat-gutter: clamp(16px, 4cqi, 48px); --chat-scrollbar-width: 10px; } From d319acac1099013c22e630a5bb8c054ccb4ddc91 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 19 Jun 2026 09:07:06 +0000 Subject: [PATCH 22/30] chore: update kilo-vscode visual regression baselines --- .../agentmanager/readable-chat-1280-chromium-linux.png | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/readable-chat-1280-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/readable-chat-1280-chromium-linux.png index 728ac2a184..a4234ebfc9 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/readable-chat-1280-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/agentmanager/readable-chat-1280-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e6ecf78a0bcfaebdb945c3caacb8e32155fe2f6e4017f766e129dd0d17ad8fab -size 51349 +oid sha256:270d3ecab98c4462a0e6af3e8fe43b3313da03759c507914d0b1048045474f1e +size 50886 From 6bd1e79c588bd7614f5bf2463f5172fd14135451 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 19 Jun 2026 11:23:35 +0200 Subject: [PATCH 23/30] fix(vscode): preserve panel command outcomes --- .../agent-manager/SessionTerminalManager.ts | 8 +-- .../unit/session-terminal-manager.test.ts | 63 +++++++++++++++++-- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts b/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts index 0af740f994..dbe7aa4867 100644 --- a/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts +++ b/packages/kilo-vscode/src/agent-manager/SessionTerminalManager.ts @@ -220,10 +220,10 @@ export class SessionTerminalManager { return await this.host.executeCommand(id, ...args) } finally { const handler = this.commandHandlers.get(id) - if (!handler) return - const replacement = this.tryRegisterCommand(id, handler) - if (!replacement) return - this.commandDisposables.set(id, replacement) + if (handler) { + const replacement = this.tryRegisterCommand(id, handler) + if (replacement) this.commandDisposables.set(id, replacement) + } } } diff --git a/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts b/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts index c7a7f29197..3025d3b0cb 100644 --- a/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts +++ b/packages/kilo-vscode/tests/unit/session-terminal-manager.test.ts @@ -1,18 +1,53 @@ /** * SessionTerminalManager tests. * - * The class is tightly coupled to VS Code terminal APIs. We use ts-morph - * static analysis to verify structural invariants that protect against - * real regressions — focusing on ordering constraints and cleanup logic - * that are easy to break during refactoring. + * Structural tests use ts-morph to protect ordering and cleanup invariants. + * Command behavior tests exercise the narrow TerminalHost interface directly. */ import { describe, it, expect } from "bun:test" import path from "node:path" import { Project, SyntaxKind } from "ts-morph" +import { SessionTerminalManager, type TerminalHost } from "../../src/agent-manager/SessionTerminalManager" const ROOT = path.resolve(import.meta.dir, "../..") const FILE = path.join(ROOT, "src/agent-manager/SessionTerminalManager.ts") +const COMMAND = "workbench.action.togglePanel" + +type Handler = (...args: unknown[]) => Promise + +function runtime(run: () => Promise) { + let blocked = false + const handlers = new Map() + const host: TerminalHost = { + createTerminal() { + throw new Error("not used") + }, + activeTerminal: () => undefined, + repoPath: () => undefined, + showWarning() {}, + setContext() {}, + onTerminalClosed: () => ({ dispose() {} }), + onActiveTerminalChanged: () => ({ dispose() {} }), + registerCommand(id, handler) { + if (blocked && id === COMMAND) throw new Error(`command '${id}' already exists`) + handlers.set(id, handler) + return { + dispose() { + if (handlers.get(id) === handler) handlers.delete(id) + }, + } + }, + executeCommand() { + blocked = true + return run() + }, + } + const manager = new SessionTerminalManager(() => {}, host) + const handler = handlers.get(COMMAND) + if (!handler) throw new Error(`command '${COMMAND}' was not registered`) + return { manager, handler } +} function getClass() { const project = new Project({ compilerOptions: { allowJs: true } }) @@ -100,3 +135,23 @@ describe("SessionTerminalManager structure", () => { expect(text).toContain("this.host.activeTerminal()") }) }) + +describe("SessionTerminalManager command restoration", () => { + it("preserves the original command result when re-registration fails", async () => { + const expected = { status: "complete" } + const state = runtime(async () => expected) + + expect(await state.handler()).toBe(expected) + state.manager.dispose() + }) + + it("preserves the original command error when re-registration fails", async () => { + const expected = new Error("panel command failed") + const state = runtime(async () => { + throw expected + }) + + await expect(state.handler()).rejects.toBe(expected) + state.manager.dispose() + }) +}) From f7e68d19d9d8b23b087d3c7c92d487abced8d7ec Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 19 Jun 2026 11:41:57 +0200 Subject: [PATCH 24/30] fix: silence subagent completion sounds --- .changeset/quiet-subagent-finishes.md | 7 + .../src/services/attention/service.ts | 25 ++-- .../kilo-vscode/tests/unit/attention.test.ts | 36 +++-- .../feature-plugins/system/notifications.ts | 4 +- .../opencode/src/kilocode/session/index.ts | 1 + packages/opencode/src/session/prompt.ts | 2 + .../test/cli/cmd/tui/notifications.test.ts | 10 +- .../session/platform-attribution.test.ts | 65 ++++++++- packages/sdk/js/src/v2/gen/types.gen.ts | 1 + packages/sdk/openapi.json | 124 +++++++++--------- 10 files changed, 175 insertions(+), 100 deletions(-) create mode 100644 .changeset/quiet-subagent-finishes.md diff --git a/.changeset/quiet-subagent-finishes.md b/.changeset/quiet-subagent-finishes.md new file mode 100644 index 0000000000..0c0cc679ff --- /dev/null +++ b/.changeset/quiet-subagent-finishes.md @@ -0,0 +1,7 @@ +--- +"@kilocode/cli": patch +"@kilocode/sdk": patch +"kilo-code": patch +--- + +Limit completion sounds to parent agent sessions. diff --git a/packages/kilo-vscode/src/services/attention/service.ts b/packages/kilo-vscode/src/services/attention/service.ts index c3659933ce..19bbe7bd7d 100644 --- a/packages/kilo-vscode/src/services/attention/service.ts +++ b/packages/kilo-vscode/src/services/attention/service.ts @@ -20,7 +20,6 @@ export class AttentionService implements vscode.Disposable { private readonly errored = new Set() private readonly questions = new Set() private readonly permissions = new Set() - private readonly parents = new Map() private readonly unsubscribeEvent: () => void private readonly unsubscribeState: () => void @@ -35,7 +34,6 @@ export class AttentionService implements vscode.Disposable { this.unsubscribeEvent() this.unsubscribeState() this.reset() - this.parents.clear() } private handle(event: SSEPayload) { @@ -44,26 +42,20 @@ export class AttentionService implements vscode.Disposable { return this.question(event) } if (event.type === "permission.asked" || event.type === "permission.replied") return this.permission(event) + if (event.type === "session.deleted") return this.remove(event.properties.sessionID) if (event.type === "session.status") return this.status(event) if (event.type === "session.turn.close") return this.close(event) if (event.type === "session.error") return this.error(event) } private sync(event: Sync) { - if (event.name === "session.created.1") { - this.parents.set(event.data.sessionID, event.data.info.parentID) - return - } - if (event.name === "session.updated.1") { - if (event.data.info.parentID !== undefined) { - this.parents.set(event.data.sessionID, event.data.info.parentID ?? undefined) - } - return - } if (event.name !== "session.deleted.1") return - this.parents.delete(event.data.sessionID) - this.active.delete(event.data.sessionID) - this.errored.delete(event.data.sessionID) + this.remove(event.data.sessionID) + } + + private remove(sessionID: string) { + this.active.delete(sessionID) + this.errored.delete(sessionID) } private question(event: Question) { @@ -98,7 +90,8 @@ export class AttentionService implements vscode.Disposable { if (!this.active.delete(sessionID)) return if (this.errored.delete(sessionID)) return if (event.properties.reason !== "completed") return - this.notify(this.parents.get(sessionID) ? "subagent_done" : "done") + if (event.properties.parentID !== undefined) return + this.notify("done") } private error(event: Error) { diff --git a/packages/kilo-vscode/tests/unit/attention.test.ts b/packages/kilo-vscode/tests/unit/attention.test.ts index 9c0acd9479..b25eb2a92c 100644 --- a/packages/kilo-vscode/tests/unit/attention.test.ts +++ b/packages/kilo-vscode/tests/unit/attention.test.ts @@ -45,23 +45,20 @@ describe("AttentionService", () => { test.service.dispose() }) - it("uses the upstream subagent completion sound", () => { + it("plays completion sounds only for parent agents", () => { const test = setup() - test.event( - event({ - type: "sync", - name: "session.created.1", - data: { sessionID: "child", info: { parentID: "parent" } }, - }), - ) - test.event( - event({ type: "sync", name: "session.updated.1", data: { sessionID: "child", info: { title: "work" } } }), - ) test.event(event({ type: "session.status", properties: { sessionID: "child", status: { type: "retry" } } })) test.event(event({ type: "session.status", properties: { sessionID: "child", status: { type: "idle" } } })) - test.event(event({ type: "session.turn.close", properties: { sessionID: "child", reason: "completed" } })) + test.event( + event({ + type: "session.turn.close", + properties: { sessionID: "child", parentID: "parent", reason: "completed" }, + }), + ) + test.event(event({ type: "session.status", properties: { sessionID: "parent", status: { type: "busy" } } })) + test.event(event({ type: "session.turn.close", properties: { sessionID: "parent", reason: "completed" } })) - expect(test.sounds).toEqual(["subagent_done"]) + expect(test.sounds).toEqual(["done"]) test.service.dispose() }) @@ -123,6 +120,19 @@ describe("AttentionService", () => { expect(test.sounds).toEqual([]) test.service.dispose() }) + + it("clears transitions when a session is deleted", () => { + const test = setup() + test.event(event({ type: "session.status", properties: { sessionID: "s1", status: { type: "busy" } } })) + test.event(event({ type: "session.deleted", properties: { sessionID: "s1", info: { id: "s1" } } })) + test.event(event({ type: "session.turn.close", properties: { sessionID: "s1", reason: "completed" } })) + test.event(event({ type: "session.status", properties: { sessionID: "s2", status: { type: "busy" } } })) + test.event(event({ type: "sync", name: "session.deleted.1", data: { sessionID: "s2" } })) + test.event(event({ type: "session.turn.close", properties: { sessionID: "s2", reason: "completed" } })) + + expect(test.sounds).toEqual([]) + test.service.dispose() + }) }) describe("attention defaults", () => { diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/notifications.ts b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/notifications.ts index 11ce2b6bc3..9afaacaf4b 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/system/notifications.ts +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/system/notifications.ts @@ -69,8 +69,8 @@ const tui: TuiPlugin = async (api) => { if (!active.delete(sessionID)) return if (errored.delete(sessionID)) return if (event.properties.reason !== "completed") return - const session = api.state.session.get(sessionID) - notify(api, sessionID, "Session done", session?.parentID ? "subagent_done" : "done") + if (event.properties.parentID !== undefined) return + notify(api, sessionID, "Session done", "done") }) // kilocode_change end diff --git a/packages/opencode/src/kilocode/session/index.ts b/packages/opencode/src/kilocode/session/index.ts index 708592fe0d..19f824ff3f 100644 --- a/packages/opencode/src/kilocode/session/index.ts +++ b/packages/opencode/src/kilocode/session/index.ts @@ -39,6 +39,7 @@ export namespace KiloSession { "session.turn.close", Schema.Struct({ sessionID: SessionID, + parentID: Schema.optional(SessionID), reason: CloseReasonSchema, }), ), diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 648f707d45..b27814000d 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1763,6 +1763,7 @@ export const layer = Layer.effect( "SessionPrompt.loop", )(function* (input: LoopInput) { // kilocode_change start + const session = yield* sessions.get(input.sessionID) yield* KiloSessionPrompt.recoverDanglingAssistant({ sessionID: input.sessionID, status, sessions }) yield* KiloSessionPrompt.recoverProviderFinishError({ sessionID: input.sessionID, status, sessions }) yield* bus.publish(KiloSession.Event.TurnOpen, { sessionID: input.sessionID }) @@ -1775,6 +1776,7 @@ export const layer = Layer.effect( Effect.fnUntraced(function* (exit) { yield* bus.publish(KiloSession.Event.TurnClose, { sessionID: input.sessionID, + parentID: session.parentID, reason: KiloSessionPrompt.resolveCloseReason({ sessionID: input.sessionID, closeReasons, diff --git a/packages/opencode/test/cli/cmd/tui/notifications.test.ts b/packages/opencode/test/cli/cmd/tui/notifications.test.ts index be5d34c9c8..432f9e1711 100644 --- a/packages/opencode/test/cli/cmd/tui/notifications.test.ts +++ b/packages/opencode/test/cli/cmd/tui/notifications.test.ts @@ -193,7 +193,7 @@ describe("internal notifications TUI plugin", () => { expect(harness.notifications).toEqual([]) }) - test("uses sound-only notifications and subagent_done sound for subagent sessions", async () => { + test("keeps subagent request notifications but suppresses completion attention", async () => { const harness = await setup() harness.emit({ id: "event-1", type: "question.asked", properties: question("question-1", "subagent") }) @@ -210,7 +210,7 @@ describe("internal notifications TUI plugin", () => { harness.emit({ id: "event-4", type: "session.turn.close", - properties: { sessionID: "subagent", reason: "completed" }, + properties: { sessionID: "subagent", parentID: "session", reason: "completed" }, }) expect(harness.notifications).toEqual([ @@ -220,12 +220,6 @@ describe("internal notifications TUI plugin", () => { notification: false, sound: { name: "question", when: "always" }, }, - { - title: "Subagent session", - message: "Session done", - notification: false, - sound: { name: "subagent_done", when: "always" }, - }, ]) }) diff --git a/packages/opencode/test/kilocode/session/platform-attribution.test.ts b/packages/opencode/test/kilocode/session/platform-attribution.test.ts index 5632e0ffd7..c4e4e21b8e 100644 --- a/packages/opencode/test/kilocode/session/platform-attribution.test.ts +++ b/packages/opencode/test/kilocode/session/platform-attribution.test.ts @@ -1,11 +1,15 @@ import { describe, expect, test } from "bun:test" import path from "path" +import { Effect } from "effect" import * as Log from "@opencode-ai/core/util/log" +import { Bus } from "@/bus" import { Session as SessionNs } from "@/session/session" +import { SessionPrompt } from "@/session/prompt" import { AppRuntime } from "../../../src/effect/app-runtime" import { KiloSession } from "../../../src/kilocode/session" import { provideTestInstance } from "../../fixture/fixture" -import type { SessionID } from "../../../src/session/schema" +import { MessageID, type SessionID } from "../../../src/session/schema" +import { ModelID, ProviderID } from "../../../src/provider/schema" const projectRoot = path.join(__dirname, "../../..") void Log.init({ print: false }) @@ -14,6 +18,38 @@ function create(input?: SessionNs.CreateInput) { return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create(input))) } +function seed(id: SessionID) { + return AppRuntime.runPromise( + SessionNs.Service.use((svc) => + Effect.gen(function* () { + const user = yield* svc.updateMessage({ + id: MessageID.ascending(), + role: "user", + sessionID: id, + agent: "build", + model: { modelID: ModelID.make("test-model"), providerID: ProviderID.make("test") }, + time: { created: Date.now() }, + }) + yield* svc.updateMessage({ + id: MessageID.ascending(), + role: "assistant", + parentID: user.id, + sessionID: id, + mode: "build", + agent: "build", + cost: 0, + path: { cwd: projectRoot, root: projectRoot }, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + modelID: ModelID.make("test-model"), + providerID: ProviderID.make("test"), + time: { created: Date.now() }, + finish: "stop", + }) + }), + ), + ) +} + function remove(id: SessionID) { return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.remove(id))) } @@ -55,4 +91,31 @@ describe("session platform attribution", () => { }, }) }) + + test("turn close events include persisted parent lineage", async () => { + await provideTestInstance({ + directory: projectRoot, + fn: async () => { + const root = await create({}) + const child = await create({ parentID: root.id, title: "child" }) + await seed(child.id) + KiloSession.clearPlatformOverride(child.id) + expect(KiloSession.resolveParent(child.id)).toBeUndefined() + + const closed = Promise.withResolvers() + const unsubscribe = await AppRuntime.runPromise( + Bus.Service.use((bus) => + bus.subscribeCallback(KiloSession.Event.TurnClose, (event) => { + if (event.properties.sessionID === child.id) closed.resolve(event.properties.parentID) + }), + ), + ) + + await AppRuntime.runPromise(SessionPrompt.Service.use((prompt) => prompt.loop({ sessionID: child.id }))) + expect(await closed.promise).toBe(root.id) + unsubscribe() + await remove(root.id) + }, + }) + }) }) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 9c6f7f898c..b6938899c8 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -3158,6 +3158,7 @@ export type EventSessionTurnClose = { type: "session.turn.close" properties: { sessionID: string + parentID?: string reason: "completed" | "error" | "interrupted" } } diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 915815243b..a018bd95a6 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -16575,6 +16575,12 @@ { "$ref": "#/components/schemas/EventSessionIdle" }, + { + "$ref": "#/components/schemas/EventInstallationUpdated" + }, + { + "$ref": "#/components/schemas/EventInstallationUpdate-available" + }, { "$ref": "#/components/schemas/EventSuggestionShown" }, @@ -16626,12 +16632,6 @@ { "$ref": "#/components/schemas/EventPtyDeleted" }, - { - "$ref": "#/components/schemas/EventInstallationUpdated" - }, - { - "$ref": "#/components/schemas/EventInstallationUpdate-available" - }, { "$ref": "#/components/schemas/EventMessageUpdated" }, @@ -19345,6 +19345,12 @@ { "$ref": "#/components/schemas/EventSessionIdle" }, + { + "$ref": "#/components/schemas/EventInstallationUpdated" + }, + { + "$ref": "#/components/schemas/EventInstallationUpdate-available" + }, { "$ref": "#/components/schemas/EventSuggestionShown" }, @@ -19396,12 +19402,6 @@ { "$ref": "#/components/schemas/EventPtyDeleted" }, - { - "$ref": "#/components/schemas/EventInstallationUpdated" - }, - { - "$ref": "#/components/schemas/EventInstallationUpdate-available" - }, { "$ref": "#/components/schemas/EventMessageUpdated" }, @@ -26186,6 +26186,10 @@ "type": "string", "pattern": "^ses" }, + "parentID": { + "type": "string", + "pattern": "^ses" + }, "reason": { "type": "string", "enum": ["completed", "error", "interrupted"] @@ -26362,6 +26366,54 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, + "EventInstallationUpdated": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["installation.updated"] + }, + "properties": { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "required": ["version"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, + "EventInstallationUpdate-available": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["installation.update-available"] + }, + "properties": { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "required": ["version"], + "additionalProperties": false + } + }, + "required": ["id", "type", "properties"], + "additionalProperties": false + }, "EventSuggestionShown": { "type": "object", "properties": { @@ -26816,54 +26868,6 @@ "required": ["id", "type", "properties"], "additionalProperties": false }, - "EventInstallationUpdated": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["installation.updated"] - }, - "properties": { - "type": "object", - "properties": { - "version": { - "type": "string" - } - }, - "required": ["version"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, - "EventInstallationUpdate-available": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["installation.update-available"] - }, - "properties": { - "type": "object", - "properties": { - "version": { - "type": "string" - } - }, - "required": ["version"], - "additionalProperties": false - } - }, - "required": ["id", "type", "properties"], - "additionalProperties": false - }, "EventMessageUpdated": { "type": "object", "properties": { From d3c58e6c1f74c1ade4da29a2e9597e9a48f7f2a1 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 19 Jun 2026 11:47:48 +0200 Subject: [PATCH 25/30] test(cli): preserve promise facade ratchet --- .../session/platform-attribution.test.ts | 16 ++++++++++------ script/check-opencode-promise-facades.ts | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/opencode/test/kilocode/session/platform-attribution.test.ts b/packages/opencode/test/kilocode/session/platform-attribution.test.ts index c4e4e21b8e..977fc09e49 100644 --- a/packages/opencode/test/kilocode/session/platform-attribution.test.ts +++ b/packages/opencode/test/kilocode/session/platform-attribution.test.ts @@ -5,7 +5,7 @@ import * as Log from "@opencode-ai/core/util/log" import { Bus } from "@/bus" import { Session as SessionNs } from "@/session/session" import { SessionPrompt } from "@/session/prompt" -import { AppRuntime } from "../../../src/effect/app-runtime" +import { AppRuntime, type AppServices } from "../../../src/effect/app-runtime" import { KiloSession } from "../../../src/kilocode/session" import { provideTestInstance } from "../../fixture/fixture" import { MessageID, type SessionID } from "../../../src/session/schema" @@ -14,12 +14,16 @@ import { ModelID, ProviderID } from "../../../src/provider/schema" const projectRoot = path.join(__dirname, "../../..") void Log.init({ print: false }) +function run(effect: Effect.Effect) { + return AppRuntime.runPromise(effect) +} + function create(input?: SessionNs.CreateInput) { - return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create(input))) + return run(SessionNs.Service.use((svc) => svc.create(input))) } function seed(id: SessionID) { - return AppRuntime.runPromise( + return run( SessionNs.Service.use((svc) => Effect.gen(function* () { const user = yield* svc.updateMessage({ @@ -51,7 +55,7 @@ function seed(id: SessionID) { } function remove(id: SessionID) { - return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.remove(id))) + return run(SessionNs.Service.use((svc) => svc.remove(id))) } describe("session platform attribution", () => { @@ -103,7 +107,7 @@ describe("session platform attribution", () => { expect(KiloSession.resolveParent(child.id)).toBeUndefined() const closed = Promise.withResolvers() - const unsubscribe = await AppRuntime.runPromise( + const unsubscribe = await run( Bus.Service.use((bus) => bus.subscribeCallback(KiloSession.Event.TurnClose, (event) => { if (event.properties.sessionID === child.id) closed.resolve(event.properties.parentID) @@ -111,7 +115,7 @@ describe("session platform attribution", () => { ), ) - await AppRuntime.runPromise(SessionPrompt.Service.use((prompt) => prompt.loop({ sessionID: child.id }))) + await run(SessionPrompt.Service.use((prompt) => prompt.loop({ sessionID: child.id }))) expect(await closed.promise).toBe(root.id) unsubscribe() await remove(root.id) diff --git a/script/check-opencode-promise-facades.ts b/script/check-opencode-promise-facades.ts index dc8042c761..4f81f70679 100644 --- a/script/check-opencode-promise-facades.ts +++ b/script/check-opencode-promise-facades.ts @@ -32,7 +32,7 @@ const testAllow: Record = { "kilocode/config-resilience.test.ts": { count: 4, reason: "existing runtime integration test" }, "kilocode/config-validation.test.ts": { count: 2, reason: "existing runtime integration test" }, "kilocode/plan-followup.test.ts": { count: 4, reason: "existing runtime integration test" }, - "kilocode/session/platform-attribution.test.ts": { count: 3, reason: "existing runtime integration test" }, + "kilocode/session/platform-attribution.test.ts": { count: 2, reason: "existing runtime integration test" }, "kilocode/session-prompt-queue.test.ts": { count: 5, reason: "prompt queue legacy instance bridge regression" }, "server/experimental-session-list.test.ts": { count: 2, reason: "Kilo session list integration test" }, "kilocode/server/listener-runtime.test.ts": { count: 3, reason: "listener and AppRuntime integration test" }, From 4d0933371ca9be212cdd0357605e250ebacf7e1b Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 19 Jun 2026 11:51:50 +0200 Subject: [PATCH 26/30] fix: hide provider errors after session revert --- .changeset/hide-reverted-provider-errors.md | 6 ++ .../tests/unit/session-queue.test.ts | 57 +++++++++++-- .../tests/unit/transcript-rows.test.ts | 28 +++++- .../src/components/chat/MessageList.tsx | 9 +- .../webview-ui/src/context/session-queue.ts | 27 ++++-- .../webview-ui/src/context/session.tsx | 6 +- .../webview-ui/src/context/transcript-rows.ts | 23 ++--- packages/opencode/src/session/revert.ts | 6 ++ .../test/kilocode/session/revert.test.ts | 85 +++++++++++++++++++ 9 files changed, 218 insertions(+), 29 deletions(-) create mode 100644 .changeset/hide-reverted-provider-errors.md create mode 100644 packages/opencode/test/kilocode/session/revert.test.ts diff --git a/.changeset/hide-reverted-provider-errors.md b/.changeset/hide-reverted-provider-errors.md new file mode 100644 index 0000000000..c54d4e302b --- /dev/null +++ b/.changeset/hide-reverted-provider-errors.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": patch +"kilo-code": patch +--- + +Hide reverted provider errors so Redo controls remain visible after rewinding a session. diff --git a/packages/kilo-vscode/tests/unit/session-queue.test.ts b/packages/kilo-vscode/tests/unit/session-queue.test.ts index 8e8f6934c3..77f8ae6a29 100644 --- a/packages/kilo-vscode/tests/unit/session-queue.test.ts +++ b/packages/kilo-vscode/tests/unit/session-queue.test.ts @@ -6,8 +6,10 @@ import { queuedUserMessageIDs, stableMessageTurns, visibleMessages, + visibleParts, + type RevertBoundary, } from "../../webview-ui/src/context/session-queue" -import type { Message, SessionStatusInfo } from "../../webview-ui/src/types/messages" +import type { Message, Part, SessionStatusInfo } from "../../webview-ui/src/types/messages" const base = { sessionID: "session", @@ -30,10 +32,12 @@ const assistant = (id: string, parentID: string, opts: Partial = {}): M ...opts, }) -const layout = (messages: Message[], status: SessionStatusInfo, boundary?: string) => { +const part = (id: string, messageID: string): Part => ({ id, messageID, type: "text", text: id }) + +const layout = (messages: Message[], status: SessionStatusInfo, revert?: RevertBoundary) => { const active = activeUserMessageID(messages, status) return partitionTurns( - messageTurns(messages, boundary), + messageTurns(messages, revert), new Set(active ? [active] : []), new Set(queuedUserMessageIDs(messages, status)), ) @@ -292,7 +296,7 @@ describe("partitionTurns", () => { it("does not render an active turn hidden by a revert boundary", () => { const messages = [user("message_1"), assistant("message_2", "message_1", { finish: "stop" }), user("message_3")] - const result = layout(messages, { type: "busy" }, "message_3") + const result = layout(messages, { type: "busy" }, { messageID: "message_3" }) expect(result.virtual.map((turn) => turn.user.id)).toEqual(["message_1"]) expect(result.direct).toEqual([]) @@ -394,7 +398,45 @@ describe("messageTurns", () => { assistant("message_4", "message_3"), ] - expect(messageTurns(messages, "message_3").map((turn) => turn.user.id)).toEqual(["message_1"]) + expect(messageTurns(messages, { messageID: "message_3" }).map((turn) => turn.user.id)).toEqual(["message_1"]) + }) + + it("keeps the part-boundary assistant and hides later provider errors", () => { + const messages = [ + user("message_1"), + assistant("message_2", "message_1"), + assistant("message_3", "message_1", { error: { name: "ProviderError" } }), + assistant("message_4", "message_1", { error: { name: "ProviderError" } }), + ] + const turns = messageTurns(messages, { messageID: "message_2", partID: "part_2" }) + + expect(turns).toHaveLength(1) + expect(turns[0]?.assistant.map((msg) => msg.id)).toEqual(["message_2"]) + }) + + it("applies assistant boundaries by id when messages arrive out of order", () => { + const messages = [ + user("message_1"), + assistant("message_4", "message_1", { error: { name: "ProviderError" } }), + assistant("message_2", "message_1"), + ] + const turns = messageTurns(messages, { messageID: "message_2", partID: "part_2" }) + + expect(turns[0]?.assistant.map((msg) => msg.id)).toEqual(["message_2"]) + }) +}) + +describe("visibleParts", () => { + it("keeps only parts before the active part boundary", () => { + const parts = [part("part_1", "message_2"), part("part_2", "message_2"), part("part_3", "message_2")] + + expect(visibleParts("message_2", parts, { messageID: "message_2", partID: "part_2" })).toEqual([parts[0]]) + }) + + it("fails closed when the boundary part is unavailable", () => { + const parts = [part("part_1", "message_2")] + + expect(visibleParts("message_2", parts, { messageID: "message_2", partID: "part_missing" })).toEqual([]) }) }) @@ -407,7 +449,10 @@ describe("visibleMessages", () => { assistant("message_4", "message_3"), ] - expect(visibleMessages(messages, "message_3").map((msg) => msg.id)).toEqual(["message_1", "message_2"]) + expect(visibleMessages(messages, { messageID: "message_3" }).map((msg) => msg.id)).toEqual([ + "message_1", + "message_2", + ]) }) it("keeps leading partial assistant output", () => { diff --git a/packages/kilo-vscode/tests/unit/transcript-rows.test.ts b/packages/kilo-vscode/tests/unit/transcript-rows.test.ts index a34628e3e8..6889aab495 100644 --- a/packages/kilo-vscode/tests/unit/transcript-rows.test.ts +++ b/packages/kilo-vscode/tests/unit/transcript-rows.test.ts @@ -84,6 +84,32 @@ describe("transcriptRows", () => { expect(rows.at(-1)).toMatchObject({ type: "error", message: a3, error: a3.error }) }) + it("hides provider errors at and after an assistant part boundary", () => { + const revert = { messageID: "message_2", partID: "part_2" } + const u1 = user("message_1") + const a1 = assistant("message_2", "message_1", { error: { name: "ProviderError" } }) + const a2 = assistant("message_3", "message_1", { error: { name: "ProviderError" } }) + const parts = { message_2: [part("part_1", "message_2"), part("part_2", "message_2")] } + const rows = transcriptRows(messageTurns([u1, a1, a2], revert), lookup(parts), { revert }) + + expect(rows.map((row) => row.type)).toEqual(["user", "assistant"]) + expect(rows.filter((row) => row.type === "assistant").flatMap((row) => row.parts.map((item) => item.id))).toEqual([ + "part_1", + ]) + expect(rows.some((row) => row.message.id === "message_3")).toBe(false) + }) + + it("keeps provider errors before an assistant part boundary", () => { + const revert = { messageID: "message_3", partID: "part_2" } + const u1 = user("message_1") + const a1 = assistant("message_2", "message_1", { error: { name: "ProviderError" } }) + const a2 = assistant("message_3", "message_1") + const parts = { message_3: [part("part_1", "message_3"), part("part_2", "message_3")] } + const rows = transcriptRows(messageTurns([u1, a1, a2], revert), lookup(parts), { revert }) + + expect(rows.at(-1)).toMatchObject({ type: "error", message: a1 }) + }) + it("keeps keys stable when older turns are prepended and parts are appended", () => { const u1 = user("u1") const a1 = assistant("a1", "u1") @@ -131,7 +157,7 @@ describe("transcriptRows", () => { }) const a2 = assistant("a2", "u1") const u3 = user("u3") - const turns = messageTurns([u1, a1, u2, a2, u3], "u3") + const turns = messageTurns([u1, a1, u2, a2, u3], { messageID: "u3" }) const rows = transcriptRows(turns, (id) => (id === "u2" ? (u2.parts ?? []) : [])) expect(rows.map((row) => `${row.turn}:${row.message.id}`)).toEqual(["u1:u1", "u1:a1", "u2:u2", "u2:a2"]) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx index 19e8f81b96..36bbdb5cc1 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/MessageList.tsx @@ -91,14 +91,14 @@ export const MessageList: Component = (props) => { const [virtualizer, setVirtualizer] = createSignal() const [layout, setLayout] = createSignal("") - const boundary = () => session.revert()?.messageID + const revert = () => session.revert() ?? undefined const turns = createMemo((prev: MessageTurn[] | undefined) => stableMessageTurns( - messageTurns(session.messages(), boundary(), (msg) => session.getParts(msg.id)), + messageTurns(session.messages(), revert(), (msg) => session.getParts(msg.id)), prev, ), ) - const isEmpty = () => turns().length === 0 && !session.loading() && !boundary() + const isEmpty = () => turns().length === 0 && !session.loading() && !revert() const activeUserID = createMemo(() => getActiveUserMessageID(session.messages(), session.statusInfo(), (msg) => session.getParts(msg.id)), @@ -115,6 +115,7 @@ export const MessageList: Component = (props) => { queued: queuedIDs(), live: new Set(active ? [active] : []), hidden: session.isErrorHidden, + revert: revert(), }, prev, ) @@ -326,7 +327,7 @@ export const MessageList: Component = (props) => {
- + {(row) => } diff --git a/packages/kilo-vscode/webview-ui/src/context/session-queue.ts b/packages/kilo-vscode/webview-ui/src/context/session-queue.ts index 066df58329..59b7833551 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session-queue.ts +++ b/packages/kilo-vscode/webview-ui/src/context/session-queue.ts @@ -1,4 +1,6 @@ -import type { Message, SessionStatusInfo } from "../types/messages" +import type { Message, Part, SessionInfo, SessionStatusInfo } from "../types/messages" + +export type RevertBoundary = Pick, "messageID" | "partID"> export interface MessageTurn { id: string @@ -55,24 +57,37 @@ function target(messages: Message[], index: number, id: string, parts?: (msg: Me return id } +function visibleMessage(id: string, revert?: RevertBoundary) { + if (!revert || id < revert.messageID) return true + return id === revert.messageID && !!revert.partID +} + +export function visibleParts(id: string, parts: Part[], revert?: RevertBoundary) { + if (!revert || id < revert.messageID) return parts + if (id !== revert.messageID || !revert.partID) return [] + const idx = parts.findIndex((part) => part.id === revert.partID) + return idx < 0 ? [] : parts.slice(0, idx) +} + export function messageTurns( messages: Message[], - boundary?: string, + revert?: RevertBoundary, parts?: (msg: Message) => Message["parts"], ): MessageTurn[] { const result: MessageTurn[] = [] const lead: Message[] = [] const by = new Map() + const projected = (msg: Message) => visibleParts(msg.id, parts?.(msg) ?? msg.parts ?? [], revert) let compact: { turn: MessageTurn; index: number } | undefined for (const msg of messages) { + if (!visibleMessage(msg.id, revert)) continue if (msg.role === "user") { - if (boundary && msg.id >= boundary) break const turn = { id: msg.id, user: msg, assistant: [] } const item = { turn, index: result.length } result.push(turn) by.set(msg.id, item) - if (isCompact(msg, parts)) compact = item + if (isCompact(msg, projected)) compact = item continue } @@ -105,10 +120,10 @@ export function messageTurns( export function visibleMessages( messages: Message[], - boundary?: string, + revert?: RevertBoundary, parts?: (msg: Message) => Message["parts"], ): Message[] { - return messageTurns(messages, boundary, parts).flatMap((turn) => + return messageTurns(messages, revert, parts).flatMap((turn) => turn.partial ? turn.assistant : [turn.user, ...turn.assistant], ) } diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index 01788f5d66..417c40214d 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -2550,8 +2550,10 @@ export const SessionProvider: ParentComponent = (props) => { const userMessages = createMemo(() => messages().filter((m) => m.role === "user")) function visible(sessionID: string) { - return filterVisibleMessages(store.messages[sessionID] ?? [], store.sessions[sessionID]?.revert?.messageID, (msg) => - getParts(msg.id), + return filterVisibleMessages( + store.messages[sessionID] ?? [], + store.sessions[sessionID]?.revert ?? undefined, + (msg) => getParts(msg.id), ) } diff --git a/packages/kilo-vscode/webview-ui/src/context/transcript-rows.ts b/packages/kilo-vscode/webview-ui/src/context/transcript-rows.ts index c6445d4c3c..89ac134335 100644 --- a/packages/kilo-vscode/webview-ui/src/context/transcript-rows.ts +++ b/packages/kilo-vscode/webview-ui/src/context/transcript-rows.ts @@ -1,5 +1,5 @@ import type { Message, Part } from "../types/messages" -import type { MessageTurn } from "./session-queue" +import { visibleParts, type MessageTurn, type RevertBoundary } from "./session-queue" interface TranscriptMeta { turn: string @@ -46,6 +46,7 @@ export interface TranscriptOptions { queued?: ReadonlySet live?: ReadonlySet hidden?: (id: string) => boolean + revert?: RevertBoundary } export interface TranscriptPartition { @@ -127,6 +128,8 @@ export function transcriptRows( ): TranscriptRow[] { const size = Math.max(1, Math.floor(opts.size ?? 8)) const rows: TranscriptRow[] = [] + const parts = (id: string) => visibleParts(id, getParts(id), opts.revert) + const terminal = (msg: Message) => !(opts.revert?.partID && msg.id === opts.revert.messageID) for (const turn of turns) { const meta = { @@ -135,7 +138,7 @@ export function transcriptRows( queued: opts.queued?.has(turn.id) === true, live: opts.live?.has(turn.id) === true, } - const copied = copy(turn.assistant, getParts) + const copied = copy(turn.assistant, parts) if (!turn.partial) { rows.push({ @@ -143,27 +146,27 @@ export function transcriptRows( type: "user", key: `${turn.id}:user`, message: turn.user, - parts: getParts(turn.user.id), - interrupted: turn.assistant.some((msg) => msg.error?.name === "MessageAbortedError"), + parts: parts(turn.user.id), + interrupted: turn.assistant.some((msg) => terminal(msg) && msg.error?.name === "MessageAbortedError"), answered: turn.assistant.length > 0, }) } for (const msg of turn.assistant) { - const parts = getParts(msg.id) - if (parts.length === 0) { + const visible = parts(msg.id) + if (visible.length === 0) { rows.push({ ...meta, type: "assistant", key: `${turn.id}:assistant:${msg.id}:empty`, message: msg, - parts, + parts: visible, copy: copied, }) continue } - for (let start = 0; start < parts.length; start += size) { - const chunk = parts.slice(start, start + size) + for (let start = 0; start < visible.length; start += size) { + const chunk = visible.slice(start, start + size) rows.push({ ...meta, type: "assistant", @@ -181,7 +184,7 @@ export function transcriptRows( } const failed = turn.assistant.find( - (msg) => msg.error && msg.error.name !== "MessageAbortedError" && opts.hidden?.(msg.id) !== true, + (msg) => terminal(msg) && msg.error && msg.error.name !== "MessageAbortedError" && opts.hidden?.(msg.id) !== true, ) if (failed?.error) { rows.push({ ...meta, type: "error", key: `${turn.id}:error:${failed.id}`, message: failed, error: failed.error }) diff --git a/packages/opencode/src/session/revert.ts b/packages/opencode/src/session/revert.ts index 0c0e33da2f..def804a109 100644 --- a/packages/opencode/src/session/revert.ts +++ b/packages/opencode/src/session/revert.ts @@ -152,6 +152,12 @@ export const layer = Layer.effect( partID: part.id, }) } + // kilocode_change start - clear a reverted provider error from the retained assistant message + if (target.info.role === "assistant" && target.info.error) { + delete target.info.error + yield* sessions.updateMessage(target.info) + } + // kilocode_change end } } yield* sessions.clearRevert(sessionID) diff --git a/packages/opencode/test/kilocode/session/revert.test.ts b/packages/opencode/test/kilocode/session/revert.test.ts new file mode 100644 index 0000000000..91f2d008ee --- /dev/null +++ b/packages/opencode/test/kilocode/session/revert.test.ts @@ -0,0 +1,85 @@ +import { describe, expect } from "bun:test" +import { Effect, Layer } from "effect" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" +import { ModelID, ProviderID } from "@/provider/schema" +import { MessageV2 } from "@/session/message-v2" +import { SessionRevert } from "@/session/revert" +import { MessageID, PartID } from "@/session/schema" +import { Session } from "@/session/session" +import { Snapshot } from "@/snapshot" +import { provideTmpdirInstance } from "../../fixture/fixture" +import { testEffect } from "../../lib/effect" + +const env = Layer.mergeAll( + Session.defaultLayer, + SessionRevert.defaultLayer, + Snapshot.defaultLayer, + CrossSpawnSpawner.defaultLayer, +) +const it = testEffect(env) + +describe("partial assistant revert", () => { + it.live( + "clears provider errors when the revert becomes permanent", + provideTmpdirInstance( + (dir) => + Effect.gen(function* () { + const sessions = yield* Session.Service + const revert = yield* SessionRevert.Service + const session = yield* sessions.create({}) + const providerID = ProviderID.make("test") + const user = yield* sessions.updateMessage({ + id: MessageID.ascending(), + sessionID: session.id, + role: "user", + agent: "default", + model: { providerID, modelID: ModelID.make("test") }, + time: { created: Date.now() }, + }) + const assistant = yield* sessions.updateMessage({ + id: MessageID.ascending(), + sessionID: session.id, + role: "assistant", + parentID: user.id, + mode: "default", + agent: "default", + path: { cwd: dir, root: dir }, + cost: 1, + tokens: { input: 1, output: 1, reasoning: 0, cache: { read: 0, write: 0 } }, + modelID: ModelID.make("test"), + providerID, + time: { created: Date.now(), completed: Date.now() }, + finish: "error", + error: MessageV2.fromError(new Error("Provider returned error"), { providerID }), + }) + const kept = yield* sessions.updatePart({ + id: PartID.ascending(), + messageID: assistant.id, + sessionID: session.id, + type: "text", + text: "keep", + }) + const boundary = yield* sessions.updatePart({ + id: PartID.ascending(), + messageID: assistant.id, + sessionID: session.id, + type: "text", + text: "remove", + }) + + yield* sessions.setRevert({ + sessionID: session.id, + revert: { messageID: assistant.id, partID: boundary.id }, + summary: { additions: 0, deletions: 0, files: 0 }, + }) + yield* revert.cleanup(yield* sessions.get(session.id)) + + const messages = yield* sessions.messages({ sessionID: session.id }) + const result = messages.find((message) => message.info.id === assistant.id) + expect(result?.parts.map((part) => part.id)).toEqual([kept.id]) + expect(result?.info).not.toHaveProperty("error") + }), + { git: true }, + ), + ) +}) From 4bfd65264fbabf801b8f130e9d33b8d51dd2ec91 Mon Sep 17 00:00:00 2001 From: "kilo-maintainer[bot]" Date: Fri, 19 Jun 2026 10:12:49 +0000 Subject: [PATCH 27/30] 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 fdee193a50..575425ce92 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-ViNoPxLiExkP4OC7F0AfXnE2Ib+oi4qtP0hqnfhV2Ho=", - "aarch64-linux": "sha256-TKp6yzSijtElRhtcInC6b9LOjvUrLunyRUXBwOb/4i8=", - "aarch64-darwin": "sha256-kW9kVAHOMpKF0/d/YOZFIENI5KyHGLwDvijd/JprYHg=", - "x86_64-darwin": "sha256-8m1uCjLAQHoEVoBETPzVVV5ji8Zlllf1f9s+d8HmLpY=" + "x86_64-linux": "sha256-6wXsrAKFGbHf3bfRmbKMXsrI4jYOzzOkwcyFnM4JVK4=", + "aarch64-linux": "sha256-Vmkvv4HEf0DQA+B2khHfAvGRjxiawtQjUeN+G+SNbH0=", + "aarch64-darwin": "sha256-u5mIwNkEmf2sJBHH/Lm3o+OXtDbl7rLG38bkLRUH1cY=", + "x86_64-darwin": "sha256-MuA/788i7hYZurDjJJnFUr/BHF9sP/mg0BvxCHQsbCY=" } } From 909ec73b9c852de6899c20f473b6da14d6c428b5 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 19 Jun 2026 12:35:39 +0200 Subject: [PATCH 28/30] fix(agent-manager): speed up worktree hover cards --- .changeset/swift-pr-hover-cards.md | 5 +++++ .../kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx | 5 +++-- .../webview-ui/agent-manager/agent-manager.css | 8 ++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 .changeset/swift-pr-hover-cards.md diff --git a/.changeset/swift-pr-hover-cards.md b/.changeset/swift-pr-hover-cards.md new file mode 100644 index 0000000000..3b154302b2 --- /dev/null +++ b/.changeset/swift-pr-hover-cards.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Speed up Agent Manager worktree hover cards so pull request details appear and dismiss more quickly. diff --git a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx index 5606e835c4..303661b74a 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx @@ -151,8 +151,9 @@ export const WorktreeItem: Component = (props) => { Date: Fri, 19 Jun 2026 12:45:42 +0200 Subject: [PATCH 29/30] fix: Add vercel bypass token to opencode release watcher (#11460) --- .github/workflows/watch-opencode-releases.yml | 5 ++++- script/check-opencode-annotations.ts | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/watch-opencode-releases.yml b/.github/workflows/watch-opencode-releases.yml index 10da78231b..a3d766b7ae 100644 --- a/.github/workflows/watch-opencode-releases.yml +++ b/.github/workflows/watch-opencode-releases.yml @@ -40,9 +40,12 @@ jobs: env: URL: ${{ secrets.OPENCODE_WATCH_SYNC_URL }} SECRET: ${{ secrets.OPENCODE_WATCH_SYNC_SECRET }} + VERCEL: ${{ secrets.OPENCODE_WATCH_VERCEL_SECRET }} run: | if [ -n "$URL" ] && [ -n "$SECRET" ]; then - curl -sf -X POST "$URL" -H "x-sync-secret: $SECRET" -H "Content-Type: application/json" -d '{}' || true + headers=(-H "x-sync-secret: $SECRET" -H "Content-Type: application/json") + [ -n "$VERCEL" ] && headers+=(-H "x-vercel-protection-bypass: $VERCEL") + curl -sf -X POST "$URL" "${headers[@]}" -d '{}' || true fi - if: steps.fetch.outputs.tag != '' && steps.cache.outputs.cache-hit != 'true' diff --git a/script/check-opencode-annotations.ts b/script/check-opencode-annotations.ts index be65e4e60b..b5c4e456ab 100644 --- a/script/check-opencode-annotations.ts +++ b/script/check-opencode-annotations.ts @@ -50,6 +50,7 @@ const EXEMPT_SCOPES = [ "script/check-opencode-annotations.ts", "packages/script/tests/check-opencode-annotations.test.ts", ".github/workflows/check-opencode-annotations.yml", + ".github/workflows/watch-opencode-releases.yml", ] const args = process.argv.slice(2) From e8bcb70b9a9d50725752fb8eea108a7db7bc6bab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catriel=20M=C3=BCller?= Date: Fri, 19 Jun 2026 08:46:11 -0300 Subject: [PATCH 30/30] fix: minimal css fix --- packages/kilo-console/src/styles/models.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/kilo-console/src/styles/models.css b/packages/kilo-console/src/styles/models.css index 626f43786a..6f12e42d1b 100644 --- a/packages/kilo-console/src/styles/models.css +++ b/packages/kilo-console/src/styles/models.css @@ -272,6 +272,10 @@ flex-wrap: wrap; } +.kilo-console .models-capabilities-list button { + text-transform: capitalize; +} + .kilo-console .explore-models { grid-template-columns: repeat(auto-fill, minmax(22.5rem, 1fr)); gap: 0.625rem;