From 46213dcebda653c1575b67ef93fc8aab065a9db7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Uruchurtu?= Date: Tue, 26 May 2026 20:48:18 -0600 Subject: [PATCH 01/57] fix(vscode): improve chat error styling --- .changeset/polite-errors-glow.md | 6 ++ packages/kilo-ui/src/components/card.css | 7 ++ .../kilo-ui/src/components/error-details.css | 70 ++++++++++-- .../kilo-ui/src/components/error-details.tsx | 2 +- packages/kilo-ui/src/styles/vscode-bridge.css | 28 ++--- .../src/components/chat/ErrorDisplay.tsx | 8 +- .../webview-ui/src/stories/chat.stories.tsx | 102 ++++++++++++++++++ 7 files changed, 196 insertions(+), 27 deletions(-) create mode 100644 .changeset/polite-errors-glow.md diff --git a/.changeset/polite-errors-glow.md b/.changeset/polite-errors-glow.md new file mode 100644 index 00000000000..bc37608e936 --- /dev/null +++ b/.changeset/polite-errors-glow.md @@ -0,0 +1,6 @@ +--- +"@kilocode/kilo-ui": patch +"kilo-code": patch +--- + +Improve chat error styling in the VS Code extension. diff --git a/packages/kilo-ui/src/components/card.css b/packages/kilo-ui/src/components/card.css index 6ae4d7d2693..360d1028608 100644 --- a/packages/kilo-ui/src/components/card.css +++ b/packages/kilo-ui/src/components/card.css @@ -5,6 +5,13 @@ padding: 8px; border: 1px solid var(--border-weak-base); + &[data-variant="error"] { + padding: 12px; + background-color: color-mix(in srgb, var(--surface-critical-strong) 10%, var(--surface-inset-base)); + border-color: color-mix(in srgb, var(--border-critical-selected) 55%, var(--border-weaker-base)); + color: var(--text-base); + } + &[data-variant="warning"] { padding: 12px 14px; } diff --git a/packages/kilo-ui/src/components/error-details.css b/packages/kilo-ui/src/components/error-details.css index 259150984e0..88f97120a2e 100644 --- a/packages/kilo-ui/src/components/error-details.css +++ b/packages/kilo-ui/src/components/error-details.css @@ -1,42 +1,92 @@ .error-card { - padding-bottom: 0; - background-color: var(--surface-critical-base); + gap: 8px; + padding-bottom: 12px; + --error-card-accent: var(--text-on-critical-base); +} + +.error-card-body { + display: flex; + align-items: flex-start; + gap: 8px; +} + +.error-card-body [data-component="icon"] { + color: var(--error-card-accent); + margin-top: 2px; +} + +.error-card-message { + flex: 1; + min-width: 0; + color: var(--text-strong); + font-size: var(--font-size-base); + line-height: var(--line-height-large); + overflow-wrap: anywhere; } .error-card [data-component="collapsible"] { - margin-top: 8px; + margin-top: 0; + padding-left: 24px; } -.error-details-trigger { +.error-card .error-details-trigger[data-slot="collapsible-trigger"] { display: inline-flex; align-items: center; - gap: 4px; + align-self: flex-start; + gap: 2px; + width: auto; + height: 22px; font-size: var(--font-size-small); + font-weight: var(--font-weight-medium); opacity: 0.85; cursor: pointer; background: none; border: none; - color: inherit; - padding: 0; + border-radius: var(--radius-sm); + color: var(--error-card-accent); + padding: 0 6px; } -.error-details-trigger:hover { +.error-card .error-details-trigger[data-slot="collapsible-trigger"]:hover { + background-color: color-mix(in srgb, var(--error-card-accent) 12%, transparent); opacity: 1; } +.error-card .error-details-trigger[data-slot="collapsible-trigger"]:focus-visible { + background-color: color-mix(in srgb, var(--error-card-accent) 12%, transparent); + outline: 1px solid var(--border-focus); + outline-offset: 2px; +} + +.error-card .error-details-trigger [data-slot="collapsible-arrow"] { + width: 16px; + height: 16px; + opacity: 1; +} + +.error-card .error-details-trigger [data-slot="collapsible-arrow-icon"] { + color: currentColor; +} + .error-details { display: flex; flex-direction: column; - gap: 4px; + gap: 6px; font-size: var(--font-size-small); - margin-top: 4px; + margin-top: 8px; } .error-detail-pre { margin: 0; max-height: 120px; overflow-y: auto; + background-color: color-mix(in srgb, var(--surface-inset-base) 82%, var(--background-base)); + border: 1px solid color-mix(in srgb, var(--border-critical-base) 30%, var(--border-weaker-base)); + border-radius: var(--radius-sm); + color: var(--text-base); font-size: var(--font-size-small); + line-height: var(--line-height-large); + padding: 8px; white-space: pre-wrap; word-break: break-all; flex: 1; diff --git a/packages/kilo-ui/src/components/error-details.tsx b/packages/kilo-ui/src/components/error-details.tsx index c720bd80f8c..1a306385865 100644 --- a/packages/kilo-ui/src/components/error-details.tsx +++ b/packages/kilo-ui/src/components/error-details.tsx @@ -11,7 +11,7 @@ export function ErrorDetails(props: ErrorDetailsProps) { return (
-
{raw()}
+
{raw()}
) } diff --git a/packages/kilo-ui/src/styles/vscode-bridge.css b/packages/kilo-ui/src/styles/vscode-bridge.css index 5ad8a6a6920..534aea426c0 100644 --- a/packages/kilo-ui/src/styles/vscode-bridge.css +++ b/packages/kilo-ui/src/styles/vscode-bridge.css @@ -68,7 +68,7 @@ html[data-theme="kilo-vscode"] { --surface-critical-base: var(--vscode-editorMarkerNavigationError-headerBackground); --surface-critical-weak: var(--vscode-editorMarkerNavigationError-headerBackground); - --surface-critical-strong: var(--vscode-charts-red); + --surface-critical-strong: var(--vscode-errorForeground, var(--vscode-charts-red)); --surface-info-base: var(--vscode-editorMarkerNavigationInfo-headerBackground); --surface-info-weak: var(--vscode-editorMarkerNavigationInfo-headerBackground); @@ -116,9 +116,9 @@ html[data-theme="kilo-vscode"] { --text-on-success-base: var(--vscode-charts-green); --text-on-success-weak: var(--vscode-charts-green); --text-on-success-strong: var(--vscode-charts-green); - --text-on-critical-base: var(--vscode-charts-red); - --text-on-critical-weak: var(--vscode-charts-red); - --text-on-critical-strong: var(--vscode-charts-red); + --text-on-critical-base: var(--vscode-errorForeground, var(--vscode-charts-red)); + --text-on-critical-weak: var(--vscode-errorForeground, var(--vscode-charts-red)); + --text-on-critical-strong: var(--vscode-errorForeground, var(--vscode-charts-red)); --text-on-warning-base: var(--vscode-charts-yellow); --text-on-warning-weak: var(--vscode-charts-yellow); --text-on-warning-strong: var(--vscode-charts-yellow); @@ -183,9 +183,9 @@ html[data-theme="kilo-vscode"] { --border-warning-base: var(--vscode-charts-yellow); --border-warning-hover: var(--vscode-charts-yellow); --border-warning-selected: var(--vscode-charts-yellow); - --border-critical-base: var(--vscode-charts-red); - --border-critical-hover: var(--vscode-charts-red); - --border-critical-selected: var(--vscode-charts-red); + --border-critical-base: var(--vscode-inputValidation-errorBorder, var(--vscode-errorForeground, var(--vscode-charts-red))); + --border-critical-hover: var(--vscode-inputValidation-errorBorder, var(--vscode-errorForeground, var(--vscode-charts-red))); + --border-critical-selected: var(--vscode-inputValidation-errorBorder, var(--vscode-errorForeground, var(--vscode-charts-red))); --border-info-base: var(--vscode-charts-blue); --border-info-hover: var(--vscode-charts-blue); --border-info-selected: var(--vscode-charts-blue); @@ -221,9 +221,9 @@ html[data-theme="kilo-vscode"] { --icon-warning-base: var(--vscode-charts-yellow); --icon-warning-hover: var(--vscode-charts-yellow); --icon-warning-active: var(--vscode-charts-yellow); - --icon-critical-base: var(--vscode-charts-red); - --icon-critical-hover: var(--vscode-charts-red); - --icon-critical-active: var(--vscode-charts-red); + --icon-critical-base: var(--vscode-errorForeground, var(--vscode-charts-red)); + --icon-critical-hover: var(--vscode-errorForeground, var(--vscode-charts-red)); + --icon-critical-active: var(--vscode-errorForeground, var(--vscode-charts-red)); --icon-info-base: var(--vscode-charts-blue); --icon-info-hover: var(--vscode-charts-blue); --icon-info-active: var(--vscode-charts-blue); @@ -239,9 +239,9 @@ html[data-theme="kilo-vscode"] { --icon-on-warning-base: var(--vscode-charts-yellow); --icon-on-warning-hover: var(--vscode-charts-yellow); --icon-on-warning-selected: var(--vscode-charts-yellow); - --icon-on-critical-base: var(--vscode-charts-red); - --icon-on-critical-hover: var(--vscode-charts-red); - --icon-on-critical-selected: var(--vscode-charts-red); + --icon-on-critical-base: var(--vscode-errorForeground, var(--vscode-charts-red)); + --icon-on-critical-hover: var(--vscode-errorForeground, var(--vscode-charts-red)); + --icon-on-critical-selected: var(--vscode-errorForeground, var(--vscode-charts-red)); --icon-on-info-base: var(--vscode-charts-blue); --icon-on-info-hover: var(--vscode-charts-blue); --icon-on-info-selected: var(--vscode-charts-blue); @@ -273,7 +273,7 @@ html[data-theme="kilo-vscode"] { --syntax-object: var(--vscode-editor-foreground); --syntax-success: var(--vscode-charts-green); --syntax-warning: var(--vscode-charts-yellow); - --syntax-critical: var(--vscode-charts-red); + --syntax-critical: var(--vscode-errorForeground, var(--vscode-charts-red)); --syntax-info: var(--vscode-charts-blue); --syntax-diff-add: var(--vscode-gitDecoration-addedResourceForeground); --syntax-diff-delete: var(--vscode-gitDecoration-deletedResourceForeground); diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx index ef600bccfc3..52ae08d88e6 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/ErrorDisplay.tsx @@ -3,6 +3,7 @@ import { Card } from "@kilocode/kilo-ui/card" import { Collapsible } from "@kilocode/kilo-ui/collapsible" import { useDialog } from "@kilocode/kilo-ui/context/dialog" import { ErrorDetails } from "@kilocode/kilo-ui/error-details" +import { Icon } from "@kilocode/kilo-ui/icon" import { Button } from "@kilocode/kilo-ui/button" import type { AssistantMessage } from "@kilocode/sdk/v2" import { useLanguage } from "../../context/language" @@ -62,8 +63,11 @@ export const ErrorDisplay: Component = (props) => { return ( - {errorText()} + +
+ +
{errorText()}
+
{t("error.details.show")} diff --git a/packages/kilo-vscode/webview-ui/src/stories/chat.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/chat.stories.tsx index b8b41bcaf92..692960c8e2d 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/chat.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/chat.stories.tsx @@ -8,8 +8,10 @@ */ import type { Meta, StoryObj } from "storybook-solidjs-vite" +import type { AssistantMessage } from "@kilocode/sdk/v2" import { StoryProviders, defaultMockData, mockSessionValue } from "./StoryProviders" import { ChatView } from "../components/chat/ChatView" +import { ErrorDisplay } from "../components/chat/ErrorDisplay" import { TaskHeader } from "../components/chat/TaskHeader" import { QuestionDock } from "../components/chat/QuestionDock" import { SuggestBar } from "../components/chat/SuggestBar" @@ -76,6 +78,72 @@ const reviewSuggestion: SuggestionRequest = { tool: { messageID: "asst-msg-002", callID: "call-suggest-001" }, } +const policyMessage = + "No endpoints found matching your data policy (Free model training). Configure: https://openrouter.ai/settings/privacy" + +const policyError: NonNullable = { + name: "APIError", + data: { + message: policyMessage, + statusCode: 400, + isRetryable: false, + responseBody: JSON.stringify( + { + error: { + type: "Bad Request", + message: "Data collection is required for this model. Please enable data collection to use this model.", + }, + }, + null, + 2, + ), + }, +} + +function BeforeErrorDisplay() { + return ( +
+
{policyMessage}
+
+ +
+
+ ) +} + // --------------------------------------------------------------------------- // Meta // --------------------------------------------------------------------------- @@ -231,6 +299,40 @@ export const SuggestBarReview: Story = { ), } +export const ErrorDisplayDataPolicy: Story = { + name: "ErrorDisplay — data policy", + render: () => ( + +
+ +
+
+ ), +} + +export const ErrorDisplayBeforeAfter: Story = { + name: "ErrorDisplay — before / after", + render: () => ( + +
+
+ Compare the previous error treatment reconstructed from pre-change CSS with the updated readable card. +
+
+
+

Before

+ +
+
+

After

+ +
+
+
+
+ ), +} + const toolUserID = "user-msg-spacing-001" const toolAssistantID = "asst-msg-spacing-001" const queuedUserID = "user-msg-spacing-002" From d02c3a6bf31f2a696363d136d8b4cffd57dc6aba Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 27 May 2026 02:51:48 +0000 Subject: [PATCH 02/57] chore: update kilo-vscode visual regression baselines --- .../chat/error-display-before-after-chromium-linux.png | 3 +++ .../chat/error-display-data-policy-chromium-linux.png | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/error-display-before-after-chromium-linux.png create mode 100644 packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/error-display-data-policy-chromium-linux.png diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/error-display-before-after-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/error-display-before-after-chromium-linux.png new file mode 100644 index 00000000000..7ff28e5ac05 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/error-display-before-after-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de207e3a717fe43dbb297ebebf018b14b010cee02fb060135c41bf8f5a599c68 +size 24422 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/error-display-data-policy-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/error-display-data-policy-chromium-linux.png new file mode 100644 index 00000000000..010968eb6a0 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/error-display-data-policy-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e3050c782d797be7dad951b76199df2c87195415e699d3e8481bbf40ad0dfccf +size 10073 From 7d2dbb0ae206f5cc3d39a06c2a3dbe3c631373ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Uruchurtu?= Date: Tue, 26 May 2026 20:56:07 -0600 Subject: [PATCH 03/57] chore(vscode): trim error story coverage --- ...or-display-before-after-chromium-linux.png | 3 - .../webview-ui/src/stories/chat.stories.tsx | 67 ------------------- 2 files changed, 70 deletions(-) delete mode 100644 packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/error-display-before-after-chromium-linux.png diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/error-display-before-after-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/error-display-before-after-chromium-linux.png deleted file mode 100644 index 7ff28e5ac05..00000000000 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/error-display-before-after-chromium-linux.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:de207e3a717fe43dbb297ebebf018b14b010cee02fb060135c41bf8f5a599c68 -size 24422 diff --git a/packages/kilo-vscode/webview-ui/src/stories/chat.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/chat.stories.tsx index 692960c8e2d..4d966d9d116 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/chat.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/chat.stories.tsx @@ -100,50 +100,6 @@ const policyError: NonNullable = { }, } -function BeforeErrorDisplay() { - return ( -
-
{policyMessage}
-
- -
-
- ) -} - // --------------------------------------------------------------------------- // Meta // --------------------------------------------------------------------------- @@ -310,29 +266,6 @@ export const ErrorDisplayDataPolicy: Story = { ), } -export const ErrorDisplayBeforeAfter: Story = { - name: "ErrorDisplay — before / after", - render: () => ( - -
-
- Compare the previous error treatment reconstructed from pre-change CSS with the updated readable card. -
-
-
-

Before

- -
-
-

After

- -
-
-
-
- ), -} - const toolUserID = "user-msg-spacing-001" const toolAssistantID = "asst-msg-spacing-001" const queuedUserID = "user-msg-spacing-002" From dcfadac83ed45a109a402a2f71f4d214347804f1 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Wed, 27 May 2026 15:13:51 +0200 Subject: [PATCH 04/57] fix(cli): prevent stale global config reloads --- .changeset/steady-indexing-provider.md | 5 ++ packages/opencode/src/server/routes/global.ts | 6 +- .../instance/httpapi/handlers/global.ts | 8 +- .../kilocode/global-config-refresh.test.ts | 90 +++++++++++++++++++ 4 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 .changeset/steady-indexing-provider.md create mode 100644 packages/opencode/test/kilocode/global-config-refresh.test.ts diff --git a/.changeset/steady-indexing-provider.md b/.changeset/steady-indexing-provider.md new file mode 100644 index 00000000000..9afaebdf819 --- /dev/null +++ b/.changeset/steady-indexing-provider.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Prevent saved global indexing provider changes from temporarily reverting in active workspaces. diff --git a/packages/opencode/src/server/routes/global.ts b/packages/opencode/src/server/routes/global.ts index 5e83c305807..eb783aacca6 100644 --- a/packages/opencode/src/server/routes/global.ts +++ b/packages/opencode/src/server/routes/global.ts @@ -194,9 +194,11 @@ export const GlobalRoutes = lazy(() => const config = c.req.valid("json") const result = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.updateGlobal(config))) if (result.changed) { - void AppRuntime.runPromise(disposeAllInstancesAndEmitGlobalDisposed({ swallowErrors: true })).catch( - () => undefined, + // kilocode_change start + await AppRuntime.runPromise( + disposeAllInstancesAndEmitGlobalDisposed({ swallowErrors: true }).pipe(Effect.catchCause(() => Effect.void)), ) + // kilocode_change end } return c.json(result.info) }, diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts index f80869b64d3..c9f202fd9bc 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts @@ -86,7 +86,13 @@ export const globalHandlers = HttpApiBuilder.group(RootHttpApi, "global", (handl const configUpdate = Effect.fn("GlobalHttpApi.configUpdate")(function* (ctx) { const result = yield* config.updateGlobal(ctx.payload) - if (result.changed) bridge.fork(disposeAllInstancesAndEmitGlobalDisposed({ swallowErrors: true })) + // kilocode_change start + if (result.changed) { + yield* bridge.run( + disposeAllInstancesAndEmitGlobalDisposed({ swallowErrors: true }).pipe(Effect.catchCause(() => Effect.void)), + ) + } + // kilocode_change end return result.info }) diff --git a/packages/opencode/test/kilocode/global-config-refresh.test.ts b/packages/opencode/test/kilocode/global-config-refresh.test.ts new file mode 100644 index 00000000000..ba7521f0bf6 --- /dev/null +++ b/packages/opencode/test/kilocode/global-config-refresh.test.ts @@ -0,0 +1,90 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { Global } from "@opencode-ai/core/global" +import { Flag } from "@opencode-ai/core/flag/flag" +import { GlobalBus } from "../../src/bus/global" +import { Server } from "../../src/server/server" +import { registerDisposer } from "../../src/effect/instance-registry" +import * as Log from "@opencode-ai/core/util/log" +import { resetDatabase } from "../fixture/db" +import { disposeAllInstances, tmpdir } from "../fixture/fixture" + +void Log.init({ print: false }) + +const experimental = Flag.KILO_EXPERIMENTAL_HTTPAPI +const root = Global.Path.config + +function app(value: boolean) { + Flag.KILO_EXPERIMENTAL_HTTPAPI = value + return value ? Server.Default().app : Server.Legacy().app +} + +async function update(target: ReturnType, provider: "kilo" | "openrouter") { + return target.request("/global/config", { + method: "PATCH", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ indexing: { provider } }), + }) +} + +async function provider(target: ReturnType, directory: string) { + const response = await target.request("/config", { headers: { "x-kilo-directory": directory } }) + return (await response.json()).indexing?.provider as string | undefined +} + +afterEach(async () => { + Flag.KILO_EXPERIMENTAL_HTTPAPI = experimental + ;(Global.Path as { config: string }).config = root + await disposeAllInstances() + await resetDatabase() +}) + +describe("global config refresh", () => { + for (const value of [false, true]) { + test(`${value ? "httpapi" : "legacy"} update reloads existing instance before responding`, async () => { + await using config = await tmpdir() + await using workspace = await tmpdir({ config: { formatter: false, lsp: false } }) + ;(Global.Path as { config: string }).config = config.path + await disposeAllInstances() + const target = app(value) + + expect((await update(target, "openrouter")).status).toBe(200) + expect(await provider(target, workspace.path)).toBe("openrouter") + + const started = Promise.withResolvers() + const release = Promise.withResolvers() + const unregister = registerDisposer(async (directory) => { + if (directory !== workspace.path) return + started.resolve() + await release.promise + }) + try { + const pending = update(target, "kilo") + await started.promise + const early = await Promise.race([pending.then(() => true), Bun.sleep(10).then(() => false)]) + expect(early).toBe(false) + release.resolve() + expect((await pending).status).toBe(200) + expect(await provider(target, workspace.path)).toBe("kilo") + } finally { + release.resolve() + unregister() + } + }) + + test(`${value ? "httpapi" : "legacy"} update ignores disposal notification failures`, async () => { + await using config = await tmpdir() + ;(Global.Path as { config: string }).config = config.path + await disposeAllInstances() + const target = app(value) + const listener = () => { + throw new Error("listener failed") + } + GlobalBus.on("event", listener) + try { + expect((await update(target, "kilo")).status).toBe(200) + } finally { + GlobalBus.off("event", listener) + } + }) + } +}) From 682e96aaf81c97b64d7ae239ddd93bd22bedfb4a Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 21:21:56 +0000 Subject: [PATCH 05/57] fix(cli): add agent-manager.json to default .kilo .gitignore --- packages/opencode/src/config/config.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index fa951214a7a..48746eb0c9f 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -577,7 +577,7 @@ export const layer = Layer.effect( yield* fs .writeFileString( gitignore, - // kilocode_change start - added pnpm-lock.yaml and yarn.lock (not in upstream) + // kilocode_change start - added pnpm-lock.yaml, yarn.lock, agent-manager.json (not in upstream) [ "node_modules", "package.json", @@ -586,6 +586,7 @@ export const layer = Layer.effect( "bun.lock", "yarn.lock", ".gitignore", + "agent-manager.json", ].join("\n"), // kilocode_change end ) From 4869d8722b423815a29832c812cf8a766c965a94 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 28 May 2026 09:08:57 +0200 Subject: [PATCH 06/57] feat(vscode): support inline session renaming --- .changeset/rename-session-titles.md | 5 + packages/kilo-vscode/src/KiloProvider.ts | 26 ++--- .../src/kilo-provider/rename-session.ts | 18 ++++ .../kilo-vscode/src/shared/session-title.ts | 16 ++++ .../tests/unit/kilo-provider-rename.test.ts | 56 +++++++++++ .../tests/unit/session-title.test.ts | 47 +++++++++ .../src/components/chat/TaskHeader.tsx | 96 ++++++++++++++++++- .../src/components/history/SessionList.tsx | 14 ++- .../kilo-vscode/webview-ui/src/i18n/ar.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/br.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/bs.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/da.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/de.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/en.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/es.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/fr.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/ja.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/ko.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/nl.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/no.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/pl.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/ru.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/th.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/tr.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/uk.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/zh.ts | 1 + .../kilo-vscode/webview-ui/src/i18n/zht.ts | 1 + .../webview-ui/src/styles/task-header.css | 75 +++++++++++++++ 28 files changed, 347 insertions(+), 25 deletions(-) create mode 100644 .changeset/rename-session-titles.md create mode 100644 packages/kilo-vscode/src/kilo-provider/rename-session.ts create mode 100644 packages/kilo-vscode/src/shared/session-title.ts create mode 100644 packages/kilo-vscode/tests/unit/kilo-provider-rename.test.ts create mode 100644 packages/kilo-vscode/tests/unit/session-title.test.ts diff --git a/.changeset/rename-session-titles.md b/.changeset/rename-session-titles.md new file mode 100644 index 00000000000..f5c780055a5 --- /dev/null +++ b/.changeset/rename-session-titles.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Allow renaming sessions inline from the active chat header with safe bounded titles. diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 8af621b930c..2f801984a40 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -57,6 +57,7 @@ import { retry } from "./services/cli-backend/retry" import { slimPart, slimParts } from "./kilo-provider/slim-metadata" import { handleSidebarWorktreeMessage } from "./kilo-provider/sidebar-worktree" import { parseMessageFiles, type MessageFile } from "./kilo-provider/message-files" +import { renameSession } from "./kilo-provider/rename-session" import { handleFileSearch } from "./kilo-provider/file-search" import { watchFontSizeConfig } from "./kilo-provider/font-size" import { getTerminalContents } from "./services/terminal/context" @@ -1659,27 +1660,18 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper * Handle renaming a session. */ private async handleRenameSession(sessionID: string, title: string): Promise { - if (!this.client) { - this.postMessage({ type: "error", message: "Not connected to CLI backend" }) - return - } - try { - const workspaceDir = this.getWorkspaceDirectory(sessionID) - const { data: updated } = await this.client.session.update( - { sessionID, directory: workspaceDir, title }, - { throwOnError: true }, - ) - if (this.currentSession?.id === sessionID) { - this.setCurrentSession(updated) - } + const updated = await renameSession({ + client: this.client, + sessionID, + title, + directory: this.getWorkspaceDirectory(sessionID), + }) + if (this.currentSession?.id === sessionID) this.setCurrentSession(updated) this.postMessage({ type: "sessionUpdated", session: this.sessionToWebview(updated) }) } catch (error) { console.error("[Kilo New] KiloProvider: Failed to rename session:", error) - this.postMessage({ - type: "error", - message: getErrorMessage(error) || "Failed to rename session", - }) + this.postMessage({ type: "error", message: getErrorMessage(error) || "Failed to rename session" }) } } diff --git a/packages/kilo-vscode/src/kilo-provider/rename-session.ts b/packages/kilo-vscode/src/kilo-provider/rename-session.ts new file mode 100644 index 00000000000..62b06a04637 --- /dev/null +++ b/packages/kilo-vscode/src/kilo-provider/rename-session.ts @@ -0,0 +1,18 @@ +import type { KiloClient, Session } from "@kilocode/sdk/v2/client" +import { parseSessionTitle } from "../shared/session-title" + +export async function renameSession(input: { + client: KiloClient | null + sessionID: string + title: unknown + directory: string +}): Promise { + if (!input.client) throw new Error("Not connected to CLI backend") + const result = parseSessionTitle(input.title) + if ("error" in result) throw new Error("Invalid session title") + const { data } = await input.client.session.update( + { sessionID: input.sessionID, directory: input.directory, title: result.value }, + { throwOnError: true }, + ) + return data +} diff --git a/packages/kilo-vscode/src/shared/session-title.ts b/packages/kilo-vscode/src/shared/session-title.ts new file mode 100644 index 00000000000..a86e01360d7 --- /dev/null +++ b/packages/kilo-vscode/src/shared/session-title.ts @@ -0,0 +1,16 @@ +export const SESSION_TITLE_LIMIT = 200 + +// Block terminal/display controls and bidi marks that can visually spoof a title. +const unsafe = /[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]/u + +type SessionTitleIssue = "invalid" | "required" | "too_long" | "control" +type SessionTitleResult = { value: string } | { error: SessionTitleIssue } + +export function parseSessionTitle(raw: unknown): SessionTitleResult { + if (typeof raw !== "string") return { error: "invalid" } + const value = raw.trim() + if (!value) return { error: "required" } + if (value.length > SESSION_TITLE_LIMIT) return { error: "too_long" } + if (unsafe.test(value)) return { error: "control" } + return { value } +} diff --git a/packages/kilo-vscode/tests/unit/kilo-provider-rename.test.ts b/packages/kilo-vscode/tests/unit/kilo-provider-rename.test.ts new file mode 100644 index 00000000000..a5d684bf71a --- /dev/null +++ b/packages/kilo-vscode/tests/unit/kilo-provider-rename.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "bun:test" +import { renameSession } from "../../src/kilo-provider/rename-session" +import { SESSION_TITLE_LIMIT } from "../../src/shared/session-title" + +type Params = { sessionID: string; directory?: string; title?: string } + +function client() { + const calls: Params[] = [] + return { + calls, + value: { + session: { + update: async (params: Params) => { + calls.push(params) + return { + data: { + id: params.sessionID, + title: params.title, + time: { created: 1, updated: 2 }, + }, + } + }, + }, + }, + } +} + +describe("renameSession", () => { + it("normalizes and persists a valid title through the backend client", async () => { + const api = client() + + const updated = await renameSession({ + client: api.value as never, + sessionID: "ses_1", + title: " Rename active session ", + directory: "/repo", + }) + + expect(api.calls).toHaveLength(1) + expect(api.calls[0]).toEqual({ sessionID: "ses_1", directory: "/repo", title: "Rename active session" }) + expect(updated.title).toBe("Rename active session") + }) + + it("rejects unsafe titles before they reach the backend", async () => { + const api = client() + const input = [" ", "a".repeat(SESSION_TITLE_LIMIT + 1), "Title\nSecond line", "Title\u202espoof"] + + for (const title of input) { + await expect( + renameSession({ client: api.value as never, sessionID: "ses_1", title, directory: "/repo" }), + ).rejects.toThrow("Invalid session title") + } + + expect(api.calls).toEqual([]) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/session-title.test.ts b/packages/kilo-vscode/tests/unit/session-title.test.ts new file mode 100644 index 00000000000..5c2b5828f9d --- /dev/null +++ b/packages/kilo-vscode/tests/unit/session-title.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from "bun:test" +import { parseSessionTitle, SESSION_TITLE_LIMIT } from "../../src/shared/session-title" + +describe("parseSessionTitle", () => { + it("rejects non-string input from untrusted callers", () => { + expect(parseSessionTitle(null)).toEqual({ error: "invalid" }) + }) + + it("trims a valid title", () => { + expect(parseSessionTitle(" Review authentication flow ")).toEqual({ value: "Review authentication flow" }) + }) + + it("rejects empty titles", () => { + expect(parseSessionTitle(" \t \n ")).toEqual({ error: "required" }) + }) + + it("accepts the display limit and rejects longer titles", () => { + expect(parseSessionTitle("a".repeat(SESSION_TITLE_LIMIT))).toEqual({ value: "a".repeat(SESSION_TITLE_LIMIT) }) + expect(parseSessionTitle("a".repeat(SESSION_TITLE_LIMIT + 1))).toEqual({ error: "too_long" }) + }) + + it("rejects every blocked control and directional formatting range", () => { + const values = [ + "Task\u0000suffix", + "Task\u001bsuffix", + "Task\u001fsuffix", + "Task\u007fsuffix", + "Task\u009fsuffix", + "Task\u061csuffix", + "Task\u200esuffix", + "Task\u200fsuffix", + "Task\u2028suffix", + "Task\u2029suffix", + "Task\u202asuffix", + "Task\u202esuffix", + "Task\u2066suffix", + "Task\u2069suffix", + ] + for (const value of values) expect(parseSessionTitle(value)).toEqual({ error: "control" }) + }) + + it("accepts normal unicode display text", () => { + expect(parseSessionTitle("Analyse de la session - \u4fee\u6b63")).toEqual({ + value: "Analyse de la session - \u4fee\u6b63", + }) + }) +}) diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx index 6360660cce3..198999fff39 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx @@ -8,11 +8,13 @@ * session activity) and a context window progress bar. */ -import { Component, For, Show, createMemo, createSignal, onMount, onCleanup } from "solid-js" +import { Component, For, Show, createMemo, createSignal, createEffect, onMount, onCleanup } from "solid-js" import { IconButton } from "@kilocode/kilo-ui/icon-button" import { Tooltip } from "@kilocode/kilo-ui/tooltip" import { Icon } from "@kilocode/kilo-ui/icon" import { Checkbox } from "@kilocode/kilo-ui/checkbox" +import { InlineInput } from "@kilocode/kilo-ui/inline-input" +import { showToast } from "@kilocode/kilo-ui/toast" import { useSession } from "../../context/session" import { calcTokenUsage, collapseCostBreakdown } from "../../context/session-utils" import { useLanguage } from "../../context/language" @@ -21,6 +23,7 @@ import { TaskTimeline } from "./TaskTimeline" import { ContextProgress } from "./ContextProgress" import { target as todoTarget } from "../../context/todo-revert" import type { Part, TodoItem, ExtensionMessage } from "../../types/messages" +import { parseSessionTitle, SESSION_TITLE_LIMIT } from "../../../../src/shared/session-title" interface TaskHeaderProps { readonly?: boolean @@ -31,6 +34,7 @@ export const TaskHeader: Component = (props) => { const language = useLanguage() const title = createMemo(() => session.currentSession()?.title ?? language.t("command.session.new")) + const canRename = createMemo(() => !props.readonly && !!session.currentSession()) const hasMessages = createMemo(() => session.messages().length > 0) const busy = createMemo(() => session.status() === "busy") const canCompact = createMemo(() => !busy() && session.visibleMessages().length > 0 && !!session.selected()) @@ -114,6 +118,45 @@ export const TaskHeader: Component = (props) => { }) const [todosOpen, setTodosOpen] = createSignal(false) + const [renaming, setRenaming] = createSignal<{ id: string; title: string }>() + const [renameValue, setRenameValue] = createSignal("") + const [cancelled, setCancelled] = createSignal(false) + + const startRename = () => { + if (props.readonly) return + const info = session.currentSession() + if (!info) return + setCancelled(false) + setRenameValue(info.title ?? "") + setRenaming({ id: info.id, title: info.title ?? "" }) + } + + const commitRename = () => { + const info = renaming() + if (!info) return + if (cancelled()) { + setCancelled(false) + return + } + const result = parseSessionTitle(renameValue()) + if ("error" in result) { + showToast({ variant: "error", title: language.t("toast.session.rename.invalid.title") }) + return + } + setRenaming(undefined) + if (result.value === info.title) return + session.renameSession(info.id, result.value) + } + + const cancelRename = () => { + setCancelled(true) + setRenaming(undefined) + } + + createEffect(() => { + const info = renaming() + if (info && session.currentSession()?.id !== info.id) setRenaming(undefined) + }) const donePart = (idx: number): Part | undefined => todoTarget({ messages: session.messages(), parts: session.allParts() }, idx) @@ -128,8 +171,55 @@ export const TaskHeader: Component = (props) => { return (
-
- {title()} +
+ + setRenameValue(e.currentTarget.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault() + commitRename() + } + if (e.key === "Escape") { + e.preventDefault() + cancelRename() + } + }} + onBlur={commitRename} + ref={(el) => + requestAnimationFrame(() => { + el.focus() + el.select() + }) + } + /> + + } + > + { + if (!canRename() || (e.key !== "Enter" && e.key !== " ")) return + e.preventDefault() + startRename() + }} + > + {title()} + +
diff --git a/packages/kilo-vscode/webview-ui/src/components/history/SessionList.tsx b/packages/kilo-vscode/webview-ui/src/components/history/SessionList.tsx index 7a99fa4e48a..70819b5906d 100644 --- a/packages/kilo-vscode/webview-ui/src/components/history/SessionList.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/history/SessionList.tsx @@ -12,11 +12,13 @@ import { Dialog } from "@kilocode/kilo-ui/dialog" import { Button } from "@kilocode/kilo-ui/button" import { IconButton } from "@kilocode/kilo-ui/icon-button" import { InlineInput } from "@kilocode/kilo-ui/inline-input" +import { showToast } from "@kilocode/kilo-ui/toast" import { useDialog } from "@kilocode/kilo-ui/context/dialog" import { useSession } from "../../context/session" import { useLanguage } from "../../context/language" import { formatRelativeDate } from "../../utils/date" import type { SessionInfo } from "../../types/messages" +import { parseSessionTitle, SESSION_TITLE_LIMIT } from "../../../../src/shared/session-title" const DATE_GROUP_KEYS = ["time.today", "time.yesterday", "time.thisWeek", "time.thisMonth", "time.older"] as const @@ -65,14 +67,15 @@ const SessionList: Component = (props) => { function saveRename() { const id = renamingId() - const title = renameValue().trim() - if (!id || !title) { - cancelRename() + if (!id) return + const result = parseSessionTitle(renameValue()) + if ("error" in result) { + showToast({ variant: "error", title: language.t("toast.session.rename.invalid.title") }) return } const existing = session.sessions().find((s) => s.id === id) - if (!existing || title !== (existing.title || "")) { - session.renameSession(id, title) + if (!existing || result.value !== (existing.title || "")) { + session.renameSession(id, result.value) } setRenamingId(null) setRenameValue("") @@ -180,6 +183,7 @@ const SessionList: Component = (props) => { requestAnimationFrame(() => el?.focus())} value={renameValue()} + maxLength={SESSION_TITLE_LIMIT} onInput={(e) => setRenameValue(e.currentTarget.value)} onKeyDown={(e) => { e.stopPropagation() diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 53973932bfb..c683c2d13b7 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -447,6 +447,7 @@ export const dict = { "toast.session.unshare.failed.title": "فشل إلغاء مشاركة الجلسة", "toast.session.unshare.failed.description": "حدث خطأ أثناء إلغاء مشاركة الجلسة", + "toast.session.rename.invalid.title": "عنوان الجلسة غير صالح", "toast.session.listFailed.title": "فشل تحميل الجلسات لـ {{project}}", "toast.update.title": "تحديث متاح", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 415bdf1ebb3..d54630eaced 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -450,6 +450,7 @@ export const dict = { "toast.session.unshare.failed.title": "Falha ao parar de compartilhar sessão", "toast.session.unshare.failed.description": "Ocorreu um erro ao parar de compartilhar a sessão", + "toast.session.rename.invalid.title": "Título de sessão inválido", "toast.session.listFailed.title": "Falha ao carregar sessões para {{project}}", "toast.update.title": "Atualização disponível", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index 26e44d27154..76be77e3f8d 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -454,6 +454,7 @@ export const dict = { "toast.session.unshare.failed.title": "Neuspjelo ukidanje dijeljenja", "toast.session.unshare.failed.description": "Došlo je do greške prilikom ukidanja dijeljenja", + "toast.session.rename.invalid.title": "Nevažeći naslov sesije", "toast.session.listFailed.title": "Neuspjelo učitavanje sesija za {{project}}", "toast.update.title": "Dostupno ažuriranje", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index 84da2dd3d9f..661333217dd 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -451,6 +451,7 @@ export const dict = { "toast.session.unshare.failed.title": "Kunne ikke stoppe deling af session", "toast.session.unshare.failed.description": "Der opstod en fejl under stop af sessionsdeling", + "toast.session.rename.invalid.title": "Ugyldig sessionstitel", "toast.session.listFailed.title": "Kunne ikke indlæse sessioner for {{project}}", "toast.update.title": "Opdatering tilgængelig", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index ae91ff5797b..2a8479d7cd1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -456,6 +456,7 @@ export const dict = { "toast.session.unshare.failed.title": "Aufheben des Teilens fehlgeschlagen", "toast.session.unshare.failed.description": "Beim Aufheben des Teilens ist ein Fehler aufgetreten", + "toast.session.rename.invalid.title": "Ungültiger Sitzungstitel", "toast.session.listFailed.title": "Sitzungen für {{project}} konnten nicht geladen werden", "toast.update.title": "Update verfügbar", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index 8cdc9688756..1e94952fe5c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -451,6 +451,7 @@ export const dict = { "toast.session.unshare.failed.title": "Failed to unshare session", "toast.session.unshare.failed.description": "An error occurred while unsharing the session", + "toast.session.rename.invalid.title": "Invalid session title", "toast.session.listFailed.title": "Failed to load sessions for {{project}}", "toast.update.title": "Update available", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index 5866769662b..29062e0b22c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -454,6 +454,7 @@ export const dict = { "toast.session.unshare.failed.title": "Fallo al dejar de compartir sesión", "toast.session.unshare.failed.description": "Ocurrió un error al dejar de compartir la sesión", + "toast.session.rename.invalid.title": "Título de sesión no válido", "toast.session.listFailed.title": "Fallo al cargar sesiones para {{project}}", "toast.update.title": "Actualización disponible", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index 8af4a24f7c7..35a07298dfa 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -457,6 +457,7 @@ export const dict = { "toast.session.unshare.failed.title": "Échec de l'annulation du partage", "toast.session.unshare.failed.description": "Une erreur s'est produite lors de l'annulation du partage de la session", + "toast.session.rename.invalid.title": "Titre de session non valide", "toast.session.listFailed.title": "Échec du chargement des sessions pour {{project}}", "toast.update.title": "Mise à jour disponible", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 97b9afbaa08..d16e728f48f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -448,6 +448,7 @@ export const dict = { "toast.session.unshare.failed.title": "セッションの共有解除に失敗しました", "toast.session.unshare.failed.description": "セッションの共有解除中にエラーが発生しました", + "toast.session.rename.invalid.title": "無効なセッションタイトル", "toast.session.listFailed.title": "{{project}}のセッション読み込みに失敗しました", "toast.update.title": "アップデートが利用可能です", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index c9fe4badb78..ce7e2ddbb95 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -450,6 +450,7 @@ export const dict = { "toast.session.unshare.failed.title": "세션 공유 해제 실패", "toast.session.unshare.failed.description": "세션 공유를 해제하는 동안 오류가 발생했습니다", + "toast.session.rename.invalid.title": "잘못된 세션 제목", "toast.session.listFailed.title": "{{project}}에 대한 세션을 로드하지 못했습니다", "toast.update.title": "업데이트 가능", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index 6e24fc5880f..f71c26438a9 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -454,6 +454,7 @@ export const dict = { "toast.session.unshare.success.description": "Delen van sessie succesvol gestopt!", "toast.session.unshare.failed.title": "Delen van sessie stoppen mislukt", "toast.session.unshare.failed.description": "Er is een fout opgetreden bij het stoppen van het delen van de sessie", + "toast.session.rename.invalid.title": "Ongeldige sessietitel", "toast.session.listFailed.title": "Laden van sessies voor {{project}} is mislukt", "toast.update.title": "Update beschikbaar", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 4ccd72ea3f6..81c5908d6d8 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -454,6 +454,7 @@ export const dict = { "toast.session.unshare.failed.title": "Kunne ikke stoppe deling av sesjon", "toast.session.unshare.failed.description": "Det oppstod en feil da delingen av sesjonen skulle stoppes", + "toast.session.rename.invalid.title": "Ugyldig sesjonstittel", "toast.session.listFailed.title": "Kunne ikke laste sesjoner for {{project}}", "toast.update.title": "Oppdatering tilgjengelig", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 441111e2534..4e4ee09935d 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -452,6 +452,7 @@ export const dict = { "toast.session.unshare.failed.title": "Nie udało się zatrzymać udostępniania sesji", "toast.session.unshare.failed.description": "Wystąpił błąd podczas zatrzymywania udostępniania sesji", + "toast.session.rename.invalid.title": "Nieprawidłowy tytuł sesji", "toast.session.listFailed.title": "Nie udało się załadować sesji dla {{project}}", "toast.update.title": "Dostępna aktualizacja", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index f66101686a5..a1d02e2dcff 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -451,6 +451,7 @@ export const dict = { "toast.session.unshare.failed.title": "Не удалось отменить публикацию", "toast.session.unshare.failed.description": "Произошла ошибка при отмене публикации", + "toast.session.rename.invalid.title": "Недопустимое название сессии", "toast.session.listFailed.title": "Не удалось загрузить сессии для {{project}}", "toast.update.title": "Доступно обновление", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index f318e568b54..fb52b91b76a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -450,6 +450,7 @@ export const dict = { "toast.session.unshare.failed.title": "ไม่สามารถยกเลิกการแชร์เซสชัน", "toast.session.unshare.failed.description": "เกิดข้อผิดพลาดระหว่างการยกเลิกการแชร์เซสชัน", + "toast.session.rename.invalid.title": "ชื่อเซสชันไม่ถูกต้อง", "toast.session.listFailed.title": "ไม่สามารถโหลดเซสชันสำหรับ {{project}}", "toast.update.title": "มีการอัปเดต", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index aef008e726d..72a561ed3d4 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -451,6 +451,7 @@ export const dict = { "toast.session.unshare.failed.title": "Oturum paylaşımı kaldırılamadı", "toast.session.unshare.failed.description": "Oturum paylaşımı kaldırılırken bir hata oluştu", + "toast.session.rename.invalid.title": "Geçersiz oturum başlığı", "toast.session.listFailed.title": "{{project}} için oturumlar yüklenemedi", "toast.update.title": "Güncelleme mevcut", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index 92ef49b58a6..a8e535d949e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -455,6 +455,7 @@ export const dict = { "toast.session.unshare.failed.title": "Не вдалося закрити доступ до сесії", "toast.session.unshare.failed.description": "Під час закриття доступу до сесії сталася помилка", + "toast.session.rename.invalid.title": "Некоректна назва сесії", "toast.session.listFailed.title": "Не вдалося завантажити сесії для {{project}}", "toast.update.title": "Доступне оновлення", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 0091f870dc8..5730412c642 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -447,6 +447,7 @@ export const dict = { "toast.session.unshare.failed.title": "取消分享失败", "toast.session.unshare.failed.description": "取消分享会话时发生错误", + "toast.session.rename.invalid.title": "无效的会话标题", "toast.session.listFailed.title": "无法加载 {{project}} 的会话", "toast.update.title": "有可用更新", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index 1c72a48fd23..e489fdd4342 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -447,6 +447,7 @@ export const dict = { "toast.session.unshare.failed.title": "取消分享失敗", "toast.session.unshare.failed.description": "取消分享工作階段時發生錯誤", + "toast.session.rename.invalid.title": "無效的工作階段標題", "toast.session.listFailed.title": "無法載入 {{project}} 的工作階段", "toast.update.title": "有可用更新", diff --git a/packages/kilo-vscode/webview-ui/src/styles/task-header.css b/packages/kilo-vscode/webview-ui/src/styles/task-header.css index 51b5c73c66b..3ec32445e87 100644 --- a/packages/kilo-vscode/webview-ui/src/styles/task-header.css +++ b/packages/kilo-vscode/webview-ui/src/styles/task-header.css @@ -1,3 +1,78 @@ +/* ============================================ + Task Header Rename + ============================================ */ + +[data-slot="task-header-title"] { + display: flex; + align-items: center; + min-width: 0; +} + +[data-slot="task-header-title-trigger"] { + display: inline-flex; + max-width: 100%; + min-width: 0; + border-radius: var(--radius-md); + transition: + color 0.15s ease, + background-color 0.15s ease; +} + +[data-slot="task-header-title-trigger"][data-renamable] { + margin: -4px -6px; + padding: 4px 6px; + cursor: text; +} + +[data-slot="task-header-title-trigger"][data-renamable] [data-slot="task-header-title-label"] { + text-decoration-line: underline; + text-decoration-style: dotted; + text-decoration-color: color-mix(in srgb, var(--text-weak) 45%, transparent); + text-underline-offset: 5px; + transition: text-decoration-color 0.15s ease; +} + +[data-slot="task-header-title-trigger"][data-renamable]:hover, +[data-slot="task-header-title-trigger"][data-renamable]:focus-visible { + background-color: var(--surface-interactive-hover, var(--vscode-toolbar-hoverBackground)); + outline: none; +} + +[data-slot="task-header-title-trigger"][data-renamable]:hover [data-slot="task-header-title-label"], +[data-slot="task-header-title-trigger"][data-renamable]:focus-visible [data-slot="task-header-title-label"] { + text-decoration-color: var(--text-weak); +} + +[data-slot="task-header-title-label"] { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +[data-slot="task-header-title-editor"] { + display: inline-flex; + max-width: 100%; + min-width: min(140px, 100%); + padding: 4px 8px; + box-sizing: border-box; + border: 1px solid var(--border-focus, var(--vscode-focusBorder)); + border-radius: var(--radius-md); + background-color: var(--input-base, var(--vscode-input-background)); +} + +.task-header-rename-input { + flex: 0 1 auto; + min-width: 0; + max-width: 100%; + color: var(--text-strong); + background-color: transparent; +} + +[data-slot="task-header-title-editor"] [data-component="inline-input"]:focus { + box-shadow: none; +} + /* ============================================ Task Header Todos ============================================ */ From 117691e4d6fe48f91223bb7d7e24103c67cde73f Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 28 May 2026 09:19:25 +0200 Subject: [PATCH 07/57] fix: constrain Kilo indexing embedding models --- .changeset/kilo-embedding-model-presets.md | 8 ++ packages/kilo-indexing/src/config.ts | 11 +- .../tests/indexing-provider-blur-race.spec.ts | 61 +++++++- .../src/components/settings/IndexingTab.tsx | 70 +++++----- .../webview-ui/src/stories/StoryProviders.tsx | 8 +- .../src/stories/settings.stories.tsx | 63 +++++++++ .../webview-ui/src/types/messages/config.ts | 4 +- .../kilocode/components/dialog-indexing.tsx | 106 +++++++++----- packages/opencode/src/kilocode/indexing.ts | 22 ++- .../server/routes/instance/httpapi/public.ts | 5 + .../test/kilocode/config/config.test.ts | 20 +++ .../test/kilocode/indexing-startup.test.ts | 132 ++++++++++++++++++ .../kilocode/server/httpapi-bridge.test.ts | 20 +++ packages/sdk/js/src/v2/gen/types.gen.ts | 4 +- packages/sdk/openapi.json | 20 ++- 15 files changed, 463 insertions(+), 91 deletions(-) create mode 100644 .changeset/kilo-embedding-model-presets.md diff --git a/.changeset/kilo-embedding-model-presets.md b/.changeset/kilo-embedding-model-presets.md new file mode 100644 index 00000000000..ae3e3f912e4 --- /dev/null +++ b/.changeset/kilo-embedding-model-presets.md @@ -0,0 +1,8 @@ +--- +"kilo-code": patch +"@kilocode/cli": patch +"@kilocode/kilo-indexing": patch +"@kilocode/sdk": patch +--- + +Use supported hosted model presets for Kilo indexing and clear obsolete model and dimension overrides. diff --git a/packages/kilo-indexing/src/config.ts b/packages/kilo-indexing/src/config.ts index 8eae226e0ca..a4477388036 100644 --- a/packages/kilo-indexing/src/config.ts +++ b/packages/kilo-indexing/src/config.ts @@ -21,11 +21,12 @@ export const IndexingConfig = z .object({ enabled: z.boolean().optional().describe("Enable codebase indexing"), provider: z.enum(providers).optional().describe("Embedding provider to use for codebase indexing"), - model: z.string().optional().describe("Embedding model ID (uses provider default if omitted)"), + model: z.string().nullable().optional().describe("Embedding model ID (uses provider default if omitted)"), dimension: z .number() .int() .positive() + .nullable() .optional() .describe("Override embedding vector dimension (auto-detected from model if omitted)"), vectorStore: z.enum(stores).optional().describe("Vector store backend (default: qdrant)"), @@ -140,10 +141,10 @@ export const IndexingSchema = Schema.Struct({ provider: Schema.optional(Provider).annotate({ description: "Embedding provider to use for codebase indexing", }), - model: Schema.optional(Schema.String).annotate({ + model: Schema.optional(Schema.NullOr(Schema.String)).annotate({ description: "Embedding model ID (uses provider default if omitted)", }), - dimension: Schema.optional(PositiveInt).annotate({ + dimension: Schema.optional(Schema.NullOr(PositiveInt)).annotate({ description: "Override embedding vector dimension (auto-detected from model if omitted)", }), vectorStore: Schema.optional(Store).annotate({ description: "Vector store backend (default: qdrant)" }), @@ -237,8 +238,8 @@ export function toIndexingConfigInput(cfg: IndexingConfig | undefined): Indexing enabled: cfg?.enabled ?? false, embedderProvider: provider, vectorStoreProvider: cfg?.vectorStore, - modelId: cfg?.model, - modelDimension: cfg?.dimension, + modelId: cfg?.model ?? undefined, + modelDimension: cfg?.dimension ?? undefined, lancedbVectorStoreDirectory: cfg?.lancedb?.directory, qdrantUrl: cfg?.qdrant?.url, qdrantApiKey: cfg?.qdrant?.apiKey, diff --git a/packages/kilo-vscode/tests/indexing-provider-blur-race.spec.ts b/packages/kilo-vscode/tests/indexing-provider-blur-race.spec.ts index 3b5b35beeae..c8f362f5705 100644 --- a/packages/kilo-vscode/tests/indexing-provider-blur-race.spec.ts +++ b/packages/kilo-vscode/tests/indexing-provider-blur-race.spec.ts @@ -10,15 +10,19 @@ if (IS_DARWIN) { const GLOBALS = "colorScheme:dark;theme:kilo-vscode;vscodeTheme:dark-modern" const STORY_ID = "settings--indexing-provider-blur-race" +const KILO_STORY_ID = "settings--indexing-kilo-model-preset" +const KILO_LOADING_STORY_ID = "settings--indexing-kilo-catalog-loading" type Saved = { provider?: string + model?: string | null + dimension?: number | null openai?: { apiKey?: string } gemini?: { apiKey?: string } } -function storyUrl() { - return `/iframe.html?id=${STORY_ID}&viewMode=story&globals=${GLOBALS}` +function storyUrl(id = STORY_ID) { + return `/iframe.html?id=${id}&viewMode=story&globals=${GLOBALS}` } async function disableAnimations(page: Page) { @@ -34,6 +38,10 @@ async function disableAnimations(page: Page) { }) } +function field(page: Page, title: string) { + return page.locator('[data-slot="settings-row"]', { hasText: title }).locator("input") +} + test("provider switch writes to selected provider bucket", async ({ page }) => { await page.setViewportSize({ width: 420, height: 720 }) await page.goto(storyUrl(), { waitUntil: "load" }) @@ -58,6 +66,55 @@ test("provider switch writes to selected provider bucket", async ({ page }) => { const cfg = JSON.parse(text) as Saved expect(cfg.provider).toBe("gemini") + expect(cfg.model).toBeNull() + expect(cfg.dimension).toBeNull() expect(cfg.openai?.apiKey ?? "").toBe("") expect(cfg.gemini?.apiKey ?? "").toBe("") + + const model = field(page, "Embedding model").first() + await expect(model).toHaveValue("") + await expect(model).toHaveAttribute("placeholder", "Enter model ID") +}) + +test("Kilo exposes only supported embedding model presets", async ({ page }) => { + await page.setViewportSize({ width: 420, height: 720 }) + await page.goto(storyUrl(KILO_STORY_ID), { waitUntil: "load" }) + await disableAnimations(page) + await page.waitForSelector("#storybook-root *", { state: "attached" }) + + await expect(page.getByText("Kilo model preset", { exact: true })).toBeVisible() + await expect(page.getByText("Embedding model", { exact: true })).toHaveCount(0) + await expect(page.getByText("Vector dimension", { exact: true })).toBeVisible() + + const preset = page.locator('[data-component="select"] [data-slot="select-select-trigger"]').nth(1) + await expect(preset).toContainText("Provider Model") + + const dimension = field(page, "Vector dimension").first() + await expect(dimension).toHaveValue("") + + await preset.click() + await page.locator('[data-slot="select-select-item-label"]', { hasText: "Provider Compact" }).click() + await expect(preset).toContainText("Provider Compact") +}) + +test("enabling Kilo before its catalog loads does not store an empty model", async ({ page }) => { + const saved = page.getByTestId("indexing-kilo-loading-save") + const cfg = async () => JSON.parse(((await saved.textContent()) ?? "{}").trim()) as Saved + const verify = async () => { + await expect.poll(async () => (await cfg()).provider).toBe("kilo") + expect((await cfg()).model).toBeNull() + expect((await cfg()).dimension).toBeNull() + } + + await page.setViewportSize({ width: 420, height: 720 }) + await page.goto(storyUrl(KILO_LOADING_STORY_ID), { waitUntil: "load" }) + await disableAnimations(page) + await page.waitForSelector("#storybook-root *", { state: "attached" }) + await page.locator('[data-component="switch"] [data-slot="switch-control"]').nth(1).click() + await verify() + + await page.goto(storyUrl(KILO_LOADING_STORY_ID), { waitUntil: "load" }) + await page.waitForSelector("#storybook-root *", { state: "attached" }) + await page.locator('[data-component="switch"] [data-slot="switch-control"]').first().click() + await verify() }) diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/IndexingTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/IndexingTab.tsx index 60cd1199fbc..188250c315c 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/IndexingTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/IndexingTab.tsx @@ -1,10 +1,6 @@ import { Component, For, Show, createMemo, createSignal } from "solid-js" import { Card } from "@kilocode/kilo-ui/card" -import { - formatKiloEmbeddingModelLabel, - getKiloEmbeddingModel, - normalizeKiloEmbeddingModelId, -} from "@kilocode/kilo-indexing/embedding-models" +import { formatKiloEmbeddingModelLabel, getKiloEmbeddingModel } from "@kilocode/kilo-indexing/embedding-models" import { Select } from "@kilocode/kilo-ui/select" import { Switch } from "@kilocode/kilo-ui/switch" import { TextField } from "@kilocode/kilo-ui/text-field" @@ -96,16 +92,20 @@ const IndexingTab: Component = () => { } const vectorStore = () => cfg().vectorStore ?? "qdrant" - const kiloDefault = () => embeds.catalog().defaultModel + const kiloDefault = () => + getKiloEmbeddingModel(embeds.catalog().defaultModel, embeds.catalog())?.id ?? embeds.catalog().defaultModel const kiloModels = createMemo(() => embeds.catalog().models.map((model) => ({ value: model.id, label: formatKiloEmbeddingModelLabel(model), })), ) - const knownKiloModel = (model: string | undefined) => getKiloEmbeddingModel(model, embeds.catalog())?.id + const knownKiloModel = (model: string | null | undefined) => + getKiloEmbeddingModel(model ?? undefined, embeds.catalog())?.id + const kiloValue = () => knownKiloModel(cfg().model) ?? kiloDefault() const kiloAvailable = () => !!server.profileData() || provider.authStates()[KILO_PROVIDER_ID] !== undefined const selectedProvider = () => cfg().provider ?? (kiloAvailable() ? "kilo" : undefined) + const staleKiloModel = () => selectedProvider() === "kilo" && !!cfg().model && !knownKiloModel(cfg().model) const providers = createMemo(() => allProviders.filter((item) => item.value !== "kilo" || kiloAvailable() || selectedProvider() === "kilo"), ) @@ -113,15 +113,15 @@ const IndexingTab: Component = () => { const saveProvider = (next: ProviderId | undefined) => { if (next === "kilo") { - const model = knownKiloModel(cfg().model) ?? (kiloDefault() || undefined) + const model = knownKiloModel(cfg().model) ?? (kiloDefault() || null) updateIndexing({ provider: next, model, - dimension: undefined, + dimension: null, }) return } - updateIndexing({ provider: next, model: undefined, dimension: undefined }) + updateIndexing({ provider: next, model: null, dimension: null }) } const saveEnabled = (enabled: boolean) => { @@ -129,7 +129,8 @@ const IndexingTab: Component = () => { updateIndexing({ enabled, provider: "kilo", - model: knownKiloModel(cfg().model) ?? kiloDefault(), + model: knownKiloModel(cfg().model) ?? (kiloDefault() || null), + dimension: null, }) return } @@ -142,7 +143,8 @@ const IndexingTab: Component = () => { indexing: { enabled, provider: "kilo", - model: knownKiloModel(cfg().model) ?? kiloDefault(), + model: knownKiloModel(cfg().model) ?? (kiloDefault() || null), + dimension: null, }, }) return @@ -151,15 +153,9 @@ const IndexingTab: Component = () => { } const saveModel = (value: string) => { + if (selectedProvider() === "kilo") return const trimmed = value.trim() - if (!trimmed) { - updateIndexing({ model: undefined }) - return - } - updateIndexing({ - model: - selectedProvider() === "kilo" ? (normalizeKiloEmbeddingModelId(trimmed, embeds.catalog()) ?? trimmed) : trimmed, - }) + updateIndexing({ model: trimmed || null }) } const providerValue = (group: string, key: string) => { @@ -189,13 +185,13 @@ const IndexingTab: Component = () => { } const saveNumber = ( - key: keyof IndexingConfig, + key: TuningKey | "dimension", value: string, options?: { integer?: boolean; min?: number; max?: number }, ) => { const trimmed = value.trim() if (!trimmed) { - updateIndexing({ [key]: undefined }) + updateIndexing({ [key]: key === "dimension" ? null : undefined }) return } @@ -272,35 +268,37 @@ const IndexingTab: Component = () => { >