diff --git a/packages/kilo-vscode/docs/migrate-settings.md b/packages/kilo-vscode/docs/migrate-settings.md new file mode 100644 index 00000000000..a0f452e159b --- /dev/null +++ b/packages/kilo-vscode/docs/migrate-settings.md @@ -0,0 +1,233 @@ +# Settings Migration Plan: Desktop App → VSCode Extension + +## Overview + +The desktop app (`packages/app`) has settings spread across four tabs: +- **General** — appearance, notifications, sounds, updates, linux display +- **Shortcuts** — keybinds (not applicable to VSCode — handled by VS Code keybindings) +- **Providers** — connect/disconnect providers, custom providers +- **Models** — toggle model visibility per provider + +Additionally, backend `Config` (persisted by the CLI server) controls agent behavior, permissions, MCP, commands, and experimental flags. The app's **Permissions** component (`settings-permissions.tsx`) manages per-tool permission levels (allow/ask/deny) via `globalSync.updateConfig({ permission: … })`. + +This document maps every setting to an extension tab and describes the implementation path. + +--- + +## 1. Backend Config Settings (via `PATCH /config`) + +These are server-side settings stored in the opencode config file. All reads use `GET /config` (with optional `?directory=`), all writes use `PATCH /config` (merges the partial `Config` object). + +### 1.1 Mapping to Extension Tabs + +| Backend Config Field | Type | Extension Tab | Notes | +|---|---|---|---| +| `permission` | `PermissionConfig` | **Auto Approve** | Per-tool allow/ask/deny — matches app's `settings-permissions.tsx` | +| `permission.` | `allow \| ask \| deny` | **Auto Approve** | Tools: read, edit, glob, grep, list, bash, task, skill, lsp, todoread, todowrite, webfetch, websearch, codesearch, external_directory, doom_loop | +| `model` | `string` | **Providers** | Default model (format: `provider/model`) | +| `small_model` | `string` | **Providers** | Small model for title generation etc. | +| `default_agent` | `string` | **Agent Behaviour** | Default agent when none specified | +| `agent` | `Record` | **Agent Behaviour** | Per-agent config (model, prompt, temperature, etc.) | +| `agent..model` | `string` | **Agent Behaviour** (Agents subtab) | Override model per agent | +| `agent..prompt` | `string` | **Agent Behaviour** (Agents subtab) | Custom system prompt per agent | +| `agent..temperature` | `number` | **Agent Behaviour** (Agents subtab) | Temperature override | +| `agent..top_p` | `number` | **Agent Behaviour** (Agents subtab) | Top-p override | +| `agent..steps` | `number` | **Agent Behaviour** (Agents subtab) | Max agentic iterations | +| `agent..permission` | `PermissionConfig` | **Agent Behaviour** (Agents subtab) | Per-agent permission overrides | +| `provider` | `Record` | **Providers** | Custom provider configuration | +| `disabled_providers` | `string[]` | **Providers** | Providers to disable | +| `enabled_providers` | `string[]` | **Providers** | Exclusive provider allowlist | +| `mcp` | `Record` | **Agent Behaviour** (MCP subtab) | MCP server configurations | +| `command` | `Record` | **Agent Behaviour** (Commands subtab) | Custom commands | +| `instructions` | `string[]` | **Prompts** | Additional instruction file paths | +| `skills.paths` | `string[]` | **Agent Behaviour** (Skills subtab) | Additional skill folder paths | +| `skills.urls` | `string[]` | **Agent Behaviour** (Skills subtab) | Skill URLs | +| `snapshot` | `boolean` | **Checkpoints** | Enable/disable snapshots | +| `share` | `manual \| auto \| disabled` | **Experimental** | Sharing behavior | +| `username` | `string` | **Display** | Custom username in conversations | +| `watcher.ignore` | `string[]` | **Context** | File watcher ignore patterns | +| `formatter` | `false \| Record<…>` | **Experimental** | Formatter configuration | +| `lsp` | `false \| Record<…>` | **Experimental** | LSP configuration | +| `compaction.auto` | `boolean` | **Agent Behaviour** | Auto-compaction when context is full | +| `compaction.prune` | `boolean` | **Agent Behaviour** | Prune old tool outputs | +| `tools` | `Record` | **Experimental** | Enable/disable specific tools globally | +| `layout` | `auto \| stretch` | **Display** | Layout mode | +| `experimental.disable_paste_summary` | `boolean` | **Experimental** | Disable paste summary | +| `experimental.batch_tool` | `boolean` | **Experimental** | Enable batch tool | +| `experimental.primary_tools` | `string[]` | **Experimental** | Primary-agent-only tools | +| `experimental.continue_loop_on_deny` | `boolean` | **Experimental** | Continue on permission deny | +| `experimental.mcp_timeout` | `number` | **Experimental** | MCP request timeout (ms) | + +### 1.2 Desktop-Only / Not Applicable in VSCode + +| Setting | Reason | +|---|---| +| `theme` | VSCode owns theming | +| `keybinds` | VSCode keybindings system | +| `tui.*` (scroll_speed, scroll_acceleration, diff_style) | TUI-specific | +| `server.*` | Server managed by extension automatically | + +--- + +## 2. Client-Side Settings (from `app/src/context/settings.tsx`) + +These are persisted client-side in the app (localStorage). In the extension, the equivalent would be VS Code's `ExtensionContext.globalState` or webview-local storage. + +| App Setting | Type | Extension Tab | Notes | +|---|---|---|---| +| `appearance.font` | `string` | **Display** | Mono font selection — VSCode has its own font settings, could be skipped or kept for webview only | +| `appearance.fontSize` | `number` | **Display** | Font size — same consideration as above | +| `notifications.agent` | `boolean` | **Notifications** | Notify when agent completes | +| `notifications.permissions` | `boolean` | **Notifications** | Notify on permission requests | +| `notifications.errors` | `boolean` | **Notifications** | Notify on errors | +| `sounds.agent` | `string` | **Notifications** | Sound on agent completion | +| `sounds.permissions` | `string` | **Notifications** | Sound on permission request | +| `sounds.errors` | `string` | **Notifications** | Sound on error | +| `general.releaseNotes` | `boolean` | **Experimental** | Show release notes | +| `general.autoSave` | `boolean` | N/A | VSCode has its own auto-save | +| `updates.startup` | `boolean` | N/A | VSCode handles extension updates | + +--- + +## 3. Provider & Model Settings (from app) + +| Feature | Extension Tab | Backend API | Notes | +|---|---|---|---| +| List connected providers | **Providers** | `GET /provider` | Already implemented in extension | +| Connect provider (OAuth) | **Providers** | `POST /provider/{id}/oauth/authorize` + `callback` | Already implemented | +| Disconnect provider | **Providers** | `DELETE /auth/{id}` + config `disabled_providers` update | Already implemented | +| Custom provider (OpenAI-compat) | **Providers** | `PATCH /config` with `provider` field | New | +| Toggle model visibility | **Providers** | `PATCH /config` with model visibility config | New | + +--- + +## 4. Implementation Plan + +### Phase 1: Infrastructure — Config Read/Write + +1. **Add `getConfig()` to [`HttpClient`](src/services/cli-backend/http-client.ts)** + ```typescript + async getConfig(directory: string): Promise { + return this.request("GET", "/config", undefined, { directory }) + } + ``` + +2. **Add `updateConfig()` to [`HttpClient`](src/services/cli-backend/http-client.ts)** + ```typescript + async updateConfig(config: Partial, directory: string): Promise { + return this.request("PATCH", "/config", config, { directory }) + } + ``` + +3. **Add Config types to [`types.ts`](src/services/cli-backend/types.ts)** + Import or re-define `Config`, `PermissionConfig`, `AgentConfig`, `ProviderConfig` etc. from the SDK types. + +4. **Add cached config message pattern in [`KiloProvider`](src/KiloProvider.ts)** + - `fetchAndSendConfig()` method + - Handle `requestConfig` message from webview + - Handle `updateConfig` message from webview (call `httpClient.updateConfig()`, then re-fetch and push) + +5. **Add message types to [`messages.ts`](webview-ui/src/types/messages.ts)** + - `ConfigLoadedMessage` (extension→webview) + - `RequestConfigMessage` (webview→extension) + - `UpdateConfigMessage` (webview→extension, carries partial Config) + - `ConfigUpdatedMessage` (extension→webview, confirmation) + +6. **Create config context in `webview-ui/src/context/config.tsx`** + - Subscribe to `ConfigLoadedMessage` + - Expose `config()` accessor and `updateConfig(partial)` method + - `updateConfig` posts `UpdateConfigMessage` to extension, optimistically updates local state + +### Phase 2: Populate Existing Tabs with Backend Settings + +Each tab should read from the config context and call `updateConfig()` on change. + +#### Auto Approve Tab +- Per-tool permission selects (allow/ask/deny) for all 16 tools +- Reference: [`settings-permissions.tsx`](../app/src/components/settings-permissions.tsx) + +#### Agent Behaviour Tab +- Default agent selector (`config.default_agent`) +- Compaction settings (`config.compaction.auto`, `config.compaction.prune`) +- **Agents subtab**: Per-agent config — model override, prompt, temperature, top_p, max steps, per-agent permissions +- **MCP subtab**: MCP server configurations (`config.mcp`) +- **Commands subtab**: Custom commands (`config.command`) +- **Skills subtab**: Skill folder paths and URLs (`config.skills`) + +#### Providers Tab +- Default model (`config.model`) +- Small model (`config.small_model`) +- Custom provider creation (`config.provider`) +- Provider enable/disable (`config.disabled_providers`, `config.enabled_providers`) +- Model visibility toggles (reference: [`settings-models.tsx`](../app/src/components/settings-models.tsx)) + +#### Prompts Tab +- Per-agent prompt (`config.agent..prompt`) +- Additional instructions (`config.instructions`) + +#### Checkpoints Tab +- Snapshot toggle (`config.snapshot`) + +#### Display Tab +- Username (`config.username`) +- Layout mode (`config.layout`) + +#### Context Tab +- Watcher ignore patterns (`config.watcher.ignore`) + +#### Notifications Tab +- Client-side notification toggles (agent, permissions, errors) +- Sound selection (agent, permissions, errors) +- These are client-side only; store in VS Code `globalState` or webview localStorage + +#### Experimental Tab +- Share mode (`config.share`) +- Formatter config (`config.formatter`) +- LSP config (`config.lsp`) +- Tool toggles (`config.tools`) +- Remaining `config.experimental.*` flags + +#### Language Tab +- Already implemented (client-side locale selection) + +### Phase 3: Client-Side Settings + +For notification and sound settings (not backed by the CLI server): + +1. **Option A**: Store in VS Code `globalState` via extension messages (preferred — survives webview reloads) +2. **Option B**: Store in webview `localStorage` (simpler but resets on webview rebuild) + +Recommended: Option A — add `SaveLocalSettingMessage` / `LocalSettingLoadedMessage` message types, persist via `context.globalState`. + +--- + +## 5. Settings not relevant for VSCode extension + +| Setting | Reason | +|---|---| +| `theme` / `colorScheme` | VS Code owns theming | +| `keybinds` | VS Code keybindings | +| `tui.*` | TUI-only settings | +| `server.*` | Server lifecycle managed by extension | +| `general.autoSave` | VS Code has native auto-save | +| `updates.startup` | VS Code handles extension updates | +| `appearance.font` / `appearance.fontSize` | VS Code editor font settings (could optionally be kept for webview chat font) | +| Wayland display backend | Linux desktop app only | + +--- + +## 6. Priority Order + +1. **P0 — Permissions** (Auto Approve tab): Most impactful for daily use +2. **P0 — Default model/agent** (Providers + Agent Behaviour): Core workflow +3. **P1 — Agent config** (Agent Behaviour): model overrides, temperature, steps +4. **P1 — MCP servers** (Agent Behaviour subtab): Server configuration +5. **P1 — Notifications & sounds** (Notifications tab): User experience +6. **P2 — Prompts/instructions** (Prompts tab): Custom system prompts +7. **P2 — Compaction settings** (Agent Behaviour): Auto-compaction +8. **P2 — Checkpoints/snapshots** (Checkpoints tab): Snapshot toggle +9. **P3 — Display settings** (Display tab): Username, layout +10. **P3 — Context settings** (Context tab): Watcher ignore +11. **P3 — Experimental flags** (Experimental tab): All experimental.* flags +12. **P3 — Custom providers** (Providers tab): OpenAI-compatible providers +13. **P3 — Model visibility** (Providers tab): Per-model show/hide diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json index 8fd2f05e882..46a04840704 100644 --- a/packages/kilo-vscode/package.json +++ b/packages/kilo-vscode/package.json @@ -181,6 +181,48 @@ "type": "boolean", "default": false, "description": "Run browser automation in headless mode (no visible window). When disabled, you can watch the agent interact with the browser." + }, + "kilo-code.new.notifications.agent": { + "type": "boolean", + "default": true, + "description": "Show notification when agent completes a task" + }, + "kilo-code.new.notifications.permissions": { + "type": "boolean", + "default": true, + "description": "Show notification on permission requests" + }, + "kilo-code.new.notifications.errors": { + "type": "boolean", + "default": true, + "description": "Show notification on errors" + }, + "kilo-code.new.sounds.agent": { + "type": "string", + "default": "default", + "enum": [ + "default", + "none" + ], + "description": "Sound to play when agent completes" + }, + "kilo-code.new.sounds.permissions": { + "type": "string", + "default": "default", + "enum": [ + "default", + "none" + ], + "description": "Sound to play on permission requests" + }, + "kilo-code.new.sounds.errors": { + "type": "string", + "default": "default", + "enum": [ + "default", + "none" + ], + "description": "Sound to play on errors" } } } diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 3e6468a6436..71fb997b592 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -13,6 +13,8 @@ export class KiloProvider implements vscode.WebviewViewProvider { private cachedProvidersMessage: unknown = null /** Cached agentsLoaded payload so requestAgents can be served before httpClient is ready */ private cachedAgentsMessage: unknown = null + /** Cached configLoaded payload so requestConfig can be served before httpClient is ready */ + private cachedConfigMessage: unknown = null private trackedSessionIds: Set = new Set() private unsubscribeEvent: (() => void) | null = null @@ -209,6 +211,12 @@ export class KiloProvider implements vscode.WebviewViewProvider { case "questionReject": await this.handleQuestionReject(message.requestID) break + case "requestConfig": + await this.fetchAndSendConfig() + break + case "updateConfig": + await this.handleUpdateConfig(message.config) + break case "setLanguage": await vscode.workspace .getConfiguration("kilo-code.new") @@ -226,6 +234,9 @@ export class KiloProvider implements vscode.WebviewViewProvider { case "requestBrowserSettings": this.sendBrowserSettings() break + case "requestNotificationSettings": + this.sendNotificationSettings() + break } }) } @@ -307,6 +318,8 @@ export class KiloProvider implements vscode.WebviewViewProvider { // Fetch providers and agents, then send to webview await this.fetchAndSendProviders() await this.fetchAndSendAgents() + await this.fetchAndSendConfig() + this.sendNotificationSettings() console.log("[Kilo New] KiloProvider: ✅ initializeConnection completed successfully") } catch (error) { @@ -588,6 +601,80 @@ export class KiloProvider implements vscode.WebviewViewProvider { } } + /** + * Fetch backend config and send to webview. + */ + private async fetchAndSendConfig(): Promise { + if (!this.httpClient) { + if (this.cachedConfigMessage) { + this.postMessage(this.cachedConfigMessage) + } + return + } + + try { + const workspaceDir = this.getWorkspaceDirectory() + const config = await this.httpClient.getConfig(workspaceDir) + + const message = { + type: "configLoaded", + config, + } + this.cachedConfigMessage = message + this.postMessage(message) + } catch (error) { + console.error("[Kilo New] KiloProvider: Failed to fetch config:", error) + } + } + + /** + * Read notification/sound settings from VS Code config and push to webview. + */ + private sendNotificationSettings(): void { + const notifications = vscode.workspace.getConfiguration("kilo-code.new.notifications") + const sounds = vscode.workspace.getConfiguration("kilo-code.new.sounds") + this.postMessage({ + type: "notificationSettingsLoaded", + settings: { + notifyAgent: notifications.get("agent", true), + notifyPermissions: notifications.get("permissions", true), + notifyErrors: notifications.get("errors", true), + soundAgent: sounds.get("agent", "default"), + soundPermissions: sounds.get("permissions", "default"), + soundErrors: sounds.get("errors", "default"), + }, + }) + } + + /** + * Handle config update request from the webview. + * Applies a partial config update via the global config endpoint, then pushes + * the full merged config back to the webview. + */ + private async handleUpdateConfig(partial: Record): Promise { + if (!this.httpClient) { + this.postMessage({ type: "error", message: "Not connected to CLI backend" }) + return + } + + try { + const updated = await this.httpClient.updateConfig(partial) + + const message = { + type: "configUpdated", + config: updated, + } + this.cachedConfigMessage = { type: "configLoaded", config: updated } + this.postMessage(message) + } catch (error) { + console.error("[Kilo New] KiloProvider: Failed to update config:", error) + this.postMessage({ + type: "error", + message: error instanceof Error ? error.message : "Failed to update config", + }) + } + } + /** * Handle sending a message from the webview. */ diff --git a/packages/kilo-vscode/src/services/cli-backend/http-client.ts b/packages/kilo-vscode/src/services/cli-backend/http-client.ts index ad5d4b990ad..51992e90c39 100644 --- a/packages/kilo-vscode/src/services/cli-backend/http-client.ts +++ b/packages/kilo-vscode/src/services/cli-backend/http-client.ts @@ -9,6 +9,7 @@ import type { ProviderListResponse, McpStatus, McpConfig, + Config, } from "./types" /** @@ -174,6 +175,27 @@ export class HttpClient { return this.request("GET", "/agent", undefined, { directory }) } + // ============================================ + // Config Methods + // ============================================ + + /** + * Get the current backend configuration. + */ + async getConfig(directory: string): Promise { + return this.request("GET", "/config", undefined, { directory }) + } + + /** + * Update backend configuration (partial merge). + * Uses the global config endpoint so changes persist to the user's global + * config file (matching the desktop app behaviour). The instance-scoped + * PATCH /config writes to a project-local file that is not loaded on restart. + */ + async updateConfig(config: Partial): Promise { + return this.request("PATCH", "/global/config", config) + } + // ============================================ // Messaging Methods // ============================================ diff --git a/packages/kilo-vscode/src/services/cli-backend/index.ts b/packages/kilo-vscode/src/services/cli-backend/index.ts index 1489987a231..8ceccad4556 100644 --- a/packages/kilo-vscode/src/services/cli-backend/index.ts +++ b/packages/kilo-vscode/src/services/cli-backend/index.ts @@ -25,6 +25,7 @@ export type { McpLocalConfig, McpRemoteConfig, McpConfig, + Config, } from "./types" export { ServerManager } from "./server-manager" diff --git a/packages/kilo-vscode/src/services/cli-backend/types.ts b/packages/kilo-vscode/src/services/cli-backend/types.ts index 93f841cb937..06c3a907558 100644 --- a/packages/kilo-vscode/src/services/cli-backend/types.ts +++ b/packages/kilo-vscode/src/services/cli-backend/types.ts @@ -225,3 +225,98 @@ export interface McpRemoteConfig { // Union of all MCP server config types export type McpConfig = McpLocalConfig | McpRemoteConfig + +// ============================================ +// Backend Config Types (from CLI server) +// ============================================ + +/** Permission level for a tool */ +export type PermissionLevel = "allow" | "ask" | "deny" + +/** Per-tool permission configuration */ +export type PermissionConfig = Partial> + +/** Per-agent configuration */ +export interface AgentConfig { + model?: string + prompt?: string + temperature?: number + top_p?: number + steps?: number + permission?: PermissionConfig +} + +/** Custom provider configuration (OpenAI-compatible) */ +export interface ProviderConfig { + name?: string + api_key?: string + base_url?: string + models?: Record +} + +/** MCP server configuration (backend config shape) */ +export interface McpServerConfig { + command?: string + args?: string[] + env?: Record + url?: string + headers?: Record +} + +/** Custom command configuration */ +export interface CommandConfig { + command: string + description?: string +} + +/** Skills configuration */ +export interface SkillsConfig { + paths?: string[] + urls?: string[] +} + +/** Compaction configuration */ +export interface CompactionConfig { + auto?: boolean + prune?: boolean +} + +/** Watcher configuration */ +export interface WatcherConfig { + ignore?: string[] +} + +/** Experimental flags */ +export interface ExperimentalConfig { + disable_paste_summary?: boolean + batch_tool?: boolean + primary_tools?: string[] + continue_loop_on_deny?: boolean + mcp_timeout?: number +} + +/** Full backend Config object (partial — all fields optional for PATCH) */ +export interface Config { + permission?: PermissionConfig + model?: string + small_model?: string + default_agent?: string + agent?: Record + provider?: Record + disabled_providers?: string[] + enabled_providers?: string[] + mcp?: Record + command?: Record + instructions?: string[] + skills?: SkillsConfig + snapshot?: boolean + share?: "manual" | "auto" | "disabled" + username?: string + watcher?: WatcherConfig + formatter?: false | Record + lsp?: false | Record + compaction?: CompactionConfig + tools?: Record + layout?: "auto" | "stretch" + experimental?: ExperimentalConfig +} diff --git a/packages/kilo-vscode/webview-ui/src/App.tsx b/packages/kilo-vscode/webview-ui/src/App.tsx index 381929b6b91..c90a9881d58 100644 --- a/packages/kilo-vscode/webview-ui/src/App.tsx +++ b/packages/kilo-vscode/webview-ui/src/App.tsx @@ -9,6 +9,7 @@ import ProfileView from "./components/ProfileView" import { VSCodeProvider } from "./context/vscode" import { ServerProvider, useServer } from "./context/server" import { ProviderProvider } from "./context/provider" +import { ConfigProvider } from "./context/config" import { SessionProvider, useSession } from "./context/session" import { LanguageProvider } from "./context/language" import { ChatView } from "./components/chat" @@ -165,11 +166,13 @@ const App: Component = () => { - - - - - + + + + + + + diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/ModelSelector.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/ModelSelector.tsx index a7b2172848a..09e7c8b64f8 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/ModelSelector.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/ModelSelector.tsx @@ -2,6 +2,9 @@ * ModelSelector component * Popover-based selector for choosing a provider/model in the chat prompt area. * Uses kilo-ui Popover component (Phase 4.5 of UI implementation plan). + * + * ModelSelectorBase — reusable core that accepts value/onSelect props. + * ModelSelector — thin wrapper wired to session context for chat usage. */ import { Component, createSignal, createMemo, createEffect, For, Show } from "solid-js" @@ -10,6 +13,7 @@ import { Button } from "@kilocode/kilo-ui/button" import { useProvider, EnrichedModel } from "../../context/provider" import { useSession } from "../../context/session" import { useLanguage } from "../../context/language" +import type { ModelSelection } from "../../types/messages" interface ModelGroup { providerName: string @@ -26,11 +30,27 @@ function providerSortKey(providerID: string): number { return idx >= 0 ? idx : PROVIDER_ORDER.length } -export const ModelSelector: Component = () => { +// --------------------------------------------------------------------------- +// Reusable base component +// --------------------------------------------------------------------------- + +export interface ModelSelectorBaseProps { + /** Current selection (null = nothing selected) */ + value: ModelSelection | null + /** Called when the user picks a model */ + onSelect: (providerID: string, modelID: string) => void + /** Popover placement — defaults to "top-start" */ + placement?: "top-start" | "bottom-start" | "bottom-end" | "top-end" + /** Allow clearing the selection (shows a "Not set" option) */ + allowClear?: boolean + /** Label shown for the clear option */ + clearLabel?: string +} + +export const ModelSelectorBase: Component = (props) => { const { connected, models, findModel } = useProvider() - const session = useSession() const language = useLanguage() - const selectedModel = () => findModel(session.selected()) + const selectedModel = () => findModel(props.value) const [open, setOpen] = createSignal(false) const [search, setSearch] = createSignal("") @@ -77,6 +97,9 @@ export const ModelSelector: Component = () => { // Flat list for keyboard indexing (mirrors render order) const flatFiltered = createMemo(() => groups().flatMap((g) => g.models)) + // Offset for "clear" option at the top of the list + const clearOffset = () => (props.allowClear ? 1 : 0) + // Reset active index when filter changes createEffect(() => { filtered() // track @@ -93,13 +116,18 @@ export const ModelSelector: Component = () => { }) function pick(model: EnrichedModel) { - session.selectModel(model.providerID, model.id) + props.onSelect(model.providerID, model.id) + setOpen(false) + } + + function pickClear() { + props.onSelect("", "") setOpen(false) } function handleKeyDown(e: KeyboardEvent) { const items = flatFiltered() - const len = items.length + const totalLen = items.length + clearOffset() if (e.key === "Escape") { e.preventDefault() @@ -107,23 +135,28 @@ export const ModelSelector: Component = () => { return } - if (len === 0) { + if (totalLen === 0) { return } if (e.key === "ArrowDown") { e.preventDefault() - setActiveIndex((i) => (i + 1) % len) + setActiveIndex((i) => (i + 1) % totalLen) scrollActiveIntoView() } else if (e.key === "ArrowUp") { e.preventDefault() - setActiveIndex((i) => (i - 1 + len) % len) + setActiveIndex((i) => (i - 1 + totalLen) % totalLen) scrollActiveIntoView() } else if (e.key === "Enter") { e.preventDefault() - const item = items[activeIndex()] - if (item) { - pick(item) + const idx = activeIndex() + if (props.allowClear && idx === 0) { + pickClear() + } else { + const item = items[idx - clearOffset()] + if (item) { + pick(item) + } } } } @@ -146,7 +179,7 @@ export const ModelSelector: Component = () => { // Track flat index across groups for active highlighting function flatIndex(model: EnrichedModel): number { - return flatFiltered().indexOf(model) + return flatFiltered().indexOf(model) + clearOffset() } const triggerLabel = () => { @@ -155,16 +188,19 @@ export const ModelSelector: Component = () => { return sel.name } // Fallback: raw selection exists but findModel didn't resolve — show raw IDs - const raw = session.selected() + const raw = props.value if (raw?.providerID && raw?.modelID) { return raw.providerID === KILO_GATEWAY_ID ? raw.modelID : `${raw.providerID} / ${raw.modelID}` } + if (props.allowClear) { + return props.clearLabel ?? "Not set" + } return hasProviders() ? language.t("dialog.model.select.title") : language.t("dialog.model.noProviders") } return ( {
- +
{language.t("dialog.model.empty")}
+ +
pickClear()} + onMouseEnter={() => setActiveIndex(0)} + > + + {props.clearLabel ?? "Not set (use server default)"} + +
+
+ {(group) => ( <> @@ -229,3 +279,18 @@ export const ModelSelector: Component = () => { ) } + +// --------------------------------------------------------------------------- +// Chat-specific wrapper (backwards-compatible default export) +// --------------------------------------------------------------------------- + +export const ModelSelector: Component = () => { + const session = useSession() + + return ( + session.selectModel(providerID, modelID)} + /> + ) +} diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx index 3d51b1dd3c7..1cb32b98089 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx @@ -1,7 +1,16 @@ -import { Component, createSignal, For } from "solid-js" -import { useLanguage } from "../../context/language" +import { Component, createSignal, createMemo, For, Show } from "solid-js" +import { Select } from "@kilocode/kilo-ui/select" +import { TextField } from "@kilocode/kilo-ui/text-field" +import { Card } from "@kilocode/kilo-ui/card" +import { Button } from "@kilocode/kilo-ui/button" +import { IconButton } from "@kilocode/kilo-ui/icon-button" -type SubtabId = "modes" | "mcpServers" | "rules" | "workflows" | "skills" +import { useConfig } from "../../context/config" +import { useSession } from "../../context/session" +import { useLanguage } from "../../context/language" +import type { AgentConfig } from "../../types/messages" + +type SubtabId = "agents" | "mcpServers" | "rules" | "workflows" | "skills" interface SubtabConfig { id: SubtabId @@ -9,43 +18,528 @@ interface SubtabConfig { } const subtabs: SubtabConfig[] = [ - { id: "modes", labelKey: "settings.agentBehaviour.subtab.modes" }, + { id: "agents", labelKey: "settings.agentBehaviour.subtab.agents" }, { id: "mcpServers", labelKey: "settings.agentBehaviour.subtab.mcpServers" }, { id: "rules", labelKey: "settings.agentBehaviour.subtab.rules" }, { id: "workflows", labelKey: "settings.agentBehaviour.subtab.workflows" }, { id: "skills", labelKey: "settings.agentBehaviour.subtab.skills" }, ] +interface SelectOption { + value: string + label: string +} + +interface SettingRowProps { + label: string + description: string + last?: boolean + children: any +} + +const SettingRow: Component = (props) => ( +
+
+
{props.label}
+
+ {props.description} +
+
+ {props.children} +
+) + +const Placeholder: Component<{ text: string }> = (props) => ( + +

+ Not yet implemented. {props.text} +

+
+) + const AgentBehaviourTab: Component = () => { const language = useLanguage() - const [activeSubtab, setActiveSubtab] = createSignal("modes") + const { config, updateConfig } = useConfig() + const session = useSession() + const [activeSubtab, setActiveSubtab] = createSignal("agents") + const [selectedAgent, setSelectedAgent] = createSignal("") + const [newSkillPath, setNewSkillPath] = createSignal("") + const [newSkillUrl, setNewSkillUrl] = createSignal("") + const [newInstruction, setNewInstruction] = createSignal("") + + const agentNames = createMemo(() => { + const names = session.agents().map((a) => a.name) + // Also include any agents from config that might not be in the agent list + const configAgents = Object.keys(config().agent ?? {}) + for (const name of configAgents) { + if (!names.includes(name)) { + names.push(name) + } + } + return names.sort() + }) + + const defaultAgentOptions = createMemo(() => [ + { value: "", label: "Default" }, + ...agentNames().map((name) => ({ value: name, label: name })), + ]) + + const agentSelectorOptions = createMemo(() => [ + { value: "", label: "Select an agent to configure…" }, + ...agentNames().map((name) => ({ value: name, label: name })), + ]) + + const currentAgentConfig = createMemo(() => { + const name = selectedAgent() + if (!name) { + return {} + } + return config().agent?.[name] ?? {} + }) + + const updateAgentConfig = (name: string, partial: Partial) => { + const existing = config().agent ?? {} + const current = existing[name] ?? {} + updateConfig({ + agent: { + ...existing, + [name]: { ...current, ...partial }, + }, + }) + } + + const instructions = () => config().instructions ?? [] + + const addInstruction = () => { + const value = newInstruction().trim() + if (!value) { + return + } + const current = [...instructions()] + if (!current.includes(value)) { + current.push(value) + updateConfig({ instructions: current }) + } + setNewInstruction("") + } + + const removeInstruction = (index: number) => { + const current = [...instructions()] + current.splice(index, 1) + updateConfig({ instructions: current }) + } + + const skillPaths = () => config().skills?.paths ?? [] + const skillUrls = () => config().skills?.urls ?? [] + + const addSkillPath = () => { + const value = newSkillPath().trim() + if (!value) { + return + } + const current = [...skillPaths()] + if (!current.includes(value)) { + current.push(value) + updateConfig({ skills: { ...config().skills, paths: current } }) + } + setNewSkillPath("") + } + + const removeSkillPath = (index: number) => { + const current = [...skillPaths()] + current.splice(index, 1) + updateConfig({ skills: { ...config().skills, paths: current } }) + } + + const addSkillUrl = () => { + const value = newSkillUrl().trim() + if (!value) { + return + } + const current = [...skillUrls()] + if (!current.includes(value)) { + current.push(value) + updateConfig({ skills: { ...config().skills, urls: current } }) + } + setNewSkillUrl("") + } + + const removeSkillUrl = (index: number) => { + const current = [...skillUrls()] + current.splice(index, 1) + updateConfig({ skills: { ...config().skills, urls: current } }) + } + + const renderAgentsSubtab = () => ( +
+ {/* Default agent */} + + + o.value === selectedAgent())} + value={(o) => o.value} + label={(o) => o.label} + onSelect={(o) => o && setSelectedAgent(o.value)} + variant="secondary" + size="small" + triggerVariant="settings" + /> +
+ + + + {/* Model override */} + + + updateAgentConfig(selectedAgent(), { + model: val.trim() || undefined, + }) + } + /> + + + {/* System prompt */} + + + updateAgentConfig(selectedAgent(), { + prompt: val.trim() || undefined, + }) + } + /> + + + {/* Temperature */} + + { + const parsed = parseFloat(val) + updateAgentConfig(selectedAgent(), { temperature: isNaN(parsed) ? undefined : parsed }) + }} + /> + + + {/* Top-p */} + + { + const parsed = parseFloat(val) + updateAgentConfig(selectedAgent(), { top_p: isNaN(parsed) ? undefined : parsed }) + }} + /> + + + {/* Max steps */} + + { + const parsed = parseInt(val, 10) + updateAgentConfig(selectedAgent(), { steps: isNaN(parsed) ? undefined : parsed }) + }} + /> + + + +
+ ) + + const renderMcpSubtab = () => { + const mcpEntries = createMemo(() => Object.entries(config().mcp ?? {})) - const renderSubtabContent = () => { return ( -
-

+ 0} + fallback={ + +

+ No MCP servers configured. Edit the opencode config file to add MCP servers. +
+ + } > - This section is not implemented yet. It will - contain configuration options for {activeSubtab()}. During reimplementation, use this space to validate - layout, spacing, scrolling behavior, and navigation state before wiring up real controls. -

+ + + {([name, mcp], index) => ( +
+
{name}
+
+ +
+ command: {mcp.command} {(mcp.args ?? []).join(" ")} +
+
+ +
url: {mcp.url}
+
+
+
+ )} +
+
+
) } + const renderSkillsSubtab = () => ( +
+ {/* Skill paths */} +

Skill Folder Paths

+ +
0 ? "1px solid var(--border-weak-base)" : "none", + }} + > +
+ setNewSkillPath(val)} + onKeyDown={(e: KeyboardEvent) => { + if (e.key === "Enter") addSkillPath() + }} + /> +
+ +
+ + {(path, index) => ( +
+ + {path} + + removeSkillPath(index())} /> +
+ )} +
+
+ + {/* Skill URLs */} +

