Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/autocomplete-model-picker.md
Original file line number Diff line number Diff line change
@@ -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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion packages/kilo-vscode/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
2 changes: 2 additions & 0 deletions packages/kilo-vscode/src/KiloProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -2865,6 +2866,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
*/
private async handleUpdateSetting(key: string, value: unknown): Promise<void> {
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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest"

const state = new Map<string, unknown>()
const update = vi.fn(async (key: string, value: unknown) => {
const update = vi.fn((key: string, value: unknown) => {
state.set(key, value)
})

Expand Down Expand Up @@ -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)
})
})
31 changes: 6 additions & 25 deletions packages/kilo-vscode/src/services/autocomplete/settings.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
}

Expand All @@ -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
}
Original file line number Diff line number Diff line change
@@ -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,
})),
)
})
})
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -42,8 +26,8 @@ const AutocompleteTab: Component = () => {
description={language.t("settings.autocomplete.autoTrigger.description")}
>
<Switch
checked={enableAutoTrigger()}
onChange={(checked) => updateSetting("enableAutoTrigger", checked)}
checked={enabled("autocomplete.enableAutoTrigger", true)}
onChange={(checked) => save("enableAutoTrigger", checked)}
hideLabel
>
{language.t("settings.autocomplete.autoTrigger.title")}
Expand All @@ -55,8 +39,8 @@ const AutocompleteTab: Component = () => {
description={language.t("settings.autocomplete.smartKeybinding.description")}
>
<Switch
checked={enableSmartInlineTaskKeybinding()}
onChange={(checked) => updateSetting("enableSmartInlineTaskKeybinding", checked)}
checked={enabled("autocomplete.enableSmartInlineTaskKeybinding", false)}
onChange={(checked) => save("enableSmartInlineTaskKeybinding", checked)}
hideLabel
>
{language.t("settings.autocomplete.smartKeybinding.title")}
Expand All @@ -69,14 +53,38 @@ const AutocompleteTab: Component = () => {
last
>
<Switch
checked={enableChatAutocomplete()}
onChange={(checked) => updateSetting("enableChatAutocomplete", checked)}
checked={enabled("autocomplete.enableChatAutocomplete", false)}
onChange={(checked) => save("enableChatAutocomplete", checked)}
hideLabel
>
{language.t("settings.autocomplete.chatAutocomplete.title")}
</Switch>
</SettingsRow>
</Card>
<p
data-slot="autocomplete-models-hint"
style={{
"margin-top": "20px",
"font-size": "var(--kilo-font-size-12)",
"text-align": "right",
color: "var(--text-weak-base, var(--vscode-descriptionForeground))",
}}
>
<a
href="#"
style={{
color: "var(--vscode-textLink-foreground)",
"text-decoration": "none",
cursor: "pointer",
}}
onClick={(e) => {
e.preventDefault()
props.onNavigateToModels?.()
}}
>
{language.t("settings.autocomplete.modelsHint")}
</a>
</p>
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string>(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) => {
Expand All @@ -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 (
<div>
<Card>
Expand Down Expand Up @@ -82,18 +76,12 @@ const ModelsTab: Component = () => {
description={language.t("settings.autocomplete.model.description")}
last
>
<Select
options={AUTOCOMPLETE_MODELS.map((m) => m.id)}
current={autocompleteModel()}
label={(opt: string) => AUTOCOMPLETE_MODELS.find((m) => m.id === opt)?.label ?? opt}
value={(opt: string) => opt}
onSelect={(opt) => {
if (opt !== undefined) {
setAutocompleteModel(opt)
vscode.postMessage({ type: "updateAutocompleteSetting", key: "model", value: opt })
}
}}
triggerVariant="settings"
<ModelSelectorBase
value={{ providerID: AUTOCOMPLETE_PROVIDER_ID, modelID: autocompleteModel() }}
onSelect={handleAutocompleteModelSelect}
placement="bottom-start"
models={AUTOCOMPLETE_SELECTOR_MODELS}
favorites={false}
/>
</SettingsRow>
</Card>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ const Settings: Component<SettingsProps> = (props) => {
</Tabs.Content>
<Tabs.Content value="autocomplete">
<h3>{language.t("settings.autocomplete.title")}</h3>
<AutocompleteTab />
<AutocompleteTab onNavigateToModels={() => onTabChange("models")} />
</Tabs.Content>
<Tabs.Content value="notifications">
<h3>{language.t("settings.notifications.title")}</h3>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { AUTOCOMPLETE_MODELS } from "../../../../src/shared/autocomplete-models"
import type { EnrichedModel } from "../../context/provider"

export const AUTOCOMPLETE_PROVIDER_ID = "kilo"
export const AUTOCOMPLETE_PROVIDER_NAME = "Kilo Gateway"

export const AUTOCOMPLETE_SELECTOR_MODELS: EnrichedModel[] = AUTOCOMPLETE_MODELS.map((m) => ({
id: m.id,
name: m.label,
providerID: AUTOCOMPLETE_PROVIDER_ID,
providerName: AUTOCOMPLETE_PROVIDER_NAME,
}))
Loading
Loading