From c4898d78d164638628836d9186661dadccaec779 Mon Sep 17 00:00:00 2001 From: Thomas Brugman Date: Sun, 15 Mar 2026 10:25:52 +0100 Subject: [PATCH 001/264] fix: always deny task permission in subagent sessions --- packages/opencode/src/tool/task.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index 8c8cf827aba..dacbdaca5f7 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -61,8 +61,6 @@ export const TaskTool = Tool.define("task", async (ctx) => { const agent = await Agent.get(params.subagent_type) if (!agent) throw new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`) - const hasTaskPermission = agent.permission.some((rule) => rule.permission === "task") - const session = await iife(async () => { if (params.task_id) { const found = await Session.get(params.task_id).catch(() => {}) @@ -83,15 +81,11 @@ export const TaskTool = Tool.define("task", async (ctx) => { pattern: "*", action: "deny", }, - ...(hasTaskPermission - ? [] - : [ - { - permission: "task" as const, - pattern: "*" as const, - action: "deny" as const, - }, - ]), + { + permission: "task" as const, + pattern: "*" as const, + action: "deny" as const, + }, ...(config.experimental?.primary_tools?.map((t) => ({ pattern: "*", action: "allow" as const, @@ -136,7 +130,7 @@ export const TaskTool = Tool.define("task", async (ctx) => { tools: { todowrite: false, todoread: false, - ...(hasTaskPermission ? {} : { task: false }), + task: false, ...Object.fromEntries((config.experimental?.primary_tools ?? []).map((t) => [t, false])), }, parts: promptParts, From 4ee4d49d92f125a45bfb4ca026a3ed61bb7d9efb Mon Sep 17 00:00:00 2001 From: Thomas Brugman Date: Sun, 15 Mar 2026 10:32:46 +0100 Subject: [PATCH 002/264] fix: deny task in subagent sessions unless agent explicitly allows it --- packages/opencode/src/tool/task.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts index dacbdaca5f7..a76eb9f756b 100644 --- a/packages/opencode/src/tool/task.ts +++ b/packages/opencode/src/tool/task.ts @@ -61,6 +61,8 @@ export const TaskTool = Tool.define("task", async (ctx) => { const agent = await Agent.get(params.subagent_type) if (!agent) throw new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`) + const allowsTask = agent.permission.some((rule) => rule.permission === "task" && rule.action === "allow") + const session = await iife(async () => { if (params.task_id) { const found = await Session.get(params.task_id).catch(() => {}) @@ -81,11 +83,15 @@ export const TaskTool = Tool.define("task", async (ctx) => { pattern: "*", action: "deny", }, - { - permission: "task" as const, - pattern: "*" as const, - action: "deny" as const, - }, + ...(allowsTask + ? [] + : [ + { + permission: "task" as const, + pattern: "*" as const, + action: "deny" as const, + }, + ]), ...(config.experimental?.primary_tools?.map((t) => ({ pattern: "*", action: "allow" as const, @@ -130,7 +136,7 @@ export const TaskTool = Tool.define("task", async (ctx) => { tools: { todowrite: false, todoread: false, - task: false, + ...(allowsTask ? {} : { task: false }), ...Object.fromEntries((config.experimental?.primary_tools ?? []).map((t) => [t, false])), }, parts: promptParts, From 175562c34ff30f88230474b762c2b50dd47fae1f Mon Sep 17 00:00:00 2001 From: Alex Gold Date: Sun, 15 Mar 2026 08:41:37 -0400 Subject: [PATCH 003/264] Answer some questions about KiloClaw --- packages/kilo-docs/pages/kiloclaw/dashboard.md | 4 ++++ .../kilo-docs/pages/kiloclaw/troubleshooting.md | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/packages/kilo-docs/pages/kiloclaw/dashboard.md b/packages/kilo-docs/pages/kiloclaw/dashboard.md index 9904f07bc74..246e7682778 100644 --- a/packages/kilo-docs/pages/kiloclaw/dashboard.md +++ b/packages/kilo-docs/pages/kiloclaw/dashboard.md @@ -74,6 +74,10 @@ If the gateway crashes, it's automatically restarted. The machine itself can be Gateway process info is only available when the machine is running. {% /callout %} +## Instance Specs + +The specs of your instance, including number of CPUs, memory, and storage, are visibile at the top right of the instance controls section. + ## Settings ### Changing the Model diff --git a/packages/kilo-docs/pages/kiloclaw/troubleshooting.md b/packages/kilo-docs/pages/kiloclaw/troubleshooting.md index 4d158a5b869..7129bb19652 100644 --- a/packages/kilo-docs/pages/kiloclaw/troubleshooting.md +++ b/packages/kilo-docs/pages/kiloclaw/troubleshooting.md @@ -73,6 +73,20 @@ Direct filesystem access is not available at this time. You can interact with fi For security reasons, SSH access is currently disabled for all KiloClaw instances. Our primary goal is to provide a secure environment for all users, and restricting direct SSH access is one of the many measures we take to ensure the platform remains safe and protected for everyone. +### Can I install tailscale on my KiloClaw instance? + +Not at this time. + +### Can I update the version of Node on my instance? + +Not at this time. + +### Can I upgrade the version of OpenClaw myself? + +We do not recommend upgrading the version of OpenClaw yourself. Different versions of OpenClaw may have different requirements and dependencies, and manually upgrading could potentially lead to compatibility issues or break your setup. + +If you upgrade to a version of OpenClaw that breaks your setup, you can use the version pinning dialog at the bottom of the settings tab on the [KiloClaw Dashboard](https://app.kilo.ai/claw) to revert to an earlier version of OpenClaw. + ### How can I update my OpenClaw? Do **not** click **Update Now** inside the OpenClaw Control UI — this is not supported for KiloClaw instances and may break your setup. From 876e439726585c83c02a8353b02ef0d8d6c2965c Mon Sep 17 00:00:00 2001 From: Alex Gold Date: Sun, 15 Mar 2026 08:46:30 -0400 Subject: [PATCH 004/264] simplify upgrade answer --- packages/kilo-docs/pages/kiloclaw/troubleshooting.md | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/kilo-docs/pages/kiloclaw/troubleshooting.md b/packages/kilo-docs/pages/kiloclaw/troubleshooting.md index 7129bb19652..ab96736a308 100644 --- a/packages/kilo-docs/pages/kiloclaw/troubleshooting.md +++ b/packages/kilo-docs/pages/kiloclaw/troubleshooting.md @@ -81,17 +81,11 @@ Not at this time. Not at this time. -### Can I upgrade the version of OpenClaw myself? +### How do I upgrade the OpenClaw version? -We do not recommend upgrading the version of OpenClaw yourself. Different versions of OpenClaw may have different requirements and dependencies, and manually upgrading could potentially lead to compatibility issues or break your setup. +Use **Upgrade & Redeploy** from the [KiloClaw Dashboard](https://app.kilo.ai/claw). Do **not** click **Update Now** inside the OpenClaw Control UI — this is not supported for KiloClaw instances and may break your setup. -If you upgrade to a version of OpenClaw that breaks your setup, you can use the version pinning dialog at the bottom of the settings tab on the [KiloClaw Dashboard](https://app.kilo.ai/claw) to revert to an earlier version of OpenClaw. - -### How can I update my OpenClaw? - -Do **not** click **Update Now** inside the OpenClaw Control UI — this is not supported for KiloClaw instances and may break your setup. - -Updates are managed by the KiloClaw platform team to ensure stability. When a new version is available, it will be announced in the **Changelog** on your dashboard. To apply the update, click **Upgrade & Redeploy** from the [KiloClaw Dashboard](/docs/kiloclaw/dashboard#redeploy). +When a new version is available, it will be announced in the **Changelog** on your dashboard. If you need to roll back, use the [version pinning](/docs/kiloclaw/version-pinning) dialog in the Settings tab. ## Architecture Notes From 4560c48aa32a0a325c38a504e177b32530db9206 Mon Sep 17 00:00:00 2001 From: Alex Gold Date: Sun, 15 Mar 2026 08:50:43 -0400 Subject: [PATCH 005/264] clarify --- packages/kilo-docs/pages/kiloclaw/troubleshooting.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/kilo-docs/pages/kiloclaw/troubleshooting.md b/packages/kilo-docs/pages/kiloclaw/troubleshooting.md index ab96736a308..73f2147288c 100644 --- a/packages/kilo-docs/pages/kiloclaw/troubleshooting.md +++ b/packages/kilo-docs/pages/kiloclaw/troubleshooting.md @@ -81,11 +81,13 @@ Not at this time. Not at this time. -### How do I upgrade the OpenClaw version? +### How do I upgrade the OpenClaw version? Can I upgrade it myself? -Use **Upgrade & Redeploy** from the [KiloClaw Dashboard](https://app.kilo.ai/claw). Do **not** click **Update Now** inside the OpenClaw Control UI — this is not supported for KiloClaw instances and may break your setup. +Updates are managed by the KiloClaw platform team to ensure stability and dependency availability. When a new version is available, it will be announced in the **Changelog** on your dashboard. -When a new version is available, it will be announced in the **Changelog** on your dashboard. If you need to roll back, use the [version pinning](/docs/kiloclaw/version-pinning) dialog in the Settings tab. +To apply the update, click **Upgrade & Redeploy** from the [KiloClaw Dashboard](https://app.kilo.ai/claw). Do **not** click **Update Now** inside the OpenClaw Control UI — this is not supported for KiloClaw instances and may break your setup. + +If you need to roll back, use the [version pinning](/docs/kiloclaw/version-pinning) dialog in the Settings tab. ## Architecture Notes From ebeb463ab076ddd3259ae2cb645e1c3ea1df8cfb Mon Sep 17 00:00:00 2001 From: Alex Gold Date: Mon, 16 Mar 2026 09:09:03 -0400 Subject: [PATCH 006/264] Update packages/kilo-docs/pages/kiloclaw/dashboard.md Co-authored-by: kilo-code-bot[bot] <240665456+kilo-code-bot[bot]@users.noreply.github.com> --- packages/kilo-docs/pages/kiloclaw/dashboard.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kilo-docs/pages/kiloclaw/dashboard.md b/packages/kilo-docs/pages/kiloclaw/dashboard.md index 246e7682778..9e75225fdd0 100644 --- a/packages/kilo-docs/pages/kiloclaw/dashboard.md +++ b/packages/kilo-docs/pages/kiloclaw/dashboard.md @@ -76,7 +76,7 @@ Gateway process info is only available when the machine is running. ## Instance Specs -The specs of your instance, including number of CPUs, memory, and storage, are visibile at the top right of the instance controls section. +The specs of your instance, including number of CPUs, memory, and storage, are visible at the top right of the instance controls section. ## Settings From 1fc0e644dd103805387d4f192f9bf497ff5c0f82 Mon Sep 17 00:00:00 2001 From: Josh Holmer Date: Mon, 16 Mar 2026 20:15:56 -0400 Subject: [PATCH 007/264] fix(cli): use configured Code model when implementing a plan --- .../opencode/src/kilocode/plan-followup.ts | 45 +++- .../test/kilocode/plan-followup.test.ts | 209 ++++++++++++++++-- 2 files changed, 237 insertions(+), 17 deletions(-) diff --git a/packages/opencode/src/kilocode/plan-followup.ts b/packages/opencode/src/kilocode/plan-followup.ts index ab11e366f0c..a95f9a69c85 100644 --- a/packages/opencode/src/kilocode/plan-followup.ts +++ b/packages/opencode/src/kilocode/plan-followup.ts @@ -2,6 +2,8 @@ import { Telemetry } from "@kilocode/kilo-telemetry" import { Agent } from "@/agent/agent" import { Bus } from "@/bus" import { TuiEvent } from "@/cli/cmd/tui/event" +import { Flag } from "@/flag/flag" +import { Global } from "@/global" import { Identifier } from "@/id/id" import { Provider } from "@/provider/provider" import { Question } from "@/question" @@ -10,6 +12,8 @@ import { LLM } from "@/session/llm" import { MessageV2 } from "@/session/message-v2" import { Todo } from "@/session/todo" import { Log } from "@/util/log" +import fs from "fs/promises" +import path from "path" function toText(item: MessageV2.WithParts): string { return item.parts @@ -111,6 +115,41 @@ export namespace PlanFollowup { export const ANSWER_NEW_SESSION = "Start new session" export const ANSWER_CONTINUE = "Continue here" + async function resolveCodeModel(model: MessageV2.User["model"]) { + const saved = + Flag.KILO_CLIENT === "cli" + ? await fs + .readFile(path.join(Global.Path.state, "model.json"), "utf-8") + .then( + (item) => + JSON.parse(item) as { + model?: Record< + string, + { + providerID: string + modelID: string + } + > + }, + ) + .then((item) => item.model?.code) + .catch(() => undefined) + : undefined + if (saved) { + const match = await Provider.getModel(saved.providerID, saved.modelID).catch(() => undefined) + if (match) { + return { + providerID: saved.providerID, + modelID: saved.modelID, + } + } + } + + const agent = await Agent.get("code") + if (agent?.model) return agent.model + return model + } + async function resolvePlan(input: { assistant?: MessageV2.WithParts messages: MessageV2.WithParts[] @@ -211,6 +250,7 @@ export namespace PlanFollowup { model: MessageV2.User["model"] abort?: AbortSignal }) { + const model = await resolveCodeModel(input.model) const [handover, todos] = await Promise.all([ generateHandover({ messages: input.messages, model: input.model, abort: input.abort }), Todo.get(input.sessionID), @@ -231,7 +271,7 @@ export namespace PlanFollowup { await inject({ sessionID: next.id, agent: "code", - model: input.model, + model, text: sections.join("\n\n"), synthetic: false, }) @@ -289,10 +329,11 @@ export namespace PlanFollowup { if (answer === ANSWER_CONTINUE) { Telemetry.trackPlanFollowup(input.sessionID, "continue") + const model = await resolveCodeModel(user.model) await inject({ sessionID: input.sessionID, agent: "code", - model: user.model, + model, text: "Implement the plan above.", }) return "continue" diff --git a/packages/opencode/test/kilocode/plan-followup.test.ts b/packages/opencode/test/kilocode/plan-followup.test.ts index 593b258906b..badc8c42ced 100644 --- a/packages/opencode/test/kilocode/plan-followup.test.ts +++ b/packages/opencode/test/kilocode/plan-followup.test.ts @@ -12,19 +12,42 @@ import { LLM } from "../../src/session/llm" import { MessageV2 } from "../../src/session/message-v2" import { SessionPrompt } from "../../src/session/prompt" import { Todo } from "../../src/session/todo" +import { Global } from "../../src/global" import { Log } from "../../src/util/log" +import path from "path" +import fs from "fs/promises" import { tmpdir } from "../fixture/fixture" Log.init({ print: false }) +process.env.KILO_CLIENT = "cli" const model = { providerID: "openai", modelID: "gpt-4", } +const saved = { + providerID: "openai", + modelID: "gpt-5", +} + +const config = { + providerID: "openai", + modelID: "gpt-4.1", +} + +const statePath = path.join(Global.Path.state, "model.json") + async function withInstance(fn: () => Promise) { await using tmp = await tmpdir({ git: true }) - await Instance.provide({ directory: tmp.path, fn }) + await fs.rm(statePath, { force: true }).catch(() => {}) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + await fs.rm(statePath, { force: true }).catch(() => {}) + await fn() + }, + }) } async function seed(input: { @@ -126,6 +149,20 @@ async function sessions() { return Array.fromAsync(Session.list()) } +async function waitQuestion(sessionID: string) { + for (let i = 0; i < 50; i++) { + const list = await Question.list() + const item = list.find((q) => q.sessionID === sessionID) + if (item) return item + await Bun.sleep(10) + } +} + +async function writeState(input: { model?: Record }) { + await fs.mkdir(Global.Path.state, { recursive: true }) + await fs.writeFile(statePath, JSON.stringify(input)) +} + const fakeAgent: Agent.Info = { name: "compaction", mode: "subagent", @@ -171,15 +208,33 @@ describe("plan follow-up", () => { abort: AbortSignal.any([]), }) - const list = await Question.list() - expect(list).toHaveLength(1) - await Question.reject(list[0].id) + const item = await waitQuestion(seeded.sessionID) + expect(item).toBeDefined() + if (!item) return + await Question.reject(item.id) await expect(pending).resolves.toBe("break") })) test("ask - returns continue and creates code message on Continue here", () => withInstance(async () => { + const get = spyOn(Agent, "get").mockImplementation(async (name: string) => { + if (name === "code") { + return { + name: "code", + mode: "primary", + permission: [], + options: {}, + model: saved, + } as any + } + return undefined as any + }) + using _ = { + [Symbol.dispose]() { + get.mockRestore() + }, + } const seeded = await seed({ text: "1. Build\n2. Test" }) const pending = PlanFollowup.ask({ sessionID: seeded.sessionID, @@ -187,9 +242,11 @@ describe("plan follow-up", () => { abort: AbortSignal.any([]), }) - const list = await Question.list() + const item = await waitQuestion(seeded.sessionID) + expect(item).toBeDefined() + if (!item) return await Question.reply({ - requestID: list[0].id, + requestID: item.id, answers: [[PlanFollowup.ANSWER_CONTINUE]], }) @@ -199,6 +256,7 @@ describe("plan follow-up", () => { expect(user?.info.role).toBe("user") if (!user || user.info.role !== "user") return expect(user.info.agent).toBe("code") + expect(user.info.model).toEqual(saved) const part = user.parts.find((item) => item.type === "text") expect(part?.type).toBe("text") @@ -216,8 +274,11 @@ describe("plan follow-up", () => { abort: AbortSignal.any([]), }) + const item = await waitQuestion(seeded.sessionID) + expect(item).toBeDefined() + if (!item) return await Question.reply({ - requestID: (await Question.list())[0].id, + requestID: item.id, answers: [["Add rollback support too"]], }) @@ -237,6 +298,24 @@ describe("plan follow-up", () => { test("ask - creates a new session on Start new session with handover and todos", () => withInstance(async () => { + const get = spyOn(Agent, "get").mockImplementation(async (name: string) => { + if (name === "code") { + return { + name: "code", + mode: "primary", + permission: [], + options: {}, + model: saved, + } as any + } + if (name === "compaction") return fakeAgent as any + return undefined as any + }) + using _file = { + [Symbol.dispose]() { + get.mockRestore() + }, + } const loop = spyOn(SessionPrompt, "loop").mockResolvedValue({ info: { id: "msg_test", @@ -268,9 +347,19 @@ describe("plan follow-up", () => { }, parts: [], }) - using _mocks = mockHandoverDeps( - "## Discoveries\n\nFound REST endpoints in src/api.ts\n\n## Relevant Files\n\n- src/api.ts: REST endpoints\n- src/db.ts: Database layer", - ) + const modelSpy = spyOn(Provider, "getModel").mockResolvedValue(fakeModel) + const llmSpy = spyOn(LLM, "stream").mockResolvedValue({ + text: Promise.resolve( + "## Discoveries\n\nFound REST endpoints in src/api.ts\n\n## Relevant Files\n\n- src/api.ts: REST endpoints\n- src/db.ts: Database layer", + ), + } as any) + using _mocks = { + llmSpy, + [Symbol.dispose]() { + modelSpy.mockRestore() + llmSpy.mockRestore() + }, + } using _loop = { [Symbol.dispose]() { loop.mockRestore() @@ -300,8 +389,11 @@ describe("plan follow-up", () => { abort: AbortSignal.any([]), }) + const item = await waitQuestion(seeded.sessionID) + expect(item).toBeDefined() + if (!item) return await Question.reply({ - requestID: (await Question.list())[0].id, + requestID: item.id, answers: [[PlanFollowup.ANSWER_NEW_SESSION]], }) @@ -323,6 +415,7 @@ describe("plan follow-up", () => { expect(user?.info.role).toBe("user") if (!user || user.info.role !== "user") throw new Error("expected seeded user message") expect(user.info.agent).toBe("code") + expect(user.info.model).toEqual(saved) const part = user.parts.find((item) => item.type === "text") expect(part?.type).toBe("text") @@ -344,6 +437,85 @@ describe("plan follow-up", () => { SessionPrompt.cancel(newSessionID) })) + test("ask - falls back to configured code model when saved CLI code model is unavailable", () => + withInstance(async () => { + await writeState({ model: { code: { providerID: "missing", modelID: "ghost" } } }) + const get = spyOn(Agent, "get").mockImplementation(async (name: string) => { + if (name === "code") { + return { + name: "code", + mode: "primary", + permission: [], + options: {}, + model: config, + } as any + } + return undefined as any + }) + using _ = { + [Symbol.dispose]() { + get.mockRestore() + }, + } + const seeded = await seed({ text: "1. Build\n2. Test" }) + const pending = PlanFollowup.ask({ + sessionID: seeded.sessionID, + messages: seeded.messages, + abort: AbortSignal.any([]), + }) + + const item = await waitQuestion(seeded.sessionID) + expect(item).toBeDefined() + if (!item) return + await Question.reply({ + requestID: item.id, + answers: [[PlanFollowup.ANSWER_CONTINUE]], + }) + + await expect(pending).resolves.toBe("continue") + + const user = await latestUser(seeded.sessionID) + expect(user?.info.role).toBe("user") + if (!user || user.info.role !== "user") return + expect(user.info.agent).toBe("code") + expect(user.info.model).toEqual(config) + })) + + test("ask - falls back to planning model when no saved or configured code model exists", () => + withInstance(async () => { + const get = spyOn(Agent, "get").mockImplementation(async (name: string) => { + if (name === "code") return undefined as any + return undefined as any + }) + using _ = { + [Symbol.dispose]() { + get.mockRestore() + }, + } + const seeded = await seed({ text: "1. Build\n2. Test" }) + const pending = PlanFollowup.ask({ + sessionID: seeded.sessionID, + messages: seeded.messages, + abort: AbortSignal.any([]), + }) + + const item = await waitQuestion(seeded.sessionID) + expect(item).toBeDefined() + if (!item) return + await Question.reply({ + requestID: item.id, + answers: [[PlanFollowup.ANSWER_CONTINUE]], + }) + + await expect(pending).resolves.toBe("continue") + + const user = await latestUser(seeded.sessionID) + expect(user?.info.role).toBe("user") + if (!user || user.info.role !== "user") return + expect(user.info.agent).toBe("code") + expect(user.info.model).toEqual(model) + })) + test("ask - new session omits handover section when LLM returns empty", () => withInstance(async () => { const loop = spyOn(SessionPrompt, "loop").mockResolvedValue({ @@ -387,8 +559,11 @@ describe("plan follow-up", () => { abort: AbortSignal.any([]), }) + const item = await waitQuestion(seeded.sessionID) + expect(item).toBeDefined() + if (!item) return await Question.reply({ - requestID: (await Question.list())[0].id, + requestID: item.id, answers: [[PlanFollowup.ANSWER_NEW_SESSION]], }) @@ -444,8 +619,9 @@ describe("plan follow-up", () => { abort: abort.signal, }) - const list = await Question.list() - expect(list).toHaveLength(1) + const item = await waitQuestion(seeded.sessionID) + expect(item).toBeDefined() + if (!item) return abort.abort() @@ -462,8 +638,11 @@ describe("plan follow-up", () => { abort: AbortSignal.any([]), }) + const item = await waitQuestion(seeded.sessionID) + expect(item).toBeDefined() + if (!item) return await Question.reply({ - requestID: (await Question.list())[0].id, + requestID: item.id, answers: [[" "]], }) From 040284034a4dbec78c917ad54e6dd617f10f2867 Mon Sep 17 00:00:00 2001 From: Josh Holmer Date: Mon, 16 Mar 2026 20:46:45 -0400 Subject: [PATCH 008/264] fix(cli): preserve selected variant as well --- .../opencode/src/kilocode/plan-followup.ts | 69 ++++++++---- .../test/kilocode/plan-followup.test.ts | 102 +++++++++++++++++- 2 files changed, 148 insertions(+), 23 deletions(-) diff --git a/packages/opencode/src/kilocode/plan-followup.ts b/packages/opencode/src/kilocode/plan-followup.ts index a95f9a69c85..4dc71b89411 100644 --- a/packages/opencode/src/kilocode/plan-followup.ts +++ b/packages/opencode/src/kilocode/plan-followup.ts @@ -115,39 +115,55 @@ export namespace PlanFollowup { export const ANSWER_NEW_SESSION = "Start new session" export const ANSWER_CONTINUE = "Continue here" - async function resolveCodeModel(model: MessageV2.User["model"]) { - const saved = + function resolveVariant(input: { value: string | undefined; model: Provider.Model | undefined }) { + if (!input.value) return undefined + if (!input.model?.variants?.[input.value]) return undefined + return input.value + } + + async function resolveCodeModel(input: Pick) { + const state = Flag.KILO_CLIENT === "cli" ? await fs .readFile(path.join(Global.Path.state, "model.json"), "utf-8") .then( (item) => JSON.parse(item) as { - model?: Record< - string, - { - providerID: string - modelID: string - } - > + model?: Record + variant?: Record }, ) - .then((item) => item.model?.code) .catch(() => undefined) : undefined + const saved = state?.model?.code if (saved) { - const match = await Provider.getModel(saved.providerID, saved.modelID).catch(() => undefined) - if (match) { + const full = await Provider.getModel(saved.providerID, saved.modelID).catch(() => undefined) + if (full) { + const key = `${saved.providerID}/${saved.modelID}` return { - providerID: saved.providerID, - modelID: saved.modelID, + model: saved, + variant: resolveVariant({ + value: state?.variant?.[key], + model: full, + }), } } } const agent = await Agent.get("code") - if (agent?.model) return agent.model - return model + if (agent?.model) { + const full = agent.variant + ? await Provider.getModel(agent.model.providerID, agent.model.modelID).catch(() => undefined) + : undefined + return { + model: agent.model, + variant: resolveVariant({ + value: agent.variant, + model: full, + }), + } + } + return input } async function resolvePlan(input: { @@ -180,6 +196,7 @@ export namespace PlanFollowup { sessionID: string agent: string model: MessageV2.User["model"] + variant?: MessageV2.User["variant"] text: string synthetic?: boolean }) { @@ -192,6 +209,7 @@ export namespace PlanFollowup { }, agent: input.agent, model: input.model, + variant: input.variant, } await Session.updateMessage(msg) await Session.updatePart({ @@ -248,9 +266,13 @@ export namespace PlanFollowup { plan: string messages: MessageV2.WithParts[] model: MessageV2.User["model"] + variant?: MessageV2.User["variant"] abort?: AbortSignal }) { - const model = await resolveCodeModel(input.model) + const code = await resolveCodeModel({ + model: input.model, + variant: input.variant, + }) const [handover, todos] = await Promise.all([ generateHandover({ messages: input.messages, model: input.model, abort: input.abort }), Todo.get(input.sessionID), @@ -271,7 +293,8 @@ export namespace PlanFollowup { await inject({ sessionID: next.id, agent: "code", - model, + model: code.model, + variant: code.variant, text: sections.join("\n\n"), synthetic: false, }) @@ -322,6 +345,7 @@ export namespace PlanFollowup { plan, messages: input.messages, model: user.model, + variant: user.variant, abort: input.abort, }) return "break" @@ -329,11 +353,15 @@ export namespace PlanFollowup { if (answer === ANSWER_CONTINUE) { Telemetry.trackPlanFollowup(input.sessionID, "continue") - const model = await resolveCodeModel(user.model) + const code = await resolveCodeModel({ + model: user.model, + variant: user.variant, + }) await inject({ sessionID: input.sessionID, agent: "code", - model, + model: code.model, + variant: code.variant, text: "Implement the plan above.", }) return "continue" @@ -344,6 +372,7 @@ export namespace PlanFollowup { sessionID: input.sessionID, agent: "plan", model: user.model, + variant: user.variant, text: answer, }) return "continue" diff --git a/packages/opencode/test/kilocode/plan-followup.test.ts b/packages/opencode/test/kilocode/plan-followup.test.ts index badc8c42ced..bc19cdbc5ce 100644 --- a/packages/opencode/test/kilocode/plan-followup.test.ts +++ b/packages/opencode/test/kilocode/plan-followup.test.ts @@ -31,12 +31,18 @@ const saved = { modelID: "gpt-5", } +const savedVar = "high" + const config = { providerID: "openai", modelID: "gpt-4.1", } +const configVar = "max" +const planVar = "medium" + const statePath = path.join(Global.Path.state, "model.json") +const savedKey = `${saved.providerID}/${saved.modelID}` async function withInstance(fn: () => Promise) { await using tmp = await tmpdir({ git: true }) @@ -52,6 +58,7 @@ async function withInstance(fn: () => Promise) { async function seed(input: { text: string + variant?: string tools?: Array<{ tool: string; input: Record; output: string }> }) { const session = await Session.create({}) @@ -64,6 +71,7 @@ async function seed(input: { }, agent: "plan", model, + variant: input.variant, }) await Session.updatePart({ id: Identifier.ascending("part"), @@ -158,7 +166,10 @@ async function waitQuestion(sessionID: string) { } } -async function writeState(input: { model?: Record }) { +async function writeState(input: { + model?: Record + variant?: Record +}) { await fs.mkdir(Global.Path.state, { recursive: true }) await fs.writeFile(statePath, JSON.stringify(input)) } @@ -178,6 +189,19 @@ const fakeModel = { capabilities: {}, } as Provider.Model +function full(input: { providerID: string; modelID: string }, vars: string[]) { + return { + ...fakeModel, + id: input.modelID, + providerID: input.providerID, + variants: Object.fromEntries(vars.map((item) => [item, {}])), + } as Provider.Model +} + +const savedFull = full(saved, [savedVar, "low"]) +const savedConfigFull = full(saved, [configVar, "low"]) +const configFull = full(config, [configVar, "low"]) + function mockHandoverDeps(text: string, opts?: { agent?: Agent.Info | null }) { const agentSpy = spyOn(Agent, "get").mockResolvedValue( (opts?.agent === null ? undefined : (opts?.agent ?? fakeAgent)) as any, @@ -226,13 +250,16 @@ describe("plan follow-up", () => { permission: [], options: {}, model: saved, + variant: configVar, } as any } return undefined as any }) + const modelSpy = spyOn(Provider, "getModel").mockResolvedValue(savedConfigFull) using _ = { [Symbol.dispose]() { get.mockRestore() + modelSpy.mockRestore() }, } const seeded = await seed({ text: "1. Build\n2. Test" }) @@ -257,6 +284,7 @@ describe("plan follow-up", () => { if (!user || user.info.role !== "user") return expect(user.info.agent).toBe("code") expect(user.info.model).toEqual(saved) + expect(user.info.variant).toBe(configVar) const part = user.parts.find((item) => item.type === "text") expect(part?.type).toBe("text") @@ -306,6 +334,7 @@ describe("plan follow-up", () => { permission: [], options: {}, model: saved, + variant: configVar, } as any } if (name === "compaction") return fakeAgent as any @@ -347,7 +376,10 @@ describe("plan follow-up", () => { }, parts: [], }) - const modelSpy = spyOn(Provider, "getModel").mockResolvedValue(fakeModel) + const modelSpy = spyOn(Provider, "getModel").mockImplementation(async (providerID: string, modelID: string) => { + if (providerID === saved.providerID && modelID === saved.modelID) return savedConfigFull + return fakeModel + }) const llmSpy = spyOn(LLM, "stream").mockResolvedValue({ text: Promise.resolve( "## Discoveries\n\nFound REST endpoints in src/api.ts\n\n## Relevant Files\n\n- src/api.ts: REST endpoints\n- src/db.ts: Database layer", @@ -416,6 +448,7 @@ describe("plan follow-up", () => { if (!user || user.info.role !== "user") throw new Error("expected seeded user message") expect(user.info.agent).toBe("code") expect(user.info.model).toEqual(saved) + expect(user.info.variant).toBe(configVar) const part = user.parts.find((item) => item.type === "text") expect(part?.type).toBe("text") @@ -437,6 +470,61 @@ describe("plan follow-up", () => { SessionPrompt.cancel(newSessionID) })) + test("ask - prefers saved code variant over configured code variant", () => + withInstance(async () => { + await writeState({ + model: { code: saved }, + variant: { [savedKey]: savedVar }, + }) + const get = spyOn(Agent, "get").mockImplementation(async (name: string) => { + if (name === "code") { + return { + name: "code", + mode: "primary", + permission: [], + options: {}, + model: config, + variant: configVar, + } as any + } + return undefined as any + }) + const modelSpy = spyOn(Provider, "getModel").mockImplementation(async (providerID: string, modelID: string) => { + if (providerID === saved.providerID && modelID === saved.modelID) return savedFull + if (providerID === config.providerID && modelID === config.modelID) return configFull + throw new Error(`unexpected model lookup ${providerID}/${modelID}`) + }) + using _ = { + [Symbol.dispose]() { + get.mockRestore() + modelSpy.mockRestore() + }, + } + const seeded = await seed({ text: "1. Build\n2. Test" }) + const pending = PlanFollowup.ask({ + sessionID: seeded.sessionID, + messages: seeded.messages, + abort: AbortSignal.any([]), + }) + + const item = await waitQuestion(seeded.sessionID) + expect(item).toBeDefined() + if (!item) return + await Question.reply({ + requestID: item.id, + answers: [[PlanFollowup.ANSWER_CONTINUE]], + }) + + await expect(pending).resolves.toBe("continue") + + const user = await latestUser(seeded.sessionID) + expect(user?.info.role).toBe("user") + if (!user || user.info.role !== "user") return + expect(user.info.agent).toBe("code") + expect(user.info.model).toEqual(saved) + expect(user.info.variant).toBe(savedVar) + })) + test("ask - falls back to configured code model when saved CLI code model is unavailable", () => withInstance(async () => { await writeState({ model: { code: { providerID: "missing", modelID: "ghost" } } }) @@ -448,13 +536,19 @@ describe("plan follow-up", () => { permission: [], options: {}, model: config, + variant: configVar, } as any } return undefined as any }) + const modelSpy = spyOn(Provider, "getModel").mockImplementation(async (providerID: string, modelID: string) => { + if (providerID === "missing" && modelID === "ghost") throw new Error("missing model") + return configFull + }) using _ = { [Symbol.dispose]() { get.mockRestore() + modelSpy.mockRestore() }, } const seeded = await seed({ text: "1. Build\n2. Test" }) @@ -479,6 +573,7 @@ describe("plan follow-up", () => { if (!user || user.info.role !== "user") return expect(user.info.agent).toBe("code") expect(user.info.model).toEqual(config) + expect(user.info.variant).toBe(configVar) })) test("ask - falls back to planning model when no saved or configured code model exists", () => @@ -492,7 +587,7 @@ describe("plan follow-up", () => { get.mockRestore() }, } - const seeded = await seed({ text: "1. Build\n2. Test" }) + const seeded = await seed({ text: "1. Build\n2. Test", variant: planVar }) const pending = PlanFollowup.ask({ sessionID: seeded.sessionID, messages: seeded.messages, @@ -514,6 +609,7 @@ describe("plan follow-up", () => { if (!user || user.info.role !== "user") return expect(user.info.agent).toBe("code") expect(user.info.model).toEqual(model) + expect(user.info.variant).toBe(planVar) })) test("ask - new session omits handover section when LLM returns empty", () => From b74d4b4481814f776ce1369aa4e1a589a6e4a484 Mon Sep 17 00:00:00 2001 From: Josh Holmer Date: Thu, 19 Mar 2026 10:47:05 -0400 Subject: [PATCH 009/264] fix(cli): improve agent handling --- .../opencode/src/kilocode/plan-followup.ts | 20 ++++++++++--------- .../test/kilocode/plan-followup.test.ts | 6 +++++- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/opencode/src/kilocode/plan-followup.ts b/packages/opencode/src/kilocode/plan-followup.ts index 4dc71b89411..cc6408b6655 100644 --- a/packages/opencode/src/kilocode/plan-followup.ts +++ b/packages/opencode/src/kilocode/plan-followup.ts @@ -152,15 +152,17 @@ export namespace PlanFollowup { const agent = await Agent.get("code") if (agent?.model) { - const full = agent.variant - ? await Provider.getModel(agent.model.providerID, agent.model.modelID).catch(() => undefined) - : undefined - return { - model: agent.model, - variant: resolveVariant({ - value: agent.variant, - model: full, - }), + const full = await Provider.getModel(agent.model.providerID, agent.model.modelID).catch(() => undefined) + if (full) { + return { + model: agent.model, + variant: agent.variant + ? resolveVariant({ + value: agent.variant, + model: full, + }) + : undefined, + } } } return input diff --git a/packages/opencode/test/kilocode/plan-followup.test.ts b/packages/opencode/test/kilocode/plan-followup.test.ts index bc19cdbc5ce..5ee3475e539 100644 --- a/packages/opencode/test/kilocode/plan-followup.test.ts +++ b/packages/opencode/test/kilocode/plan-followup.test.ts @@ -51,7 +51,11 @@ async function withInstance(fn: () => Promise) { directory: tmp.path, fn: async () => { await fs.rm(statePath, { force: true }).catch(() => {}) - await fn() + try { + await fn() + } finally { + await fs.rm(statePath, { force: true }).catch(() => {}) + } }, }) } From 4c9bc9ca0f72042501eac9ac4f94763a76dc362f Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 21:10:41 +0000 Subject: [PATCH 010/264] fix(vscode): add circuit breaker and backoff for autocomplete API errors When credits are depleted (402), auth is invalid (401/403), or the server is rate-limiting (429) / erroring (5xx), autocomplete requests are now blocked instead of firing on every keystroke. - Add ErrorBackoff class with fatal/retriable/transient error classification - Fatal errors (402/401/403) block requests until explicitly reset - Retriable errors (429/5xx) use exponential backoff with circuit breaker - Show user notification when credits are depleted with link to add credits - Reset backoff on successful response or CLI backend reconnection - Add 32 unit tests for ErrorBackoff Closes #7488 --- .../AutocompleteServiceManager.ts | 25 +++ .../AutocompleteInlineCompletionProvider.ts | 30 +++ .../classic-auto-complete/ErrorBackoff.ts | 145 +++++++++++++ .../src/services/autocomplete/i18n/en.ts | 5 + .../unit/autocomplete-error-backoff.test.ts | 199 ++++++++++++++++++ 5 files changed, 404 insertions(+) create mode 100644 packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/ErrorBackoff.ts create mode 100644 packages/kilo-vscode/tests/unit/autocomplete-error-backoff.test.ts diff --git a/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts b/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts index 91cc34284e8..fb9de205702 100644 --- a/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts +++ b/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts @@ -84,11 +84,15 @@ export class AutocompleteServiceManager { () => this.settings, workspacePath, new AutocompleteTelemetry(), + (status) => this.handleFatalAutocompleteError(status), ) // Reload when CLI backend connection state changes so autocomplete // picks up the connected state even if it wasn't ready at startup. + // Also reset error backoff — a reconnect may mean the user re-authenticated + // or added credits, so we should give autocomplete a fresh chance. this.unsubscribeState = connectionService.onStateChange(() => { + this.inlineCompletionProvider.backoff.reset() void this.load() }) @@ -312,6 +316,27 @@ export class AutocompleteServiceManager { return !this.model.hasValidCredentials() } + /** + * Handle a fatal (non-retriable) autocomplete error such as 402 Payment Required. + * Shows a one-time notification to the user so they know autocomplete is paused. + */ + private handleFatalAutocompleteError(status: number | null): void { + const msg = + status === 402 + ? t("kilocode:autocomplete.creditsExhausted.message") + : t("kilocode:autocomplete.authError.message") + + if (status === 402) { + vscode.window.showWarningMessage(msg, t("kilocode:autocomplete.creditsExhausted.addCredits")).then((choice) => { + if (choice === t("kilocode:autocomplete.creditsExhausted.addCredits")) { + vscode.env.openExternal(vscode.Uri.parse("https://kilo.ai/credits")) + } + }) + } else { + vscode.window.showWarningMessage(msg) + } + } + private updateCostTracking(cost: number, _inputTokens: number, _outputTokens: number): void { this.completionCount++ this.sessionCost += cost diff --git a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteInlineCompletionProvider.ts b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteInlineCompletionProvider.ts index dd8d0f6069f..d72be5dfb78 100644 --- a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteInlineCompletionProvider.ts +++ b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteInlineCompletionProvider.ts @@ -34,6 +34,7 @@ import { postprocessAutocompleteSuggestion } from "./uselessSuggestionFilter" import { shouldSkipAutocomplete } from "./contextualSkip" import { FileIgnoreController } from "../shims/FileIgnoreController" import { AutocompleteTelemetry } from "./AutocompleteTelemetry" +import { ErrorBackoff } from "./ErrorBackoff" const MAX_SUGGESTIONS_HISTORY = 20 @@ -137,6 +138,12 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple private telemetry: AutocompleteTelemetry | null /** Information about the last suggestion shown to the user */ private lastSuggestion: LastSuggestionInfo | null = null + /** Circuit breaker / exponential backoff for API errors */ + public readonly backoff = new ErrorBackoff() + /** Optional callback fired once when a fatal (non-retriable) error is first detected */ + private onFatalError: ((status: number | null) => void) | null = null + /** Whether the fatal error notification has already been fired (avoid repeating) */ + private fatalNotified = false constructor( context: vscode.ExtensionContext, @@ -145,11 +152,13 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple getSettings: () => AutocompleteServiceSettings | null, workspacePath: string, telemetry: AutocompleteTelemetry | null = null, + onFatalError?: (status: number | null) => void, ) { this.telemetry = telemetry this.model = model this.costTrackingCallback = costTrackingCallback this.getSettings = getSettings + this.onFatalError = onFatalError ?? null this.ignoreController = (async () => { const ignoreController = new FileIgnoreController(workspacePath) @@ -333,6 +342,14 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple return [] } + // Circuit breaker / backoff: skip requests when the API is returning errors. + // This prevents flooding the API with thousands of failed requests when + // credits are depleted (402), auth is invalid (401/403), or the server + // is rate-limiting (429) / having issues (5xx). + if (this.backoff.blocked()) { + return [] + } + if (!document?.uri?.fsPath) { return [] } @@ -562,6 +579,10 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple this.costTrackingCallback(result.cost, result.inputTokens, result.outputTokens) + // Successful response — reset any backoff / circuit breaker state + this.backoff.success() + this.fatalNotified = false + // Always update suggestions, even if text is empty (for caching) this.updateSuggestions(result.suggestion) } catch (error) { @@ -576,6 +597,15 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple }, telemetryContext, ) + + // Update circuit breaker / backoff state based on the error kind + const kind = this.backoff.failure(error) + + // Notify once when a fatal error (402/401/403) is first detected + if (kind === "fatal" && !this.fatalNotified) { + this.fatalNotified = true + this.onFatalError?.(this.backoff.getFatalStatus()) + } } } } diff --git a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/ErrorBackoff.ts b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/ErrorBackoff.ts new file mode 100644 index 00000000000..0c4e812e409 --- /dev/null +++ b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/ErrorBackoff.ts @@ -0,0 +1,145 @@ +/** + * Circuit breaker and exponential backoff for autocomplete request errors. + * + * Classifies errors into: + * - **fatal**: 401, 402, 403 — stops all requests until explicitly reset + * - **retriable**: 429, 5xx, network errors — exponential backoff with cap + * - **transient**: abort, unknown — no special handling + * + * When a fatal error (like 402 Payment Required) is detected, autocomplete + * requests are blocked entirely to prevent thousands of wasted API calls. + */ + +/** Base backoff delay in ms for retriable errors */ +const BASE_DELAY_MS = 2_000 + +/** Maximum backoff delay in ms (2 minutes) */ +const MAX_DELAY_MS = 120_000 + +/** Number of consecutive retriable failures before circuit opens */ +const CIRCUIT_THRESHOLD = 5 + +/** Duration in ms the circuit stays open before allowing a probe (5 minutes) */ +const CIRCUIT_COOLDOWN_MS = 300_000 + +export type ErrorKind = "fatal" | "retriable" | "transient" + +/** + * Extract an HTTP status code from an error message like "SSE failed: 402 Payment Required" + * or "FIM request failed: 429 Too Many Requests". + */ +export function extractStatus(error: unknown): number | null { + const msg = error instanceof Error ? error.message : String(error) + const match = msg.match(/\b([1-5]\d{2})\b/) + return match ? Number(match[1]) : null +} + +/** + * Classify an error into one of three categories based on its HTTP status code. + */ +export function classify(error: unknown): ErrorKind { + const status = extractStatus(error) + if (status === null) return "transient" + if (status === 401 || status === 402 || status === 403) return "fatal" + if (status === 429 || status >= 500) return "retriable" + return "transient" +} + +export class ErrorBackoff { + /** The kind of the most recent fatal error, if any */ + private fatal: ErrorKind | null = null + /** HTTP status of the fatal error (for notification messages) */ + private fatalStatus: number | null = null + /** Timestamp when the circuit was opened (for retriable circuit breaker) */ + private opened = 0 + /** Consecutive retriable failure count */ + private failures = 0 + /** Timestamp until which requests should be delayed (retriable backoff) */ + private blockedUntil = 0 + + /** + * Record a successful request. Resets all backoff/circuit state. + */ + success(): void { + this.fatal = null + this.fatalStatus = null + this.failures = 0 + this.blockedUntil = 0 + this.opened = 0 + } + + /** + * Record an error and update backoff state. + * Returns the error kind for the caller to act on. + */ + failure(error: unknown): ErrorKind { + const kind = classify(error) + + if (kind === "fatal") { + this.fatal = kind + this.fatalStatus = extractStatus(error) + return kind + } + + if (kind === "retriable") { + this.failures++ + // Exponential backoff: 2s, 4s, 8s, 16s, … capped at MAX_DELAY_MS + const delay = Math.min(BASE_DELAY_MS * 2 ** (this.failures - 1), MAX_DELAY_MS) + this.blockedUntil = Date.now() + delay + + // Open circuit after threshold consecutive failures + if (this.failures >= CIRCUIT_THRESHOLD && this.opened === 0) { + this.opened = Date.now() + } + return kind + } + + // transient — no state change + return kind + } + + /** + * Whether autocomplete requests should be blocked right now. + */ + blocked(): boolean { + // Fatal errors block permanently until reset + if (this.fatal) return true + + // Circuit breaker is open + if (this.opened > 0) { + const elapsed = Date.now() - this.opened + if (elapsed < CIRCUIT_COOLDOWN_MS) return true + // Cooldown expired — allow one probe, reset circuit + this.opened = 0 + this.failures = 0 + this.blockedUntil = 0 + return false + } + + // Exponential backoff still active + if (this.blockedUntil > 0 && Date.now() < this.blockedUntil) return true + + return false + } + + /** + * Whether a fatal (non-retriable) error is active — credits depleted, auth invalid, etc. + */ + isFatal(): boolean { + return this.fatal !== null + } + + /** + * The HTTP status code of the fatal error, or null. + */ + getFatalStatus(): number | null { + return this.fatalStatus + } + + /** + * Manually reset all state (e.g. when user adds credits or re-authenticates). + */ + reset(): void { + this.success() + } +} diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/en.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/en.ts index bd536d23c07..c9d7fb229c8 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/en.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/en.ts @@ -41,4 +41,9 @@ export const dict = { "The Kilo Code Autocomplete is being blocked by a conflict with GitHub Copilot. To fix this, you must disable Copilot's inline suggestions.", "kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Disable Copilot", "kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Disable Autocomplete", + "kilocode:autocomplete.creditsExhausted.message": + "Kilo Code Autocomplete has been paused because your account has no remaining credits. Add credits to resume autocomplete.", + "kilocode:autocomplete.creditsExhausted.addCredits": "Add Credits", + "kilocode:autocomplete.authError.message": + "Kilo Code Autocomplete has been paused due to an authentication error. Please sign in again.", } diff --git a/packages/kilo-vscode/tests/unit/autocomplete-error-backoff.test.ts b/packages/kilo-vscode/tests/unit/autocomplete-error-backoff.test.ts new file mode 100644 index 00000000000..0a8f234595e --- /dev/null +++ b/packages/kilo-vscode/tests/unit/autocomplete-error-backoff.test.ts @@ -0,0 +1,199 @@ +import { describe, it, expect, beforeEach } from "bun:test" +import { + ErrorBackoff, + extractStatus, + classify, +} from "../../src/services/autocomplete/classic-auto-complete/ErrorBackoff" + +describe("extractStatus", () => { + it("extracts 402 from SSE error message", () => { + expect(extractStatus(new Error("SSE failed: 402 Payment Required"))).toBe(402) + }) + + it("extracts 429 from FIM error message", () => { + expect(extractStatus(new Error("FIM request failed: 429 Too Many Requests"))).toBe(429) + }) + + it("extracts 500 from generic error", () => { + expect(extractStatus(new Error("Server returned 500 Internal Server Error"))).toBe(500) + }) + + it("returns null for message without status code", () => { + expect(extractStatus(new Error("Connection timed out"))).toBeNull() + }) + + it("handles string errors", () => { + expect(extractStatus("SSE failed: 401 Unauthorized")).toBe(401) + }) + + it("returns null for empty error", () => { + expect(extractStatus(new Error(""))).toBeNull() + }) +}) + +describe("classify", () => { + it("classifies 402 as fatal", () => { + expect(classify(new Error("SSE failed: 402 Payment Required"))).toBe("fatal") + }) + + it("classifies 401 as fatal", () => { + expect(classify(new Error("SSE failed: 401 Unauthorized"))).toBe("fatal") + }) + + it("classifies 403 as fatal", () => { + expect(classify(new Error("SSE failed: 403 Forbidden"))).toBe("fatal") + }) + + it("classifies 429 as retriable", () => { + expect(classify(new Error("SSE failed: 429 Too Many Requests"))).toBe("retriable") + }) + + it("classifies 500 as retriable", () => { + expect(classify(new Error("SSE failed: 500 Internal Server Error"))).toBe("retriable") + }) + + it("classifies 503 as retriable", () => { + expect(classify(new Error("SSE failed: 503 Service Unavailable"))).toBe("retriable") + }) + + it("classifies no-status errors as transient", () => { + expect(classify(new Error("Connection timed out"))).toBe("transient") + }) + + it("classifies 404 as transient", () => { + expect(classify(new Error("SSE failed: 404 Not Found"))).toBe("transient") + }) +}) + +describe("ErrorBackoff", () => { + let backoff: ErrorBackoff + + beforeEach(() => { + backoff = new ErrorBackoff() + }) + + it("is not blocked initially", () => { + expect(backoff.blocked()).toBe(false) + }) + + it("is not fatal initially", () => { + expect(backoff.isFatal()).toBe(false) + expect(backoff.getFatalStatus()).toBeNull() + }) + + describe("fatal errors (402/401/403)", () => { + it("blocks after a 402 error", () => { + backoff.failure(new Error("SSE failed: 402 Payment Required")) + expect(backoff.blocked()).toBe(true) + expect(backoff.isFatal()).toBe(true) + expect(backoff.getFatalStatus()).toBe(402) + }) + + it("blocks after a 401 error", () => { + backoff.failure(new Error("SSE failed: 401 Unauthorized")) + expect(backoff.blocked()).toBe(true) + expect(backoff.isFatal()).toBe(true) + expect(backoff.getFatalStatus()).toBe(401) + }) + + it("blocks after a 403 error", () => { + backoff.failure(new Error("SSE failed: 403 Forbidden")) + expect(backoff.blocked()).toBe(true) + expect(backoff.isFatal()).toBe(true) + expect(backoff.getFatalStatus()).toBe(403) + }) + + it("remains blocked until explicitly reset", () => { + backoff.failure(new Error("SSE failed: 402 Payment Required")) + expect(backoff.blocked()).toBe(true) + + // Even after a long time, still blocked + expect(backoff.blocked()).toBe(true) + }) + + it("unblocks after reset()", () => { + backoff.failure(new Error("SSE failed: 402 Payment Required")) + expect(backoff.blocked()).toBe(true) + + backoff.reset() + expect(backoff.blocked()).toBe(false) + expect(backoff.isFatal()).toBe(false) + expect(backoff.getFatalStatus()).toBeNull() + }) + + it("unblocks after success()", () => { + backoff.failure(new Error("SSE failed: 402 Payment Required")) + expect(backoff.blocked()).toBe(true) + + backoff.success() + expect(backoff.blocked()).toBe(false) + expect(backoff.isFatal()).toBe(false) + }) + }) + + describe("retriable errors (429/5xx)", () => { + it("blocks after a retriable error with backoff", () => { + backoff.failure(new Error("SSE failed: 429 Too Many Requests")) + expect(backoff.blocked()).toBe(true) + }) + + it("returns correct error kind", () => { + const kind = backoff.failure(new Error("SSE failed: 500 Internal Server Error")) + expect(kind).toBe("retriable") + }) + + it("resets on success", () => { + backoff.failure(new Error("SSE failed: 500 Internal Server Error")) + expect(backoff.blocked()).toBe(true) + + backoff.success() + expect(backoff.blocked()).toBe(false) + }) + }) + + describe("transient errors", () => { + it("does not block after a transient error", () => { + backoff.failure(new Error("Connection timed out")) + expect(backoff.blocked()).toBe(false) + }) + + it("returns transient kind", () => { + const kind = backoff.failure(new Error("Connection timed out")) + expect(kind).toBe("transient") + }) + }) + + describe("failure() return value", () => { + it("returns 'fatal' for 402", () => { + expect(backoff.failure(new Error("SSE failed: 402 Payment Required"))).toBe("fatal") + }) + + it("returns 'retriable' for 429", () => { + expect(backoff.failure(new Error("SSE failed: 429 Too Many Requests"))).toBe("retriable") + }) + + it("returns 'transient' for unknown errors", () => { + expect(backoff.failure(new Error("Unknown error"))).toBe("transient") + }) + }) + + describe("mixed scenarios", () => { + it("fatal error overrides retriable backoff", () => { + backoff.failure(new Error("SSE failed: 500 Internal Server Error")) + backoff.failure(new Error("SSE failed: 402 Payment Required")) + expect(backoff.isFatal()).toBe(true) + expect(backoff.blocked()).toBe(true) + }) + + it("success resets everything", () => { + backoff.failure(new Error("SSE failed: 500 Internal Server Error")) + backoff.failure(new Error("SSE failed: 500 Internal Server Error")) + backoff.failure(new Error("SSE failed: 500 Internal Server Error")) + expect(backoff.blocked()).toBe(true) + + backoff.success() + expect(backoff.blocked()).toBe(false) + expect(backoff.isFatal()).toBe(false) + }) + }) +}) From 788a012aadf5264e239ef3bc583760213588f908 Mon Sep 17 00:00:00 2001 From: kirillk Date: Mon, 23 Mar 2026 17:23:13 -0400 Subject: [PATCH 011/264] feat(vscode): restyle onboarding notifications banner with wizard-style navigation - Replace X dismiss button with Previous/Next/Close navigation flow - Show X/Y progress indicator in header when multiple notifications - Add outlined button style for action buttons using VS Code theme tokens - Raise z-index to 1000 so banner renders above membership panel - Add i18n keys for Previous, Next, Close, and Try model across all 18 locales --- .../src/components/chat/KiloNotifications.tsx | 60 +++++++++--------- .../kilo-vscode/webview-ui/src/i18n/ar.ts | 4 ++ .../kilo-vscode/webview-ui/src/i18n/br.ts | 4 ++ .../kilo-vscode/webview-ui/src/i18n/bs.ts | 4 ++ .../kilo-vscode/webview-ui/src/i18n/da.ts | 4 ++ .../kilo-vscode/webview-ui/src/i18n/de.ts | 4 ++ .../kilo-vscode/webview-ui/src/i18n/en.ts | 5 ++ .../kilo-vscode/webview-ui/src/i18n/es.ts | 4 ++ .../kilo-vscode/webview-ui/src/i18n/fr.ts | 4 ++ .../kilo-vscode/webview-ui/src/i18n/ja.ts | 4 ++ .../kilo-vscode/webview-ui/src/i18n/ko.ts | 4 ++ .../kilo-vscode/webview-ui/src/i18n/nl.ts | 4 ++ .../kilo-vscode/webview-ui/src/i18n/no.ts | 4 ++ .../kilo-vscode/webview-ui/src/i18n/pl.ts | 4 ++ .../kilo-vscode/webview-ui/src/i18n/ru.ts | 4 ++ .../kilo-vscode/webview-ui/src/i18n/th.ts | 4 ++ .../kilo-vscode/webview-ui/src/i18n/tr.ts | 4 ++ .../kilo-vscode/webview-ui/src/i18n/zh.ts | 4 ++ .../kilo-vscode/webview-ui/src/i18n/zht.ts | 4 ++ .../webview-ui/src/styles/chat.css | 61 ++++++++++++++++++- 20 files changed, 163 insertions(+), 31 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/KiloNotifications.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/KiloNotifications.tsx index 2bacc16c8c5..9b839e73194 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/KiloNotifications.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/KiloNotifications.tsx @@ -1,11 +1,9 @@ import { Component, Show, createMemo, createSignal } from "solid-js" -import { Button } from "@kilocode/kilo-ui/button" -import { IconButton } from "@kilocode/kilo-ui/icon-button" -import { Icon } from "@kilocode/kilo-ui/icon" import { useNotifications } from "../../context/notifications" import { useVSCode } from "../../context/vscode" import { useSession } from "../../context/session" import { useProvider } from "../../context/provider" +import { useLanguage } from "../../context/language" import { KILO_PROVIDER_ID } from "../../../../src/shared/provider-model" import { TelemetryEventName } from "../../../../src/services/telemetry/types" @@ -14,6 +12,7 @@ export const KiloNotifications: Component = () => { const vscode = useVSCode() const session = useSession() const provider = useProvider() + const language = useLanguage() const [index, setIndex] = createSignal(0) const items = filteredNotifications @@ -21,18 +20,18 @@ export const KiloNotifications: Component = () => { const safeIndex = () => Math.min(index(), Math.max(0, total() - 1)) const current = createMemo(() => (total() === 0 ? undefined : items()[safeIndex()])) - const prev = () => setIndex((i) => (i - 1 + total()) % total()) - const next = () => setIndex((i) => (i + 1) % total()) - const handleAction = (url: string) => { vscode.postMessage({ type: "openExternal", url }) } - const handleDismiss = () => { - const n = current() - if (!n) return - dismiss(n.id) - setIndex((i) => Math.min(i, Math.max(0, total() - 2))) + const isLast = () => safeIndex() === total() - 1 + + const handleNext = () => { + if (isLast()) { + for (const n of items()) dismiss(n.id) + } else { + setIndex((i) => i + 1) + } } /** @@ -73,35 +72,36 @@ export const KiloNotifications: Component = () => {
{current()?.title} - + 1}> + + {safeIndex() + 1} / {total()} + +

{current()?.message}

diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 9c5f727ae78..f2b84d80c79 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -1188,4 +1188,8 @@ export const dict = { "settings.saveBar.warning.many": "عدة جلسات تعمل وستتوقف", "settings.saveBar.saveAnyway": "حفظ على أي حال", "settings.saveBar.cancel": "إلغاء", + "notifications.action.previous": "Previous", + "notifications.action.next": "Next", + "notifications.action.close": "Close", + "notifications.action.tryModel": "Try model", } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 6bb8a8357c9..003bbe2bcbf 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -1211,4 +1211,8 @@ export const dict = { "settings.saveBar.warning.many": "Várias sessões estão em execução e serão interrompidas", "settings.saveBar.saveAnyway": "Salvar mesmo assim", "settings.saveBar.cancel": "Cancelar", + "notifications.action.previous": "Previous", + "notifications.action.next": "Next", + "notifications.action.close": "Close", + "notifications.action.tryModel": "Try model", } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index 6bffbb3ca30..8d9a97cccd2 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -1210,4 +1210,8 @@ export const dict = { "settings.saveBar.warning.many": "Nekoliko sesija je pokrenuto i bit će prekinuto", "settings.saveBar.saveAnyway": "Spremi svejedno", "settings.saveBar.cancel": "Otkaži", + "notifications.action.previous": "Previous", + "notifications.action.next": "Next", + "notifications.action.close": "Close", + "notifications.action.tryModel": "Try model", } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index 512571f61b6..1e05686dad3 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -1203,4 +1203,8 @@ export const dict = { "settings.saveBar.warning.many": "Flere sessioner kører og vil blive afbrudt", "settings.saveBar.saveAnyway": "Gem alligevel", "settings.saveBar.cancel": "Annuller", + "notifications.action.previous": "Previous", + "notifications.action.next": "Next", + "notifications.action.close": "Close", + "notifications.action.tryModel": "Try model", } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 5f6018e047c..c95c970bdf3 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -1225,4 +1225,8 @@ export const dict = { "settings.saveBar.warning.many": "Mehrere Sitzungen laufen und werden unterbrochen", "settings.saveBar.saveAnyway": "Trotzdem speichern", "settings.saveBar.cancel": "Abbrechen", + "notifications.action.previous": "Previous", + "notifications.action.next": "Next", + "notifications.action.close": "Close", + "notifications.action.tryModel": "Try model", } satisfies Partial> diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index 7558d52c738..b508de7a234 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -1216,4 +1216,9 @@ export const dict = { "settings.saveBar.warning.many": "Several sessions are running and will be interrupted", "settings.saveBar.saveAnyway": "Save anyway", "settings.saveBar.cancel": "Cancel", + + "notifications.action.previous": "Previous", + "notifications.action.next": "Next", + "notifications.action.close": "Close", + "notifications.action.tryModel": "Try model", } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index 4bf2c991a0e..7fbd52c4aac 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -1214,4 +1214,8 @@ export const dict = { "settings.saveBar.warning.many": "Varias sesiones están en ejecución y se interrumpirán", "settings.saveBar.saveAnyway": "Guardar de todas formas", "settings.saveBar.cancel": "Cancelar", + "notifications.action.previous": "Previous", + "notifications.action.next": "Next", + "notifications.action.close": "Close", + "notifications.action.tryModel": "Try model", } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index 6f3a2067355..56070e8e505 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -1226,4 +1226,8 @@ export const dict = { "settings.saveBar.warning.many": "Plusieurs sessions sont en cours et seront interrompues", "settings.saveBar.saveAnyway": "Enregistrer quand même", "settings.saveBar.cancel": "Annuler", + "notifications.action.previous": "Previous", + "notifications.action.next": "Next", + "notifications.action.close": "Close", + "notifications.action.tryModel": "Try model", } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index df4a29e69db..6a4ebbb7dba 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -1200,4 +1200,8 @@ export const dict = { "settings.saveBar.warning.many": "複数のセッションが実行中で中断されます", "settings.saveBar.saveAnyway": "それでも保存", "settings.saveBar.cancel": "キャンセル", + "notifications.action.previous": "Previous", + "notifications.action.next": "Next", + "notifications.action.close": "Close", + "notifications.action.tryModel": "Try model", } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 1935ce8b9c4..cb6e9d95115 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -1191,4 +1191,8 @@ export const dict = { "settings.saveBar.warning.many": "여러 세션이 실행 중이며 중단됩니다", "settings.saveBar.saveAnyway": "그래도 저장", "settings.saveBar.cancel": "취소", + "notifications.action.previous": "Previous", + "notifications.action.next": "Next", + "notifications.action.close": "Close", + "notifications.action.tryModel": "Try model", } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index 0ec1a54c039..456236caf96 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -1232,4 +1232,8 @@ export const dict = { "settings.saveBar.warning.many": "Meerdere sessies zijn actief en worden onderbroken", "settings.saveBar.saveAnyway": "Toch opslaan", "settings.saveBar.cancel": "Annuleren", + "notifications.action.previous": "Previous", + "notifications.action.next": "Next", + "notifications.action.close": "Close", + "notifications.action.tryModel": "Try model", } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 41f5d36b390..f6a325738bd 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -1201,4 +1201,8 @@ export const dict = { "settings.saveBar.warning.many": "Flere økter kjører og vil bli avbrutt", "settings.saveBar.saveAnyway": "Lagre uansett", "settings.saveBar.cancel": "Avbryt", + "notifications.action.previous": "Previous", + "notifications.action.next": "Next", + "notifications.action.close": "Close", + "notifications.action.tryModel": "Try model", } satisfies Partial> diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 24e68ab8ba5..c5ded7a8b31 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -1208,4 +1208,8 @@ export const dict = { "settings.saveBar.warning.many": "Kilka sesji jest uruchomionych i zostanie przerwanych", "settings.saveBar.saveAnyway": "Zapisz mimo to", "settings.saveBar.cancel": "Anuluj", + "notifications.action.previous": "Previous", + "notifications.action.next": "Next", + "notifications.action.close": "Close", + "notifications.action.tryModel": "Try model", } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 16f690a128a..d743b1cd680 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -1207,4 +1207,8 @@ export const dict = { "settings.saveBar.warning.many": "Несколько сеансов выполняются и будут прерваны", "settings.saveBar.saveAnyway": "Сохранить в любом случае", "settings.saveBar.cancel": "Отмена", + "notifications.action.previous": "Previous", + "notifications.action.next": "Next", + "notifications.action.close": "Close", + "notifications.action.tryModel": "Try model", } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index d41aa865244..f6b9a97d219 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -1187,4 +1187,8 @@ export const dict = { "settings.saveBar.warning.many": "มีหลายเซสชันกำลังทำงานและจะถูกขัดจังหวะ", "settings.saveBar.saveAnyway": "บันทึกต่อไป", "settings.saveBar.cancel": "ยกเลิก", + "notifications.action.previous": "Previous", + "notifications.action.next": "Next", + "notifications.action.close": "Close", + "notifications.action.tryModel": "Try model", } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index 0da999bcab6..7139c4364e1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -1226,4 +1226,8 @@ export const dict = { "settings.saveBar.warning.many": "Birden fazla oturum çalışıyor ve kesintiye uğrayacak", "settings.saveBar.saveAnyway": "Yine de kaydet", "settings.saveBar.cancel": "İptal", + "notifications.action.previous": "Previous", + "notifications.action.next": "Next", + "notifications.action.close": "Close", + "notifications.action.tryModel": "Try model", } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 57039a79909..d4424a61bfa 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -1171,4 +1171,8 @@ export const dict = { "settings.saveBar.warning.many": "多个会话正在运行,将被中断", "settings.saveBar.saveAnyway": "仍然保存", "settings.saveBar.cancel": "取消", + "notifications.action.previous": "Previous", + "notifications.action.next": "Next", + "notifications.action.close": "Close", + "notifications.action.tryModel": "Try model", } satisfies Partial> diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index 461c0f25cf7..e200222b813 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -1172,4 +1172,8 @@ export const dict = { "settings.saveBar.warning.many": "多個工作階段正在執行,將被中斷", "settings.saveBar.saveAnyway": "仍然儲存", "settings.saveBar.cancel": "取消", + "notifications.action.previous": "Previous", + "notifications.action.next": "Next", + "notifications.action.close": "Close", + "notifications.action.tryModel": "Try model", } satisfies Partial> diff --git a/packages/kilo-vscode/webview-ui/src/styles/chat.css b/packages/kilo-vscode/webview-ui/src/styles/chat.css index cc6fc06855f..ee7b045df24 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/chat.css +++ b/packages/kilo-vscode/webview-ui/src/styles/chat.css @@ -2124,7 +2124,7 @@ top: 0; left: 0; right: 0; - z-index: 10; + z-index: 1000; display: flex; flex-direction: column; border-bottom: 1px solid var(--vscode-panel-border); @@ -2185,6 +2185,65 @@ gap: 2px; } +.kilo-notifications-next-group { + display: flex; + align-items: center; + gap: 6px; +} + +.kilo-notifications-action-btn { + display: inline-flex; + align-items: center; + justify-content: center; + height: 24px; + padding: 0 8px; + background: none; + border: 1px solid var(--vscode-focusBorder); + border-radius: 3px; + color: var(--vscode-focusBorder); + font-size: 12px; + font-family: var(--vscode-font-family); + cursor: pointer; + white-space: nowrap; +} + +.kilo-notifications-action-btn:hover { + background-color: var(--vscode-toolbar-hoverBackground); +} + +.kilo-notifications-primary-btn { + display: inline-flex; + align-items: center; + justify-content: center; + height: 24px; + padding: 0 8px; + background-color: var(--vscode-button-background); + border: 1px solid transparent; + border-radius: 3px; + color: var(--vscode-button-foreground); + font-size: 12px; + font-family: var(--vscode-font-family); + cursor: pointer; + white-space: nowrap; +} + +.kilo-notifications-primary-btn:hover { + background-color: var(--vscode-button-hoverBackground); +} + +.kilo-notifications-back-link { + background: none; + border: none; + padding: 0; + cursor: pointer; + color: var(--vscode-descriptionForeground); + font-size: 12px; +} + +.kilo-notifications-back-link:hover { + color: var(--vscode-foreground); +} + .kilo-notifications-nav-btn { background: none; border: none; From 3f4598266c0a2a06d089e091a53014fa94aa2993 Mon Sep 17 00:00:00 2001 From: kirillk Date: Mon, 23 Mar 2026 17:33:34 -0400 Subject: [PATCH 012/264] fix(vscode): clamp notification index when list shrinks and derive navigation from safeIndex --- .../src/components/chat/KiloNotifications.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/KiloNotifications.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/KiloNotifications.tsx index 9b839e73194..7beffc2fc00 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/KiloNotifications.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/KiloNotifications.tsx @@ -1,4 +1,4 @@ -import { Component, Show, createMemo, createSignal } from "solid-js" +import { Component, Show, createEffect, createMemo, createSignal } from "solid-js" import { useNotifications } from "../../context/notifications" import { useVSCode } from "../../context/vscode" import { useSession } from "../../context/session" @@ -20,6 +20,12 @@ export const KiloNotifications: Component = () => { const safeIndex = () => Math.min(index(), Math.max(0, total() - 1)) const current = createMemo(() => (total() === 0 ? undefined : items()[safeIndex()])) + // Clamp index whenever the list shrinks so navigation always reflects reality + createEffect(() => { + const max = Math.max(0, total() - 1) + if (index() > max) setIndex(max) + }) + const handleAction = (url: string) => { vscode.postMessage({ type: "openExternal", url }) } @@ -30,7 +36,7 @@ export const KiloNotifications: Component = () => { if (isLast()) { for (const n of items()) dismiss(n.id) } else { - setIndex((i) => i + 1) + setIndex(safeIndex() + 1) } } @@ -94,7 +100,7 @@ export const KiloNotifications: Component = () => {
0}> - From 628a7a6f8ec5039afa1da3b2a0d0a01708c2dbf2 Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 24 Mar 2026 09:05:55 +0100 Subject: [PATCH 013/264] feat(vscode): add translations for autocomplete credit/auth error notifications --- packages/kilo-vscode/src/services/autocomplete/i18n/ar.ts | 5 +++++ packages/kilo-vscode/src/services/autocomplete/i18n/ca.ts | 5 +++++ packages/kilo-vscode/src/services/autocomplete/i18n/cs.ts | 5 +++++ packages/kilo-vscode/src/services/autocomplete/i18n/de.ts | 5 +++++ packages/kilo-vscode/src/services/autocomplete/i18n/es.ts | 5 +++++ packages/kilo-vscode/src/services/autocomplete/i18n/fr.ts | 5 +++++ packages/kilo-vscode/src/services/autocomplete/i18n/hi.ts | 5 +++++ packages/kilo-vscode/src/services/autocomplete/i18n/id.ts | 5 +++++ packages/kilo-vscode/src/services/autocomplete/i18n/it.ts | 5 +++++ packages/kilo-vscode/src/services/autocomplete/i18n/ja.ts | 5 +++++ packages/kilo-vscode/src/services/autocomplete/i18n/ko.ts | 5 +++++ packages/kilo-vscode/src/services/autocomplete/i18n/nl.ts | 5 +++++ packages/kilo-vscode/src/services/autocomplete/i18n/pl.ts | 5 +++++ packages/kilo-vscode/src/services/autocomplete/i18n/pt-BR.ts | 5 +++++ packages/kilo-vscode/src/services/autocomplete/i18n/ru.ts | 5 +++++ packages/kilo-vscode/src/services/autocomplete/i18n/sk.ts | 5 +++++ packages/kilo-vscode/src/services/autocomplete/i18n/th.ts | 5 +++++ packages/kilo-vscode/src/services/autocomplete/i18n/tr.ts | 5 +++++ packages/kilo-vscode/src/services/autocomplete/i18n/uk.ts | 5 +++++ packages/kilo-vscode/src/services/autocomplete/i18n/vi.ts | 5 +++++ packages/kilo-vscode/src/services/autocomplete/i18n/zh-CN.ts | 4 ++++ packages/kilo-vscode/src/services/autocomplete/i18n/zh-TW.ts | 4 ++++ 22 files changed, 108 insertions(+) diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/ar.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/ar.ts index c2df6d81c72..0d82dce70d6 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/ar.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/ar.ts @@ -41,4 +41,9 @@ export const dict = { "يتم حظر Kilo Code Autocomplete بسبب تعارض مع GitHub Copilot. لإصلاح هذا، يجب عليك تعطيل اقتراحات Copilot المضمنة.", "kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "تعطيل Copilot", "kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "تعطيل الإكمال التلقائي", + "kilocode:autocomplete.creditsExhausted.message": + "تم إيقاف Kilo Code Autocomplete مؤقتًا لأن حسابك لا يحتوي على رصيد متبقٍ. أضف رصيدًا لاستئناف الإكمال التلقائي.", + "kilocode:autocomplete.creditsExhausted.addCredits": "إضافة رصيد", + "kilocode:autocomplete.authError.message": + "تم إيقاف Kilo Code Autocomplete مؤقتًا بسبب خطأ في المصادقة. يرجى تسجيل الدخول مرة أخرى.", } diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/ca.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/ca.ts index 92a7802c492..8fd2ed6ef49 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/ca.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/ca.ts @@ -41,4 +41,9 @@ export const dict = { "El Kilo Code Autocomplete està sent bloquejat per un conflicte amb GitHub Copilot. Per solucionar això, has de desactivar els suggeriments en línia de Copilot.", "kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Desactivar Copilot", "kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Desactivar Autocomplete", + "kilocode:autocomplete.creditsExhausted.message": + "Kilo Code Autocomplete s'ha pausat perquè el teu compte no té crèdits restants. Afegeix crèdits per reprendre l'autocompletat.", + "kilocode:autocomplete.creditsExhausted.addCredits": "Afegir crèdits", + "kilocode:autocomplete.authError.message": + "Kilo Code Autocomplete s'ha pausat a causa d'un error d'autenticació. Si us plau, torna a iniciar sessió.", } diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/cs.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/cs.ts index a12a46c8a0c..4d0871c75ff 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/cs.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/cs.ts @@ -41,4 +41,9 @@ export const dict = { "Kilo Code Autocomplete je blokováno konfliktem s GitHub Copilot. Pro vyřešení tohoto problému musíš zakázat inline návrhy Copilot.", "kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Zakázat Copilot", "kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Zakázat Autocomplete", + "kilocode:autocomplete.creditsExhausted.message": + "Kilo Code Autocomplete bylo pozastaveno, protože na tvém účtu nezbyly žádné kredity. Přidej kredity pro obnovení automatického doplňování.", + "kilocode:autocomplete.creditsExhausted.addCredits": "Přidat kredity", + "kilocode:autocomplete.authError.message": + "Kilo Code Autocomplete bylo pozastaveno kvůli chybě ověření. Prosím, přihlas se znovu.", } diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/de.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/de.ts index 2525a491c04..79a96f7183f 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/de.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/de.ts @@ -42,4 +42,9 @@ export const dict = { "Das Kilo Code Autocomplete wird durch einen Konflikt mit GitHub Copilot blockiert. Um dies zu beheben, musst du Copilots Inline-Vorschläge deaktivieren.", "kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Copilot deaktivieren", "kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Autocomplete deaktivieren", + "kilocode:autocomplete.creditsExhausted.message": + "Kilo Code Autocomplete wurde pausiert, weil dein Konto kein Guthaben mehr hat. Füge Guthaben hinzu, um Autocomplete fortzusetzen.", + "kilocode:autocomplete.creditsExhausted.addCredits": "Guthaben hinzufügen", + "kilocode:autocomplete.authError.message": + "Kilo Code Autocomplete wurde aufgrund eines Authentifizierungsfehlers pausiert. Bitte melde dich erneut an.", } diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/es.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/es.ts index 2348ca32e9a..2a4f1be5f2b 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/es.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/es.ts @@ -41,4 +41,9 @@ export const dict = { "El Kilo Code Autocomplete está siendo bloqueado por un conflicto con GitHub Copilot. Para solucionarlo, debes desactivar las sugerencias en línea de Copilot.", "kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Desactivar Copilot", "kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Desactivar Autocomplete", + "kilocode:autocomplete.creditsExhausted.message": + "Kilo Code Autocomplete se ha pausado porque tu cuenta no tiene créditos restantes. Añade créditos para reanudar el autocompletado.", + "kilocode:autocomplete.creditsExhausted.addCredits": "Añadir créditos", + "kilocode:autocomplete.authError.message": + "Kilo Code Autocomplete se ha pausado debido a un error de autenticación. Por favor, inicia sesión de nuevo.", } diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/fr.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/fr.ts index 56dd0f139aa..10f3ef7024d 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/fr.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/fr.ts @@ -41,4 +41,9 @@ export const dict = { "Le Kilo Code Autocomplete est bloqué par un conflit avec GitHub Copilot. Pour résoudre cela, tu dois désactiver les suggestions en ligne de Copilot.", "kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Désactiver Copilot", "kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Désactiver Autocomplete", + "kilocode:autocomplete.creditsExhausted.message": + "Kilo Code Autocomplete a été mis en pause car ton compte n'a plus de crédits. Ajoute des crédits pour reprendre l'autocomplétion.", + "kilocode:autocomplete.creditsExhausted.addCredits": "Ajouter des crédits", + "kilocode:autocomplete.authError.message": + "Kilo Code Autocomplete a été mis en pause en raison d'une erreur d'authentification. Veuillez te reconnecter.", } diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/hi.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/hi.ts index b23e34b5bb4..6c8d7ad6298 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/hi.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/hi.ts @@ -42,4 +42,9 @@ export const dict = { "Kilo Code Autocomplete को GitHub Copilot के साथ संघर्ष के कारण ब्लॉक किया जा रहा है। इसे ठीक करने के लिए, आपको Copilot के इनलाइन सुझावों को अक्षम करना होगा।", "kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Copilot अक्षम करें", "kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Autocomplete अक्षम करें", + "kilocode:autocomplete.creditsExhausted.message": + "Kilo Code Autocomplete रोक दिया गया है क्योंकि आपके खाते में शेष क्रेडिट नहीं है। ऑटोकम्प्लीट फिर से शुरू करने के लिए क्रेडिट जोड़ें।", + "kilocode:autocomplete.creditsExhausted.addCredits": "क्रेडिट जोड़ें", + "kilocode:autocomplete.authError.message": + "प्रमाणीकरण त्रुटि के कारण Kilo Code Autocomplete रोक दिया गया है। कृपया फिर से साइन इन करें।", } diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/id.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/id.ts index 4f7384d404d..30096caa196 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/id.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/id.ts @@ -42,4 +42,9 @@ export const dict = { "Kilo Code Autocomplete diblokir oleh konflik dengan GitHub Copilot. Untuk memperbaiki ini, Anda harus menonaktifkan saran inline Copilot.", "kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Nonaktifkan Copilot", "kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Nonaktifkan Autocomplete", + "kilocode:autocomplete.creditsExhausted.message": + "Kilo Code Autocomplete telah dijeda karena akun Anda tidak memiliki sisa kredit. Tambahkan kredit untuk melanjutkan autocomplete.", + "kilocode:autocomplete.creditsExhausted.addCredits": "Tambah Kredit", + "kilocode:autocomplete.authError.message": + "Kilo Code Autocomplete telah dijeda karena kesalahan autentikasi. Silakan masuk kembali.", } diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/it.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/it.ts index 5bd075d981b..ca0133c09ea 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/it.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/it.ts @@ -41,4 +41,9 @@ export const dict = { "Il Kilo Code Autocomplete è bloccato da un conflitto con GitHub Copilot. Per risolvere questo problema, devi disabilitare i suggerimenti in linea di Copilot.", "kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Disabilita Copilot", "kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Disabilita Autocomplete", + "kilocode:autocomplete.creditsExhausted.message": + "Kilo Code Autocomplete è stato messo in pausa perché il tuo account non ha più crediti. Aggiungi crediti per riprendere l'autocompletamento.", + "kilocode:autocomplete.creditsExhausted.addCredits": "Aggiungi crediti", + "kilocode:autocomplete.authError.message": + "Kilo Code Autocomplete è stato messo in pausa a causa di un errore di autenticazione. Effettua nuovamente l'accesso.", } diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/ja.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/ja.ts index ad59af271d7..f09f6e6f4ac 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/ja.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/ja.ts @@ -41,4 +41,9 @@ export const dict = { "Kilo Code AutocompleteがGitHub Copilotとの競合によってブロックされています。これを修正するには、Copilotのインライン提案を無効にする必要があります。", "kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Copilotを無効にする", "kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Autocompleteを無効にする", + "kilocode:autocomplete.creditsExhausted.message": + "アカウントのクレジットが残っていないため、Kilo Code Autocompleteが一時停止されました。オートコンプリートを再開するにはクレジットを追加してください。", + "kilocode:autocomplete.creditsExhausted.addCredits": "クレジットを追加", + "kilocode:autocomplete.authError.message": + "認証エラーのため、Kilo Code Autocompleteが一時停止されました。再度サインインしてください。", } diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/ko.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/ko.ts index cc4eab6dfb8..9258f8f7a5f 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/ko.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/ko.ts @@ -40,4 +40,9 @@ export const dict = { "Kilo Code Autocomplete가 GitHub Copilot과의 충돌로 인해 차단되고 있습니다. 이를 해결하려면 Copilot의 인라인 제안을 비활성화해야 합니다.", "kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Copilot 비활성화", "kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Autocomplete 비활성화", + "kilocode:autocomplete.creditsExhausted.message": + "계정에 남은 크레딧이 없어 Kilo Code Autocomplete가 일시 중지되었습니다. 자동완성을 재개하려면 크레딧을 추가하세요.", + "kilocode:autocomplete.creditsExhausted.addCredits": "크레딧 추가", + "kilocode:autocomplete.authError.message": + "인증 오류로 인해 Kilo Code Autocomplete가 일시 중지되었습니다. 다시 로그인해 주세요.", } diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/nl.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/nl.ts index 2c873ad1d17..f7859dff23b 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/nl.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/nl.ts @@ -42,4 +42,9 @@ export const dict = { "De Kilo Code Autocomplete wordt geblokkeerd door een conflict met GitHub Copilot. Om dit op te lossen, moet je de inline suggesties van Copilot uitschakelen.", "kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Copilot uitschakelen", "kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Autocomplete uitschakelen", + "kilocode:autocomplete.creditsExhausted.message": + "Kilo Code Autocomplete is gepauzeerd omdat je account geen tegoed meer heeft. Voeg tegoed toe om autocomplete te hervatten.", + "kilocode:autocomplete.creditsExhausted.addCredits": "Tegoed toevoegen", + "kilocode:autocomplete.authError.message": + "Kilo Code Autocomplete is gepauzeerd vanwege een authenticatiefout. Meld je opnieuw aan.", } diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/pl.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/pl.ts index 64251cd2df7..68dc4fa7cac 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/pl.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/pl.ts @@ -41,4 +41,9 @@ export const dict = { "Kilo Code Autocomplete jest blokowane przez konflikt z GitHub Copilot. Aby to naprawić, musisz wyłączyć sugestie inline Copilot.", "kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Wyłącz Copilot", "kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Wyłącz Autocomplete", + "kilocode:autocomplete.creditsExhausted.message": + "Kilo Code Autocomplete zostało wstrzymane, ponieważ na twoim koncie nie ma pozostałych kredytów. Dodaj kredyty, aby wznowić autouzupełnianie.", + "kilocode:autocomplete.creditsExhausted.addCredits": "Dodaj kredyty", + "kilocode:autocomplete.authError.message": + "Kilo Code Autocomplete zostało wstrzymane z powodu błędu uwierzytelniania. Zaloguj się ponownie.", } diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/pt-BR.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/pt-BR.ts index 8c879217de2..067371a6fac 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/pt-BR.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/pt-BR.ts @@ -40,4 +40,9 @@ export const dict = { "O Kilo Code Autocomplete está sendo bloqueado por um conflito com o GitHub Copilot. Para corrigir isso, você deve desabilitar as sugestões inline do Copilot.", "kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Desabilitar Copilot", "kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Desabilitar Autocomplete", + "kilocode:autocomplete.creditsExhausted.message": + "O Kilo Code Autocomplete foi pausado porque sua conta não tem créditos restantes. Adicione créditos para retomar o autocompletar.", + "kilocode:autocomplete.creditsExhausted.addCredits": "Adicionar créditos", + "kilocode:autocomplete.authError.message": + "O Kilo Code Autocomplete foi pausado devido a um erro de autenticação. Por favor, faça login novamente.", } diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/ru.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/ru.ts index bb572c5f3e4..7fdfa0a89a6 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/ru.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/ru.ts @@ -42,4 +42,9 @@ export const dict = { "Kilo Code Autocomplete блокируется конфликтом с GitHub Copilot. Чтобы исправить это, ты должен отключить встроенные предложения Copilot.", "kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Отключить Copilot", "kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Отключить Autocomplete", + "kilocode:autocomplete.creditsExhausted.message": + "Kilo Code Autocomplete приостановлено, так как на твоём аккаунте не осталось кредитов. Добавь кредиты, чтобы возобновить автодополнение.", + "kilocode:autocomplete.creditsExhausted.addCredits": "Добавить кредиты", + "kilocode:autocomplete.authError.message": + "Kilo Code Autocomplete приостановлено из-за ошибки аутентификации. Пожалуйста, войди снова.", } diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/sk.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/sk.ts index 8ff2f3c8e15..04b76caa5c5 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/sk.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/sk.ts @@ -41,4 +41,9 @@ export const dict = { "Kilo Code Autocomplete je blokované konfliktom s GitHub Copilot. Pre vyriešenie tohto problému musíš zakázať inline návrhy Copilot.", "kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Zakázať Copilot", "kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Zakázať Autocomplete", + "kilocode:autocomplete.creditsExhausted.message": + "Kilo Code Autocomplete bolo pozastavené, pretože na tvojom účte nezostali žiadne kredity. Pridaj kredity na obnovenie automatického dopĺňania.", + "kilocode:autocomplete.creditsExhausted.addCredits": "Pridať kredity", + "kilocode:autocomplete.authError.message": + "Kilo Code Autocomplete bolo pozastavené kvôli chybe overenia. Prosím, prihlás sa znova.", } diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/th.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/th.ts index 3ceeca319f3..4a36a3ba6f6 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/th.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/th.ts @@ -41,4 +41,9 @@ export const dict = { "Kilo Code Autocomplete ถูกบล็อกโดยความขัดแย้งกับ GitHub Copilot เพื่อแก้ไขปัญหานี้ คุณต้องปิดใช้งานคำแนะนำแบบอินไลน์ของ Copilot", "kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "ปิดใช้งาน Copilot", "kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "ปิดใช้งาน Autocomplete", + "kilocode:autocomplete.creditsExhausted.message": + "Kilo Code Autocomplete ถูกหยุดชั่วคราวเนื่องจากบัญชีของคุณไม่มีเครดิตเหลือ เพิ่มเครดิตเพื่อใช้งานการเติมข้อความอัตโนมัติต่อ", + "kilocode:autocomplete.creditsExhausted.addCredits": "เพิ่มเครดิต", + "kilocode:autocomplete.authError.message": + "Kilo Code Autocomplete ถูกหยุดชั่วคราวเนื่องจากข้อผิดพลาดในการยืนยันตัวตน กรุณาเข้าสู่ระบบอีกครั้ง", } diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/tr.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/tr.ts index 417ba03ee1c..56c5951178f 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/tr.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/tr.ts @@ -42,4 +42,9 @@ export const dict = { "Kilo Code Autocomplete, GitHub Copilot ile bir çakışma nedeniyle engelleniyor. Bunu düzeltmek için Copilot'un satır içi önerilerini devre dışı bırakmalısın.", "kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Copilot'u Devre Dışı Bırak", "kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Autocomplete'i Devre Dışı Bırak", + "kilocode:autocomplete.creditsExhausted.message": + "Hesabında kalan kredin olmadığı için Kilo Code Autocomplete duraklatıldı. Otomatik tamamlamayı sürdürmek için kredi ekle.", + "kilocode:autocomplete.creditsExhausted.addCredits": "Kredi Ekle", + "kilocode:autocomplete.authError.message": + "Bir kimlik doğrulama hatası nedeniyle Kilo Code Autocomplete duraklatıldı. Lütfen tekrar giriş yap.", } diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/uk.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/uk.ts index 3dc1b1b32a4..9478c4aa7a8 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/uk.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/uk.ts @@ -41,4 +41,9 @@ export const dict = { "Kilo Code Autocomplete блокується конфліктом з GitHub Copilot. Щоб виправити це, ти повинен вимкнути вбудовані пропозиції Copilot.", "kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Вимкнути Copilot", "kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Вимкнути Autocomplete", + "kilocode:autocomplete.creditsExhausted.message": + "Kilo Code Autocomplete призупинено, оскільки на твоєму обліковому записі не залишилося кредитів. Додай кредити, щоб відновити автодоповнення.", + "kilocode:autocomplete.creditsExhausted.addCredits": "Додати кредити", + "kilocode:autocomplete.authError.message": + "Kilo Code Autocomplete призупинено через помилку автентифікації. Будь ласка, увійди знову.", } diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/vi.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/vi.ts index f645e3e2fe0..17910b54e45 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/vi.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/vi.ts @@ -42,4 +42,9 @@ export const dict = { "Kilo Code Autocomplete đang bị chặn do xung đột với GitHub Copilot. Để khắc phục điều này, bạn phải tắt các gợi ý inline của Copilot.", "kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "Tắt Copilot", "kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "Tắt Autocomplete", + "kilocode:autocomplete.creditsExhausted.message": + "Kilo Code Autocomplete đã bị tạm dừng vì tài khoản của bạn không còn tín dụng. Thêm tín dụng để tiếp tục sử dụng tự động hoàn thành.", + "kilocode:autocomplete.creditsExhausted.addCredits": "Thêm tín dụng", + "kilocode:autocomplete.authError.message": + "Kilo Code Autocomplete đã bị tạm dừng do lỗi xác thực. Vui lòng đăng nhập lại.", } diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/zh-CN.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/zh-CN.ts index da8cf67adbf..8098e4d0fde 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/zh-CN.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/zh-CN.ts @@ -40,4 +40,8 @@ export const dict = { "Kilo Code Autocomplete 被与 GitHub Copilot 的冲突阻止。要解决此问题,你必须禁用 Copilot 的内联建议。", "kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "禁用 Copilot", "kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "禁用 Autocomplete", + "kilocode:autocomplete.creditsExhausted.message": + "Kilo Code Autocomplete 已暂停,因为你的账户没有剩余额度。请添加额度以恢复自动补全。", + "kilocode:autocomplete.creditsExhausted.addCredits": "添加额度", + "kilocode:autocomplete.authError.message": "Kilo Code Autocomplete 因身份验证错误已暂停。请重新登录。", } diff --git a/packages/kilo-vscode/src/services/autocomplete/i18n/zh-TW.ts b/packages/kilo-vscode/src/services/autocomplete/i18n/zh-TW.ts index 7bce63c2476..37e0037fb30 100644 --- a/packages/kilo-vscode/src/services/autocomplete/i18n/zh-TW.ts +++ b/packages/kilo-vscode/src/services/autocomplete/i18n/zh-TW.ts @@ -40,4 +40,8 @@ export const dict = { "Kilo Code Autocomplete 因與 GitHub Copilot 衝突而被封鎖。若要修正此問題,必須停用 Copilot 的行內建議。", "kilocode:autocomplete.incompatibilityExtensionPopup.disableCopilot": "停用 Copilot", "kilocode:autocomplete.incompatibilityExtensionPopup.disableInlineAssist": "停用 Autocomplete", + "kilocode:autocomplete.creditsExhausted.message": + "Kilo Code Autocomplete 已暫停,因為你的帳戶沒有剩餘額度。請新增額度以恢復自動補齊。", + "kilocode:autocomplete.creditsExhausted.addCredits": "新增額度", + "kilocode:autocomplete.authError.message": "Kilo Code Autocomplete 因驗證錯誤已暫停。請重新登入。", } From f2c2e6d81a2a8e6849897688d2fd9b43b7d0f4f0 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:03:02 +0000 Subject: [PATCH 014/264] fix(ui): suppress read-tool prerequisite error in ToolPartDisplay --- packages/kilo-ui/src/components/message-part.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/kilo-ui/src/components/message-part.tsx b/packages/kilo-ui/src/components/message-part.tsx index d2fc659fad4..90211754aaf 100644 --- a/packages/kilo-ui/src/components/message-part.tsx +++ b/packages/kilo-ui/src/components/message-part.tsx @@ -1068,6 +1068,9 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { {(error) => { const cleaned = error().replace("Error: ", "") + if (cleaned.includes("before overwriting it. Use the Read tool first")) { + return null + } if (part.tool === "question" && cleaned.includes("dismissed this question")) { return (
From b6bfa64c9bcfb5bcf9d118b758a54d62d13b11d6 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:18:56 +0000 Subject: [PATCH 015/264] feat(ui): render tool-hint for common tool errors display a tool-hint panel with an icon and message when tool errors match known patterns replacing suppression of a specific read/write error --- .../kilo-ui/src/components/message-part.css | 23 +++++++++++++++++++ .../kilo-ui/src/components/message-part.tsx | 17 +++++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/packages/kilo-ui/src/components/message-part.css b/packages/kilo-ui/src/components/message-part.css index cac42ad5f26..9d9d9c70667 100644 --- a/packages/kilo-ui/src/components/message-part.css +++ b/packages/kilo-ui/src/components/message-part.css @@ -291,3 +291,26 @@ html[data-theme="kilo-vscode"] [data-component="bash-output"] { opacity: 1; } } + +[data-component="tool-hint"] { + display: flex; + align-items: start; + gap: 6px; + padding: 4px 8px; + border-left: 2px solid var(--border-weak-base); + + [data-component="icon"] { + flex-shrink: 0; + color: var(--icon-weak-base); + margin-top: 1px; + } + + [data-slot="tool-hint-message"] { + font-family: var(--font-family-sans); + font-size: var(--font-size-small); + font-style: italic; + color: var(--text-weak); + opacity: 0.9; + line-height: var(--line-height-large); + } +} diff --git a/packages/kilo-ui/src/components/message-part.tsx b/packages/kilo-ui/src/components/message-part.tsx index 90211754aaf..f41d3cbd8ed 100644 --- a/packages/kilo-ui/src/components/message-part.tsx +++ b/packages/kilo-ui/src/components/message-part.tsx @@ -1068,9 +1068,6 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { {(error) => { const cleaned = error().replace("Error: ", "") - if (cleaned.includes("before overwriting it. Use the Read tool first")) { - return null - } if (part.tool === "question" && cleaned.includes("dismissed this question")) { return (
@@ -1080,6 +1077,20 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
) } + const hint = + cleaned.includes("before overwriting it. Use the Read tool first") || + cleaned.includes("has been modified since it was last read") || + cleaned.includes("oldString and newString are identical") || + cleaned.includes("must match exactly, including whitespace") || + cleaned.includes("Found multiple matches for oldString") + if (hint) { + return ( +
+ + {cleaned} +
+ ) + } const [title, ...rest] = cleaned.split(": ") return ( From 8f03cbc2c4bb002877619714e7b9afeb4470334a Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 24 Mar 2026 09:57:54 +0100 Subject: [PATCH 016/264] fix(vscode): propagate SSE errors to ErrorBackoff for autocomplete The SDK's SSE client catches HTTP errors (402/401/429/5xx) internally and retries indefinitely, so errors never reached the ErrorBackoff circuit breaker. Disable SSE retries with sseMaxRetryAttempts: 1 and capture errors via onSseError callback to re-throw after the stream ends. --- .../src/services/autocomplete/AutocompleteModel.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/kilo-vscode/src/services/autocomplete/AutocompleteModel.ts b/packages/kilo-vscode/src/services/autocomplete/AutocompleteModel.ts index bfadd50204b..c7218edc9ed 100644 --- a/packages/kilo-vscode/src/services/autocomplete/AutocompleteModel.ts +++ b/packages/kilo-vscode/src/services/autocomplete/AutocompleteModel.ts @@ -65,6 +65,10 @@ export class AutocompleteModel { let inputTokens = 0 let outputTokens = 0 + // Capture SSE-level errors so they propagate to the caller. The SDK's SSE + // client catches HTTP errors (402, 401, 429, 5xx) internally and silently + // ends the stream. Without this, errors never reach ErrorBackoff. + let sseError: Error | undefined const { stream } = await client.kilo.fim( { prefix, @@ -73,7 +77,13 @@ export class AutocompleteModel { maxTokens: 256, temperature: 0.2, }, - { signal }, + { + signal, + sseMaxRetryAttempts: 1, + onSseError: (error) => { + sseError = error instanceof Error ? error : new Error(String(error)) + }, + }, ) for await (const chunk of stream) { @@ -86,6 +96,8 @@ export class AutocompleteModel { if (chunk.cost !== undefined) cost = chunk.cost } + if (sseError) throw sseError + return { cost, inputTokens, From 5c044af576b155756a7f9df9d38899c6bde15df3 Mon Sep 17 00:00:00 2001 From: Marius Date: Tue, 24 Mar 2026 09:59:02 +0100 Subject: [PATCH 017/264] fix(vscode): auto-submit custom question answer on Enter and make collapsed dock clickable (#7500) Single-select questions now submit immediately when pressing Enter after typing a custom answer. Clicking anywhere on the collapsed question dock header expands it, not just the chevron button. --- .../webview-ui/src/components/chat/QuestionDock.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/QuestionDock.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/QuestionDock.tsx index cdf4bce13dd..0c581bb4b0d 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/QuestionDock.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/QuestionDock.tsx @@ -173,6 +173,7 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => pick(value, true) setStore("editing", false) + if (single()) submit() } const toggleCollapse = () => setStore("collapsed", !store.collapsed) @@ -217,14 +218,14 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => onKeyDown={onRoot} > {/* Single unified header row — always visible */} -
+
{summary()}
{question()?.question}
-
+
e.stopPropagation()}>
) @@ -2412,34 +2396,46 @@ const AgentManagerContent: Component = () => { > {(s) => ( -
- + {s.title || t("agentManager.session.untitled")} + {formatRelativeDate(s.updatedAt)} +
+ + handlePromote(s.id, e)} + /> + +
+ + + + + handlePromote(s.id, new MouseEvent("click"))}> + + {t("agentManager.session.openInWorktree")} + + + + )} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx index b1d89522cca..1bf24c5c087 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/WorktreeItem.tsx @@ -8,11 +8,14 @@ import { IconButton } from "@kilocode/kilo-ui/icon-button" import { Spinner } from "@kilocode/kilo-ui/spinner" import { Tooltip, TooltipKeybind } from "@kilocode/kilo-ui/tooltip" import { HoverCard } from "@kilocode/kilo-ui/hover-card" +import { ContextMenu } from "@kilocode/kilo-ui/context-menu" import { Button } from "@kilocode/kilo-ui/button" import type { WorktreeState, WorktreeGitStats } from "../src/types/messages" import { useLanguage } from "../src/context/language" import { formatRelativeDate } from "../src/utils/date" +import { parseBindingTokens } from "./keybind-tokens" + const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent) interface WorktreeItemProps { @@ -46,6 +49,8 @@ interface WorktreeItemProps { renameValue: string /** Keybinding string for the close/delete action. */ closeKeybind: string + /** Keybinding string for the open-in-vscode action. */ + openKeybind: string onClick: () => void onDelete: (e: MouseEvent) => void @@ -54,6 +59,8 @@ interface WorktreeItemProps { onCommitRename: () => void onCancelRename: () => void onRemoveStale: () => void + onCopyPath: () => void + onOpen: () => void } const MAX_SHORTCUT = 9 @@ -74,232 +81,281 @@ export const WorktreeItem: Component = (props) => { {t("agentManager.worktree.versions", { count: props.groupSize })}
- setHovered(open)} - trigger={ -
props.onClick()} - > - }> - - - - - - - - - - { - e.stopPropagation() - props.onStartRename(props.label) - }} - title={t("agentManager.worktree.doubleClickRename")} - > - {props.label} - - } - > - props.onRenameInput(e.currentTarget.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault() - props.onCommitRename() - } - if (e.key === "Escape") { - e.preventDefault() - props.onCancelRename() - } + + setHovered(open)} + trigger={ + +
props.onCommitRename()} - onClick={(e) => e.stopPropagation()} - ref={(el) => - requestAnimationFrame(() => { - el.focus() - el.select() - }) - } - /> + data-sidebar-id={props.worktree.id} + onClick={() => props.onClick()} + > + }> + + + + + + + + + + { + e.stopPropagation() + props.onStartRename(props.label) + }} + title={t("agentManager.worktree.doubleClickRename")} + > + {props.label} + + } + > + props.onRenameInput(e.currentTarget.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault() + props.onCommitRename() + } + if (e.key === "Escape") { + e.preventDefault() + props.onCancelRename() + } + }} + onBlur={() => props.onCommitRename()} + onClick={(e) => e.stopPropagation()} + ref={(el) => + requestAnimationFrame(() => + requestAnimationFrame(() => { + el.focus() + el.select() + }), + ) + } + /> + + = 2 && props.shortcut <= MAX_SHORTCUT}> + + {isMac ? "⌘" : "Ctrl+"} + {props.shortcut} + + + +
+
+
+
+ + +
+ 0 || props.stats!.deletions > 0} + fallback={ + 0}> + {props.stats!.files}f + + } + > +
+ 0}> + +{props.stats!.additions} + + 0}> + −{props.stats!.deletions} + +
+
+ 0 || props.stats!.behind > 0}> +
+ 0}> + ↑{props.stats!.ahead} + + 0}> + ↓{props.stats!.behind} + +
+
+
+
+ + {t("agentManager.worktree.confirmDelete")} + + +
setOverClose(true)} + onMouseLeave={() => setOverClose(false)} + > + + props.onDelete(e)} + /> + +
+
+
+ + } + > +
+
+
+
{t("agentManager.hoverCard.branch")}
+
{props.worktree.branch}
+
{formatRelativeDate(props.worktree.createdAt)}
+
+ + {props.navHint} + +
+ +
+
+ {t("agentManager.hoverCard.base")} + + {props.worktree.remote + ? `${props.worktree.remote}/${props.worktree.parentBranch}` + : props.worktree.parentBranch} + +
- = 2 && props.shortcut <= MAX_SHORTCUT}> - - {isMac ? "⌘" : "Ctrl+"} - {props.shortcut} - - - -
-
-
+
+
+ {t("agentManager.hoverCard.sessions")} + {props.sessions} +
+ +
+
+ {t("agentManager.worktree.stale")} + + + {t("agentManager.worktree.stale")} + +
+
{t("agentManager.worktree.staleTooltip")}
+
+
-
- 0 || props.stats!.deletions > 0} - fallback={ - 0}> - {props.stats!.files}f - - } - > -
+
+ 0}> +
+ {t("agentManager.hoverCard.files")} + {props.stats!.files} +
+
+ 0 || props.stats!.deletions > 0}> +
+ {t("agentManager.hoverCard.changes")} + 0}> +{props.stats!.additions} 0}> −{props.stats!.deletions} -
-
- 0 || props.stats!.behind > 0}> -
+ +
+
+ 0 || props.stats!.behind > 0}> +
+ {t("agentManager.hoverCard.commits")} + 0}> ↑{props.stats!.ahead} 0}> ↓{props.stats!.behind} -
-
-
+ +
+
- - {t("agentManager.worktree.confirmDelete")} - - -
setOverClose(true)} - onMouseLeave={() => setOverClose(false)} - > - - props.onDelete(e)} - /> - -
-
-
- } - > -
-
-
-
{t("agentManager.hoverCard.branch")}
-
{props.worktree.branch}
-
{formatRelativeDate(props.worktree.createdAt)}
-
- - {props.navHint} - -
-
-
- {t("agentManager.hoverCard.base")} - - {props.worktree.remote - ? `${props.worktree.remote}/${props.worktree.parentBranch}` - : props.worktree.parentBranch} - +
+ + {t("agentManager.worktree.doubleClickRename")}
- -
-
- {t("agentManager.hoverCard.sessions")} - {props.sessions}
- -
-
- {t("agentManager.worktree.stale")} - - - {t("agentManager.worktree.stale")} - -
-
{t("agentManager.worktree.staleTooltip")}
-
- -
- - -
- 0}> -
- {t("agentManager.hoverCard.files")} - {props.stats!.files} -
-
- 0 || props.stats!.deletions > 0}> -
- {t("agentManager.hoverCard.changes")} - - 0}> - +{props.stats!.additions} - - 0}> - −{props.stats!.deletions} - + + + + props.onStartRename(props.label)}> + + {t("agentManager.worktree.rename")} + + props.onDelete(new MouseEvent("click"))}> + + {t("agentManager.worktree.delete")} + + + {parseBindingTokens(props.closeKeybind).map((token) => ( + {token} + ))} -
-
- 0 || props.stats!.behind > 0}> -
- {t("agentManager.hoverCard.commits")} - - 0}> - ↑{props.stats!.ahead} - - 0}> - ↓{props.stats!.behind} - + + + + props.onOpen()}> + + {t("agentManager.worktree.openInVscode")} + + + {parseBindingTokens(props.openKeybind).map((token) => ( + {token} + ))} -
-
- -
-
- - {t("agentManager.worktree.doubleClickRename")} -
-
- + + + props.onCopyPath()}> + + {t("agentManager.worktree.copyPath")} + + + + ) } 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 217de5038e7..0ea7f4236f8 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -3475,3 +3475,32 @@ body.vscode-high-contrast-light { outline-offset: -1px; } } + +/* Context menu — restyle to match dropdown-menu visuals. + The kilo-ui ContextMenu component hardcodes its own data-component / data-slot + attributes after the prop spread, so we can't override them via props. + Instead we apply a class and re-declare the styles here. */ + +.am-ctx-menu { + min-width: 210px; + border: 1px solid color-mix(in oklch, var(--border-base) 50%, transparent) !important; + box-shadow: var(--shadow-md) !important; + + [data-slot="context-menu-item"], + [data-slot="context-menu-checkbox-item"], + [data-slot="context-menu-radio-item"], + [data-slot="context-menu-sub-trigger"] { + padding: 6px 10px; + font-size: var(--font-size-small); + transition: none; + + &:hover, + &[data-highlighted] { + background: var(--surface-raised-base-hover); + } + } + + [data-slot="context-menu-separator"] { + margin: 4px 0; + } +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts index 17dd3d354d9..a09b471fd38 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts @@ -16,6 +16,9 @@ export const dict = { "agentManager.worktree.advancedOptions": "خيارات Worktree متقدمة", "agentManager.worktree.defaultBaseBranch": "الفرع الأساسي الافتراضي", "agentManager.worktree.defaultBaseBranchAuto": "اكتشاف تلقائي", + "agentManager.worktree.copyPath": "نسخ المسار", + "agentManager.worktree.openInVscode": "فتح في VS Code", + "agentManager.worktree.rename": "إعادة تسمية", "agentManager.hoverCard.branch": "الفرع", "agentManager.hoverCard.base": "الأساس", "agentManager.hoverCard.sessions": "الجلسات", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts index 983879bff5c..3bab2d50e70 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts @@ -16,6 +16,9 @@ export const dict = { "agentManager.worktree.advancedOptions": "Opções avançadas de Worktree", "agentManager.worktree.defaultBaseBranch": "Branch base padrão", "agentManager.worktree.defaultBaseBranchAuto": "Detecção automática", + "agentManager.worktree.copyPath": "Copiar Caminho", + "agentManager.worktree.openInVscode": "Abrir no VS Code", + "agentManager.worktree.rename": "Renomear", "agentManager.hoverCard.branch": "BRANCH", "agentManager.hoverCard.base": "Base", "agentManager.hoverCard.sessions": "Sessões", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts index 9d64195b11d..43054fb08ff 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts @@ -16,6 +16,9 @@ export const dict = { "agentManager.worktree.advancedOptions": "Napredne opcije Worktree-a", "agentManager.worktree.defaultBaseBranch": "Zadana bazna grana", "agentManager.worktree.defaultBaseBranchAuto": "Automatsko otkrivanje", + "agentManager.worktree.copyPath": "Kopiraj putanju", + "agentManager.worktree.openInVscode": "Otvori u VS Code", + "agentManager.worktree.rename": "Preimenuj", "agentManager.hoverCard.branch": "BRANCH", "agentManager.hoverCard.base": "Baza", "agentManager.hoverCard.sessions": "Sesije", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts index d547f114097..c5809034c44 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts @@ -16,6 +16,9 @@ export const dict = { "agentManager.worktree.advancedOptions": "Avancerede Worktree-indstillinger", "agentManager.worktree.defaultBaseBranch": "Standard basisgren", "agentManager.worktree.defaultBaseBranchAuto": "Automatisk registrering", + "agentManager.worktree.copyPath": "Kopier sti", + "agentManager.worktree.openInVscode": "Åbn i VS Code", + "agentManager.worktree.rename": "Omdøb", "agentManager.hoverCard.branch": "BRANCH", "agentManager.hoverCard.base": "Base", "agentManager.hoverCard.sessions": "Sessioner", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts index 8426901d6cc..2f539383304 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts @@ -16,6 +16,9 @@ export const dict = { "agentManager.worktree.advancedOptions": "Erweiterte Worktree-Optionen", "agentManager.worktree.defaultBaseBranch": "Standard-Basiszweig", "agentManager.worktree.defaultBaseBranchAuto": "Automatisch erkennen", + "agentManager.worktree.copyPath": "Pfad kopieren", + "agentManager.worktree.openInVscode": "In VS Code öffnen", + "agentManager.worktree.rename": "Umbenennen", "agentManager.hoverCard.branch": "BRANCH", "agentManager.hoverCard.base": "Basis", "agentManager.hoverCard.sessions": "Sitzungen", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts index 430003a9cb1..d5d44e3c848 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts @@ -17,6 +17,9 @@ export const dict = { "agentManager.worktree.advancedOptions": "Advanced worktree options", "agentManager.worktree.defaultBaseBranch": "Default Base Branch", "agentManager.worktree.defaultBaseBranchAuto": "Auto-detect", + "agentManager.worktree.copyPath": "Copy Path", + "agentManager.worktree.openInVscode": "Open in VS Code", + "agentManager.worktree.rename": "Rename", "agentManager.hoverCard.branch": "BRANCH", "agentManager.hoverCard.base": "Base", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts index bf96d0f5361..609892de9cf 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts @@ -16,6 +16,9 @@ export const dict = { "agentManager.worktree.advancedOptions": "Opciones avanzadas de Worktree", "agentManager.worktree.defaultBaseBranch": "Rama base predeterminada", "agentManager.worktree.defaultBaseBranchAuto": "Detección automática", + "agentManager.worktree.copyPath": "Copiar ruta", + "agentManager.worktree.openInVscode": "Abrir en VS Code", + "agentManager.worktree.rename": "Renombrar", "agentManager.hoverCard.branch": "BRANCH", "agentManager.hoverCard.base": "Base", "agentManager.hoverCard.sessions": "Sesiones", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts index 2c672e5ce7b..ba0cba15f1e 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts @@ -16,6 +16,9 @@ export const dict = { "agentManager.worktree.advancedOptions": "Options avancées du Worktree", "agentManager.worktree.defaultBaseBranch": "Branche de base par défaut", "agentManager.worktree.defaultBaseBranchAuto": "Détection automatique", + "agentManager.worktree.copyPath": "Copier le chemin", + "agentManager.worktree.openInVscode": "Ouvrir dans VS Code", + "agentManager.worktree.rename": "Renommer", "agentManager.hoverCard.branch": "BRANCHE", "agentManager.hoverCard.base": "Base", "agentManager.hoverCard.sessions": "Sessions", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts index e73ed3bccd8..a1a47f8fa25 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts @@ -16,6 +16,9 @@ export const dict = { "agentManager.worktree.advancedOptions": "Worktreeの詳細オプション", "agentManager.worktree.defaultBaseBranch": "デフォルトベースブランチ", "agentManager.worktree.defaultBaseBranchAuto": "自動検出", + "agentManager.worktree.copyPath": "パスをコピー", + "agentManager.worktree.openInVscode": "VS Codeで開く", + "agentManager.worktree.rename": "名前を変更", "agentManager.hoverCard.branch": "ブランチ", "agentManager.hoverCard.base": "ベース", "agentManager.hoverCard.sessions": "セッション", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts index 92df68890d4..cea20c31c6b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts @@ -16,6 +16,9 @@ export const dict = { "agentManager.worktree.advancedOptions": "Worktree 고급 옵션", "agentManager.worktree.defaultBaseBranch": "기본 베이스 브랜치", "agentManager.worktree.defaultBaseBranchAuto": "자동 감지", + "agentManager.worktree.copyPath": "경로 복사", + "agentManager.worktree.openInVscode": "VS Code에서 열기", + "agentManager.worktree.rename": "이름 바꾸기", "agentManager.hoverCard.branch": "브랜치", "agentManager.hoverCard.base": "베이스", "agentManager.hoverCard.sessions": "세션", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts index cf6b39fce00..b1bf9283097 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts @@ -17,6 +17,9 @@ export const dict = { "agentManager.worktree.advancedOptions": "Geavanceerde worktree opties", "agentManager.worktree.defaultBaseBranch": "Standaard basis branch", "agentManager.worktree.defaultBaseBranchAuto": "Automatisch detecteren", + "agentManager.worktree.copyPath": "Pad kopiëren", + "agentManager.worktree.openInVscode": "Openen in VS Code", + "agentManager.worktree.rename": "Hernoemen", "agentManager.hoverCard.branch": "BRANCH", "agentManager.hoverCard.base": "Basis", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts index 37f3ac6a93e..df6fa52b802 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts @@ -16,6 +16,9 @@ export const dict = { "agentManager.worktree.advancedOptions": "Avanserte Worktree-alternativer", "agentManager.worktree.defaultBaseBranch": "Standard basisgren", "agentManager.worktree.defaultBaseBranchAuto": "Automatisk gjenkjenning", + "agentManager.worktree.copyPath": "Kopier sti", + "agentManager.worktree.openInVscode": "Åpne i VS Code", + "agentManager.worktree.rename": "Gi nytt navn", "agentManager.hoverCard.branch": "BRANCH", "agentManager.hoverCard.base": "Base", "agentManager.hoverCard.sessions": "Økter", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts index 504baadd599..17b75bea45b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts @@ -16,6 +16,9 @@ export const dict = { "agentManager.worktree.advancedOptions": "Zaawansowane opcje Worktree", "agentManager.worktree.defaultBaseBranch": "Domyślna gałąź bazowa", "agentManager.worktree.defaultBaseBranchAuto": "Automatyczne wykrywanie", + "agentManager.worktree.copyPath": "Kopiuj ścieżkę", + "agentManager.worktree.openInVscode": "Otwórz w VS Code", + "agentManager.worktree.rename": "Zmień nazwę", "agentManager.hoverCard.branch": "BRANCH", "agentManager.hoverCard.base": "Baza", "agentManager.hoverCard.sessions": "Sesje", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts index c01e829ac47..f1eec260d2c 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts @@ -16,6 +16,9 @@ export const dict = { "agentManager.worktree.advancedOptions": "Расширенные настройки Worktree", "agentManager.worktree.defaultBaseBranch": "Базовая ветка по умолчанию", "agentManager.worktree.defaultBaseBranchAuto": "Автоопределение", + "agentManager.worktree.copyPath": "Копировать путь", + "agentManager.worktree.openInVscode": "Открыть в VS Code", + "agentManager.worktree.rename": "Переименовать", "agentManager.hoverCard.branch": "ВЕТКА", "agentManager.hoverCard.base": "Основа", "agentManager.hoverCard.sessions": "Сессии", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts index d8451dfa4e4..2e62d880b8f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts @@ -16,6 +16,9 @@ export const dict = { "agentManager.worktree.advancedOptions": "ตัวเลือก Worktree ขั้นสูง", "agentManager.worktree.defaultBaseBranch": "สาขาฐานเริ่มต้น", "agentManager.worktree.defaultBaseBranchAuto": "ตรวจจับอัตโนมัติ", + "agentManager.worktree.copyPath": "คัดลอกเส้นทาง", + "agentManager.worktree.openInVscode": "เปิดใน VS Code", + "agentManager.worktree.rename": "เปลี่ยนชื่อ", "agentManager.hoverCard.branch": "BRANCH", "agentManager.hoverCard.base": "ฐาน", "agentManager.hoverCard.sessions": "เซสชัน", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts index ef876c6a915..363c92c5959 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts @@ -17,6 +17,9 @@ export const dict = { "agentManager.worktree.advancedOptions": "Gelişmiş worktree seçenekleri", "agentManager.worktree.defaultBaseBranch": "Varsayılan Temel Dal", "agentManager.worktree.defaultBaseBranchAuto": "Otomatik algıla", + "agentManager.worktree.copyPath": "Yolu Kopyala", + "agentManager.worktree.openInVscode": "VS Code'da Aç", + "agentManager.worktree.rename": "Yeniden Adlandır", "agentManager.hoverCard.branch": "DAL", "agentManager.hoverCard.base": "Temel", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts index 42e3c048450..afab6dd2269 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts @@ -16,6 +16,9 @@ export const dict = { "agentManager.worktree.advancedOptions": "Worktree 高级选项", "agentManager.worktree.defaultBaseBranch": "默认基础分支", "agentManager.worktree.defaultBaseBranchAuto": "自动检测", + "agentManager.worktree.copyPath": "复制路径", + "agentManager.worktree.openInVscode": "在 VS Code 中打开", + "agentManager.worktree.rename": "重命名", "agentManager.hoverCard.branch": "分支", "agentManager.hoverCard.base": "基础", "agentManager.hoverCard.sessions": "会话", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts index 1640249553b..d6b6f1b95c4 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts @@ -16,6 +16,9 @@ export const dict = { "agentManager.worktree.advancedOptions": "Worktree 進階選項", "agentManager.worktree.defaultBaseBranch": "預設基礎分支", "agentManager.worktree.defaultBaseBranchAuto": "自動偵測", + "agentManager.worktree.copyPath": "複製路徑", + "agentManager.worktree.openInVscode": "在 VS Code 中打開", + "agentManager.worktree.rename": "重新命名", "agentManager.hoverCard.branch": "分支", "agentManager.hoverCard.base": "基底", "agentManager.hoverCard.sessions": "工作階段", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/keybind-tokens.ts b/packages/kilo-vscode/webview-ui/agent-manager/keybind-tokens.ts new file mode 100644 index 00000000000..923cd2a5596 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/keybind-tokens.ts @@ -0,0 +1,23 @@ +/** Parse a display keybinding string into separate key tokens for rendering. + * Windows/Linux format ("Ctrl+Shift+W") splits on "+". + * Mac format ("⌘⇧W") splits on known modifier symbols. */ +export function parseBindingTokens(binding: string): string[] { + if (!binding) return [] + // Windows/Linux: "Ctrl+Shift+W" → ["Ctrl", "Shift", "W"] + if (binding.includes("+")) return binding.split("+") + // Mac: "⌘⇧W" → ["⌘", "⇧", "W"] — peel off known modifier symbols + const tokens: string[] = [] + let rest = binding + const modifiers = ["⌘", "⇧", "⌃", "⌥"] + while (rest.length > 0) { + const mod = modifiers.find((m) => rest.startsWith(m)) + if (mod) { + tokens.push(mod) + rest = rest.slice(mod.length) + } else { + tokens.push(rest) + break + } + } + return tokens +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/sortable-tab.tsx b/packages/kilo-vscode/webview-ui/agent-manager/sortable-tab.tsx index 2432e5d74c9..7238060c76f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/sortable-tab.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/sortable-tab.tsx @@ -20,6 +20,7 @@ import { Icon } from "@kilocode/kilo-ui/icon" import { TooltipKeybind } from "@kilocode/kilo-ui/tooltip" import { ContextMenu } from "@kilocode/kilo-ui/context-menu" import { useLanguage } from "../src/context/language" +import { parseBindingTokens } from "./keybind-tokens" /** Lock drag movement to the X axis (horizontal-only tab dragging). */ export const ConstrainDragYAxis: Component = () => { @@ -92,7 +93,7 @@ export const SortableTab: Component<{ - + props.onFork?.()}> @@ -103,6 +104,13 @@ export const SortableTab: Component<{ {t("agentManager.tab.close")} + + + {parseBindingTokens(props.closeKeybind ?? "").map((token) => ( + {token} + ))} + + diff --git a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx index 4e6cd27824d..61fa53e7a21 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx @@ -170,6 +170,7 @@ const defaultProps = { renaming: false, renameValue: "", closeKeybind: "⌘⇧W", + openKeybind: "⌘⇧O", onClick: noop, onDelete: noop, onStartRename: noop, @@ -177,6 +178,8 @@ const defaultProps = { onCommitRename: noop, onCancelRename: noop, onRemoveStale: noop, + onCopyPath: noop, + onOpen: noop, } // --------------------------------------------------------------------------- From 84d6fa553ea9de675e017d866c31c048f32373f9 Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 24 Mar 2026 11:08:47 +0100 Subject: [PATCH 032/264] fix(vscode): anchor extractStatus regex to colon to avoid false matches The previous regex /\b([1-5]\d{2})\b/ matched the first 3-digit number in range, so 'after 128 retries: 429' would extract 128 instead of 429. Anchor to colon with /:\s*([1-5]\d{2})\b/ since our error messages always use the 'SSE failed: 402' / 'FIM request failed: 429' format. --- .../autocomplete/classic-auto-complete/ErrorBackoff.ts | 3 ++- .../tests/unit/autocomplete-error-backoff.test.ts | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/ErrorBackoff.ts b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/ErrorBackoff.ts index 32734c2d894..612a5acb85d 100644 --- a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/ErrorBackoff.ts +++ b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/ErrorBackoff.ts @@ -32,10 +32,11 @@ export type ErrorKind = "fatal" | "retriable" | "transient" /** * Extract an HTTP status code from an error message like "SSE failed: 402 Payment Required" * or "FIM request failed: 429 Too Many Requests". + * Matches the status code after a colon to avoid false positives from other numbers. */ export function extractStatus(error: unknown): number | null { const msg = error instanceof Error ? error.message : String(error) - const match = msg.match(/\b([1-5]\d{2})\b/) + const match = msg.match(/:\s*([1-5]\d{2})\b/) return match ? Number(match[1]) : null } diff --git a/packages/kilo-vscode/tests/unit/autocomplete-error-backoff.test.ts b/packages/kilo-vscode/tests/unit/autocomplete-error-backoff.test.ts index e9c3138afa5..46c986b972f 100644 --- a/packages/kilo-vscode/tests/unit/autocomplete-error-backoff.test.ts +++ b/packages/kilo-vscode/tests/unit/autocomplete-error-backoff.test.ts @@ -14,8 +14,8 @@ describe("extractStatus", () => { expect(extractStatus(new Error("FIM request failed: 429 Too Many Requests"))).toBe(429) }) - it("extracts 500 from generic error", () => { - expect(extractStatus(new Error("Server returned 500 Internal Server Error"))).toBe(500) + it("extracts 500 from SSE error message", () => { + expect(extractStatus(new Error("SSE failed: 500 Internal Server Error"))).toBe(500) }) it("returns null for message without status code", () => { @@ -29,6 +29,10 @@ describe("extractStatus", () => { it("returns null for empty error", () => { expect(extractStatus(new Error(""))).toBeNull() }) + + it("extracts 429 when message contains other numbers before the status", () => { + expect(extractStatus(new Error("after 128 retries: 429 Too Many Requests"))).toBe(429) + }) }) describe("classify", () => { From a637715a5ebc19dadbd2dc1e4858f25c440b6edd Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 24 Mar 2026 11:09:37 +0100 Subject: [PATCH 033/264] Revert "feat(cli,vscode): change default bash auto-approve rule to ask" --- .../webview-ui/src/components/settings/AutoApproveTab.tsx | 1 - packages/opencode/src/agent/agent.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/AutoApproveTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/AutoApproveTab.tsx index b51ea53d870..751dd2a1e6a 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/AutoApproveTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/AutoApproveTab.tsx @@ -112,7 +112,6 @@ const TRAILING_TOOLS: ToolDef[] = [ * change, this map must be updated to match. */ const TOOL_DEFAULTS: Partial> = { - bash: "ask", doom_loop: "ask", external_directory: "ask", } diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index a7a8a490bbc..ceb52d374c2 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -63,7 +63,6 @@ export namespace Agent { const whitelistedDirs = [Truncate.GLOB, ...skillDirs.map((dir) => path.join(dir, "*"))] const defaults = PermissionNext.fromConfig({ "*": "allow", - bash: "ask", doom_loop: "ask", external_directory: { "*": "ask", From 682f80011312ca29957717e9f39b0467b3c9d9ce Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 24 Mar 2026 11:13:55 +0100 Subject: [PATCH 034/264] fix(vscode): reset fatalNotified alongside backoff on auth changes After a reconnect or global.disposed, only backoff state was reset but fatalNotified stayed true. A subsequent 402 would block autocomplete without showing the warning notification again. Add resetBackoff() method that resets both, and use it from all external call sites. --- .../services/autocomplete/AutocompleteServiceManager.ts | 4 ++-- .../AutocompleteInlineCompletionProvider.ts | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts b/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts index eaa03dc5c45..329240445d5 100644 --- a/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts +++ b/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts @@ -93,7 +93,7 @@ export class AutocompleteServiceManager { // Also reset error backoff — a reconnect may mean the user re-authenticated // or added credits, so we should give autocomplete a fresh chance. this.unsubscribeState = connectionService.onStateChange(() => { - this.inlineCompletionProvider.backoff.reset() + this.inlineCompletionProvider.resetBackoff() void this.load() }) @@ -102,7 +102,7 @@ export class AutocompleteServiceManager { // reliable signal that credentials may have changed. this.unsubscribeEvent = connectionService.onEventFiltered( (event) => event.type === "global.disposed", - () => this.inlineCompletionProvider.backoff.reset(), + () => this.inlineCompletionProvider.resetBackoff(), ) void this.load() diff --git a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteInlineCompletionProvider.ts b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteInlineCompletionProvider.ts index 8a773b3b9d3..c09f5eb2d2a 100644 --- a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteInlineCompletionProvider.ts +++ b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteInlineCompletionProvider.ts @@ -286,6 +286,15 @@ export class AutocompleteInlineCompletionProvider implements vscode.InlineComple } } + /** + * Reset error backoff and allow fatal notifications to fire again. + * Call this when auth state changes (login, reconnect, org switch). + */ + public resetBackoff(): void { + this.backoff.reset() + this.fatalNotified = false + } + public dispose(): void { if (this.debounceTimer !== null) { clearTimeout(this.debounceTimer) From 9fbb3120808cb38dc7b28addad72a905191538bb Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 24 Mar 2026 11:15:35 +0100 Subject: [PATCH 035/264] fix(vscode): restrict extractStatus regex to 4xx/5xx status codes We only care about client (4xx) and server (5xx) errors. Tightening the regex from [1-5] to [45] avoids matching 1xx/2xx/3xx numbers that could appear in error messages. --- .../autocomplete/classic-auto-complete/ErrorBackoff.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/ErrorBackoff.ts b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/ErrorBackoff.ts index 612a5acb85d..0d211aea235 100644 --- a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/ErrorBackoff.ts +++ b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/ErrorBackoff.ts @@ -32,11 +32,12 @@ export type ErrorKind = "fatal" | "retriable" | "transient" /** * Extract an HTTP status code from an error message like "SSE failed: 402 Payment Required" * or "FIM request failed: 429 Too Many Requests". - * Matches the status code after a colon to avoid false positives from other numbers. + * Only matches 4xx/5xx after a colon — the two error sources (SDK SSE client and + * gateway FIM route) both use this format. */ export function extractStatus(error: unknown): number | null { const msg = error instanceof Error ? error.message : String(error) - const match = msg.match(/:\s*([1-5]\d{2})\b/) + const match = msg.match(/:\s*([45]\d{2})\b/) return match ? Number(match[1]) : null } From b20cedb5710079b7b557f7576c75d023f12dde08 Mon Sep 17 00:00:00 2001 From: "kilo-maintainer[bot]" Date: Tue, 24 Mar 2026 10:45:11 +0000 Subject: [PATCH 036/264] release: v7.1.3 --- bun.lock | 32 +++++++++++++------------- package.json | 2 +- packages/app/package.json | 2 +- packages/desktop-electron/package.json | 2 +- packages/desktop/package.json | 2 +- packages/extensions/zed/extension.toml | 12 +++++----- packages/kilo-docs/package.json | 2 +- packages/kilo-gateway/package.json | 2 +- packages/kilo-i18n/package.json | 2 +- packages/kilo-telemetry/package.json | 2 +- packages/kilo-ui/package.json | 2 +- packages/kilo-vscode/package.json | 2 +- packages/opencode/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 +- packages/util/package.json | 2 +- script/upstream/package.json | 2 +- sdks/vscode/package.json | 2 +- 21 files changed, 41 insertions(+), 41 deletions(-) diff --git a/bun.lock b/bun.lock index 54a2892e862..67dd616ec1d 100644 --- a/bun.lock +++ b/bun.lock @@ -27,7 +27,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "7.1.2", + "version": "7.1.3", "dependencies": { "@kilocode/kilo-i18n": "workspace:*", "@kilocode/kilo-ui": "workspace:*", @@ -79,7 +79,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "7.1.2", + "version": "7.1.3", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -112,7 +112,7 @@ }, "packages/desktop-electron": { "name": "@opencode-ai/desktop-electron", - "version": "7.1.2", + "version": "7.1.3", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -142,7 +142,7 @@ }, "packages/kilo-docs": { "name": "@kilocode/kilo-docs", - "version": "7.1.2", + "version": "7.1.3", "dependencies": { "@docsearch/css": "^4", "@docsearch/js": "^4", @@ -171,7 +171,7 @@ }, "packages/kilo-gateway": { "name": "@kilocode/kilo-gateway", - "version": "7.1.2", + "version": "7.1.3", "dependencies": { "@clack/prompts": "1.0.0-alpha.1", "@kilocode/plugin": "workspace:*", @@ -203,7 +203,7 @@ }, "packages/kilo-i18n": { "name": "@kilocode/kilo-i18n", - "version": "7.1.2", + "version": "7.1.3", "devDependencies": { "@tsconfig/node22": "catalog:", "@types/bun": "catalog:", @@ -213,7 +213,7 @@ }, "packages/kilo-telemetry": { "name": "@kilocode/kilo-telemetry", - "version": "7.1.2", + "version": "7.1.3", "dependencies": { "@kilocode/kilo-gateway": "workspace:*", "@opentelemetry/api": "1.9.0", @@ -233,7 +233,7 @@ }, "packages/kilo-ui": { "name": "@kilocode/kilo-ui", - "version": "7.1.2", + "version": "7.1.3", "dependencies": { "@kobalte/core": "0.13.11", "@opencode-ai/util": "workspace:*", @@ -268,7 +268,7 @@ }, "packages/kilo-vscode": { "name": "kilo-code", - "version": "7.1.2", + "version": "7.1.3", "dependencies": { "@anthropic-ai/sdk": "^0.39.0", "@kilocode/kilo-i18n": "workspace:*", @@ -321,7 +321,7 @@ }, "packages/opencode": { "name": "@kilocode/cli", - "version": "7.1.2", + "version": "7.1.3", "bin": { "kilo": "./bin/kilo", "kilocode": "./bin/kilo", @@ -443,7 +443,7 @@ }, "packages/plugin": { "name": "@kilocode/plugin", - "version": "7.1.2", + "version": "7.1.3", "dependencies": { "@kilocode/sdk": "workspace:*", "zod": "catalog:", @@ -457,14 +457,14 @@ }, "packages/script": { "name": "@opencode-ai/script", - "version": "7.1.2", + "version": "7.1.3", "devDependencies": { "@types/bun": "catalog:", }, }, "packages/sdk/js": { "name": "@kilocode/sdk", - "version": "7.1.2", + "version": "7.1.3", "devDependencies": { "@hey-api/openapi-ts": "0.90.10", "@tsconfig/node22": "catalog:", @@ -475,7 +475,7 @@ }, "packages/storybook": { "name": "@opencode-ai/storybook", - "version": "7.1.2", + "version": "7.1.3", "devDependencies": { "@opencode-ai/ui": "workspace:*", "@solidjs/meta": "catalog:", @@ -498,7 +498,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "7.1.2", + "version": "7.1.3", "dependencies": { "@kilocode/sdk": "workspace:*", "@kobalte/core": "catalog:", @@ -545,7 +545,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "7.1.2", + "version": "7.1.3", "dependencies": { "zod": "catalog:", }, diff --git a/package.json b/package.json index c5aa660df26..dba58cb6a59 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,6 @@ "@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch", "ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch" }, - "version": "7.1.2", + "version": "7.1.3", "peerDependencies": {} } diff --git a/packages/app/package.json b/packages/app/package.json index f5814dc83e3..9d0b6e9b88a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "7.1.2", + "version": "7.1.3", "description": "", "type": "module", "exports": { diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json index 2b6cf704d9a..663dc883665 100644 --- a/packages/desktop-electron/package.json +++ b/packages/desktop-electron/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop-electron", "private": true, - "version": "7.1.2", + "version": "7.1.3", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 5984c959a43..dda2b1029fd 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "7.1.2", + "version": "7.1.3", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index d3365c36ff7..333088147a6 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.1.2" +version = "7.1.3" 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.1.2/opencode-darwin-arm64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.3/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.1.2/opencode-darwin-x64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.3/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.2/opencode-linux-arm64.tar.gz" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.3/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.1.2/opencode-linux-x64.tar.gz" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.3/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.1.2/opencode-windows-x64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.3/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/kilo-docs/package.json b/packages/kilo-docs/package.json index e14fa6df803..6c72df87e34 100644 --- a/packages/kilo-docs/package.json +++ b/packages/kilo-docs/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-docs", - "version": "7.1.2", + "version": "7.1.3", "private": true, "scripts": { "dev": "next dev --webpack --port 3002", diff --git a/packages/kilo-gateway/package.json b/packages/kilo-gateway/package.json index a88041fe96f..966daff9313 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.1.2", + "version": "7.1.3", "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 fa58760b7e4..fced9e2474b 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.1.2", + "version": "7.1.3", "type": "module", "license": "MIT", "description": "Kilo-specific i18n translations and overrides", diff --git a/packages/kilo-telemetry/package.json b/packages/kilo-telemetry/package.json index fd9a924f757..e907ce0451f 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.1.2", + "version": "7.1.3", "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 80f90256db6..eb8f9e0b9a7 100644 --- a/packages/kilo-ui/package.json +++ b/packages/kilo-ui/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-ui", - "version": "7.1.2", + "version": "7.1.3", "type": "module", "license": "MIT", "exports": { diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json index 1d85ef0705d..38924ae85c4 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.1.2", + "version": "7.1.3", "icon": "assets/icons/logo-outline-black.png", "galleryBanner": { "color": "#FFFFFF", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index c098d010b83..05063ec6088 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.1.2", + "version": "7.1.3", "name": "@kilocode/cli", "type": "module", "license": "MIT", diff --git a/packages/plugin/package.json b/packages/plugin/package.json index d906934297c..df44cb420a4 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.1.2", + "version": "7.1.3", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/script/package.json b/packages/script/package.json index 5bee846d87c..6aef80d1f9a 100644 --- a/packages/script/package.json +++ b/packages/script/package.json @@ -8,7 +8,7 @@ "exports": { ".": "./src/index.ts" }, - "version": "7.1.2", + "version": "7.1.3", "dependencies": {}, "peerDependencies": {} } diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 5ae00ad7db6..9c3eb73d48b 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.1.2", + "version": "7.1.3", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/storybook/package.json b/packages/storybook/package.json index 8ffeb3a6dad..42b358de4fe 100644 --- a/packages/storybook/package.json +++ b/packages/storybook/package.json @@ -26,7 +26,7 @@ "typescript": "catalog:", "vite": "catalog:" }, - "version": "7.1.2", + "version": "7.1.3", "dependencies": {}, "peerDependencies": {} } diff --git a/packages/ui/package.json b/packages/ui/package.json index 609876bad4b..3357a93dd7c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "7.1.2", + "version": "7.1.3", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index 9b7daa6c590..6936d568692 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "7.1.2", + "version": "7.1.3", "private": true, "type": "module", "license": "MIT", diff --git a/script/upstream/package.json b/script/upstream/package.json index 751e8e92fb0..c04071c2ae7 100644 --- a/script/upstream/package.json +++ b/script/upstream/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/upstream-merge", - "version": "7.1.2", + "version": "7.1.3", "private": true, "type": "module", "description": "Scripts for automating upstream opencode merges into Kilo", diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index a0e0a42c0a6..2f6717a19af 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "7.1.2", + "version": "7.1.3", "publisher": "sst-dev", "repository": { "type": "git", From a08905f5680b61a53280b68d67fec5158c836783 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 11:00:43 +0000 Subject: [PATCH 037/264] docs(kilo-docs): update Auto Balanced model from M2.5 (free) to M2.7 (paid) The Auto Balanced tier now uses Minimax M2.7 (paid) for implementation modes instead of Minimax M2.5 (Free). Update references in the user-facing auto-model docs and the gateway models-and-providers page. --- .../pages/code-with-ai/agents/auto-model.md | 28 +++++++++---------- .../pages/gateway/models-and-providers.md | 10 +++---- 2 files changed, 19 insertions(+), 19 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 ce89c2efb24..94a48c21970 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 @@ -45,28 +45,28 @@ That's it. No configuration needed. ## Auto Balanced -`kilo-auto/balanced` follows the same mode-based routing structure as Frontier but uses more cost-effective models — Kimi K2.5 for reasoning-heavy modes and Minimax M2.5 (Free) for implementation modes. +`kilo-auto/balanced` follows the same mode-based routing structure as Frontier but uses more cost-effective models — Kimi K2.5 for reasoning-heavy modes and Minimax M2.7 for implementation modes. ### Mode-to-Model Mapping -| Mode | Model Used | Best For | -| -------------- | ------------------- | ---------------------------- | -| `architect` | Kimi K2.5 | System design, planning | -| `orchestrator` | Kimi K2.5 | Multi-step task coordination | -| `ask` | Kimi K2.5 | Questions, explanations | -| `plan` | Kimi K2.5 | Planning, reasoning | -| `general` | Kimi K2.5 | General assistance | -| `debug` | Kimi K2.5 | Debugging and fixing issues | -| `code` | Minimax M2.5 (Free) | Writing and editing code | -| `build` | Minimax M2.5 (Free) | Implementation tasks | -| `explore` | Minimax M2.5 (Free) | Codebase exploration | +| Mode | Model Used | Best For | +| -------------- | ------------ | ---------------------------- | +| `architect` | Kimi K2.5 | System design, planning | +| `orchestrator` | Kimi K2.5 | Multi-step task coordination | +| `ask` | Kimi K2.5 | Questions, explanations | +| `plan` | Kimi K2.5 | Planning, reasoning | +| `general` | Kimi K2.5 | General assistance | +| `debug` | Kimi K2.5 | Debugging and fixing issues | +| `code` | Minimax M2.7 | Writing and editing code | +| `build` | Minimax M2.7 | Implementation tasks | +| `explore` | Minimax M2.7 | Codebase exploration | **Planning and reasoning tasks** use Kimi K2.5, a strong open-weight reasoning model from Moonshot AI. -**Implementation tasks** use Minimax M2.5, which provides fast, capable code generation at a fraction of frontier model costs. +**Implementation tasks** use Minimax M2.7, which provides fast, capable code generation at a fraction of frontier model costs. {% callout type="info" title="Image support" %} -Auto Balanced does not support image inputs, since Minimax M2.5 does not have vision capabilities. +Auto Balanced does not support image inputs, since Minimax M2.7 does not have vision capabilities. {% /callout %} ## Benefits diff --git a/packages/kilo-docs/pages/gateway/models-and-providers.md b/packages/kilo-docs/pages/gateway/models-and-providers.md index aa5c7cf8c01..1a902b2c392 100644 --- a/packages/kilo-docs/pages/gateway/models-and-providers.md +++ b/packages/kilo-docs/pages/gateway/models-and-providers.md @@ -82,11 +82,11 @@ Routes to the most capable paid models optimizing for cost, performance, and cap Follows the same mode-based routing as Frontier but uses more cost-effective models. -| Mode | Resolved Model | -| -------------------------------------------------------------- | --------------------------- | -| `plan`, `general`, `architect`, `orchestrator`, `ask`, `debug` | `moonshotai/kimi-k2.5` | -| `build`, `explore`, `code` | `minimax/minimax-m2.5:free` | -| Default (no mode specified) | `minimax/minimax-m2.5:free` | +| Mode | Resolved Model | +| -------------------------------------------------------------- | ---------------------- | +| `plan`, `general`, `architect`, `orchestrator`, `ask`, `debug` | `moonshotai/kimi-k2.5` | +| `build`, `explore`, `code` | `minimax/minimax-m2.7` | +| Default (no mode specified) | `minimax/minimax-m2.7` | ### `kilo-auto/free` From c87457ba5a82198bf40e5a1770df99fceb038a75 Mon Sep 17 00:00:00 2001 From: "kilo-code-bot[bot]" <240665456+kilo-code-bot[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 12:25:17 +0100 Subject: [PATCH 038/264] fix(cli,vscode): consume stream explicitly to prevent commit message infinite loading (#7434) * fix(cli,vscode): prevent infinite loading in commit message generation Add timeout-based AbortController to both server-side LLM stream (30s) and client-side HTTP request (35s) so the SCM progress spinner cannot hang indefinitely. Make the VS Code progress indicator cancellable so users can abort manually. Also fix outdated test mocks that referenced the removed getHttpClient API instead of the current SDK getClient. * fix(cli,vscode): consume stream explicitly to prevent infinite loading Replace `await stream.text` with `for await (chunk of stream.textStream)` so that stream-level errors surface immediately instead of leaving the promise hanging indefinitely. With some providers and Vercel AI SDK versions, the `.text` promise never resolves when the underlying stream errors out early, causing the spinner to load forever (fixes #7345). Add a catch block that distinguishes timeout aborts from other errors and re-throws with a human-readable message (partially addresses #6568). In the VS Code extension, log the actual error in the getClient catch block instead of silently discarding it. --------- Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .../commit-message/__tests__/index.spec.ts | 64 ++++++++------ .../src/services/commit-message/index.ts | 48 +++++++--- .../opencode/src/commit-message/generate.ts | 88 ++++++++++++------- 3 files changed, 129 insertions(+), 71 deletions(-) diff --git a/packages/kilo-vscode/src/services/commit-message/__tests__/index.spec.ts b/packages/kilo-vscode/src/services/commit-message/__tests__/index.spec.ts index 6afa163c866..40b8c8980cd 100644 --- a/packages/kilo-vscode/src/services/commit-message/__tests__/index.spec.ts +++ b/packages/kilo-vscode/src/services/commit-message/__tests__/index.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach } from "vitest" +import { describe, it, expect, vi, beforeEach, type Mock } from "vitest" // Mock vscode following the pattern from AutocompleteServiceManager.spec.ts vi.mock("vscode", () => { @@ -38,7 +38,7 @@ import type { KiloConnectionService } from "../../cli-backend/connection-service describe("commit-message service", () => { let mockContext: vscode.ExtensionContext let mockConnectionService: KiloConnectionService - let mockHttpClient: { generateCommitMessage: ReturnType } + let mockClient: { commitMessage: { generate: Mock } } beforeEach(() => { vi.clearAllMocks() @@ -47,12 +47,14 @@ describe("commit-message service", () => { subscriptions: [], } as any - mockHttpClient = { - generateCommitMessage: vi.fn().mockResolvedValue("feat: add new feature"), + mockClient = { + commitMessage: { + generate: vi.fn().mockResolvedValue({ data: { message: "feat: add new feature" } }), + }, } mockConnectionService = { - getHttpClient: vi.fn().mockReturnValue(mockHttpClient), + getClient: vi.fn().mockReturnValue(mockClient), } as any }) @@ -87,12 +89,12 @@ describe("commit-message service", () => { registerCommitMessageService(mockContext, mockConnectionService) // Extract the registered command callback - const registerCall = vi.mocked(vscode.commands.registerCommand).mock.calls[0]! + const registerCall = (vscode.commands.registerCommand as Mock).mock.calls[0]! commandCallback = registerCall[1] as (...args: any[]) => Promise }) it("shows error when git extension is not found", async () => { - vi.mocked(vscode.extensions.getExtension).mockReturnValue(undefined) + ;(vscode.extensions.getExtension as Mock).mockReturnValue(undefined) await commandCallback() @@ -100,13 +102,13 @@ describe("commit-message service", () => { }) it("shows error when no git repository is found", async () => { - vi.mocked(vscode.extensions.getExtension).mockReturnValue({ + ;(vscode.extensions.getExtension as Mock).mockReturnValue({ isActive: true, activate: vi.fn().mockResolvedValue(undefined), exports: { getAPI: () => ({ repositories: [] }), }, - } as any) + }) await commandCallback() @@ -114,7 +116,7 @@ describe("commit-message service", () => { }) it("shows error when backend is not connected", async () => { - vi.mocked(vscode.extensions.getExtension).mockReturnValue({ + ;(vscode.extensions.getExtension as Mock).mockReturnValue({ isActive: true, activate: vi.fn().mockResolvedValue(undefined), exports: { @@ -122,8 +124,8 @@ describe("commit-message service", () => { repositories: [{ inputBox: { value: "" }, rootUri: { fsPath: "/repo" } }], }), }, - } as any) - vi.mocked(mockConnectionService.getHttpClient as any).mockImplementation(() => { + }) + ;(mockConnectionService.getClient as Mock).mockImplementation(() => { throw new Error("Not connected") }) @@ -134,9 +136,9 @@ describe("commit-message service", () => { ) }) - it("calls generateCommitMessage on the HTTP client with repository root path", async () => { + it("calls commitMessage.generate on the SDK client with repository root path", async () => { const mockInputBox = { value: "" } - vi.mocked(vscode.extensions.getExtension).mockReturnValue({ + ;(vscode.extensions.getExtension as Mock).mockReturnValue({ isActive: true, activate: vi.fn().mockResolvedValue(undefined), exports: { @@ -144,21 +146,24 @@ describe("commit-message service", () => { repositories: [{ inputBox: mockInputBox, rootUri: { fsPath: "/repo" } }], }), }, - } as any) + }) - // Make withProgress execute its callback - vi.mocked(vscode.window.withProgress).mockImplementation(async (_options, task) => { - await task({} as any, {} as any) + const mockToken = { onCancellationRequested: vi.fn() } + ;(vscode.window.withProgress as Mock).mockImplementation(async (_options: any, task: any) => { + await task({}, mockToken) }) await commandCallback() - expect(mockHttpClient.generateCommitMessage).toHaveBeenCalledWith("/repo", undefined, undefined) + expect(mockClient.commitMessage.generate).toHaveBeenCalledWith( + { path: "/repo", selectedFiles: undefined, previousMessage: undefined }, + expect.objectContaining({ throwOnError: true }), + ) }) it("sets the generated message on the repository inputBox", async () => { const mockInputBox = { value: "" } - vi.mocked(vscode.extensions.getExtension).mockReturnValue({ + ;(vscode.extensions.getExtension as Mock).mockReturnValue({ isActive: true, activate: vi.fn().mockResolvedValue(undefined), exports: { @@ -166,10 +171,11 @@ describe("commit-message service", () => { repositories: [{ inputBox: mockInputBox, rootUri: { fsPath: "/repo" } }], }), }, - } as any) + }) - vi.mocked(vscode.window.withProgress).mockImplementation(async (_options, task) => { - await task({} as any, {} as any) + const mockToken = { onCancellationRequested: vi.fn() } + ;(vscode.window.withProgress as Mock).mockImplementation(async (_options: any, task: any) => { + await task({}, mockToken) }) await commandCallback() @@ -177,9 +183,9 @@ describe("commit-message service", () => { expect(mockInputBox.value).toBe("feat: add new feature") }) - it("shows progress in SourceControl location", async () => { + it("shows cancellable progress in SourceControl location", async () => { const mockInputBox = { value: "" } - vi.mocked(vscode.extensions.getExtension).mockReturnValue({ + ;(vscode.extensions.getExtension as Mock).mockReturnValue({ isActive: true, activate: vi.fn().mockResolvedValue(undefined), exports: { @@ -187,10 +193,11 @@ describe("commit-message service", () => { repositories: [{ inputBox: mockInputBox, rootUri: { fsPath: "/repo" } }], }), }, - } as any) + }) - vi.mocked(vscode.window.withProgress).mockImplementation(async (_options, task) => { - await task({} as any, {} as any) + const mockToken = { onCancellationRequested: vi.fn() } + ;(vscode.window.withProgress as Mock).mockImplementation(async (_options: any, task: any) => { + await task({}, mockToken) }) await commandCallback() @@ -199,6 +206,7 @@ describe("commit-message service", () => { expect.objectContaining({ location: vscode.ProgressLocation.SourceControl, title: "Generating commit message...", + cancellable: true, }), expect.any(Function), ) diff --git a/packages/kilo-vscode/src/services/commit-message/index.ts b/packages/kilo-vscode/src/services/commit-message/index.ts index c81c6205da8..1cebbd9f04a 100644 --- a/packages/kilo-vscode/src/services/commit-message/index.ts +++ b/packages/kilo-vscode/src/services/commit-message/index.ts @@ -44,7 +44,8 @@ export function registerCommitMessageService( let client: KiloClient | undefined try { client = connectionService.getClient() - } catch { + } catch (err) { + console.error("[Kilo New] Failed to get client:", err) vscode.window.showErrorMessage("Kilo backend is not connected. Please wait for the connection to establish.") return } @@ -57,22 +58,45 @@ export function registerCommitMessageService( const previousMessage = lastWorkspacePath === path ? lastGeneratedMessage : undefined + const controller = new AbortController() + await vscode.window .withProgress( - { location: vscode.ProgressLocation.SourceControl, title: "Generating commit message..." }, - async () => { - const { data } = await client.commitMessage.generate( - { path, selectedFiles: undefined, previousMessage }, - { throwOnError: true }, - ) - const message = data.message - repository.inputBox.value = message - lastGeneratedMessage = message - lastWorkspacePath = path - console.log("[Kilo New] Commit message generated successfully") + { + location: vscode.ProgressLocation.SourceControl, + title: "Generating commit message...", + cancellable: true, + }, + async (_progress, token) => { + // Wire VS Code cancellation to abort the HTTP request + token.onCancellationRequested(() => controller.abort()) + + // Client-side safety timeout (35s) — slightly longer than the + // server-side 30s timeout so the server can respond with a proper + // error first, but still ensures the spinner never hangs forever. + const timeout = 35_000 + const timer = setTimeout(() => controller.abort(), timeout) + + try { + const { data } = await client.commitMessage.generate( + { path, selectedFiles: undefined, previousMessage }, + { throwOnError: true, signal: controller.signal }, + ) + const message = data.message + repository.inputBox.value = message + lastGeneratedMessage = message + lastWorkspacePath = path + console.log("[Kilo New] Commit message generated successfully") + } finally { + clearTimeout(timer) + } }, ) .then(undefined, (error: unknown) => { + if (controller.signal.aborted) { + console.log("[Kilo New] Commit message generation was cancelled or timed out") + return + } const msg = getErrorMessage(error) console.error("[Kilo New] Failed to generate commit message:", msg) vscode.window.showErrorMessage(`Failed to generate commit message: ${msg}`) diff --git a/packages/opencode/src/commit-message/generate.ts b/packages/opencode/src/commit-message/generate.ts index f5321ace7b3..9dbbb246756 100644 --- a/packages/opencode/src/commit-message/generate.ts +++ b/packages/opencode/src/commit-message/generate.ts @@ -110,6 +110,11 @@ function clean(text: string): string { return result.trim() } +// Maximum time (ms) to wait for the LLM to produce a commit message before +// aborting. Prevents the HTTP request from hanging indefinitely when the +// provider is slow or the stream stalls (e.g. due to config state races). +const TIMEOUT_MS = 30_000 + export async function generateCommitMessage(request: CommitMessageRequest): Promise { const ctx = await getGitContext(request.path, request.selectedFiles) if (ctx.files.length === 0) { @@ -141,38 +146,59 @@ export async function generateCommitMessage(request: CommitMessageRequest): Prom userMessage = `IMPORTANT: Generate a COMPLETELY DIFFERENT commit message from the previous one. The previous message was: "${request.previousMessage}". Use a different type, scope, or description approach.\n\n${userMessage}` } - const stream = await LLM.stream({ - agent, - user: { - id: "commit-message", + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), TIMEOUT_MS) + + try { + const stream = await LLM.stream({ + agent, + user: { + id: "commit-message", + sessionID: "commit-message", + role: "user", + model: { + providerID: model.providerID, + modelID: model.id, + }, + time: { + created: Date.now(), + completed: Date.now(), + }, + } as any, + tools: {}, + model, + small: true, + messages: [ + { + role: "user" as const, + content: userMessage, + }, + ], + abort: controller.signal, sessionID: "commit-message", - role: "user", - model: { - providerID: model.providerID, - modelID: model.id, - }, - time: { - created: Date.now(), - completed: Date.now(), - }, - } as any, - tools: {}, - model, - small: true, - messages: [ - { - role: "user" as const, - content: userMessage, - }, - ], - abort: new AbortController().signal, - sessionID: "commit-message", - system: [], - retries: 3, - }) + system: [], + retries: 3, + }) - const result = await stream.text - log.info("generated", { message: result }) + // Consume the stream explicitly so that stream-level errors surface + // immediately instead of leaving the .text promise hanging (issue #7345). + // With some providers/versions of the Vercel AI SDK, `await stream.text` + // never resolves when the underlying stream errors out early. + let result = "" + for await (const chunk of stream.textStream) { + result += chunk + } - return { message: clean(result) } + log.info("generated", { message: result }) + return { message: clean(result) } + } catch (err) { + if (controller.signal.aborted) { + throw new Error("Commit message generation timed out after 30 seconds") + } + const msg = err instanceof Error ? err.message : String(err) + log.error("generation failed", { error: msg }) + throw new Error(`Failed to generate commit message: ${msg}`) + } finally { + clearTimeout(timer) + } } From 53ab8e96522c3ec5755264f762f25361ceb481ff Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Tue, 24 Mar 2026 13:11:56 +0100 Subject: [PATCH 039/264] fix: prevent cmd.exe windows from appearing on Windows - Set process.type in mcp/index.ts so MCP SDK's StdioClientTransport uses windowsHide:true when spawning MCP servers on Windows (the SDK only enables this in Electron, checking 'type' in process) - Add windowsHide:true to GitOps.ts git execFile calls - Add windowsHide:true to marketplace installer tar execFile call --- packages/kilo-vscode/src/agent-manager/GitOps.ts | 1 + packages/kilo-vscode/src/services/marketplace/installer.ts | 2 +- packages/opencode/src/mcp/index.ts | 7 +++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/kilo-vscode/src/agent-manager/GitOps.ts b/packages/kilo-vscode/src/agent-manager/GitOps.ts index ff9c123fdef..93434ebeb74 100644 --- a/packages/kilo-vscode/src/agent-manager/GitOps.ts +++ b/packages/kilo-vscode/src/agent-manager/GitOps.ts @@ -357,6 +357,7 @@ export class GitOps { env: options?.env, encoding: "utf8", maxBuffer: 64 * 1024 * 1024, + windowsHide: true, }, (error, stdout, stderr) => { if (!error) { diff --git a/packages/kilo-vscode/src/services/marketplace/installer.ts b/packages/kilo-vscode/src/services/marketplace/installer.ts index bac22e79f4b..eb56d3f6a6d 100644 --- a/packages/kilo-vscode/src/services/marketplace/installer.ts +++ b/packages/kilo-vscode/src/services/marketplace/installer.ts @@ -152,7 +152,7 @@ export class MarketplaceInstaller { await fs.writeFile(tarball, buffer) await fs.mkdir(staging, { recursive: true }) - await exec("tar", ["-xzf", tarball, "--strip-components=1", "-C", staging]) + await exec("tar", ["-xzf", tarball, "--strip-components=1", "-C", staging], { windowsHide: true }) const escaped = await findEscapedPaths(staging) if (escaped.length > 0) { diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 49607716a3a..e188bcc830d 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -3,6 +3,13 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js" import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js" import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js" +// kilocode_change start +// The MCP SDK only sets windowsHide:true in Electron (checks `'type' in process`). +// Set process.type so the SDK hides cmd.exe windows when spawning MCP servers on Windows. +if (process.platform === "win32" && !("type" in process)) { + ;(process as NodeJS.Process & { type: string }).type = "browser" +} +// kilocode_change end import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js" import { CallToolResultSchema, From d6d082ea8016ff1a6711099ccd01f84a60123bdf Mon Sep 17 00:00:00 2001 From: Marius Date: Tue, 24 Mar 2026 13:12:40 +0100 Subject: [PATCH 040/264] fix(vscode): use correct SCM repository for commit message generation (#7528) When multiple git repositories are present (e.g. worktrees), the command always used repositories[0] regardless of which SCM panel the user clicked. Now accepts the SourceControl argument passed by VS Code from scm/title and scm/input menus to match the correct repository. Co-authored-by: Mark IJbema --- .../commit-message/__tests__/index.spec.ts | 51 ++++++ .../src/services/commit-message/index.ts | 151 ++++++++++-------- 2 files changed, 133 insertions(+), 69 deletions(-) diff --git a/packages/kilo-vscode/src/services/commit-message/__tests__/index.spec.ts b/packages/kilo-vscode/src/services/commit-message/__tests__/index.spec.ts index 40b8c8980cd..124002d8028 100644 --- a/packages/kilo-vscode/src/services/commit-message/__tests__/index.spec.ts +++ b/packages/kilo-vscode/src/services/commit-message/__tests__/index.spec.ts @@ -211,5 +211,56 @@ describe("commit-message service", () => { expect.any(Function), ) }) + + it("uses the matching repository when SourceControl arg is provided", async () => { + const mainInputBox = { value: "" } + const worktreeInputBox = { value: "" } + vi.mocked(vscode.extensions.getExtension).mockReturnValue({ + isActive: true, + activate: vi.fn().mockResolvedValue(undefined), + exports: { + getAPI: () => ({ + repositories: [ + { inputBox: mainInputBox, rootUri: { fsPath: "/main-repo" } }, + { inputBox: worktreeInputBox, rootUri: { fsPath: "/worktree-repo" } }, + ], + }), + }, + } as any) + + vi.mocked(vscode.window.withProgress).mockImplementation(async (_options, task) => { + await task({} as any, {} as any) + }) + + // Simulate SCM title/input passing the SourceControl for the worktree repo + const scmArg = { rootUri: { fsPath: "/worktree-repo" } } as vscode.SourceControl + await commandCallback(scmArg) + + // The worktree repo's inputBox should be updated, not the main repo's + expect(worktreeInputBox.value).toBe("feat: add new feature") + expect(mainInputBox.value).toBe("") + }) + + it("falls back to first repository when SourceControl arg has no match", async () => { + const mainInputBox = { value: "" } + vi.mocked(vscode.extensions.getExtension).mockReturnValue({ + isActive: true, + activate: vi.fn().mockResolvedValue(undefined), + exports: { + getAPI: () => ({ + repositories: [{ inputBox: mainInputBox, rootUri: { fsPath: "/main-repo" } }], + }), + }, + } as any) + + vi.mocked(vscode.window.withProgress).mockImplementation(async (_options, task) => { + await task({} as any, {} as any) + }) + + const scmArg = { rootUri: { fsPath: "/nonexistent-repo" } } as vscode.SourceControl + await commandCallback(scmArg) + + expect(mainInputBox.value).toBe("feat: add new feature") + }) }) }) diff --git a/packages/kilo-vscode/src/services/commit-message/index.ts b/packages/kilo-vscode/src/services/commit-message/index.ts index 1cebbd9f04a..4c57712edcc 100644 --- a/packages/kilo-vscode/src/services/commit-message/index.ts +++ b/packages/kilo-vscode/src/services/commit-message/index.ts @@ -19,89 +19,102 @@ interface GitExtensionExports { getAPI(version: number): GitAPI } +function findRepository(repositories: GitRepository[], arg?: vscode.SourceControl): GitRepository | undefined { + if (!repositories.length) return undefined + if (arg?.rootUri) { + const target = arg.rootUri.fsPath + const match = repositories.find((r) => r.rootUri.fsPath === target) + if (match) return match + } + return repositories[0] +} + export function registerCommitMessageService( context: vscode.ExtensionContext, connectionService: KiloConnectionService, ): vscode.Disposable[] { - const command = vscode.commands.registerCommand("kilo-code.new.generateCommitMessage", async () => { - const extension = vscode.extensions.getExtension("vscode.git") - if (!extension) { - vscode.window.showErrorMessage("Git extension not found") - return - } + const command = vscode.commands.registerCommand( + "kilo-code.new.generateCommitMessage", + async (arg?: vscode.SourceControl) => { + const extension = vscode.extensions.getExtension("vscode.git") + if (!extension) { + vscode.window.showErrorMessage("Git extension not found") + return + } - if (!extension.isActive) { - await extension.activate() - } + if (!extension.isActive) { + await extension.activate() + } - const git = extension.exports?.getAPI(1) - const repository = git?.repositories[0] - if (!repository) { - vscode.window.showErrorMessage("No Git repository found") - return - } + const git = extension.exports?.getAPI(1) + const repository = findRepository(git?.repositories ?? [], arg) + if (!repository) { + vscode.window.showErrorMessage("No Git repository found") + return + } - let client: KiloClient | undefined - try { - client = connectionService.getClient() - } catch (err) { - console.error("[Kilo New] Failed to get client:", err) - vscode.window.showErrorMessage("Kilo backend is not connected. Please wait for the connection to establish.") - return - } - if (!client) { - vscode.window.showErrorMessage("Kilo backend is not connected. Please wait for the connection to establish.") - return - } + let client: KiloClient | undefined + try { + client = connectionService.getClient() + } catch (err) { + console.error("[Kilo New] Failed to get client:", err) + vscode.window.showErrorMessage("Kilo backend is not connected. Please wait for the connection to establish.") + return + } + if (!client) { + vscode.window.showErrorMessage("Kilo backend is not connected. Please wait for the connection to establish.") + return + } - const path = repository.rootUri.fsPath + const path = repository.rootUri.fsPath - const previousMessage = lastWorkspacePath === path ? lastGeneratedMessage : undefined + const previousMessage = lastWorkspacePath === path ? lastGeneratedMessage : undefined - const controller = new AbortController() + const controller = new AbortController() - await vscode.window - .withProgress( - { - location: vscode.ProgressLocation.SourceControl, - title: "Generating commit message...", - cancellable: true, - }, - async (_progress, token) => { - // Wire VS Code cancellation to abort the HTTP request - token.onCancellationRequested(() => controller.abort()) + await vscode.window + .withProgress( + { + location: vscode.ProgressLocation.SourceControl, + title: "Generating commit message...", + cancellable: true, + }, + async (_progress, token) => { + // Wire VS Code cancellation to abort the HTTP request + token.onCancellationRequested(() => controller.abort()) - // Client-side safety timeout (35s) — slightly longer than the - // server-side 30s timeout so the server can respond with a proper - // error first, but still ensures the spinner never hangs forever. - const timeout = 35_000 - const timer = setTimeout(() => controller.abort(), timeout) + // Client-side safety timeout (35s) — slightly longer than the + // server-side 30s timeout so the server can respond with a proper + // error first, but still ensures the spinner never hangs forever. + const timeout = 35_000 + const timer = setTimeout(() => controller.abort(), timeout) - try { - const { data } = await client.commitMessage.generate( - { path, selectedFiles: undefined, previousMessage }, - { throwOnError: true, signal: controller.signal }, - ) - const message = data.message - repository.inputBox.value = message - lastGeneratedMessage = message - lastWorkspacePath = path - console.log("[Kilo New] Commit message generated successfully") - } finally { - clearTimeout(timer) + try { + const { data } = await client.commitMessage.generate( + { path, selectedFiles: undefined, previousMessage }, + { throwOnError: true, signal: controller.signal }, + ) + const message = data.message + repository.inputBox.value = message + lastGeneratedMessage = message + lastWorkspacePath = path + console.log("[Kilo New] Commit message generated successfully") + } finally { + clearTimeout(timer) + } + }, + ) + .then(undefined, (error: unknown) => { + if (controller.signal.aborted) { + console.log("[Kilo New] Commit message generation was cancelled or timed out") + return } - }, - ) - .then(undefined, (error: unknown) => { - if (controller.signal.aborted) { - console.log("[Kilo New] Commit message generation was cancelled or timed out") - return - } - const msg = getErrorMessage(error) - console.error("[Kilo New] Failed to generate commit message:", msg) - vscode.window.showErrorMessage(`Failed to generate commit message: ${msg}`) - }) - }) + const msg = getErrorMessage(error) + console.error("[Kilo New] Failed to generate commit message:", msg) + vscode.window.showErrorMessage(`Failed to generate commit message: ${msg}`) + }) + }, + ) context.subscriptions.push(command) return [command] From 181e3a24f8429d5f1fa5963f757e5ad308aaaf96 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Tue, 24 Mar 2026 08:37:12 -0400 Subject: [PATCH 041/264] docs(kilo-docs): add platform tabs infrastructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the foundation for platform-specific documentation: - Platform type (legacy/new/all) in NavLink interface - PageVersionSwitcher banner for legacy-only and new-only pages - PlatformBadge component in SideNav with Legacy/New labels - Tabs hydration fix (SSR-safe useState + useEffect) - _app.tsx reads frontmatter.platform and renders banner No content changes — frontmatter markers and nav tags will be added in their respective section PRs. --- .../components/PageVersionSwitcher.tsx | 44 +++++++++++++++++++ packages/kilo-docs/components/SideNav.tsx | 38 +++++++++++++++- packages/kilo-docs/components/Tabs.tsx | 4 +- packages/kilo-docs/components/index.js | 1 + packages/kilo-docs/lib/types.ts | 3 ++ packages/kilo-docs/pages/_app.tsx | 5 ++- 6 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 packages/kilo-docs/components/PageVersionSwitcher.tsx diff --git a/packages/kilo-docs/components/PageVersionSwitcher.tsx b/packages/kilo-docs/components/PageVersionSwitcher.tsx new file mode 100644 index 00000000000..1ff9ec3a91a --- /dev/null +++ b/packages/kilo-docs/components/PageVersionSwitcher.tsx @@ -0,0 +1,44 @@ +import React from "react" +import type { Platform } from "../lib/types" + +interface Props { + platform?: Platform +} + +export function PageVersionSwitcher({ platform }: Props) { + if (!platform || platform === "all") return null + + const legacy = platform === "legacy" + return ( +
+ {legacy ? "\u24D8" : "\u2728"} + + {legacy + ? "This page documents the legacy VSCode extension. The current VSCode extension & CLI does not have an equivalent page yet." + : "This page documents the current VSCode extension & CLI. There is no equivalent page for the legacy VSCode extension."} + + + +
+ ) +} diff --git a/packages/kilo-docs/components/SideNav.tsx b/packages/kilo-docs/components/SideNav.tsx index 1960fa1f842..2897576d3fb 100644 --- a/packages/kilo-docs/components/SideNav.tsx +++ b/packages/kilo-docs/components/SideNav.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from "react" import { useRouter } from "next/router" import Link from "next/link" -import { SectionNav } from "../lib/types" +import { Platform, SectionNav } from "../lib/types" import { Nav } from "../lib/nav" // Define navigation items for each major section @@ -67,6 +67,16 @@ const ChevronLeft = () => ( ) +function PlatformBadge({ platform }: { platform?: Platform }) { + if (!platform || platform === "all") return null + const label = platform === "legacy" ? "Legacy" : "New" + return ( + + {label} + + ) +} + interface SideNavProps { isMobileOpen?: boolean onMobileClose?: () => void @@ -268,6 +278,7 @@ export function SideNav({ isMobileOpen = false, onMobileClose }: SideNavProps) {
{link.children} +
) diff --git a/packages/kilo-docs/components/Tabs.tsx b/packages/kilo-docs/components/Tabs.tsx index 43fe16707ca..6cc69abda61 100644 --- a/packages/kilo-docs/components/Tabs.tsx +++ b/packages/kilo-docs/components/Tabs.tsx @@ -27,16 +27,16 @@ export function Tabs({ children }: TabsProps) { ) const indexFromHash = () => { - if (typeof window === "undefined") return 0 const hash = window.location.hash.slice(1) if (!hash) return 0 const found = tabs.findIndex((tab) => slugify(tab.props.label) === hash) return found >= 0 ? found : 0 } - const [activeIndex, setActiveIndex] = useState(indexFromHash) + const [activeIndex, setActiveIndex] = useState(0) useEffect(() => { + setActiveIndex(indexFromHash()) const onHashChange = () => setActiveIndex(indexFromHash()) window.addEventListener("hashchange", onHashChange) return () => window.removeEventListener("hashchange", onHashChange) diff --git a/packages/kilo-docs/components/index.js b/packages/kilo-docs/components/index.js index 9bb2cc21b24..9297d31ab0e 100644 --- a/packages/kilo-docs/components/index.js +++ b/packages/kilo-docs/components/index.js @@ -7,6 +7,7 @@ export * from "./Icon" export * from "./Image" export * from "./KiloCodeIcon" export * from "./PageFooter" +export * from "./PageVersionSwitcher" export * from "./SideNav" export * from "./Table" export * from "./TableOfContents" diff --git a/packages/kilo-docs/lib/types.ts b/packages/kilo-docs/lib/types.ts index 7e5fdf7a22e..5feef035784 100644 --- a/packages/kilo-docs/lib/types.ts +++ b/packages/kilo-docs/lib/types.ts @@ -1,6 +1,9 @@ +export type Platform = "legacy" | "new" | "all" + export interface NavLink { href: string children: string + platform?: Platform // "legacy" = stable VSCode only, "new" = new VSCode + CLI only, omitted = universal subLinks?: NavLink[] // Optional nested links for second-level navigation } diff --git a/packages/kilo-docs/pages/_app.tsx b/packages/kilo-docs/pages/_app.tsx index 6d45a81782c..245ab29af70 100644 --- a/packages/kilo-docs/pages/_app.tsx +++ b/packages/kilo-docs/pages/_app.tsx @@ -3,7 +3,7 @@ import Head from "next/head" import { useRouter } from "next/router" import posthog from "posthog-js" -import { CopyPageButton, PageFooter, SideNav, TableOfContents, TopNav } from "../components" +import { CopyPageButton, PageFooter, PageVersionSwitcher, SideNav, TableOfContents, TopNav } from "../components" import "prismjs" import "prismjs/components/prism-bash.min" @@ -129,6 +129,8 @@ export default function MyApp({ Component, pageProps }: AppProps) { const toc = pageProps.markdoc?.content ? collectHeadings(pageProps.markdoc.content) : [] + const platform = markdoc?.frontmatter?.platform + return ( <> @@ -179,6 +181,7 @@ export default function MyApp({ Component, pageProps }: AppProps) {
+ {markdoc && } {markdoc && }
From 04d0175d5576e553840e353615c85789495e72b7 Mon Sep 17 00:00:00 2001 From: Marius Date: Tue, 24 Mar 2026 13:45:39 +0100 Subject: [PATCH 042/264] fix(vscode): fix file path drop mentions to work with attachment system (#7477) * feat(vscode): support file path drops as @-mentions in chat input (#7476) Dragging files from VS Code's explorer or editor tabs into the chat prompt now inserts them as @/relative/path mentions (matching the legacy extension behavior). Image drops from the OS file manager continue to work as image attachments. Adds convertToMentionPath utility with full test coverage including protocol stripping, URI decoding, Windows paths, and vscode-remote. * fix(vscode): fix file path drop mentions to work with attachment system Three bugs from #7476: 1. Dropped mentions were never registered in mentionedPaths, so buildFileAttachments could not resolve them into file attachments. Fix: expose addPaths() on useFileMention, call it from the drop handler. 2. convertToMentionPath returned @/relative (with @ prefix and leading /) but the mention system expects bare relative paths like src/index.ts with callers adding the @ prefix. Fix: return bare relative path. 3. handleDragOver accepted text/plain, which intercepted normal text drags from the editor. extractDropPaths also treated any text as file paths. Fix: only accept application/vnd.code.uri-list in dragover; fall back to text/plain in extractDropPaths only when every line is a file path. --- .../tests/unit/path-mentions.test.ts | 130 ++++++++++++++++++ .../agent-manager/NewWorktreeDialog.tsx | 22 +++ .../src/components/chat/PromptInput.tsx | 20 +++ .../webview-ui/src/hooks/useFileMention.ts | 12 ++ .../src/hooks/useImageAttachments.ts | 32 ++++- .../webview-ui/src/utils/path-mentions.ts | 85 ++++++++++++ 6 files changed, 298 insertions(+), 3 deletions(-) create mode 100644 packages/kilo-vscode/tests/unit/path-mentions.test.ts create mode 100644 packages/kilo-vscode/webview-ui/src/utils/path-mentions.ts diff --git a/packages/kilo-vscode/tests/unit/path-mentions.test.ts b/packages/kilo-vscode/tests/unit/path-mentions.test.ts new file mode 100644 index 00000000000..ce52deca9e0 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/path-mentions.test.ts @@ -0,0 +1,130 @@ +import { describe, it, expect } from "bun:test" +import { convertToMentionPath, extractDropPaths } from "../../webview-ui/src/utils/path-mentions" + +describe("convertToMentionPath", () => { + it("converts an absolute path under cwd to a relative path", () => { + expect(convertToMentionPath("/home/user/project/src/index.ts", "/home/user/project")).toBe("src/index.ts") + }) + + it("strips file:// protocol", () => { + expect(convertToMentionPath("file:///home/user/project/lib/util.ts", "/home/user/project")).toBe("lib/util.ts") + }) + + it("strips vscode-remote:// protocol", () => { + expect(convertToMentionPath("vscode-remote://ssh-remote+host/home/user/project/app.ts", "/home/user/project")).toBe( + "app.ts", + ) + }) + + it("decodes URI-encoded characters", () => { + expect(convertToMentionPath("file:///home/user/my%20project/file.ts", "/home/user/my project")).toBe("file.ts") + }) + + it("preserves spaces in relative paths (no escaping)", () => { + expect(convertToMentionPath("/workspace/my folder/file.ts", "/workspace")).toBe("my folder/file.ts") + }) + + it("does not match cwd prefix without boundary", () => { + expect(convertToMentionPath("/workspace/app2/file.ts", "/workspace/app")).toBe("/workspace/app2/file.ts") + }) + + it("handles Windows file:// paths with leading slash", () => { + expect(convertToMentionPath("file:///D:/Projects/app/src/main.ts", "D:\\Projects\\app")).toBe("src/main.ts") + }) + + it("handles Windows backslash cwd", () => { + expect(convertToMentionPath("D:\\Projects\\app\\src\\main.ts", "D:\\Projects\\app")).toBe("src/main.ts") + }) + + it("uses case-insensitive comparison for matching", () => { + expect(convertToMentionPath("D:/projects/App/src/main.ts", "D:/Projects/app")).toBe("src/main.ts") + }) + + it("returns raw path when outside cwd", () => { + expect(convertToMentionPath("/other/dir/file.ts", "/home/user/project")).toBe("/other/dir/file.ts") + }) + + it("returns cleaned path when cwd is empty", () => { + expect(convertToMentionPath("/some/file.ts", "")).toBe("/some/file.ts") + }) + + it("handles trailing slash on cwd", () => { + expect(convertToMentionPath("/workspace/src/index.ts", "/workspace/")).toBe("src/index.ts") + }) + + it("does not include @ prefix (callers add it)", () => { + const result = convertToMentionPath("/workspace/src/app.ts", "/workspace") + expect(result).toBe("src/app.ts") + expect(result.startsWith("@")).toBe(false) + }) +}) + +describe("extractDropPaths", () => { + it("returns null when no text or URI data is present", () => { + const dt = { getData: () => "" } as unknown as DataTransfer + expect(extractDropPaths(dt)).toBe(null) + }) + + it("extracts paths from application/vnd.code.uri-list", () => { + const dt = { + getData: (type: string) => (type === "application/vnd.code.uri-list" ? "file:///home/user/a.ts" : ""), + } as unknown as DataTransfer + expect(extractDropPaths(dt)).toEqual(["file:///home/user/a.ts"]) + }) + + it("prefers uri-list over text/plain", () => { + const dt = { + getData: (type: string) => { + if (type === "application/vnd.code.uri-list") return "file:///from-uri.ts" + if (type === "text") return "/from-text.ts" + return "" + }, + } as unknown as DataTransfer + expect(extractDropPaths(dt)).toEqual(["file:///from-uri.ts"]) + }) + + it("falls back to text/plain when lines are file paths", () => { + const dt = { + getData: (type: string) => (type === "text" ? "/home/user/file.ts" : ""), + } as unknown as DataTransfer + expect(extractDropPaths(dt)).toEqual(["/home/user/file.ts"]) + }) + + it("accepts Windows absolute paths from text/plain", () => { + const dt = { + getData: (type: string) => (type === "text" ? "C:\\Users\\me\\file.ts" : ""), + } as unknown as DataTransfer + expect(extractDropPaths(dt)).toEqual(["C:\\Users\\me\\file.ts"]) + }) + + it("rejects plain text that is not a file path", () => { + const dt = { + getData: (type: string) => (type === "text" ? "hello world" : ""), + } as unknown as DataTransfer + expect(extractDropPaths(dt)).toBe(null) + }) + + it("rejects mixed text with non-path lines", () => { + const dt = { + getData: (type: string) => (type === "text" ? "/valid/path.ts\nnot a path" : ""), + } as unknown as DataTransfer + expect(extractDropPaths(dt)).toBe(null) + }) + + it("splits multiple paths on newlines from uri-list", () => { + const dt = { + getData: (type: string) => + type === "application/vnd.code.uri-list" + ? "file:///home/user/a.ts\nfile:///home/user/b.ts\r\nfile:///home/user/c.ts" + : "", + } as unknown as DataTransfer + expect(extractDropPaths(dt)).toEqual(["file:///home/user/a.ts", "file:///home/user/b.ts", "file:///home/user/c.ts"]) + }) + + it("filters out empty lines", () => { + const dt = { + getData: (type: string) => (type === "application/vnd.code.uri-list" ? "file:///a.ts\n\n \nfile:///b.ts" : ""), + } as unknown as DataTransfer + expect(extractDropPaths(dt)).toEqual(["file:///a.ts", "file:///b.ts"]) + }) +}) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx index 883ceca1b4f..eef2a0a4031 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx @@ -10,6 +10,7 @@ import { Spinner } from "@kilocode/kilo-ui/spinner" import { Popover } from "@kilocode/kilo-ui/popover" import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { useVSCode } from "../src/context/vscode" +import { useServer } from "../src/context/server" import { useSession } from "../src/context/session" import { ModelSelectorBase } from "../src/components/shared/ModelSelector" import { ModeSwitcherBase } from "../src/components/shared/ModeSwitcher" @@ -22,6 +23,7 @@ import { } from "./MultiModelSelector" import { useLanguage } from "../src/context/language" import { useImageAttachments } from "../src/hooks/useImageAttachments" +import { convertToMentionPath } from "../src/utils/path-mentions" import { BranchSelect } from "./BranchSelect" type VersionCount = 1 | 2 | 3 | 4 @@ -56,6 +58,7 @@ function sanitizeBranchName(name: string): string { export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBranch?: string }> = (props) => { const { t } = useLanguage() const vscode = useVSCode() + const server = useServer() const session = useSession() const [tab, setTab] = createSignal("new") @@ -84,6 +87,25 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran const [highlightedIndex, setHighlightedIndex] = createSignal(0) const imageAttach = useImageAttachments() + imageAttach.setFilePathDropHandler((paths) => { + const cwd = server.workspaceDirectory() + const resolved = paths.map((p) => convertToMentionPath(p, cwd)) + const ref = textareaRef + if (!ref) return + const val = ref.value + const cursor = ref.selectionStart ?? val.length + const before = val.substring(0, cursor) + const after = val.substring(cursor) + const inserted = resolved.map((p) => `@${p}`).join(" ") + const result = before + inserted + " " + after + ref.value = result + setPrompt(result) + persistPrompt(result) + const pos = cursor + inserted.length + 1 + ref.setSelectionRange(pos, pos) + ref.focus() + adjustHeight() + }) const persistPrompt = (value: string) => { const state = vscode.getState>() ?? {} diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx index eb246e806ed..cf8804cff15 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx @@ -22,6 +22,7 @@ import { useFileMention } from "../../hooks/useFileMention" import { useSlashCommand } from "../../hooks/useSlashCommand" import { useGhostText } from "../../hooks/useGhostText" import { useImageAttachments } from "../../hooks/useImageAttachments" +import { convertToMentionPath } from "../../utils/path-mentions" import { usePromptHistory } from "../../hooks/usePromptHistory" import { WandSparkles } from "@kilocode/kilo-ui/lucide" import { fileName, dirName, buildHighlightSegments, atEnd } from "./prompt-input-utils" @@ -56,6 +57,25 @@ export const PromptInput: Component = (props) => { const excluded = worktree ? new Set(["sessions"]) : undefined const slash = useSlashCommand(vscode, excluded) const imageAttach = useImageAttachments() + imageAttach.setFilePathDropHandler((paths) => { + const cwd = server.workspaceDirectory() + const resolved = paths.map((p) => convertToMentionPath(p, cwd)) + const ref = textareaRef + if (!ref) return + const val = ref.value + const cursor = ref.selectionStart ?? val.length + const before = val.substring(0, cursor) + const after = val.substring(cursor) + const inserted = resolved.map((p) => `@${p}`).join(" ") + const result = before + inserted + " " + after + ref.value = result + setText(result) + mention.addPaths(resolved, cwd) + const pos = cursor + inserted.length + 1 + ref.setSelectionRange(pos, pos) + ref.focus() + adjustHeight() + }) const history = usePromptHistory() const sessionKey = () => session.currentSessionID() ?? "__new__" diff --git a/packages/kilo-vscode/webview-ui/src/hooks/useFileMention.ts b/packages/kilo-vscode/webview-ui/src/hooks/useFileMention.ts index c937f1bb091..687cd21fe5f 100644 --- a/packages/kilo-vscode/webview-ui/src/hooks/useFileMention.ts +++ b/packages/kilo-vscode/webview-ui/src/hooks/useFileMention.ts @@ -36,6 +36,8 @@ export interface FileMention { setMentionIndex: (index: number) => void closeMention: () => void parseFileAttachments: (text: string) => FileAttachment[] + /** Register paths as active mentions (used by drag-and-drop). Pass cwd to ensure buildFileAttachments resolves correctly. */ + addPaths: (paths: string[], cwd: string) => void } export function useFileMention(vscode: VSCodeContext): FileMention { @@ -157,6 +159,15 @@ export function useFileMention(vscode: VSCodeContext): FileMention { return false } + const addPaths = (paths: string[], cwd: string) => { + if (cwd) workspaceDir = cwd + setMentionedPaths((prev) => { + const next = new Set(prev) + for (const p of paths) next.add(p) + return next + }) + } + const parseFileAttachments = (text: string): FileAttachment[] => buildFileAttachments(text, mentionedPaths(), workspaceDir) @@ -171,5 +182,6 @@ export function useFileMention(vscode: VSCodeContext): FileMention { setMentionIndex, closeMention, parseFileAttachments, + addPaths, } } diff --git a/packages/kilo-vscode/webview-ui/src/hooks/useImageAttachments.ts b/packages/kilo-vscode/webview-ui/src/hooks/useImageAttachments.ts index c72d6f123fd..b72328b0f3a 100644 --- a/packages/kilo-vscode/webview-ui/src/hooks/useImageAttachments.ts +++ b/packages/kilo-vscode/webview-ui/src/hooks/useImageAttachments.ts @@ -1,5 +1,6 @@ import { createSignal } from "solid-js" import { ACCEPTED_IMAGE_TYPES, isAcceptedImageType, isDragLeavingComponent } from "./image-attachments-utils" +import { extractDropPaths } from "../utils/path-mentions" export interface ImageAttachment { id: string @@ -8,9 +9,18 @@ export interface ImageAttachment { dataUrl: string } +/** Callback for handling text/URI file path drops. */ +export type FilePathDropHandler = (paths: string[]) => void + export function useImageAttachments() { const [images, setImages] = createSignal([]) const [dragging, setDragging] = createSignal(false) + let onFilePaths: FilePathDropHandler | undefined + + /** Register a handler for file path drops (text/URI-list). */ + const setFilePathDropHandler = (handler: FilePathDropHandler) => { + onFilePaths = handler + } const add = (file: File) => { if (!isAcceptedImageType(file.type)) return @@ -47,8 +57,12 @@ export function useImageAttachments() { } const handleDragOver = (event: DragEvent) => { - const hasFiles = event.dataTransfer?.types.includes("Files") - if (!hasFiles) return + const types = event.dataTransfer?.types + if (!types) return + // Accept file drops and VS Code URI-list drops (explorer, editor tabs). + // Do NOT accept bare text/plain here — that would intercept normal text drags. + const acceptable = types.includes("Files") || types.includes("application/vnd.code.uri-list") + if (!acceptable) return event.preventDefault() setDragging(true) } @@ -62,7 +76,18 @@ export function useImageAttachments() { const handleDrop = (event: DragEvent) => { setDragging(false) event.preventDefault() - const files = event.dataTransfer?.files + const dt = event.dataTransfer + if (!dt) return + + // First: check for text/URI file path drops (VS Code explorer, editor tabs) + const paths = extractDropPaths(dt) + if (paths && paths.length > 0 && onFilePaths) { + onFilePaths(paths) + return + } + + // Second: fall through to image file drops + const files = dt.files if (!files) return for (const file of Array.from(files)) add(file) } @@ -78,5 +103,6 @@ export function useImageAttachments() { handleDragOver, handleDragLeave, handleDrop, + setFilePathDropHandler, } } diff --git a/packages/kilo-vscode/webview-ui/src/utils/path-mentions.ts b/packages/kilo-vscode/webview-ui/src/utils/path-mentions.ts new file mode 100644 index 00000000000..ba991d001b0 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/utils/path-mentions.ts @@ -0,0 +1,85 @@ +/** + * Convert a dropped file URI or absolute path into a relative workspace path. + * Strips file:// and vscode-remote:// protocols, decodes URI components, + * and produces a relative path (e.g. "src/index.ts") when the file is inside + * the workspace. Returns the cleaned absolute path for files outside the workspace. + * + * The returned path does NOT include the "@" prefix — callers add that when + * inserting into the textarea so the path can also be registered in mentionedPaths. + */ +export function convertToMentionPath(path: string, cwd: string): string { + let cleaned = path + + if (cleaned.startsWith("file://")) { + cleaned = cleaned.substring(7) + } else if (cleaned.startsWith("vscode-remote://")) { + const rest = cleaned.substring("vscode-remote://".length) + const idx = rest.indexOf("/") + cleaned = idx !== -1 ? rest.substring(idx) : "" + } + + try { + cleaned = decodeURIComponent(cleaned) + // Remove leading slash for Windows paths like /d:/... + if (cleaned.startsWith("/") && cleaned[2] === ":") { + cleaned = cleaned.substring(1) + } + } catch (err) { + console.error("[Kilo New] Failed to decode dropped URI:", err, cleaned) + } + + const normalized = cleaned.replace(/\\/g, "/") + let root = cwd.replace(/\\/g, "/") + if (root.endsWith("/")) root = root.slice(0, -1) + + if (!root) return cleaned + + if (normalized.toLowerCase().startsWith(root.toLowerCase())) { + const tail = normalized.substring(root.length) + // Boundary check: next char must be "/" or end of string to avoid + // /workspace/app matching /workspace/app2/file.ts + if (tail === "" || tail.startsWith("/")) { + const relative = tail.startsWith("/") ? tail.substring(1) : tail + return relative || cleaned + } + } + + return cleaned +} + +/** Returns true when the line looks like a file URI or absolute path. */ +function isFilePath(line: string): boolean { + if (line.startsWith("file://") || line.startsWith("vscode-remote://")) return true + // Unix absolute path + if (line.startsWith("/")) return true + // Windows absolute path (e.g. C:\, D:/) + if (/^[A-Za-z]:[\\/]/.test(line)) return true + return false +} + +/** + * Extract file paths from a drop's DataTransfer. + * Only considers the URI-list data type (set by VS Code when dragging files + * from the explorer or editor tabs). Falls back to text/plain but only when + * every non-empty line looks like a file URI or absolute path, to avoid + * intercepting normal text drags from the editor. + * + * Returns null if no file paths are found. + */ +export function extractDropPaths(dt: DataTransfer): string[] | null { + // Prefer the VS Code-specific URI list — this is always file paths + const uri = dt.getData("application/vnd.code.uri-list") + if (uri) { + const paths = uri.split(/\r?\n/).filter((line) => line.trim() !== "") + if (paths.length > 0) return paths + } + + // Fall back to text/plain only if every line is a recognizable file path + const text = dt.getData("text") + if (text) { + const lines = text.split(/\r?\n/).filter((line) => line.trim() !== "") + if (lines.length > 0 && lines.every(isFilePath)) return lines + } + + return null +} From 37b866f1ef7c8424d11c32b968deb71ef4054603 Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Tue, 24 Mar 2026 13:54:30 +0100 Subject: [PATCH 043/264] feat(cli): add remote session relay over WebSocket Introduce WebSocket-based remote session relay so external clients can subscribe to sessions, send messages, answer questions, and respond to permissions in real time. - RemoteWS manages the authenticated WebSocket lifecycle with reconnect - RemoteSender dispatches inbound commands and forwards server events - Backfill pending questions/permissions when a client subscribes - Add question_reject command for dismissing questions remotely - Consolidate /remote TUI command into kilo-commands - Add session.viewed endpoint to the generated SDK - RemoteProtocol.Preview schema for diagnostic logging --- packages/opencode/src/cli/cmd/remote.ts | 31 + packages/opencode/src/cli/cmd/tui/app.tsx | 5 +- .../opencode/src/cli/cmd/tui/routes/home.tsx | 4 + .../src/cli/cmd/tui/routes/session/footer.tsx | 4 + .../src/cli/cmd/tui/routes/session/index.tsx | 2 + packages/opencode/src/index.ts | 2 + .../src/kilo-sessions/kilo-sessions.ts | 119 +++ .../src/kilo-sessions/remote-protocol.ts | 78 ++ .../src/kilo-sessions/remote-sender.ts | 321 +++++++ .../opencode/src/kilo-sessions/remote-ws.ts | 149 +++ .../opencode/src/kilocode/kilo-commands.tsx | 39 + packages/opencode/src/kilocode/remote-tui.tsx | 36 + packages/opencode/src/server/routes/remote.ts | 84 ++ .../opencode/src/server/routes/session.ts | 20 + packages/opencode/src/server/server.ts | 2 + .../kilo-sessions-enable-remote.test.ts | 202 ++++ .../kilo-sessions/remote-protocol.test.ts | 228 +++++ .../test/kilo-sessions/remote-sender.test.ts | 893 ++++++++++++++++++ .../test/kilo-sessions/remote-ws.test.ts | 264 ++++++ packages/sdk/js/src/v2/gen/sdk.gen.ts | 138 +++ packages/sdk/js/src/v2/gen/types.gen.ts | 87 ++ packages/sdk/openapi.json | 205 ++++ 22 files changed, 2912 insertions(+), 1 deletion(-) create mode 100644 packages/opencode/src/cli/cmd/remote.ts create mode 100644 packages/opencode/src/kilo-sessions/remote-protocol.ts create mode 100644 packages/opencode/src/kilo-sessions/remote-sender.ts create mode 100644 packages/opencode/src/kilo-sessions/remote-ws.ts create mode 100644 packages/opencode/src/kilocode/remote-tui.tsx create mode 100644 packages/opencode/src/server/routes/remote.ts create mode 100644 packages/opencode/test/kilo-sessions/kilo-sessions-enable-remote.test.ts create mode 100644 packages/opencode/test/kilo-sessions/remote-protocol.test.ts create mode 100644 packages/opencode/test/kilo-sessions/remote-sender.test.ts create mode 100644 packages/opencode/test/kilo-sessions/remote-ws.test.ts diff --git a/packages/opencode/src/cli/cmd/remote.ts b/packages/opencode/src/cli/cmd/remote.ts new file mode 100644 index 00000000000..f3ea09aea18 --- /dev/null +++ b/packages/opencode/src/cli/cmd/remote.ts @@ -0,0 +1,31 @@ +// kilocode_change - new file +import { cmd } from "./cmd" +import { bootstrap } from "../bootstrap" +import { KiloSessions } from "@/kilo-sessions/kilo-sessions" +import { Instance } from "@/project/instance" + +export const RemoteCommand = cmd({ + command: "remote", + describe: "enable remote connection for real-time session relay", + builder: (yargs) => yargs, + handler: async () => { + await bootstrap(process.cwd(), async () => { + await KiloSessions.enableRemote() + console.log("Remote connection enabled.") + + const abort = new AbortController() + const shutdown = async () => { + try { + KiloSessions.disableRemote() + await Instance.dispose() + } finally { + abort.abort() + } + } + process.on("SIGTERM", shutdown) + process.on("SIGINT", shutdown) + process.on("SIGHUP", shutdown) + await new Promise((resolve) => abort.signal.addEventListener("abort", resolve)) + }) + }, +}) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index e1eb218015e..a0236bd4569 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -261,9 +261,12 @@ function App() { } const [terminalTitleEnabled, setTerminalTitleEnabled] = createSignal(kv.get("terminal_title_enabled", true)) + // kilocode_change start — notify server which session the user is viewing (for live session indicators) createEffect(() => { - console.log(JSON.stringify(route.data)) + const sessionID = route.data.type === "session" ? route.data.sessionID : undefined + sdk.client.session.viewed({ sessionID }).catch(() => {}) }) + // kilocode_change end // Update terminal window title based on current route and session createEffect(() => { diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx index 9cf976e4da6..2dbc34e1888 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx @@ -16,6 +16,8 @@ import { useKV } from "../context/kv" import { useCommandDialog } from "../component/dialog-command" import { KiloNews } from "@/kilocode/components/kilo-news" // kilocode_change import { useConnected } from "../component/dialog-model" // kilocode_change +import { RemoteIndicator } from "@/kilocode/remote-tui" // kilocode_change +import { useSDK } from "../context/sdk" // kilocode_change // TODO: what is the best way to do this? let once = false @@ -27,6 +29,7 @@ export function Home() { const route = useRouteData("home") const promptRef = usePromptRef() const command = useCommandDialog() + const sdk = useSDK() // kilocode_change const mcp = createMemo(() => Object.keys(sync.data.mcp).length > 0) const mcpError = createMemo(() => { return Object.values(sync.data.mcp).some((x) => x.status === "failed") @@ -149,6 +152,7 @@ export function Home() { {directory()} + diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx index 8ace2fff372..801281bd148 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx @@ -3,8 +3,10 @@ import { useTheme } from "../../context/theme" import { useSync } from "../../context/sync" import { useDirectory } from "../../context/directory" import { useConnected } from "../../component/dialog-model" +import { useSDK } from "../../context/sdk" // kilocode_change import { createStore } from "solid-js/store" import { useRoute } from "../../context/route" +import { RemoteIndicator } from "@/kilocode/remote-tui" // kilocode_change export function Footer() { const { theme } = useTheme() @@ -19,6 +21,7 @@ export function Footer() { }) const directory = useDirectory() const connected = useConnected() + const sdk = useSDK() // kilocode_change const [store, setStore] = createStore({ welcome: false, @@ -53,6 +56,7 @@ export function Footer() { {directory()} + diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 289be84da67..20117cc48e2 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1222,6 +1222,8 @@ export function Session() { + {/* kilocode_change */} +