From daa1c2a2e82076ae94165707cc4e16a5d59e19a3 Mon Sep 17 00:00:00 2001 From: Kilo Agent Date: Mon, 16 Feb 2026 22:49:27 +0000 Subject: [PATCH 01/17] feat: load recommended models dynamically from models endpoint Replace hardcoded KILO_RECOMMENDED_MODELS with dynamic data from the API's preferredIndex field. The cloud API already returns preferredIndex on recommended models - this change parses it and flows it through to the TUI model picker. Changes: - Parse preferredIndex from API response in kilo-gateway - Add recommended/recommendedIndex fields to ModelsDev.Model schema - Use dynamic recommended data in TUI model dialog with fallback to hardcoded list for backward compatibility --- packages/kilo-gateway/src/api/models.ts | 5 +++++ .../src/cli/cmd/tui/component/dialog-model.tsx | 16 ++++++++-------- packages/opencode/src/provider/models.ts | 2 ++ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/kilo-gateway/src/api/models.ts b/packages/kilo-gateway/src/api/models.ts index b9a4a73aafe..1a89769650d 100644 --- a/packages/kilo-gateway/src/api/models.ts +++ b/packages/kilo-gateway/src/api/models.ts @@ -29,6 +29,7 @@ const openRouterModelSchema = z.object({ architecture: openRouterArchitectureSchema.optional(), top_provider: z.object({ max_completion_tokens: z.number().nullish() }).optional(), supported_parameters: z.array(z.string()).optional(), + preferredIndex: z.number().optional(), }) const openRouterModelsResponseSchema = z.object({ @@ -167,6 +168,10 @@ function transformToModelDevFormat(model: OpenRouterModel): any { output: mapModalities(outputModalities), }, }), + ...(model.preferredIndex !== undefined && { + recommended: true, + recommendedIndex: model.preferredIndex, + }), options: { ...(model.description && { description: model.description }), }, diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx index ea9db0103fc..aa1389de006 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx @@ -8,8 +8,8 @@ import { createDialogProviderOptions, DialogProvider } from "./dialog-provider" import { useKeybind } from "../context/keybind" import * as fuzzysort from "fuzzysort" -// kilocode_change start - Recommended models for Kilo Gateway (order determines display priority) -const KILO_RECOMMENDED_MODELS = [ +// kilocode_change start - Fallback recommended models when API doesn't return preferredIndex +const FALLBACK_RECOMMENDED_MODELS = [ "kilo/auto", "anthropic/claude-sonnet-4.5", "anthropic/claude-opus-4.5", @@ -100,7 +100,7 @@ export function DialogModel(props: { providerID?: string }) { : undefined, // kilocode_change start category: connected() - ? provider.id === "kilo" && KILO_RECOMMENDED_MODELS.includes(model) + ? provider.id === "kilo" && (info.recommended || FALLBACK_RECOMMENDED_MODELS.includes(model)) ? "Recommended" : provider.name : undefined, @@ -123,11 +123,11 @@ export function DialogModel(props: { providerID?: string }) { sortBy( // kilocode_change start - Sort recommended models first for Kilo Gateway (x) => { - if (x.value.providerID === "kilo") { - const idx = KILO_RECOMMENDED_MODELS.indexOf(x.value.modelID) - return idx >= 0 ? idx : KILO_RECOMMENDED_MODELS.length + 1 - } - return 0 + if (x.value.providerID !== "kilo") return 0 + const model = sync.data.provider.find((p) => p.id === "kilo")?.models[x.value.modelID] + if (model?.recommendedIndex !== undefined) return model.recommendedIndex + const idx = FALLBACK_RECOMMENDED_MODELS.indexOf(x.value.modelID) + return idx >= 0 ? idx : FALLBACK_RECOMMENDED_MODELS.length + 1 }, // kilocode_change end (x) => x.footer !== "Free", diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index 10360e9184f..6d2db59959b 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -78,6 +78,8 @@ export namespace ModelsDev { output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])), }) .optional(), + recommended: z.boolean().optional(), // kilocode_change + recommendedIndex: z.number().optional(), // kilocode_change experimental: z.boolean().optional(), status: z.enum(["alpha", "beta", "deprecated"]).optional(), options: z.record(z.string(), z.any()), From 0741f0ca2b0d2f669c59d061a9a99efa97a5bd05 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 20:56:49 +0000 Subject: [PATCH 02/17] fix: add recommended and recommendedIndex fields to Provider.Model schema Add missing recommended and recommendedIndex optional fields to the Provider.Model zod schema and propagate them in fromModelsDevModel and config model construction. Also regenerate SDK types and OpenAPI spec to include these fields. Fixes typecheck errors in dialog-model.tsx where these properties were referenced but not defined on the Model type. --- packages/opencode/src/provider/provider.ts | 6 ++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 6 ++++++ packages/sdk/openapi.json | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 169d18bed4e..bb8bb11ec16 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -619,6 +619,8 @@ export namespace Provider { headers: z.record(z.string(), z.string()), release_date: z.string(), variants: z.record(z.string(), z.record(z.string(), z.any())).optional(), + recommended: z.boolean().optional(), // kilocode_change + recommendedIndex: z.number().optional(), // kilocode_change }) .meta({ ref: "Model", @@ -700,6 +702,8 @@ export namespace Provider { }, release_date: model.release_date, variants: {}, + recommended: model.recommended, // kilocode_change + recommendedIndex: model.recommendedIndex, // kilocode_change } m.variants = mapValues(ProviderTransform.variants(m), (v) => v) @@ -843,6 +847,8 @@ export namespace Provider { family: model.family ?? existingModel?.family ?? "", release_date: model.release_date ?? existingModel?.release_date ?? "", variants: {}, + recommended: model.recommended ?? existingModel?.recommended, // kilocode_change + recommendedIndex: model.recommendedIndex ?? existingModel?.recommendedIndex, // kilocode_change } const merged = mergeDeep(ProviderTransform.variants(parsedModel), model.variants ?? {}) parsedModel.variants = mapValues( diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index d964a2990ae..e2b189bb55c 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1532,6 +1532,8 @@ export type ProviderConfig = { input: Array<"text" | "audio" | "image" | "video" | "pdf"> output: Array<"text" | "audio" | "image" | "video" | "pdf"> } + recommended?: boolean + recommendedIndex?: number experimental?: boolean status?: "alpha" | "beta" | "deprecated" options?: { @@ -1984,6 +1986,8 @@ export type Model = { [key: string]: unknown } } + recommended?: boolean + recommendedIndex?: number } export type Provider = { @@ -4052,6 +4056,8 @@ export type ProviderListResponses = { input: Array<"text" | "audio" | "image" | "video" | "pdf"> output: Array<"text" | "audio" | "image" | "video" | "pdf"> } + recommended?: boolean + recommendedIndex?: number experimental?: boolean status?: "alpha" | "beta" | "deprecated" options: { diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index e10a696aa2e..837dbbea127 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -3762,6 +3762,12 @@ }, "required": ["input", "output"] }, + "recommended": { + "type": "boolean" + }, + "recommendedIndex": { + "type": "number" + }, "experimental": { "type": "boolean" }, @@ -9654,6 +9660,12 @@ }, "required": ["input", "output"] }, + "recommended": { + "type": "boolean" + }, + "recommendedIndex": { + "type": "number" + }, "experimental": { "type": "boolean" }, @@ -10635,6 +10647,12 @@ }, "additionalProperties": {} } + }, + "recommended": { + "type": "boolean" + }, + "recommendedIndex": { + "type": "number" } }, "required": [ From a2d18131a8192b47591af0885fad8562298b69de Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Wed, 18 Feb 2026 22:38:46 +0100 Subject: [PATCH 03/17] feat(kilo-sessions): send git remote URL and branch to ingest Add gitUrl and gitBranch fields to kilo_meta so shared sessions include repository context. Resolves the best remote (origin > single available > upstream) and fetches the URL with a 10s in-flight cache. --- .../src/kilo-sessions/ingest-queue.ts | 2 + .../src/kilo-sessions/kilo-sessions.ts | 47 +++++++++++++++++++ .../test/kilo-sessions/ingest-queue.test.ts | 13 ++++- 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/kilo-sessions/ingest-queue.ts b/packages/opencode/src/kilo-sessions/ingest-queue.ts index fefcd453271..1ec105ea4af 100644 --- a/packages/opencode/src/kilo-sessions/ingest-queue.ts +++ b/packages/opencode/src/kilo-sessions/ingest-queue.ts @@ -16,6 +16,8 @@ export namespace IngestQueue { data: { platform: string orgId?: string + gitUrl?: string + gitBranch?: string } } | { diff --git a/packages/opencode/src/kilo-sessions/kilo-sessions.ts b/packages/opencode/src/kilo-sessions/kilo-sessions.ts index 5f00adb63fe..04cc2803cf7 100644 --- a/packages/opencode/src/kilo-sessions/kilo-sessions.ts +++ b/packages/opencode/src/kilo-sessions/kilo-sessions.ts @@ -10,6 +10,9 @@ import { clearInFlightCache, withInFlightCache } from "@/kilo-sessions/inflight- import type * as SDK from "@kilocode/sdk/v2" import z from "zod" import { KILO_API_BASE } from "@kilocode/kilo-gateway" +import { $ } from "bun" +import { Instance } from "@/project/instance" +import { Vcs } from "@/project/vcs" export namespace KiloSessions { const log = Log.create({ service: "kilo-sessions" }) @@ -23,6 +26,7 @@ export namespace KiloSessions { const tokenKey = "kilo-sessions:token" const orgKey = "kilo-sessions:org" const clientKey = "kilo-sessions:client" + const gitUrlKey = "kilo-sessions:git-url" const ttlMs = 10_000 @@ -31,6 +35,7 @@ export namespace KiloSessions { clearInFlightCache(tokenValidKey) clearInFlightCache(clientKey) clearInFlightCache(orgKey) + clearInFlightCache(gitUrlKey) } async function authValid(token: string) { @@ -370,13 +375,55 @@ export namespace KiloSessions { ]) } + async function getGitUrl(): Promise { + return withInFlightCache(gitUrlKey, ttlMs, async () => { + const result = await $`git remote` + .quiet() + .nothrow() + .cwd(Instance.worktree) + .text() + .catch(() => "") + + const remotes = result + .split("\n") + .map((line) => line.trim()) + .filter(Boolean) + + if (remotes.length === 0) return undefined + + const remote = remotes.includes("origin") + ? "origin" + : remotes.length === 1 + ? remotes[0] + : remotes.includes("upstream") + ? "upstream" + : undefined + + if (!remote) return undefined + + const url = await $`git config --get remote.${remote}.url` + .quiet() + .nothrow() + .cwd(Instance.worktree) + .text() + .then((x) => x.trim()) + .catch(() => "") + + return url || undefined + }) + } + async function meta() { const platform = process.env["KILO_PLATFORM"] || "cli" const orgId = await getOrgId() + const gitBranch = await Vcs.branch().catch(() => undefined) + const gitUrl = await getGitUrl().catch(() => undefined) return { platform, ...(orgId ? { orgId } : {}), + ...(gitUrl ? { gitUrl } : {}), + ...(gitBranch ? { gitBranch } : {}), } } diff --git a/packages/opencode/test/kilo-sessions/ingest-queue.test.ts b/packages/opencode/test/kilo-sessions/ingest-queue.test.ts index 7c094771793..529be44b21a 100644 --- a/packages/opencode/test/kilo-sessions/ingest-queue.test.ts +++ b/packages/opencode/test/kilo-sessions/ingest-queue.test.ts @@ -137,9 +137,16 @@ describe("share ingest queue", () => { }), }) - await q.sync("s7", [{ type: "kilo_meta", data: { platform: "cli" } }]) + await q.sync("s7", [ + { type: "kilo_meta", data: { platform: "cli", gitUrl: "https://github.com/old/repo.git", gitBranch: "main" } }, + ]) clock.now = 100 - await q.sync("s7", [{ type: "kilo_meta", data: { platform: "vscode", orgId: "org-1" } }]) + await q.sync("s7", [ + { + type: "kilo_meta", + data: { platform: "vscode", orgId: "org-1", gitUrl: "https://github.com/new/repo.git", gitBranch: "feature" }, + }, + ]) clock.now = 1000 sched.run() @@ -149,6 +156,8 @@ describe("share ingest queue", () => { expect((sent[0] as any).data[0].type).toBe("kilo_meta") expect((sent[0] as any).data[0].data.platform).toBe("vscode") expect((sent[0] as any).data[0].data.orgId).toBe("org-1") + expect((sent[0] as any).data[0].data.gitUrl).toBe("https://github.com/new/repo.git") + expect((sent[0] as any).data[0].data.gitBranch).toBe("feature") }) test("network failure retries and fill preserves newer updates", async () => { From a28f0868197b2da5db3ae232ff9325f8fd134bf9 Mon Sep 17 00:00:00 2001 From: Pedro Heyerdahl <61753986+pedroheyerdahl@users.noreply.github.com> Date: Wed, 18 Feb 2026 18:46:50 -0300 Subject: [PATCH 04/17] fix: restore bin/kilo findBinary from botched merge resolution --- packages/opencode/bin/kilo | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/opencode/bin/kilo b/packages/opencode/bin/kilo index 35703397693..796a1131c3d 100755 --- a/packages/opencode/bin/kilo +++ b/packages/opencode/bin/kilo @@ -4,7 +4,6 @@ const childProcess = require("child_process") const fs = require("fs") const path = require("path") const os = require("os") -const { createRequire } = require("module") function run(target) { const result = childProcess.spawnSync(target, process.argv.slice(2), { @@ -150,10 +149,18 @@ function findBinary(startDir) { if (fs.existsSync(candidate)) return candidate } } - } catch {} + const parent = path.dirname(current) + if (parent === current) { + return + } + current = parent + } } -const resolved = findBinary() +const scriptPath = fs.realpathSync(__filename) +const scriptDir = path.dirname(scriptPath) + +const resolved = findBinary(scriptDir) if (!resolved) { console.error( "It seems that your package manager failed to install the right version of the Kilo CLI for your platform. You can try manually installing " + From 872cb4124b05cc4adc09e14f912a697cff11849c Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Wed, 18 Feb 2026 22:47:46 +0100 Subject: [PATCH 05/17] feat(kilo-sessions): strip credentials from git remote URLs before ingesting --- .../opencode/src/kilo-sessions/kilo-sessions.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/kilo-sessions/kilo-sessions.ts b/packages/opencode/src/kilo-sessions/kilo-sessions.ts index 04cc2803cf7..4450fee59b6 100644 --- a/packages/opencode/src/kilo-sessions/kilo-sessions.ts +++ b/packages/opencode/src/kilo-sessions/kilo-sessions.ts @@ -375,6 +375,19 @@ export namespace KiloSessions { ]) } + /** Strip credentials from a git remote URL (e.g. https://user:token@github.com/org/repo.git) */ + function sanitizeGitUrl(raw: string): string { + if (!raw.startsWith("http://") && !raw.startsWith("https://")) return raw + try { + const parsed = new URL(raw) + parsed.username = "" + parsed.password = "" + return parsed.toString() + } catch { + return raw + } + } + async function getGitUrl(): Promise { return withInFlightCache(gitUrlKey, ttlMs, async () => { const result = await $`git remote` @@ -409,7 +422,7 @@ export namespace KiloSessions { .then((x) => x.trim()) .catch(() => "") - return url || undefined + return url ? sanitizeGitUrl(url) : undefined }) } From 5634fc552dfbaed9a8d6ee9dc39ae6f2867e6741 Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Thu, 19 Feb 2026 11:03:07 +0100 Subject: [PATCH 06/17] feat: emit LLM_COMPLETION telemetry event after each LLM stream finishes --- packages/kilo-telemetry/src/telemetry.ts | 2 ++ packages/opencode/src/session/processor.ts | 23 ++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/packages/kilo-telemetry/src/telemetry.ts b/packages/kilo-telemetry/src/telemetry.ts index a4b4cea3de5..53a42d4a7f9 100644 --- a/packages/kilo-telemetry/src/telemetry.ts +++ b/packages/kilo-telemetry/src/telemetry.ts @@ -142,6 +142,8 @@ export namespace Telemetry { outputTokens?: number cacheReadTokens?: number cacheWriteTokens?: number + cost?: number + completionTime?: number duration?: number }) { track(TelemetryEvent.LLM_COMPLETION, properties) diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 8e7472e2f18..57c038afa2d 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -15,6 +15,7 @@ import { Config } from "@/config/config" import { SessionCompaction } from "./compaction" import { PermissionNext } from "@/permission/next" import { Question } from "@/question" +import { Telemetry } from "@kilocode/kilo-telemetry" // kilocode_change export namespace SessionProcessor { const DOOM_LOOP_THRESHOLD = 3 @@ -34,6 +35,7 @@ export namespace SessionProcessor { let blocked = false let attempt = 0 let needsCompaction = false + let stepStart = 0 // kilocode_change const result = { get message() { @@ -223,6 +225,7 @@ export namespace SessionProcessor { throw value.error case "start-step": + stepStart = performance.now() // kilocode_change snapshot = await Snapshot.track() await Session.updatePart({ id: Identifier.ascending("part"), @@ -239,6 +242,26 @@ export namespace SessionProcessor { usage: value.usage, metadata: value.providerMetadata, }) + // kilocode_change start + if ( + usage.tokens.input > 0 || + usage.tokens.output > 0 || + usage.tokens.cache.write > 0 || + usage.tokens.cache.read > 0 + ) { + Telemetry.trackLlmCompletion({ + sessionId: input.sessionID, + provider: input.model.providerID, + model: input.model.id, + inputTokens: usage.tokens.input, + outputTokens: usage.tokens.output, + cacheReadTokens: usage.tokens.cache.read, + cacheWriteTokens: usage.tokens.cache.write, + cost: usage.cost, + completionTime: Math.round(performance.now() - stepStart), + }) + } + // kilocode_change end input.assistantMessage.finish = value.finishReason input.assistantMessage.cost += usage.cost input.assistantMessage.tokens = usage.tokens From 69135ca63674340c821c848c82e8179e26c8c30b Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Thu, 19 Feb 2026 12:11:38 +0100 Subject: [PATCH 07/17] feat(kilo-sessions): normalize git remote URLs to strip credentials, query params, and hash --- bun.lock | 1 + packages/opencode/package.json | 1 + .../src/kilo-sessions/kilo-sessions.ts | 46 +++++++------------ 3 files changed, 19 insertions(+), 29 deletions(-) diff --git a/bun.lock b/bun.lock index fd3415e85c9..a36686a203d 100644 --- a/bun.lock +++ b/bun.lock @@ -296,6 +296,7 @@ "opentui-spinner": "0.0.6", "partial-json": "0.1.7", "remeda": "catalog:", + "simple-git": "3.31.1", "solid-js": "catalog:", "strip-ansi": "7.1.2", "tree-sitter-bash": "0.25.0", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 950a080154f..8c798073ffc 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -112,6 +112,7 @@ "opentui-spinner": "0.0.6", "partial-json": "0.1.7", "remeda": "catalog:", + "simple-git": "3.31.1", "solid-js": "catalog:", "strip-ansi": "7.1.2", "tree-sitter-bash": "0.25.0", diff --git a/packages/opencode/src/kilo-sessions/kilo-sessions.ts b/packages/opencode/src/kilo-sessions/kilo-sessions.ts index 4450fee59b6..1b323329097 100644 --- a/packages/opencode/src/kilo-sessions/kilo-sessions.ts +++ b/packages/opencode/src/kilo-sessions/kilo-sessions.ts @@ -10,9 +10,9 @@ import { clearInFlightCache, withInFlightCache } from "@/kilo-sessions/inflight- import type * as SDK from "@kilocode/sdk/v2" import z from "zod" import { KILO_API_BASE } from "@kilocode/kilo-gateway" -import { $ } from "bun" import { Instance } from "@/project/instance" import { Vcs } from "@/project/vcs" +import simpleGit from "simple-git" export namespace KiloSessions { const log = Log.create({ service: "kilo-sessions" }) @@ -375,54 +375,42 @@ export namespace KiloSessions { ]) } - /** Strip credentials from a git remote URL (e.g. https://user:token@github.com/org/repo.git) */ - function sanitizeGitUrl(raw: string): string { - if (!raw.startsWith("http://") && !raw.startsWith("https://")) return raw + /** Normalize a git remote URL: strip credentials, query params, and hash. Returns undefined for unrecognized formats. */ + function normalizeGitUrl(raw: string): string | undefined { + const ssh = raw.match(/^git@([^:]+):(.+)$/) + if (ssh) return `git@${ssh[1]}:${ssh[2].split("?")[0]}` try { const parsed = new URL(raw) + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return undefined parsed.username = "" parsed.password = "" + parsed.search = "" + parsed.hash = "" return parsed.toString() } catch { - return raw + return undefined } } async function getGitUrl(): Promise { return withInFlightCache(gitUrlKey, ttlMs, async () => { - const result = await $`git remote` - .quiet() - .nothrow() - .cwd(Instance.worktree) - .text() - .catch(() => "") - - const remotes = result - .split("\n") - .map((line) => line.trim()) - .filter(Boolean) - + const repo = simpleGit(Instance.worktree) + const remotes = await repo.getRemotes(true).catch(() => []) if (remotes.length === 0) return undefined - const remote = remotes.includes("origin") + const names = remotes.map((r) => r.name) + const remote = names.includes("origin") ? "origin" : remotes.length === 1 - ? remotes[0] - : remotes.includes("upstream") + ? names[0] + : names.includes("upstream") ? "upstream" : undefined if (!remote) return undefined - const url = await $`git config --get remote.${remote}.url` - .quiet() - .nothrow() - .cwd(Instance.worktree) - .text() - .then((x) => x.trim()) - .catch(() => "") - - return url ? sanitizeGitUrl(url) : undefined + const url = remotes.find((r) => r.name === remote)?.refs.fetch ?? "" + return url ? normalizeGitUrl(url) : undefined }) } From f1e23c1b87500b3305860fa97b72765754c20f82 Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Thu, 19 Feb 2026 12:41:57 +0100 Subject: [PATCH 08/17] Remove fallback --- .../cli/cmd/tui/component/dialog-model.tsx | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx index aa1389de006..189c3a26f9b 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx @@ -8,20 +8,6 @@ import { createDialogProviderOptions, DialogProvider } from "./dialog-provider" import { useKeybind } from "../context/keybind" import * as fuzzysort from "fuzzysort" -// kilocode_change start - Fallback recommended models when API doesn't return preferredIndex -const FALLBACK_RECOMMENDED_MODELS = [ - "kilo/auto", - "anthropic/claude-sonnet-4.5", - "anthropic/claude-opus-4.5", - "anthropic/claude-haiku-4.5", - "openai/gpt-5.2", - "openai/gpt-5.2-codex", - "google/gemini-3-pro-preview", - "google/gemini-3-flash-preview", - "mistralai/devstral-2512", -] -// kilocode_change end - export function useConnected() { const sync = useSync() return createMemo(() => @@ -100,7 +86,7 @@ export function DialogModel(props: { providerID?: string }) { : undefined, // kilocode_change start category: connected() - ? provider.id === "kilo" && (info.recommended || FALLBACK_RECOMMENDED_MODELS.includes(model)) + ? provider.id === "kilo" && info.recommended ? "Recommended" : provider.name : undefined, @@ -124,10 +110,10 @@ export function DialogModel(props: { providerID?: string }) { // kilocode_change start - Sort recommended models first for Kilo Gateway (x) => { if (x.value.providerID !== "kilo") return 0 - const model = sync.data.provider.find((p) => p.id === "kilo")?.models[x.value.modelID] + const provider = sync.data.provider.find((p) => p.id === "kilo") + const model = provider?.models[x.value.modelID] if (model?.recommendedIndex !== undefined) return model.recommendedIndex - const idx = FALLBACK_RECOMMENDED_MODELS.indexOf(x.value.modelID) - return idx >= 0 ? idx : FALLBACK_RECOMMENDED_MODELS.length + 1 + return Object.keys(provider?.models ?? {}).length }, // kilocode_change end (x) => x.footer !== "Free", From 26a95196858a0df4a64a7d21e31e0bb17c4b9292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 19 Feb 2026 13:00:02 +0100 Subject: [PATCH 09/17] feat(vscode): abort session with Escape key Pressing Escape now aborts a busy session in the VS Code extension, matching the behavior of the web app. Works whether the textarea is focused or not. Ghost text dismissal still takes priority over abort when both conditions are true. --- .../webview-ui/src/components/chat/ChatView.tsx | 13 ++++++++++++- .../webview-ui/src/components/chat/PromptInput.tsx | 6 ++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx index 35f5f8b36d5..56db2a1eb58 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/ChatView.tsx @@ -3,7 +3,7 @@ * Main chat container that combines all chat components */ -import { Component, For, Show, createSignal } from "solid-js" +import { Component, For, Show, createSignal, onCleanup, onMount } from "solid-js" import { Button } from "@kilocode/kilo-ui/button" import { BasicTool } from "@kilocode/kilo-ui/basic-tool" import { TaskHeader } from "./TaskHeader" @@ -32,6 +32,17 @@ export const ChatView: Component = (props) => { const [responding, setResponding] = createSignal(false) + onMount(() => { + const handler = (e: KeyboardEvent) => { + if (e.key === "Escape" && session.status() === "busy") { + e.preventDefault() + session.abort() + } + } + document.addEventListener("keydown", handler) + onCleanup(() => document.removeEventListener("keydown", handler)) + }) + const decide = (response: "once" | "always" | "reject") => { const perm = permissionRequest() if (!perm || responding()) return 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 bc952d6eafc..88270f6a80a 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx @@ -201,9 +201,15 @@ export const PromptInput: Component = () => { } if (e.key === "Escape" && ghostText()) { e.preventDefault() + e.stopPropagation() dismissSuggestion() return } + if (e.key === "Escape" && isBusy()) { + e.preventDefault() + session.abort() + return + } if (e.key === "Enter" && !e.shiftKey) { e.preventDefault() dismissSuggestion() From cc510490103b102a1461d0365e9cb48aafd50e62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 19 Feb 2026 13:05:32 +0100 Subject: [PATCH 10/17] fix: add stopPropagation to abort Escape handler to prevent double abort --- .../kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx | 1 + 1 file changed, 1 insertion(+) 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 88270f6a80a..3a386488421 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx @@ -207,6 +207,7 @@ export const PromptInput: Component = () => { } if (e.key === "Escape" && isBusy()) { e.preventDefault() + e.stopPropagation() session.abort() return } From 582dd685c008a2b25f5e8d7bbc75289f41288968 Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Thu, 19 Feb 2026 13:48:37 +0100 Subject: [PATCH 11/17] fix(kilo-sessions): scope git URL cache key per worktree --- packages/opencode/src/kilo-sessions/kilo-sessions.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/kilo-sessions/kilo-sessions.ts b/packages/opencode/src/kilo-sessions/kilo-sessions.ts index 1b323329097..b6c8f59788b 100644 --- a/packages/opencode/src/kilo-sessions/kilo-sessions.ts +++ b/packages/opencode/src/kilo-sessions/kilo-sessions.ts @@ -26,7 +26,7 @@ export namespace KiloSessions { const tokenKey = "kilo-sessions:token" const orgKey = "kilo-sessions:org" const clientKey = "kilo-sessions:client" - const gitUrlKey = "kilo-sessions:git-url" + const gitUrlKeyPrefix = "kilo-sessions:git-url:" const ttlMs = 10_000 @@ -35,7 +35,7 @@ export namespace KiloSessions { clearInFlightCache(tokenValidKey) clearInFlightCache(clientKey) clearInFlightCache(orgKey) - clearInFlightCache(gitUrlKey) + clearInFlightCache(gitUrlKeyPrefix + Instance.worktree) } async function authValid(token: string) { @@ -393,7 +393,7 @@ export namespace KiloSessions { } async function getGitUrl(): Promise { - return withInFlightCache(gitUrlKey, ttlMs, async () => { + return withInFlightCache(gitUrlKeyPrefix + Instance.worktree, ttlMs, async () => { const repo = simpleGit(Instance.worktree) const remotes = await repo.getRemotes(true).catch(() => []) if (remotes.length === 0) return undefined From 74b3afaacb000d0c177fed393c014a5a12875e45 Mon Sep 17 00:00:00 2001 From: "kilo-maintainer[bot]" Date: Thu, 19 Feb 2026 12:51:37 +0000 Subject: [PATCH 12/17] chore: generate --- packages/app/src/i18n/ar.ts | 3 +- packages/app/src/i18n/fr.ts | 6 +- packages/app/src/i18n/th.ts | 3 +- packages/app/src/i18n/zht.ts | 3 +- .../app/src/pages/layout/sidebar-items.tsx | 4 +- packages/kilo-docs/.kilocode/mcp.json | 18 +- packages/kilo-docs/AGENTS.md | 22 +- .../__tests__/previous-docs-redirects.spec.ts | 204 +- packages/kilo-docs/components/Callout.tsx | 199 +- packages/kilo-docs/components/CodeBlock.tsx | 177 +- packages/kilo-docs/components/Codicon.tsx | 28 +- packages/kilo-docs/components/Heading.tsx | 16 +- packages/kilo-docs/components/Icon.tsx | 70 +- packages/kilo-docs/components/Image.tsx | 87 +- .../kilo-docs/components/KiloCodeIcon.tsx | 4 +- packages/kilo-docs/components/SideNav.tsx | 1217 ++++++----- packages/kilo-docs/components/Table.tsx | 56 +- packages/kilo-docs/components/Tabs.tsx | 57 +- packages/kilo-docs/components/ThemeToggle.tsx | 196 +- packages/kilo-docs/components/TopNav.tsx | 926 +++++---- packages/kilo-docs/components/YouTube.tsx | 152 +- .../devcontainer-persistence.md | 34 +- .../getting-started/switching-from-cline.md | 10 +- packages/kilo-docs/lib/nav/ai-providers.ts | 162 +- packages/kilo-docs/lib/nav/automate.ts | 114 +- packages/kilo-docs/lib/nav/code-with-ai.ts | 170 +- packages/kilo-docs/lib/nav/collaborate.ts | 124 +- packages/kilo-docs/lib/nav/contributing.ts | 162 +- packages/kilo-docs/lib/nav/customize.ts | 94 +- packages/kilo-docs/lib/nav/deploy-secure.ts | 24 +- packages/kilo-docs/lib/nav/gateway.ts | 56 +- packages/kilo-docs/lib/nav/getting-started.ts | 142 +- packages/kilo-docs/lib/nav/index.ts | 20 +- packages/kilo-docs/lib/nav/tools.ts | 94 +- packages/kilo-docs/lib/types.ts | 12 +- packages/kilo-docs/markdoc/functions.ts | 16 +- .../kilo-docs/markdoc/nodes/fence.markdoc.ts | 4 +- .../markdoc/nodes/heading.markdoc.ts | 44 +- .../kilo-docs/markdoc/nodes/table.markdoc.ts | 42 +- .../markdoc/partials/install-jetbrains.md | 11 +- .../kilo-docs/markdoc/tags/callout.markdoc.ts | 38 +- .../kilo-docs/markdoc/tags/codicon.markdoc.ts | 38 +- .../kilo-docs/markdoc/tags/icon.markdoc.ts | 40 +- .../kilo-docs/markdoc/tags/image.markdoc.ts | 52 +- .../markdoc/tags/kilo-code-icon.markdoc.ts | 18 +- .../kilo-docs/markdoc/tags/tabs.markdoc.ts | 24 +- .../kilo-docs/markdoc/tags/youtube.markdoc.ts | 34 +- packages/kilo-docs/pages/404.tsx | 378 ++-- .../kilo-docs/pages/ai-providers/bedrock.md | 14 +- .../pages/ai-providers/claude-code.md | 4 +- packages/kilo-docs/pages/ai-providers/groq.md | 8 +- .../kilo-docs/pages/ai-providers/kilocode.md | 4 +- .../kilo-docs/pages/ai-providers/lmstudio.md | 1 - .../kilo-docs/pages/ai-providers/minimax.md | 2 +- .../pages/ai-providers/openai-compatible.md | 12 +- .../pages/ai-providers/sap-ai-core.md | 60 +- packages/kilo-docs/pages/ai-providers/v0.md | 10 +- .../pages/ai-providers/vercel-ai-gateway.md | 18 +- .../kilo-docs/pages/ai-providers/vertex.md | 8 +- .../ai-providers/virtual-quota-fallback.md | 3 - .../kilo-docs/pages/ai-providers/vscode-lm.md | 10 +- .../kilo-docs/pages/ai-providers/zenmux.md | 60 +- packages/kilo-docs/pages/api/llms.txt.ts | 112 +- .../kilo-docs/pages/automate/agent-manager.md | 39 +- .../pages/automate/code-reviews/github.md | 30 +- .../pages/automate/code-reviews/gitlab.md | 34 +- .../pages/automate/code-reviews/overview.md | 12 +- .../pages/automate/extending/auto-launch.md | 36 +- .../automate/extending/shell-integration.md | 69 +- .../pages/automate/mcp/mcp-vs-api.md | 34 +- .../pages/automate/mcp/server-transports.md | 6 +- .../pages/automate/mcp/using-in-cli.md | 64 +- .../pages/automate/mcp/using-in-kilo-code.md | 114 +- .../automate/tools/access-mcp-resource.md | 46 +- .../automate/tools/ask-followup-question.md | 65 +- .../automate/tools/attempt-completion.md | 47 +- .../pages/automate/tools/browser-action.md | 41 +- .../pages/automate/tools/codebase-search.md | 43 +- .../pages/automate/tools/execute-command.md | 70 +- .../kilo-docs/pages/automate/tools/index.md | 183 +- .../tools/list-code-definition-names.md | 6 +- .../pages/automate/tools/list-files.md | 22 +- .../pages/automate/tools/new-task.md | 34 +- .../pages/automate/tools/read-file.md | 40 +- .../pages/automate/tools/search-files.md | 18 +- .../pages/automate/tools/switch-mode.md | 29 +- .../pages/automate/tools/update-todo-list.md | 27 +- .../pages/automate/tools/use-mcp-tool.md | 45 +- .../pages/automate/tools/write-to-file.md | 55 +- .../code-with-ai/agents/chat-interface.md | 13 +- .../code-with-ai/agents/orchestrator-mode.md | 4 +- .../pages/code-with-ai/agents/using-modes.md | 16 +- .../pages/code-with-ai/app-builder.md | 23 +- .../code-with-ai/features/browser-use.md | 16 +- .../code-with-ai/features/checkpoints.md | 76 +- .../pages/code-with-ai/platforms/cli.md | 86 +- .../code-with-ai/platforms/cloud-agent.md | 30 +- .../pages/collaborate/enterprise/migration.md | 4 +- .../pages/collaborate/sessions-sharing.md | 4 +- .../pages/collaborate/teams/analytics.md | 6 +- .../pages/collaborate/teams/dashboard.md | 14 +- .../collaborate/teams/getting-started.md | 8 +- .../architecture/agent-observability.md | 4 +- .../contributing/architecture/benchmarking.md | 6 +- .../architecture/enterprise-mcp-controls.md | 24 +- .../pages/contributing/architecture/index.md | 22 +- .../architecture/mcp-oauth-authorization.md | 337 ++- .../architecture/model-provider-blocklist.md | 8 +- .../architecture/track-repo-url.md | 10 +- .../architecture/voice-transcription.md | 14 +- .../contributing/development-environment.md | 45 +- .../kilo-docs/pages/contributing/ecosystem.md | 2 + .../kilo-docs/pages/contributing/index.md | 39 +- .../kilo-docs/pages/customize/agents-md.md | 2 +- .../customize/context/codebase-indexing.md | 26 +- .../kilo-docs/pages/customize/custom-modes.md | 198 +- .../kilo-docs/pages/customize/custom-rules.md | 12 +- packages/kilo-docs/pages/customize/index.md | 3 - .../pages/customize/prompt-engineering.md | 8 +- packages/kilo-docs/pages/customize/skills.md | 58 +- .../pages/deploy-secure/managed-indexing.md | 6 +- .../pages/deploy-secure/security-reviews.md | 134 +- .../kilo-docs/pages/gateway/api-reference.md | 312 +-- .../kilo-docs/pages/gateway/authentication.md | 4 +- packages/kilo-docs/pages/gateway/index.md | 8 +- .../pages/gateway/models-and-providers.md | 12 +- .../kilo-docs/pages/gateway/quickstart.md | 32 +- .../pages/gateway/sdks-and-frameworks.md | 92 +- packages/kilo-docs/pages/gateway/streaming.md | 80 +- .../pages/gateway/usage-and-billing.md | 22 +- .../faq/account-and-integration.md | 4 +- .../pages/getting-started/faq/known-issues.md | 6 +- .../pages/getting-started/installing.md | 23 +- .../pages/getting-started/migrating.md | 24 +- .../pages/getting-started/settings/index.md | 30 +- .../troubleshooting-extension.md | 4 +- .../getting-started/using-kilo-for-free.md | 2 +- packages/kilo-docs/pages/index.tsx | 1805 +++++++++-------- packages/kilo-docs/postcss.config.js | 8 +- packages/kilo-docs/previous-docs-redirects.js | 1518 +++++++------- packages/kilo-docs/public/globals.css | 596 +++--- packages/kilo-docs/tailwind.config.js | 12 +- packages/kilo-docs/tsconfig.json | 36 +- packages/kilo-docs/vitest.config.ts | 10 +- packages/opencode/src/session/llm.ts | 4 +- 145 files changed, 6364 insertions(+), 6478 deletions(-) diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index aa54f1be9f5..ce435113c83 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -132,8 +132,7 @@ export const dict = { "provider.connect.oauth.code.invalid": "رمز التفويض غير صالح", "provider.connect.oauth.auto.visit.prefix": "قم بزيارة ", "provider.connect.oauth.auto.visit.link": "هذا الرابط", - "provider.connect.oauth.auto.visit.suffix": - " وأدخل الرمز أدناه لتوصيل حسابك واستخدام نماذج {{provider}} في Kilo.", + "provider.connect.oauth.auto.visit.suffix": " وأدخل الرمز أدناه لتوصيل حسابك واستخدام نماذج {{provider}} في Kilo.", "provider.connect.oauth.auto.confirmationCode": "رمز التأكيد", "provider.connect.toast.connected.title": "تم توصيل {{provider}}", "provider.connect.toast.connected.description": "نماذج {{provider}} متاحة الآن للاستخدام.", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index f7bf84e1c59..2feb0d5bde5 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -385,8 +385,7 @@ export const dict = { "toast.session.unshare.failed.description": "Une erreur s'est produite lors de l'annulation du partage de la session", "toast.session.listFailed.title": "Échec du chargement des sessions pour {{project}}", "toast.update.title": "Mise à jour disponible", - "toast.update.description": - "Une nouvelle version d'Kilo ({{version}}) est maintenant disponible pour installation.", + "toast.update.description": "Une nouvelle version d'Kilo ({{version}}) est maintenant disponible pour installation.", "toast.update.action.installRestart": "Installer et redémarrer", "toast.update.action.notYet": "Pas encore", "error.page.title": "Quelque chose s'est mal passé", @@ -517,8 +516,7 @@ export const dict = { "sidebar.workspaces.enable": "Activer les espaces de travail", "sidebar.workspaces.disable": "Désactiver les espaces de travail", "sidebar.gettingStarted.title": "Commencer", - "sidebar.gettingStarted.line1": - "Kilo inclut des modèles gratuits pour que vous puissiez commencer immédiatement.", + "sidebar.gettingStarted.line1": "Kilo inclut des modèles gratuits pour que vous puissiez commencer immédiatement.", "sidebar.gettingStarted.line2": "Connectez n'importe quel fournisseur pour utiliser des modèles, y compris Claude, GPT, Gemini etc.", "sidebar.project.recentSessions": "Sessions récentes", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index 8de06487ea0..c595b67d276 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -144,8 +144,7 @@ export const dict = { "provider.connect.oauth.code.invalid": "รหัสการอนุญาตไม่ถูกต้อง", "provider.connect.oauth.auto.visit.prefix": "เยี่ยมชม ", "provider.connect.oauth.auto.visit.link": "ลิงก์นี้", - "provider.connect.oauth.auto.visit.suffix": - " และป้อนรหัสด้านล่างเพื่อเชื่อมต่อบัญชีและใช้โมเดล {{provider}} ใน Kilo", + "provider.connect.oauth.auto.visit.suffix": " และป้อนรหัสด้านล่างเพื่อเชื่อมต่อบัญชีและใช้โมเดล {{provider}} ใน Kilo", "provider.connect.oauth.auto.confirmationCode": "รหัสยืนยัน", "provider.connect.toast.connected.title": "{{provider}} ที่เชื่อมต่อแล้ว", "provider.connect.toast.connected.description": "โมเดล {{provider}} พร้อมใช้งานแล้ว", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index d8cb85408d2..741b32464ab 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -145,8 +145,7 @@ export const dict = { "provider.connect.oauth.code.invalid": "授權碼無效", "provider.connect.oauth.auto.visit.prefix": "造訪 ", "provider.connect.oauth.auto.visit.link": "此連結", - "provider.connect.oauth.auto.visit.suffix": - " 並輸入以下程式碼,以連線你的帳戶並在 Kilo 中使用 {{provider}} 模型。", + "provider.connect.oauth.auto.visit.suffix": " 並輸入以下程式碼,以連線你的帳戶並在 Kilo 中使用 {{provider}} 模型。", "provider.connect.oauth.auto.confirmationCode": "確認碼", "provider.connect.toast.connected.title": "{{provider}} 已連線", "provider.connect.toast.connected.description": "現在可以使用 {{provider}} 模型了。", diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx index 9bbb58f3fbe..3b8f0c3c8d3 100644 --- a/packages/app/src/pages/layout/sidebar-items.tsx +++ b/packages/app/src/pages/layout/sidebar-items.tsx @@ -32,9 +32,7 @@ export const ProjectIcon = (props: { project: LocalProject; class?: string; noti
0 && props.notify }} diff --git a/packages/kilo-docs/.kilocode/mcp.json b/packages/kilo-docs/.kilocode/mcp.json index 845ac2b5abd..0ef7cc11433 100644 --- a/packages/kilo-docs/.kilocode/mcp.json +++ b/packages/kilo-docs/.kilocode/mcp.json @@ -1,11 +1,11 @@ { - "mcpServers": { - "context7": { - "command": "npx", - "args": ["-y", "@upstash/context7-mcp"], - "env": { "DEFAULT_MINIMUM_TOKENS": "" }, - "alwaysAllow": ["resolve-library-id", "query-docs"], - "disabled": true - } - } + "mcpServers": { + "context7": { + "command": "npx", + "args": ["-y", "@upstash/context7-mcp"], + "env": { "DEFAULT_MINIMUM_TOKENS": "" }, + "alwaysAllow": ["resolve-library-id", "query-docs"], + "disabled": true + } + } } diff --git a/packages/kilo-docs/AGENTS.md b/packages/kilo-docs/AGENTS.md index 14174cd8597..0eed736250a 100644 --- a/packages/kilo-docs/AGENTS.md +++ b/packages/kilo-docs/AGENTS.md @@ -72,9 +72,9 @@ Use the Markdoc codicon tag format: 1. Create your page in the appropriate directory under `pages/` 2. **Always update navigation**: Add the page to the corresponding navigation file in `lib/nav/` - - Each section has its own nav file (e.g., `getting-started.ts`, `code-with-ai.ts`, `ai-providers.ts`) - - Navigation structure is exported from `lib/nav/index.ts` - - See `lib/types.ts` for the `NavSection` and `NavLink` interfaces + - Each section has its own nav file (e.g., `getting-started.ts`, `code-with-ai.ts`, `ai-providers.ts`) + - Navigation structure is exported from `lib/nav/index.ts` + - See `lib/types.ts` for the `NavSection` and `NavLink` interfaces ### Removing or Moving Pages @@ -82,13 +82,13 @@ Use the Markdoc codicon tag format: 1. Add a redirect entry to `previous-docs-redirects.js` 2. Redirect format: - ```javascript - { - source: "/docs/old-path", - destination: "/docs/new-path", - basePath: false, - permanent: true, - } - ``` + ```javascript + { + source: "/docs/old-path", + destination: "/docs/new-path", + basePath: false, + permanent: true, + } + ``` 3. Update the navigation file to remove or update the link 4. Redirects are loaded in `next.config.js` diff --git a/packages/kilo-docs/__tests__/previous-docs-redirects.spec.ts b/packages/kilo-docs/__tests__/previous-docs-redirects.spec.ts index 48c20187cb2..1c37b406df3 100644 --- a/packages/kilo-docs/__tests__/previous-docs-redirects.spec.ts +++ b/packages/kilo-docs/__tests__/previous-docs-redirects.spec.ts @@ -10,128 +10,126 @@ import { expect, describe, it } from "vitest" import redirects from "../previous-docs-redirects.js" interface Redirect { - source: string - destination: string - basePath?: boolean - permanent?: boolean + source: string + destination: string + basePath?: boolean + permanent?: boolean } describe("previous-docs-redirects", () => { - describe("direct loop detection", () => { - it("should not have any redirects where source equals destination", () => { - const directLoops: Redirect[] = [] + describe("direct loop detection", () => { + it("should not have any redirects where source equals destination", () => { + const directLoops: Redirect[] = [] - for (const redirect of redirects as Redirect[]) { - if (redirect.source === redirect.destination) { - directLoops.push(redirect) - } - } + for (const redirect of redirects as Redirect[]) { + if (redirect.source === redirect.destination) { + directLoops.push(redirect) + } + } - if (directLoops.length > 0) { - const loopDetails = directLoops.map((r) => ` - "${r.source}" redirects to itself`).join("\n") - expect.fail(`Found ${directLoops.length} direct redirect loop(s):\n${loopDetails}`) - } - }) - }) + if (directLoops.length > 0) { + const loopDetails = directLoops.map((r) => ` - "${r.source}" redirects to itself`).join("\n") + expect.fail(`Found ${directLoops.length} direct redirect loop(s):\n${loopDetails}`) + } + }) + }) - describe("indirect loop detection", () => { - it("should not have any redirect chains that form a cycle", () => { - // Build a map of source -> destination for quick lookup - // Note: We only consider exact path matches, not wildcard patterns like :path* - // Also skip direct loops (source === destination) as they're caught by the direct loop test - const redirectMap = new Map() + describe("indirect loop detection", () => { + it("should not have any redirect chains that form a cycle", () => { + // Build a map of source -> destination for quick lookup + // Note: We only consider exact path matches, not wildcard patterns like :path* + // Also skip direct loops (source === destination) as they're caught by the direct loop test + const redirectMap = new Map() - for (const redirect of redirects as Redirect[]) { - // Skip wildcard redirects as they don't form exact chains - // Skip direct loops as they're caught by the direct loop test - if ( - !redirect.source.includes(":") && - !redirect.source.includes("*") && - redirect.source !== redirect.destination - ) { - redirectMap.set(redirect.source, redirect.destination) - } - } + for (const redirect of redirects as Redirect[]) { + // Skip wildcard redirects as they don't form exact chains + // Skip direct loops as they're caught by the direct loop test + if ( + !redirect.source.includes(":") && + !redirect.source.includes("*") && + redirect.source !== redirect.destination + ) { + redirectMap.set(redirect.source, redirect.destination) + } + } - const cycles: string[][] = [] + const cycles: string[][] = [] - /** - * Detects if following redirects from a starting path leads back to any path in the chain. - * Uses a visited set to track the current chain and detect cycles. - */ - function detectCycle(startPath: string): string[] | null { - const visited = new Set() - const chain: string[] = [startPath] - let currentPath = startPath + /** + * Detects if following redirects from a starting path leads back to any path in the chain. + * Uses a visited set to track the current chain and detect cycles. + */ + function detectCycle(startPath: string): string[] | null { + const visited = new Set() + const chain: string[] = [startPath] + let currentPath = startPath - while (redirectMap.has(currentPath)) { - const nextPath = redirectMap.get(currentPath)! + while (redirectMap.has(currentPath)) { + const nextPath = redirectMap.get(currentPath)! - if (visited.has(nextPath)) { - // Found a cycle - return the chain from the cycle start - const cycleStartIndex = chain.indexOf(nextPath) - if (cycleStartIndex !== -1) { - return [...chain.slice(cycleStartIndex), nextPath] - } - return null - } + if (visited.has(nextPath)) { + // Found a cycle - return the chain from the cycle start + const cycleStartIndex = chain.indexOf(nextPath) + if (cycleStartIndex !== -1) { + return [...chain.slice(cycleStartIndex), nextPath] + } + return null + } - visited.add(currentPath) - chain.push(nextPath) - currentPath = nextPath - } + visited.add(currentPath) + chain.push(nextPath) + currentPath = nextPath + } - return null - } + return null + } - // Check each redirect source for potential cycles - for (const source of redirectMap.keys()) { - const cycle = detectCycle(source) - if (cycle) { - // Avoid duplicate cycle reports by checking if we've already found this cycle - const cycleKey = [...cycle].sort().join(" -> ") - const isDuplicate = cycles.some( - (existingCycle) => [...existingCycle].sort().join(" -> ") === cycleKey, - ) - if (!isDuplicate) { - cycles.push(cycle) - } - } - } + // Check each redirect source for potential cycles + for (const source of redirectMap.keys()) { + const cycle = detectCycle(source) + if (cycle) { + // Avoid duplicate cycle reports by checking if we've already found this cycle + const cycleKey = [...cycle].sort().join(" -> ") + const isDuplicate = cycles.some((existingCycle) => [...existingCycle].sort().join(" -> ") === cycleKey) + if (!isDuplicate) { + cycles.push(cycle) + } + } + } - if (cycles.length > 0) { - const cycleDetails = cycles.map((cycle) => ` - ${cycle.join(" → ")}`).join("\n") - expect.fail(`Found ${cycles.length} indirect redirect cycle(s):\n${cycleDetails}`) - } - }) - }) + if (cycles.length > 0) { + const cycleDetails = cycles.map((cycle) => ` - ${cycle.join(" → ")}`).join("\n") + expect.fail(`Found ${cycles.length} indirect redirect cycle(s):\n${cycleDetails}`) + } + }) + }) - describe("redirect structure validation", () => { - it("should have valid redirect objects with required properties", () => { - const invalidRedirects: { index: number; issues: string[] }[] = [] + describe("redirect structure validation", () => { + it("should have valid redirect objects with required properties", () => { + const invalidRedirects: { index: number; issues: string[] }[] = [] - ;(redirects as Redirect[]).forEach((redirect, index) => { - const issues: string[] = [] + ;(redirects as Redirect[]).forEach((redirect, index) => { + const issues: string[] = [] - if (typeof redirect.source !== "string" || redirect.source.trim() === "") { - issues.push("missing or invalid 'source' property") - } + if (typeof redirect.source !== "string" || redirect.source.trim() === "") { + issues.push("missing or invalid 'source' property") + } - if (typeof redirect.destination !== "string" || redirect.destination.trim() === "") { - issues.push("missing or invalid 'destination' property") - } + if (typeof redirect.destination !== "string" || redirect.destination.trim() === "") { + issues.push("missing or invalid 'destination' property") + } - if (issues.length > 0) { - invalidRedirects.push({ index, issues }) - } - }) + if (issues.length > 0) { + invalidRedirects.push({ index, issues }) + } + }) - if (invalidRedirects.length > 0) { - const details = invalidRedirects - .map((r) => ` - Redirect at index ${r.index}: ${r.issues.join(", ")}`) - .join("\n") - expect.fail(`Found ${invalidRedirects.length} invalid redirect(s):\n${details}`) - } - }) - }) + if (invalidRedirects.length > 0) { + const details = invalidRedirects + .map((r) => ` - Redirect at index ${r.index}: ${r.issues.join(", ")}`) + .join("\n") + expect.fail(`Found ${invalidRedirects.length} invalid redirect(s):\n${details}`) + } + }) + }) }) diff --git a/packages/kilo-docs/components/Callout.tsx b/packages/kilo-docs/components/Callout.tsx index df902cb6893..e3540eb2b89 100644 --- a/packages/kilo-docs/components/Callout.tsx +++ b/packages/kilo-docs/components/Callout.tsx @@ -3,113 +3,112 @@ import * as React from "react" type CalloutType = "generic" | "note" | "tip" | "info" | "warning" | "danger" interface CalloutProps { - type?: CalloutType - title?: string - collapsed?: boolean - children: React.ReactNode + type?: CalloutType + title?: string + collapsed?: boolean + children: React.ReactNode } const typeConfig: Record< - CalloutType, - { - icon: string | null - defaultTitle: string | null - borderColor: string - bgColor: string - titleColor: string - iconColor: string - } + CalloutType, + { + icon: string | null + defaultTitle: string | null + borderColor: string + bgColor: string + titleColor: string + iconColor: string + } > = { - generic: { - icon: null, - defaultTitle: null, - borderColor: "border-l-gray-300 dark:border-l-gray-600", - bgColor: "bg-gray-50 dark:bg-gray-800/50", - titleColor: "text-gray-700 dark:text-gray-300", - iconColor: "text-gray-400", - }, - note: { - icon: "📝", - defaultTitle: "Note", - borderColor: "border-l-gray-500", - bgColor: "bg-gray-50 dark:bg-gray-800/50", - titleColor: "text-gray-700 dark:text-gray-300", - iconColor: "text-gray-500", - }, - tip: { - icon: "💡", - defaultTitle: "Tip", - borderColor: "border-l-green-500", - bgColor: "bg-green-50 dark:bg-green-900/20", - titleColor: "text-green-700 dark:text-green-400", - iconColor: "text-green-500", - }, - info: { - icon: "ℹ️", - defaultTitle: "Info", - borderColor: "border-l-blue-500", - bgColor: "bg-blue-50 dark:bg-blue-900/20", - titleColor: "text-blue-700 dark:text-blue-400", - iconColor: "text-blue-500", - }, - warning: { - icon: "⚠️", - defaultTitle: "Warning", - borderColor: "border-l-yellow-500", - bgColor: "bg-yellow-50 dark:bg-yellow-900/20", - titleColor: "text-yellow-700 dark:text-yellow-400", - iconColor: "text-yellow-500", - }, - danger: { - icon: "🚨", - defaultTitle: "Danger", - borderColor: "border-l-red-500", - bgColor: "bg-red-50 dark:bg-red-900/20", - titleColor: "text-red-700 dark:text-red-400", - iconColor: "text-red-500", - }, + generic: { + icon: null, + defaultTitle: null, + borderColor: "border-l-gray-300 dark:border-l-gray-600", + bgColor: "bg-gray-50 dark:bg-gray-800/50", + titleColor: "text-gray-700 dark:text-gray-300", + iconColor: "text-gray-400", + }, + note: { + icon: "📝", + defaultTitle: "Note", + borderColor: "border-l-gray-500", + bgColor: "bg-gray-50 dark:bg-gray-800/50", + titleColor: "text-gray-700 dark:text-gray-300", + iconColor: "text-gray-500", + }, + tip: { + icon: "💡", + defaultTitle: "Tip", + borderColor: "border-l-green-500", + bgColor: "bg-green-50 dark:bg-green-900/20", + titleColor: "text-green-700 dark:text-green-400", + iconColor: "text-green-500", + }, + info: { + icon: "ℹ️", + defaultTitle: "Info", + borderColor: "border-l-blue-500", + bgColor: "bg-blue-50 dark:bg-blue-900/20", + titleColor: "text-blue-700 dark:text-blue-400", + iconColor: "text-blue-500", + }, + warning: { + icon: "⚠️", + defaultTitle: "Warning", + borderColor: "border-l-yellow-500", + bgColor: "bg-yellow-50 dark:bg-yellow-900/20", + titleColor: "text-yellow-700 dark:text-yellow-400", + iconColor: "text-yellow-500", + }, + danger: { + icon: "🚨", + defaultTitle: "Danger", + borderColor: "border-l-red-500", + bgColor: "bg-red-50 dark:bg-red-900/20", + titleColor: "text-red-700 dark:text-red-400", + iconColor: "text-red-500", + }, } export function Callout({ type = "note", title, collapsed = false, children }: CalloutProps) { - const [isExpanded, setIsExpanded] = React.useState(!collapsed) - const config = typeConfig[type] - const displayTitle = title ?? config.defaultTitle - const showHeader = displayTitle || config.icon + const [isExpanded, setIsExpanded] = React.useState(!collapsed) + const config = typeConfig[type] + const displayTitle = title ?? config.defaultTitle + const showHeader = displayTitle || config.icon - // If collapsed prop is set, make the header clickable - if (collapsed) { - return ( -
- {showHeader && ( - - )} - {isExpanded &&
{children}
} -
- ) - } + // If collapsed prop is set, make the header clickable + if (collapsed) { + return ( +
+ {showHeader && ( + + )} + {isExpanded &&
{children}
} +
+ ) + } - // Default non-collapsible behavior - return ( -
- {showHeader && ( -
- {config.icon && {config.icon}} - {displayTitle && {displayTitle}} -
- )} -
{children}
-
- ) + // Default non-collapsible behavior + return ( +
+ {showHeader && ( +
+ {config.icon && {config.icon}} + {displayTitle && {displayTitle}} +
+ )} +
{children}
+
+ ) } diff --git a/packages/kilo-docs/components/CodeBlock.tsx b/packages/kilo-docs/components/CodeBlock.tsx index e5bfb896897..64a7f268885 100644 --- a/packages/kilo-docs/components/CodeBlock.tsx +++ b/packages/kilo-docs/components/CodeBlock.tsx @@ -4,102 +4,103 @@ import * as React from "react" import { Codicon } from "./Codicon" export function CodeBlock({ children, "data-language": language }) { - const ref = React.useRef(null) - const timeoutRef = React.useRef(null) - const [copied, setCopied] = React.useState(false) + const ref = React.useRef(null) + const timeoutRef = React.useRef(null) + const [copied, setCopied] = React.useState(false) - React.useEffect(() => { - if (ref.current) Prism.highlightElement(ref.current, false) - }, [children]) + React.useEffect(() => { + if (ref.current) Prism.highlightElement(ref.current, false) + }, [children]) - React.useEffect(() => { - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current) - } - } - }, []) + React.useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + } + } + }, []) - const handleCopy = async () => { - const code = ref.current?.textContent || "" - try { - await navigator.clipboard.writeText(code) - setCopied(true) - if (timeoutRef.current) { - clearTimeout(timeoutRef.current) - } - timeoutRef.current = setTimeout(() => setCopied(false), 2000) - } catch (err) { - console.error("Failed to copy code:", err) - } - } + const handleCopy = async () => { + const code = ref.current?.textContent || "" + try { + await navigator.clipboard.writeText(code) + setCopied(true) + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + } + timeoutRef.current = setTimeout(() => setCopied(false), 2000) + } catch (err) { + console.error("Failed to copy code:", err) + } + } - return ( -
- -
-				{children}
-			
- -
- ) + /* Override Prism styles */ + .code :global(pre[class*="language-"]) { + text-shadow: none; + border-radius: 4px; + padding-right: 3.5rem; + } + `} + +
+ ) } diff --git a/packages/kilo-docs/components/Codicon.tsx b/packages/kilo-docs/components/Codicon.tsx index 77c9a516b48..b3976c6204c 100644 --- a/packages/kilo-docs/components/Codicon.tsx +++ b/packages/kilo-docs/components/Codicon.tsx @@ -2,21 +2,21 @@ import React from "react" import "@vscode/codicons/dist/codicon.css" interface CodiconProps { - name: string - size?: string - className?: string + name: string + size?: string + className?: string } export function Codicon({ name, size = "1em", className = "" }: CodiconProps) { - return ( -