Skill URLs

+ +
0 ? "1px solid var(--border-weak-base)" : "none", + }} + > +
+ setNewSkillUrl(val)} + onKeyDown={(e: KeyboardEvent) => { + if (e.key === "Enter") addSkillUrl() + }} + /> +
+ +
+ + {(url, index) => ( +
+ + {url} + + removeSkillUrl(index())} /> +
+ )} +
+
+
+ ) + + const renderRulesSubtab = () => ( +
+ +
+
Additional Instruction Files
+
+ Paths to additional instruction files that are included in the system prompt +
+
+ + {/* Add new instruction path */} +
0 ? "1px solid var(--border-weak-base)" : "none", + }} + > +
+ setNewInstruction(val)} + onKeyDown={(e: KeyboardEvent) => { + if (e.key === "Enter") addInstruction() + }} + /> +
+ +
+ + {/* Instructions list */} + + {(path, index) => ( +
+ + {path} + + removeInstruction(index())} /> +
+ )} +
+
+
+ ) + + const renderSubtabContent = () => { + switch (activeSubtab()) { + case "agents": + return renderAgentsSubtab() + case "mcpServers": + return renderMcpSubtab() + case "rules": + return renderRulesSubtab() + case "workflows": + return + case "skills": + return renderSkillsSubtab() + default: + return null + } + } + return (
{/* Horizontal subtab bar */} diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/AutoApproveTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/AutoApproveTab.tsx index ef989fcf327..5b19ab95f92 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/AutoApproveTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/AutoApproveTab.tsx @@ -1,30 +1,152 @@ -import { Component } from "solid-js" +import { Component, For, createMemo } from "solid-js" +import { Select } from "@kilocode/kilo-ui/select" +import { Card } from "@kilocode/kilo-ui/card" +import { useConfig } from "../../context/config" +import type { PermissionLevel } from "../../types/messages" + +const TOOLS = [ + "read", + "edit", + "glob", + "grep", + "list", + "bash", + "task", + "skill", + "lsp", + "todoread", + "todowrite", + "webfetch", + "websearch", + "codesearch", + "external_directory", + "doom_loop", +] as const + +interface LevelOption { + value: PermissionLevel + label: string +} + +const LEVEL_OPTIONS: LevelOption[] = [ + { value: "allow", label: "Allow" }, + { value: "ask", label: "Ask" }, + { value: "deny", label: "Deny" }, +] + +const TOOL_DESCRIPTIONS: Record = { + read: "Read file contents", + edit: "Edit or create files", + glob: "Find files by pattern", + grep: "Search file contents", + list: "List directory contents", + bash: "Execute shell commands", + task: "Create sub-agent tasks", + skill: "Execute skills", + lsp: "Language server operations", + todoread: "Read todo lists", + todowrite: "Write todo lists", + webfetch: "Fetch web pages", + websearch: "Search the web", + codesearch: "Search codebase", + external_directory: "Access files outside workspace", + doom_loop: "Continue after repeated failures", +} const AutoApproveTab: Component = () => { + const { config, updateConfig } = useConfig() + + const permissions = createMemo(() => config().permission ?? {}) + + const getLevel = (tool: string): PermissionLevel => { + return permissions()[tool] ?? permissions()["*"] ?? "ask" + } + + const setPermission = (tool: string, level: PermissionLevel) => { + updateConfig({ + permission: { ...permissions(), [tool]: level }, + }) + } + + const setAll = (level: PermissionLevel) => { + const updated: Record = {} + for (const tool of TOOLS) { + updated[tool] = level + } + updateConfig({ permission: updated }) + } + return ( -
-
-

+ {/* Set All control */} + +

- This section is not implemented yet. It will - contain configuration options and explanatory text related to the selected settings category. During - reimplementation, use this space to validate layout, spacing, scrolling behavior, and navigation state before - wiring up real controls. -

-
+ Set all permissions + o.value === getLevel(tool))} + value={(o) => o.value} + label={(o) => o.label} + onSelect={(option) => option && setPermission(tool, option.value)} + variant="secondary" + size="small" + triggerVariant="settings" + /> +
+ )} + +
) } diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/CheckpointsTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/CheckpointsTab.tsx index fc4fc1b96ef..d5ee8c4a065 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/CheckpointsTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/CheckpointsTab.tsx @@ -1,30 +1,49 @@ import { Component } from "solid-js" +import { Switch } from "@kilocode/kilo-ui/switch" +import { Card } from "@kilocode/kilo-ui/card" +import { useConfig } from "../../context/config" + +const SettingsRow: Component<{ label: string; description: string; last?: boolean; children: any }> = (props) => ( +
+
+
{props.label}
+
+ {props.description} +
+
+ {props.children} +
+) const CheckpointsTab: Component = () => { + const { config, updateConfig } = useConfig() + return (
-
-

+ - This section is not implemented yet. It will - contain configuration options and explanatory text related to the selected settings category. During - reimplementation, use this space to validate layout, spacing, scrolling behavior, and navigation state before - wiring up real controls. -

-
+ updateConfig({ snapshot: checked })} + hideLabel + > + Enable Snapshots + + +
) } diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/ContextTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/ContextTab.tsx index 17f7dad22d0..8392be367c7 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/ContextTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/ContextTab.tsx @@ -1,30 +1,144 @@ -import { Component } from "solid-js" +import { Component, For, createSignal } from "solid-js" +import { Switch } from "@kilocode/kilo-ui/switch" +import { TextField } from "@kilocode/kilo-ui/text-field" +import { Card } from "@kilocode/kilo-ui/card" +import { Button } from "@kilocode/kilo-ui/button" +import { IconButton } from "@kilocode/kilo-ui/icon-button" + +import { useConfig } from "../../context/config" + +const SettingsRow: Component<{ label: string; description: string; last?: boolean; children: any }> = (props) => ( +
+
+
{props.label}
+
+ {props.description} +
+
+ {props.children} +
+) const ContextTab: Component = () => { + const { config, updateConfig } = useConfig() + const [newPattern, setNewPattern] = createSignal("") + + const patterns = () => config().watcher?.ignore ?? [] + + const addPattern = () => { + const value = newPattern().trim() + if (!value) return + const current = [...patterns()] + if (!current.includes(value)) { + current.push(value) + updateConfig({ watcher: { ignore: current } }) + } + setNewPattern("") + } + + const removePattern = (index: number) => { + const current = [...patterns()] + current.splice(index, 1) + updateConfig({ watcher: { ignore: current } }) + } + return (
-
-

+ + updateConfig({ compaction: { ...config().compaction, auto: checked } })} + hideLabel + > + Auto Compaction + + + + updateConfig({ compaction: { ...config().compaction, prune: checked } })} + hideLabel + > + Prune Old Outputs + + + + +

