diff --git a/.changeset/autocomplete-model-picker.md b/.changeset/autocomplete-model-picker.md new file mode 100644 index 00000000000..a24ba091f4d --- /dev/null +++ b/.changeset/autocomplete-model-picker.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Show the autocomplete model selector with the same picker layout as other model selectors and save changes from the settings save bar. diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-autocomplete-open-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-autocomplete-open-chromium-linux.png new file mode 100644 index 00000000000..e395b8b062d --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/settings/models-autocomplete-open-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bdcac95e7969d021ebce19001f37f86b8ab670bd79e39086385c9419617294df +size 27177 diff --git a/packages/kilo-vscode/eslint.config.mjs b/packages/kilo-vscode/eslint.config.mjs index de3e2b75e4d..0914b0366f7 100644 --- a/packages/kilo-vscode/eslint.config.mjs +++ b/packages/kilo-vscode/eslint.config.mjs @@ -38,7 +38,7 @@ export default [ // New code must stay ≤ 20. Do not raise these caps; refactor instead. { files: ["src/KiloProvider.ts"], - rules: { complexity: ["error", 150], "max-lines": ["error", 3500] }, + rules: { complexity: ["error", 150], "max-lines": ["error", 3600] }, }, { files: ["webview-ui/agent-manager/AgentManagerApp.tsx"], diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index b18192e38f4..ca77476aa76 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -62,6 +62,7 @@ import { abortSession } from "./kilo-provider/abort" import { buildAutocompleteSettingsMessage, routeAutocompleteMessage, + validAutocompleteSetting, watchAutocompleteConfig, } from "./services/autocomplete/settings" import * as ModelState from "./kilo-provider/model-state" @@ -2865,6 +2866,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper */ private async handleUpdateSetting(key: string, value: unknown): Promise { const { section, leaf } = buildSettingPath(key) + if (section === "autocomplete" && !validAutocompleteSetting(leaf, value)) return const config = vscode.workspace.getConfiguration(`kilo-code.new${section ? `.${section}` : ""}`) await config.update(leaf, value, vscode.ConfigurationTarget.Global) } diff --git a/packages/kilo-vscode/src/services/autocomplete/__tests__/settings.spec.ts b/packages/kilo-vscode/src/services/autocomplete/__tests__/settings.spec.ts index 9ee596bc23b..4cc2eb0a535 100644 --- a/packages/kilo-vscode/src/services/autocomplete/__tests__/settings.spec.ts +++ b/packages/kilo-vscode/src/services/autocomplete/__tests__/settings.spec.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest" const state = new Map() -const update = vi.fn(async (key: string, value: unknown) => { +const update = vi.fn((key: string, value: unknown) => { state.set(key, value) }) @@ -44,36 +44,21 @@ describe("autocomplete settings", () => { expect(buildAutocompleteSettingsMessage().settings.model).toBe("mistralai/codestral-2508") }) - it("persists supported model updates", async () => { - const post = vi.fn() - const { routeAutocompleteMessage } = await import("../settings") + it("validates supported model updates", async () => { + const { validAutocompleteSetting } = await import("../settings") - await routeAutocompleteMessage( - { type: "updateAutocompleteSetting", key: "model", value: "inception/mercury-edit" }, - post, - ) - - expect(update).toHaveBeenCalledWith("model", "inception/mercury-edit", 1) - expect(post).toHaveBeenCalledWith(expect.objectContaining({ type: "autocompleteSettingsLoaded" })) + expect(validAutocompleteSetting("model", "inception/mercury-edit")).toBe(true) }) it("rejects unsupported model updates", async () => { - const post = vi.fn() - const { routeAutocompleteMessage } = await import("../settings") - - await routeAutocompleteMessage({ type: "updateAutocompleteSetting", key: "model", value: "other/model" }, post) + const { validAutocompleteSetting } = await import("../settings") - expect(update).not.toHaveBeenCalled() - expect(post).not.toHaveBeenCalled() + expect(validAutocompleteSetting("model", "other/model")).toBe(false) }) it("rejects non-boolean toggle updates", async () => { - const post = vi.fn() - const { routeAutocompleteMessage } = await import("../settings") - - await routeAutocompleteMessage({ type: "updateAutocompleteSetting", key: "enableAutoTrigger", value: "true" }, post) + const { validAutocompleteSetting } = await import("../settings") - expect(update).not.toHaveBeenCalled() - expect(post).not.toHaveBeenCalled() + expect(validAutocompleteSetting("enableAutoTrigger", "true")).toBe(false) }) }) diff --git a/packages/kilo-vscode/src/services/autocomplete/settings.ts b/packages/kilo-vscode/src/services/autocomplete/settings.ts index 029d4921291..9ec341406dc 100644 --- a/packages/kilo-vscode/src/services/autocomplete/settings.ts +++ b/packages/kilo-vscode/src/services/autocomplete/settings.ts @@ -1,12 +1,8 @@ import * as vscode from "vscode" import { AUTOCOMPLETE_MODELS, getAutocompleteModel } from "../../shared/autocomplete-models" -const keys = new Set(["enableAutoTrigger", "enableSmartInlineTaskKeybinding", "enableChatAutocomplete", "model"]) - type Message = { type: string - key?: unknown - value?: unknown } type Post = (msg: unknown) => void @@ -17,13 +13,6 @@ export async function routeAutocompleteMessage(message: Message, post: Post): Pr return true } - if (message.type === "updateAutocompleteSetting") { - if (await update(message.key, message.value)) { - post(buildAutocompleteSettingsMessage()) - } - return true - } - return false } @@ -49,23 +38,15 @@ export function watchAutocompleteConfig(post: Post): vscode.Disposable { }) } -async function update(key: unknown, value: unknown) { - if (typeof key !== "string") return false - if (!keys.has(key)) return false - if (!valid(key, value)) return false - - await vscode.workspace - .getConfiguration("kilo-code.new.autocomplete") - .update(key, value, vscode.ConfigurationTarget.Global) - - return true -} - -function valid(key: string, value: unknown) { +export function validAutocompleteSetting(key: string, value: unknown) { if (key === "model") { if (typeof value !== "string") return false return AUTOCOMPLETE_MODELS.some((m) => m.id === value) } - return typeof value === "boolean" + if (key === "enableAutoTrigger") return typeof value === "boolean" + if (key === "enableSmartInlineTaskKeybinding") return typeof value === "boolean" + if (key === "enableChatAutocomplete") return typeof value === "boolean" + + return false } diff --git a/packages/kilo-vscode/tests/unit/autocomplete-model-selector.test.ts b/packages/kilo-vscode/tests/unit/autocomplete-model-selector.test.ts new file mode 100644 index 00000000000..1996041e917 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/autocomplete-model-selector.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest" +import { + AUTOCOMPLETE_PROVIDER_ID, + AUTOCOMPLETE_PROVIDER_NAME, + AUTOCOMPLETE_SELECTOR_MODELS, +} from "../../webview-ui/src/components/settings/autocomplete-model-selector" +import { AUTOCOMPLETE_MODELS } from "../../src/shared/autocomplete-models" + +describe("autocomplete model selector", () => { + it("shows only Kilo Gateway autocomplete models", () => { + expect(AUTOCOMPLETE_SELECTOR_MODELS).toEqual( + AUTOCOMPLETE_MODELS.map((m) => ({ + id: m.id, + name: m.label, + providerID: AUTOCOMPLETE_PROVIDER_ID, + providerName: AUTOCOMPLETE_PROVIDER_NAME, + })), + ) + }) +}) diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/AutocompleteTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/AutocompleteTab.tsx index 072c5e0a925..6bc66c1b588 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/AutocompleteTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/AutocompleteTab.tsx @@ -1,37 +1,21 @@ -import { Component, createSignal, onCleanup } from "solid-js" +import { Component } from "solid-js" import { Switch } from "@kilocode/kilo-ui/switch" import { Card } from "@kilocode/kilo-ui/card" -import { useVSCode } from "../../context/vscode" +import { useConfig } from "../../context/config" import { useLanguage } from "../../context/language" -import type { ExtensionMessage } from "../../types/messages" import SettingsRow from "./SettingsRow" -const AutocompleteTab: Component = () => { - const vscode = useVSCode() +const AutocompleteTab: Component<{ onNavigateToModels?: () => void }> = (props) => { + const { settings, updateSetting } = useConfig() const language = useLanguage() - const [enableAutoTrigger, setEnableAutoTrigger] = createSignal(true) - const [enableSmartInlineTaskKeybinding, setEnableSmartInlineTaskKeybinding] = createSignal(false) - const [enableChatAutocomplete, setEnableChatAutocomplete] = createSignal(false) + const enabled = (key: string, fallback: boolean) => Boolean(settings()[key] ?? fallback) - const unsubscribe = vscode.onMessage((message: ExtensionMessage) => { - if (message.type !== "autocompleteSettingsLoaded") { - return - } - setEnableAutoTrigger(message.settings.enableAutoTrigger) - setEnableSmartInlineTaskKeybinding(message.settings.enableSmartInlineTaskKeybinding) - setEnableChatAutocomplete(message.settings.enableChatAutocomplete) - }) - - onCleanup(unsubscribe) - - vscode.postMessage({ type: "requestAutocompleteSettings" }) - - const updateSetting = ( + const save = ( key: "enableAutoTrigger" | "enableSmartInlineTaskKeybinding" | "enableChatAutocomplete", value: boolean, ) => { - vscode.postMessage({ type: "updateAutocompleteSetting", key, value }) + updateSetting(`autocomplete.${key}`, value) } return ( @@ -42,8 +26,8 @@ const AutocompleteTab: Component = () => { description={language.t("settings.autocomplete.autoTrigger.description")} > updateSetting("enableAutoTrigger", checked)} + checked={enabled("autocomplete.enableAutoTrigger", true)} + onChange={(checked) => save("enableAutoTrigger", checked)} hideLabel > {language.t("settings.autocomplete.autoTrigger.title")} @@ -55,8 +39,8 @@ const AutocompleteTab: Component = () => { description={language.t("settings.autocomplete.smartKeybinding.description")} > updateSetting("enableSmartInlineTaskKeybinding", checked)} + checked={enabled("autocomplete.enableSmartInlineTaskKeybinding", false)} + onChange={(checked) => save("enableSmartInlineTaskKeybinding", checked)} hideLabel > {language.t("settings.autocomplete.smartKeybinding.title")} @@ -69,14 +53,38 @@ const AutocompleteTab: Component = () => { last > updateSetting("enableChatAutocomplete", checked)} + checked={enabled("autocomplete.enableChatAutocomplete", false)} + onChange={(checked) => save("enableChatAutocomplete", checked)} hideLabel > {language.t("settings.autocomplete.chatAutocomplete.title")} +

+ { + e.preventDefault() + props.onNavigateToModels?.() + }} + > + {language.t("settings.autocomplete.modelsHint")} + +

) } diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx index 745e533ddf1..c961e2d943b 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/ModelsTab.tsx @@ -1,31 +1,20 @@ -import { Component, For, createMemo, createSignal, onCleanup } from "solid-js" +import { Component, For, createMemo } from "solid-js" import { Card } from "@kilocode/kilo-ui/card" -import { Select } from "@kilocode/kilo-ui/select" import { useConfig } from "../../context/config" import { useLanguage } from "../../context/language" import { useSession } from "../../context/session" -import { useVSCode } from "../../context/vscode" import { parseModelString } from "../../../../src/shared/provider-model" -import { AUTOCOMPLETE_MODELS, DEFAULT_AUTOCOMPLETE_MODEL } from "../../../../src/shared/autocomplete-models" +import { DEFAULT_AUTOCOMPLETE_MODEL } from "../../../../src/shared/autocomplete-models" import { ModelSelectorBase } from "../shared/ModelSelector" import SettingsRow from "./SettingsRow" -import type { ExtensionMessage } from "../../types/messages" +import { AUTOCOMPLETE_PROVIDER_ID, AUTOCOMPLETE_SELECTOR_MODELS } from "./autocomplete-model-selector" const ModelsTab: Component = () => { - const { config, updateConfig } = useConfig() + const { config, settings, updateConfig, updateSetting } = useConfig() const language = useLanguage() const session = useSession() - const vscode = useVSCode() - const [autocompleteModel, setAutocompleteModel] = createSignal(DEFAULT_AUTOCOMPLETE_MODEL.id) - - const unsubscribe = vscode.onMessage((message: ExtensionMessage) => { - if (message.type === "autocompleteSettingsLoaded") { - setAutocompleteModel(message.settings.model) - } - }) - onCleanup(unsubscribe) - vscode.postMessage({ type: "requestAutocompleteSettings" }) + const autocompleteModel = () => String(settings()["autocomplete.model"] ?? DEFAULT_AUTOCOMPLETE_MODEL.id) function handleModelSelect(configKey: "model" | "small_model") { return (providerID: string, modelID: string) => { @@ -49,6 +38,11 @@ const ModelsTab: Component = () => { } } + function handleAutocompleteModelSelect(providerID: string, modelID: string) { + if (providerID !== AUTOCOMPLETE_PROVIDER_ID || !modelID) return + updateSetting("autocomplete.model", modelID) + } + return (
@@ -82,18 +76,12 @@ const ModelsTab: Component = () => { description={language.t("settings.autocomplete.model.description")} last > -