File Watcher Ignore Patterns

+ + +
0 || newPattern() ? "1px solid var(--border-weak-base)" : "none", }} > - This section is not implemented yet. It will - contain configuration options and explanatory text related to the selected settings category. During - reimplementation, use this space to validate layout, spacing, scrolling behavior, and navigation state before - wiring up real controls. -

-
+ Glob patterns for files the watcher should ignore +
+ + {/* Add new pattern */} +
0 ? "1px solid var(--border-weak-base)" : "none", + }} + > +
+ setNewPattern(val)} + onKeyDown={(e: KeyboardEvent) => { + if (e.key === "Enter") addPattern() + }} + /> +
+ +
+ + {/* Pattern list */} + + {(pattern, index) => ( +
+ + {pattern} + + removePattern(index())} /> +
+ )} +
+
) } diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/DisplayTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/DisplayTab.tsx index 997213b66c4..dcbdb070899 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/DisplayTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/DisplayTab.tsx @@ -1,30 +1,87 @@ import { Component } from "solid-js" +import { Select } from "@kilocode/kilo-ui/select" +import { TextField } from "@kilocode/kilo-ui/text-field" +import { Card } from "@kilocode/kilo-ui/card" +import { useConfig } from "../../context/config" + +interface LayoutOption { + value: string + label: string +} + +const LAYOUT_OPTIONS: LayoutOption[] = [ + { value: "auto", label: "Auto" }, + { value: "stretch", label: "Stretch" }, +] const DisplayTab: Component = () => { + const { config, updateConfig } = useConfig() + return (
-
-

+

- This section is not implemented yet. It will - contain configuration options and explanatory text related to the selected settings category. During - reimplementation, use this space to validate layout, spacing, scrolling behavior, and navigation state before - wiring up real controls. -

-
+
+
Username
+
+ Custom username displayed in conversations +
+
+
+ updateConfig({ username: val.trim() || undefined })} + /> +
+
+ +
+
+
Layout
+
+ Layout mode for the chat interface +
+
+ o.value === (config().share ?? "manual"))} + value={(o) => o.value} + label={(o) => o.label} + onSelect={(o) => o && updateConfig({ share: o.value as "manual" | "auto" | "disabled" })} + variant="secondary" + size="small" + triggerVariant="settings" + /> + + + + updateConfig({ formatter: checked ? {} : false })} + hideLabel + > + Formatter + + + + + updateConfig({ lsp: checked ? {} : false })} + hideLabel + > + LSP + + + + + updateExperimental("disable_paste_summary", checked)} + hideLabel + > + Disable Paste Summary + + + + + updateExperimental("batch_tool", checked)} + hideLabel + > + Batch Tool + + + + + updateExperimental("continue_loop_on_deny", checked)} + hideLabel + > + Continue on Deny + + + + {/* MCP timeout */} + + { + const num = parseInt(val, 10) + if (!isNaN(num) && num > 0) { + updateExperimental("mcp_timeout", num) + } + }} + /> + + + + {/* Tool toggles */} + 0}> +

Tool Toggles

+ + + {([name, enabled], index) => ( + = Object.keys(config().tools ?? {}).length - 1}> + updateConfig({ tools: { ...config().tools, [name]: checked } })} + hideLabel + > + {name} + + + )} + + +
) } diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/NotificationsTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/NotificationsTab.tsx index b6057ef0118..bca2a4186d0 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/NotificationsTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/NotificationsTab.tsx @@ -1,30 +1,171 @@ -import { Component } from "solid-js" +import { Component, createSignal, onCleanup } from "solid-js" +import { Switch } from "@kilocode/kilo-ui/switch" +import { Select } from "@kilocode/kilo-ui/select" +import { Card } from "@kilocode/kilo-ui/card" +import { useVSCode } from "../../context/vscode" +import type { ExtensionMessage } from "../../types/messages" + +interface SoundOption { + value: string + label: string +} + +const SOUND_OPTIONS: SoundOption[] = [ + { value: "default", label: "Default" }, + { value: "none", label: "None" }, +] const NotificationsTab: Component = () => { - return ( -
-
-

{ + if (message.type !== "notificationSettingsLoaded") { + return + } + const s = message.settings + setAgentNotify(s.notifyAgent) + setPermNotify(s.notifyPermissions) + setErrorNotify(s.notifyErrors) + setAgentSound(s.soundAgent) + setPermSound(s.soundPermissions) + setErrorSound(s.soundErrors) + }) + + onCleanup(unsubscribe) + vscode.postMessage({ type: "requestNotificationSettings" }) + + const save = (key: string, value: unknown) => { + vscode.postMessage({ type: "updateSetting", key, value }) + } + + const SettingsRow: Component<{ label: string; description: string; last?: boolean; children: any }> = (props) => ( +

+
+
{props.label}
+
- This section is not implemented yet. It will - contain configuration options and explanatory text related to the selected settings category. During - reimplementation, use this space to validate layout, spacing, scrolling behavior, and navigation state before - wiring up real controls. -

+ {props.description} +
+ {props.children} +
+ ) + + return ( +
+ + + { + setAgentNotify(checked) + save("notifications.agent", checked) + }} + hideLabel + > + Agent Completion + + + + { + setPermNotify(checked) + save("notifications.permissions", checked) + }} + hideLabel + > + Permission Requests + + + + { + setErrorNotify(checked) + save("notifications.errors", checked) + }} + hideLabel + > + Errors + + + + +

Sounds

+ + + o.value === permSound())} + value={(o) => o.value} + label={(o) => o.label} + onSelect={(o) => { + if (o) { + setPermSound(o.value) + save("sounds.permissions", o.value) + } + }} + variant="secondary" + size="small" + triggerVariant="settings" + /> + + + !disabledProviders().includes(o.value))} + current={newDisabled()} + value={(o) => o.value} + label={(o) => o.label} + onSelect={(o) => setNewDisabled(o)} + variant="secondary" + size="small" + triggerVariant="settings" + placeholder="Select provider…" + /> +
+ +
+ + {(id, index) => ( +
+ {id} + removeFromList("disabled_providers", index())} + /> +
+ )} +
+ + + {/* Enabled providers (allowlist) */} +

Enabled Providers (Allowlist)

+ +
+ If set, only these providers will be available (exclusive allowlist) +
+
0 ? "1px solid var(--border-weak-base)" : "none", + }} + > +
+