Merge pull request #272 from Kilo-Org/mark/migrate-settings-plan

feat: implement settings migration from desktop app
This commit is contained in:
Mark IJbema
2026-02-12 15:28:16 +01:00
committed by GitHub
34 changed files with 2263 additions and 192 deletions
@@ -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.<tool>` | `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<string, AgentConfig>` | **Agent Behaviour** | Per-agent config (model, prompt, temperature, etc.) |
| `agent.<name>.model` | `string` | **Agent Behaviour** (Agents subtab) | Override model per agent |
| `agent.<name>.prompt` | `string` | **Agent Behaviour** (Agents subtab) | Custom system prompt per agent |
| `agent.<name>.temperature` | `number` | **Agent Behaviour** (Agents subtab) | Temperature override |
| `agent.<name>.top_p` | `number` | **Agent Behaviour** (Agents subtab) | Top-p override |
| `agent.<name>.steps` | `number` | **Agent Behaviour** (Agents subtab) | Max agentic iterations |
| `agent.<name>.permission` | `PermissionConfig` | **Agent Behaviour** (Agents subtab) | Per-agent permission overrides |
| `provider` | `Record<string, ProviderConfig>` | **Providers** | Custom provider configuration |
| `disabled_providers` | `string[]` | **Providers** | Providers to disable |
| `enabled_providers` | `string[]` | **Providers** | Exclusive provider allowlist |
| `mcp` | `Record<string, McpConfig>` | **Agent Behaviour** (MCP subtab) | MCP server configurations |
| `command` | `Record<string, CommandConfig>` | **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<string, boolean>` | **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<Config> {
return this.request<Config>("GET", "/config", undefined, { directory })
}
```
2. **Add `updateConfig()` to [`HttpClient`](src/services/cli-backend/http-client.ts)**
```typescript
async updateConfig(config: Partial<Config>, directory: string): Promise<Config> {
return this.request<Config>("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.<name>.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
+42
View File
@@ -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"
}
}
}
+87
View File
@@ -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<string> = 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<void> {
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<boolean>("agent", true),
notifyPermissions: notifications.get<boolean>("permissions", true),
notifyErrors: notifications.get<boolean>("errors", true),
soundAgent: sounds.get<string>("agent", "default"),
soundPermissions: sounds.get<string>("permissions", "default"),
soundErrors: sounds.get<string>("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<string, unknown>): Promise<void> {
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.
*/
@@ -9,6 +9,7 @@ import type {
ProviderListResponse,
McpStatus,
McpConfig,
Config,
} from "./types"
/**
@@ -174,6 +175,27 @@ export class HttpClient {
return this.request<AgentInfo[]>("GET", "/agent", undefined, { directory })
}
// ============================================
// Config Methods
// ============================================
/**
* Get the current backend configuration.
*/
async getConfig(directory: string): Promise<Config> {
return this.request<Config>("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<Config>): Promise<Config> {
return this.request<Config>("PATCH", "/global/config", config)
}
// ============================================
// Messaging Methods
// ============================================
@@ -25,6 +25,7 @@ export type {
McpLocalConfig,
McpRemoteConfig,
McpConfig,
Config,
} from "./types"
export { ServerManager } from "./server-manager"
@@ -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<Record<string, PermissionLevel>>
/** 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<string, unknown>
}
/** MCP server configuration (backend config shape) */
export interface McpServerConfig {
command?: string
args?: string[]
env?: Record<string, string>
url?: string
headers?: Record<string, string>
}
/** 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<string, AgentConfig>
provider?: Record<string, ProviderConfig>
disabled_providers?: string[]
enabled_providers?: string[]
mcp?: Record<string, McpServerConfig>
command?: Record<string, CommandConfig>
instructions?: string[]
skills?: SkillsConfig
snapshot?: boolean
share?: "manual" | "auto" | "disabled"
username?: string
watcher?: WatcherConfig
formatter?: false | Record<string, unknown>
lsp?: false | Record<string, unknown>
compaction?: CompactionConfig
tools?: Record<string, boolean>
layout?: "auto" | "stretch"
experimental?: ExperimentalConfig
}
+8 -5
View File
@@ -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 = () => {
<LanguageBridge>
<MarkedProvider>
<ProviderProvider>
<SessionProvider>
<DataBridge>
<AppContent />
</DataBridge>
</SessionProvider>
<ConfigProvider>
<SessionProvider>
<DataBridge>
<AppContent />
</DataBridge>
</SessionProvider>
</ConfigProvider>
</ProviderProvider>
</MarkedProvider>
</LanguageBridge>
@@ -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<ModelSelectorBaseProps> = (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 (
<Popover
placement="top-start"
placement={props.placement ?? "top-start"}
open={open()}
onOpenChange={setOpen}
triggerAs={Button}
@@ -197,10 +233,24 @@ export const ModelSelector: Component = () => {
</div>
<div class="model-selector-list" role="listbox" ref={listRef}>
<Show when={flatFiltered().length === 0}>
<Show when={flatFiltered().length === 0 && !props.allowClear}>
<div class="model-selector-empty">{language.t("dialog.model.empty")}</div>
</Show>
<Show when={props.allowClear}>
<div
class={`model-selector-item${activeIndex() === 0 ? " active" : ""}${!props.value?.providerID ? " selected" : ""}`}
role="option"
aria-selected={!props.value?.providerID}
onClick={() => pickClear()}
onMouseEnter={() => setActiveIndex(0)}
>
<span class="model-selector-item-name" style={{ "font-style": "italic", opacity: 0.7 }}>
{props.clearLabel ?? "Not set (use server default)"}
</span>
</div>
</Show>
<For each={groups()}>
{(group) => (
<>
@@ -229,3 +279,18 @@ export const ModelSelector: Component = () => {
</Popover>
)
}
// ---------------------------------------------------------------------------
// Chat-specific wrapper (backwards-compatible default export)
// ---------------------------------------------------------------------------
export const ModelSelector: Component = () => {
const session = useSession()
return (
<ModelSelectorBase
value={session.selected()}
onSelect={(providerID, modelID) => session.selectModel(providerID, modelID)}
/>
)
}
@@ -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<SettingRowProps> = (props) => (
<div
data-slot="settings-row"
style={{
display: "flex",
"align-items": "center",
"justify-content": "space-between",
padding: "8px 0",
"border-bottom": props.last ? "none" : "1px solid var(--border-weak-base)",
}}
>
<div style={{ flex: 1, "min-width": 0, "margin-right": "12px" }}>
<div style={{ "font-weight": "500" }}>{props.label}</div>
<div style={{ "font-size": "11px", color: "var(--text-weak-base, var(--vscode-descriptionForeground))" }}>
{props.description}
</div>
</div>
{props.children}
</div>
)
const Placeholder: Component<{ text: string }> = (props) => (
<Card>
<p
style={{
"font-size": "12px",
color: "var(--text-weak-base, var(--vscode-descriptionForeground))",
margin: 0,
"line-height": "1.5",
}}
>
<strong>Not yet implemented.</strong> {props.text}
</p>
</Card>
)
const AgentBehaviourTab: Component = () => {
const language = useLanguage()
const [activeSubtab, setActiveSubtab] = createSignal<SubtabId>("modes")
const { config, updateConfig } = useConfig()
const session = useSession()
const [activeSubtab, setActiveSubtab] = createSignal<SubtabId>("agents")
const [selectedAgent, setSelectedAgent] = createSignal<string>("")
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<SelectOption[]>(() => [
{ value: "", label: "Default" },
...agentNames().map((name) => ({ value: name, label: name })),
])
const agentSelectorOptions = createMemo<SelectOption[]>(() => [
{ value: "", label: "Select an agent to configure…" },
...agentNames().map((name) => ({ value: name, label: name })),
])
const currentAgentConfig = createMemo<AgentConfig>(() => {
const name = selectedAgent()
if (!name) {
return {}
}
return config().agent?.[name] ?? {}
})
const updateAgentConfig = (name: string, partial: Partial<AgentConfig>) => {
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 = () => (
<div>
{/* Default agent */}
<Card style={{ "margin-bottom": "12px" }}>
<SettingRow label="Default Agent" description="Agent to use when none is specified" last>
<Select
options={defaultAgentOptions()}
current={defaultAgentOptions().find((o) => o.value === (config().default_agent ?? ""))}
value={(o) => o.value}
label={(o) => o.label}
onSelect={(o) => o && updateConfig({ default_agent: o.value || undefined })}
variant="secondary"
size="small"
triggerVariant="settings"
/>
</SettingRow>
</Card>
{/* Agent selector */}
<div style={{ "margin-bottom": "12px" }}>
<Select
options={agentSelectorOptions()}
current={agentSelectorOptions().find((o) => o.value === selectedAgent())}
value={(o) => o.value}
label={(o) => o.label}
onSelect={(o) => o && setSelectedAgent(o.value)}
variant="secondary"
size="small"
triggerVariant="settings"
/>
</div>
<Show when={selectedAgent()}>
<Card>
{/* Model override */}
<SettingRow label="Model Override" description="Override the default model for this agent">
<TextField
value={currentAgentConfig().model ?? ""}
placeholder="e.g. anthropic/claude-sonnet-4-20250514"
onChange={(val) =>
updateAgentConfig(selectedAgent(), {
model: val.trim() || undefined,
})
}
/>
</SettingRow>
{/* System prompt */}
<SettingRow label="Custom Prompt" description="Additional system prompt for this agent">
<TextField
value={currentAgentConfig().prompt ?? ""}
placeholder="Custom instructions…"
multiline
onChange={(val) =>
updateAgentConfig(selectedAgent(), {
prompt: val.trim() || undefined,
})
}
/>
</SettingRow>
{/* Temperature */}
<SettingRow label="Temperature" description="Sampling temperature (0-2)">
<TextField
value={currentAgentConfig().temperature?.toString() ?? ""}
placeholder="Default"
onChange={(val) => {
const parsed = parseFloat(val)
updateAgentConfig(selectedAgent(), { temperature: isNaN(parsed) ? undefined : parsed })
}}
/>
</SettingRow>
{/* Top-p */}
<SettingRow label="Top P" description="Nucleus sampling parameter (0-1)">
<TextField
value={currentAgentConfig().top_p?.toString() ?? ""}
placeholder="Default"
onChange={(val) => {
const parsed = parseFloat(val)
updateAgentConfig(selectedAgent(), { top_p: isNaN(parsed) ? undefined : parsed })
}}
/>
</SettingRow>
{/* Max steps */}
<SettingRow label="Max Steps" description="Maximum agentic iterations" last>
<TextField
value={currentAgentConfig().steps?.toString() ?? ""}
placeholder="Default"
onChange={(val) => {
const parsed = parseInt(val, 10)
updateAgentConfig(selectedAgent(), { steps: isNaN(parsed) ? undefined : parsed })
}}
/>
</SettingRow>
</Card>
</Show>
</div>
)
const renderMcpSubtab = () => {
const mcpEntries = createMemo(() => Object.entries(config().mcp ?? {}))
const renderSubtabContent = () => {
return (
<div
style={{
background: "var(--vscode-editor-background)",
border: "1px solid var(--vscode-panel-border)",
"border-radius": "4px",
padding: "16px",
}}
>
<p
style={{
"font-size": "12px",
color: "var(--vscode-descriptionForeground)",
margin: 0,
"line-height": "1.5",
}}
<div>
<Show
when={mcpEntries().length > 0}
fallback={
<Card>
<div
style={{
"font-size": "12px",
color: "var(--text-weak-base, var(--vscode-descriptionForeground))",
}}
>
No MCP servers configured. Edit the opencode config file to add MCP servers.
</div>
</Card>
}
>
<strong style={{ color: "var(--vscode-foreground)" }}>This section is not implemented yet.</strong> 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.
</p>
<Card>
<For each={mcpEntries()}>
{([name, mcp], index) => (
<div
style={{
padding: "8px 0",
"border-bottom": index() < mcpEntries().length - 1 ? "1px solid var(--border-weak-base)" : "none",
}}
>
<div style={{ "font-weight": "500" }}>{name}</div>
<div
style={{
"font-size": "11px",
color: "var(--text-weak-base, var(--vscode-descriptionForeground))",
"margin-top": "4px",
"font-family": "var(--vscode-editor-font-family, monospace)",
}}
>
<Show when={mcp.command}>
<div>
command: {mcp.command} {(mcp.args ?? []).join(" ")}
</div>
</Show>
<Show when={mcp.url}>
<div>url: {mcp.url}</div>
</Show>
</div>
</div>
)}
</For>
</Card>
</Show>
</div>
)
}
const renderSkillsSubtab = () => (
<div>
{/* Skill paths */}
<h4 style={{ "margin-top": "0", "margin-bottom": "8px" }}>Skill Folder Paths</h4>
<Card style={{ "margin-bottom": "16px" }}>
<div
style={{
display: "flex",
gap: "8px",
"align-items": "center",
padding: "8px 0",
"border-bottom": skillPaths().length > 0 ? "1px solid var(--border-weak-base)" : "none",
}}
>
<div style={{ flex: 1 }}>
<TextField
value={newSkillPath()}
placeholder="e.g. ./skills"
onChange={(val) => setNewSkillPath(val)}
onKeyDown={(e: KeyboardEvent) => {
if (e.key === "Enter") addSkillPath()
}}
/>
</div>
<Button size="small" onClick={addSkillPath}>
Add
</Button>
</div>
<For each={skillPaths()}>
{(path, index) => (
<div
style={{
display: "flex",
"align-items": "center",
"justify-content": "space-between",
padding: "6px 0",
"border-bottom": index() < skillPaths().length - 1 ? "1px solid var(--border-weak-base)" : "none",
}}
>
<span
style={{
"font-family": "var(--vscode-editor-font-family, monospace)",
"font-size": "12px",
}}
>
{path}
</span>
<IconButton size="small" variant="ghost" icon="close" onClick={() => removeSkillPath(index())} />
</div>
)}
</For>
</Card>
{/* Skill URLs */}
<h4 style={{ "margin-top": "0", "margin-bottom": "8px" }}>Skill URLs</h4>
<Card>
<div
style={{
display: "flex",
gap: "8px",
"align-items": "center",
padding: "8px 0",
"border-bottom": skillUrls().length > 0 ? "1px solid var(--border-weak-base)" : "none",
}}
>
<div style={{ flex: 1 }}>
<TextField
value={newSkillUrl()}
placeholder="e.g. https://example.com/skills"
onChange={(val) => setNewSkillUrl(val)}
onKeyDown={(e: KeyboardEvent) => {
if (e.key === "Enter") addSkillUrl()
}}
/>
</div>
<Button size="small" onClick={addSkillUrl}>
Add
</Button>
</div>
<For each={skillUrls()}>
{(url, index) => (
<div
style={{
display: "flex",
"align-items": "center",
"justify-content": "space-between",
padding: "6px 0",
"border-bottom": index() < skillUrls().length - 1 ? "1px solid var(--border-weak-base)" : "none",
}}
>
<span
style={{
"font-family": "var(--vscode-editor-font-family, monospace)",
"font-size": "12px",
}}
>
{url}
</span>
<IconButton size="small" variant="ghost" icon="close" onClick={() => removeSkillUrl(index())} />
</div>
)}
</For>
</Card>
</div>
)
const renderRulesSubtab = () => (
<div>
<Card>
<div
style={{
"padding-bottom": "8px",
"border-bottom": "1px solid var(--border-weak-base)",
}}
>
<div style={{ "font-weight": "500" }}>Additional Instruction Files</div>
<div
style={{
"font-size": "11px",
color: "var(--text-weak-base, var(--vscode-descriptionForeground))",
"margin-top": "2px",
}}
>
Paths to additional instruction files that are included in the system prompt
</div>
</div>
{/* Add new instruction path */}
<div
style={{
display: "flex",
gap: "8px",
"align-items": "center",
padding: "8px 0",
"border-bottom": instructions().length > 0 ? "1px solid var(--border-weak-base)" : "none",
}}
>
<div style={{ flex: 1 }}>
<TextField
value={newInstruction()}
placeholder="e.g. ./INSTRUCTIONS.md"
onChange={(val) => setNewInstruction(val)}
onKeyDown={(e: KeyboardEvent) => {
if (e.key === "Enter") addInstruction()
}}
/>
</div>
<Button size="small" onClick={addInstruction}>
Add
</Button>
</div>
{/* Instructions list */}
<For each={instructions()}>
{(path, index) => (
<div
style={{
display: "flex",
"align-items": "center",
"justify-content": "space-between",
padding: "6px 0",
"border-bottom": index() < instructions().length - 1 ? "1px solid var(--border-weak-base)" : "none",
}}
>
<span
style={{
"font-family": "var(--vscode-editor-font-family, monospace)",
"font-size": "12px",
}}
>
{path}
</span>
<IconButton size="small" variant="ghost" icon="close" onClick={() => removeInstruction(index())} />
</div>
)}
</For>
</Card>
</div>
)
const renderSubtabContent = () => {
switch (activeSubtab()) {
case "agents":
return renderAgentsSubtab()
case "mcpServers":
return renderMcpSubtab()
case "rules":
return renderRulesSubtab()
case "workflows":
return <Placeholder text="Workflows are managed via workflow files in your workspace." />
case "skills":
return renderSkillsSubtab()
default:
return null
}
}
return (
<div>
{/* Horizontal subtab bar */}
@@ -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<string, string> = {
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<string, PermissionLevel> = {}
for (const tool of TOOLS) {
updated[tool] = level
}
updateConfig({ permission: updated })
}
return (
<div>
<div
style={{
background: "var(--vscode-editor-background)",
border: "1px solid var(--vscode-panel-border)",
"border-radius": "4px",
padding: "16px",
}}
>
<p
style={{
"font-size": "12px",
color: "var(--vscode-descriptionForeground)",
margin: 0,
"line-height": "1.5",
}}
<div data-component="auto-approve-settings">
{/* Set All control */}
<Card>
<div
data-slot="settings-row"
style={{ display: "flex", "align-items": "center", "justify-content": "space-between", padding: "8px 0" }}
>
<strong style={{ color: "var(--vscode-foreground)" }}>This section is not implemented yet.</strong> 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.
</p>
</div>
<span style={{ "font-weight": "600" }}>Set all permissions</span>
<Select
options={LEVEL_OPTIONS}
value={(o) => o.value}
label={(o) => o.label}
onSelect={(option) => option && setAll(option.value)}
variant="secondary"
size="small"
triggerVariant="settings"
placeholder="Choose…"
/>
</div>
</Card>
<div style={{ "margin-top": "12px" }} />
{/* Tool permission list */}
<Card>
<For each={[...TOOLS]}>
{(tool, index) => (
<div
data-slot="settings-row"
style={{
display: "flex",
"align-items": "center",
"justify-content": "space-between",
padding: "8px 0",
"border-bottom": index() < TOOLS.length - 1 ? "1px solid var(--border-weak-base)" : "none",
}}
>
<div style={{ flex: 1, "min-width": 0 }}>
<div
style={{
"font-family": "var(--vscode-editor-font-family, monospace)",
"font-size": "12px",
}}
>
{tool}
</div>
<div
style={{
"font-size": "11px",
color: "var(--text-weak-base, var(--vscode-descriptionForeground))",
"margin-top": "2px",
}}
>
{TOOL_DESCRIPTIONS[tool] ?? tool}
</div>
</div>
<Select
options={LEVEL_OPTIONS}
current={LEVEL_OPTIONS.find((o) => 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"
/>
</div>
)}
</For>
</Card>
</div>
)
}
@@ -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) => (
<div
data-slot="settings-row"
style={{
display: "flex",
"align-items": "center",
"justify-content": "space-between",
padding: "8px 0",
"border-bottom": props.last ? "none" : "1px solid var(--border-weak-base)",
}}
>
<div style={{ flex: 1, "min-width": 0, "margin-right": "12px" }}>
<div style={{ "font-weight": "500" }}>{props.label}</div>
<div style={{ "font-size": "11px", color: "var(--text-weak-base, var(--vscode-descriptionForeground))" }}>
{props.description}
</div>
</div>
{props.children}
</div>
)
const CheckpointsTab: Component = () => {
const { config, updateConfig } = useConfig()
return (
<div>
<div
style={{
background: "var(--vscode-editor-background)",
border: "1px solid var(--vscode-panel-border)",
"border-radius": "4px",
padding: "16px",
}}
>
<p
style={{
"font-size": "12px",
color: "var(--vscode-descriptionForeground)",
margin: 0,
"line-height": "1.5",
}}
<Card>
<SettingsRow
label="Enable Snapshots"
description="Create checkpoints before file edits so you can restore previous states"
last
>
<strong style={{ color: "var(--vscode-foreground)" }}>This section is not implemented yet.</strong> 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.
</p>
</div>
<Switch
checked={config().snapshot !== false}
onChange={(checked) => updateConfig({ snapshot: checked })}
hideLabel
>
Enable Snapshots
</Switch>
</SettingsRow>
</Card>
</div>
)
}
@@ -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) => (
<div
data-slot="settings-row"
style={{
display: "flex",
"align-items": "center",
"justify-content": "space-between",
padding: "8px 0",
"border-bottom": props.last ? "none" : "1px solid var(--border-weak-base)",
}}
>
<div style={{ flex: 1, "min-width": 0, "margin-right": "12px" }}>
<div style={{ "font-weight": "500" }}>{props.label}</div>
<div style={{ "font-size": "11px", color: "var(--text-weak-base, var(--vscode-descriptionForeground))" }}>
{props.description}
</div>
</div>
{props.children}
</div>
)
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 (
<div>
<div
style={{
background: "var(--vscode-editor-background)",
border: "1px solid var(--vscode-panel-border)",
"border-radius": "4px",
padding: "16px",
}}
>
<p
{/* Compaction settings */}
<Card>
<SettingsRow label="Auto Compaction" description="Automatically compact context when it's full">
<Switch
checked={config().compaction?.auto ?? false}
onChange={(checked) => updateConfig({ compaction: { ...config().compaction, auto: checked } })}
hideLabel
>
Auto Compaction
</Switch>
</SettingsRow>
<SettingsRow label="Prune Old Outputs" description="Remove old tool outputs during compaction" last>
<Switch
checked={config().compaction?.prune ?? false}
onChange={(checked) => updateConfig({ compaction: { ...config().compaction, prune: checked } })}
hideLabel
>
Prune Old Outputs
</Switch>
</SettingsRow>
</Card>
<h4 style={{ "margin-top": "16px", "margin-bottom": "8px" }}>File Watcher Ignore Patterns</h4>
<Card>
<div
style={{
"font-size": "12px",
color: "var(--vscode-descriptionForeground)",
margin: 0,
"line-height": "1.5",
"font-size": "11px",
color: "var(--text-weak-base, var(--vscode-descriptionForeground))",
"padding-bottom": "8px",
"border-bottom": patterns().length > 0 || newPattern() ? "1px solid var(--border-weak-base)" : "none",
}}
>
<strong style={{ color: "var(--vscode-foreground)" }}>This section is not implemented yet.</strong> 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.
</p>
</div>
Glob patterns for files the watcher should ignore
</div>
{/* Add new pattern */}
<div
style={{
display: "flex",
gap: "8px",
"align-items": "center",
padding: "8px 0",
"border-bottom": patterns().length > 0 ? "1px solid var(--border-weak-base)" : "none",
}}
>
<div style={{ flex: 1 }}>
<TextField
value={newPattern()}
placeholder="e.g. **/node_modules/**"
onChange={(val) => setNewPattern(val)}
onKeyDown={(e: KeyboardEvent) => {
if (e.key === "Enter") addPattern()
}}
/>
</div>
<Button size="small" onClick={addPattern}>
Add
</Button>
</div>
{/* Pattern list */}
<For each={patterns()}>
{(pattern, index) => (
<div
style={{
display: "flex",
"align-items": "center",
"justify-content": "space-between",
padding: "6px 0",
"border-bottom": index() < patterns().length - 1 ? "1px solid var(--border-weak-base)" : "none",
}}
>
<span
style={{
"font-family": "var(--vscode-editor-font-family, monospace)",
"font-size": "12px",
}}
>
{pattern}
</span>
<IconButton size="small" variant="ghost" icon="close" onClick={() => removePattern(index())} />
</div>
)}
</For>
</Card>
</div>
)
}
@@ -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 (
<div>
<div
style={{
background: "var(--vscode-editor-background)",
border: "1px solid var(--vscode-panel-border)",
"border-radius": "4px",
padding: "16px",
}}
>
<p
<Card>
<div
data-slot="settings-row"
style={{
"font-size": "12px",
color: "var(--vscode-descriptionForeground)",
margin: 0,
"line-height": "1.5",
display: "flex",
"align-items": "center",
"justify-content": "space-between",
padding: "8px 0",
"border-bottom": "1px solid var(--border-weak-base)",
}}
>
<strong style={{ color: "var(--vscode-foreground)" }}>This section is not implemented yet.</strong> 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.
</p>
</div>
<div style={{ flex: 1, "min-width": 0, "margin-right": "12px" }}>
<div style={{ "font-weight": "500" }}>Username</div>
<div
style={{
"font-size": "11px",
color: "var(--text-weak-base, var(--vscode-descriptionForeground))",
}}
>
Custom username displayed in conversations
</div>
</div>
<div style={{ width: "160px" }}>
<TextField
value={config().username ?? ""}
placeholder="User"
onChange={(val) => updateConfig({ username: val.trim() || undefined })}
/>
</div>
</div>
<div
data-slot="settings-row"
style={{
display: "flex",
"align-items": "center",
"justify-content": "space-between",
padding: "8px 0",
}}
>
<div style={{ flex: 1, "min-width": 0, "margin-right": "12px" }}>
<div style={{ "font-weight": "500" }}>Layout</div>
<div
style={{
"font-size": "11px",
color: "var(--text-weak-base, var(--vscode-descriptionForeground))",
}}
>
Layout mode for the chat interface
</div>
</div>
<Select
options={LAYOUT_OPTIONS}
current={LAYOUT_OPTIONS.find((o) => o.value === (config().layout ?? "auto"))}
value={(o) => o.value}
label={(o) => o.label}
onSelect={(o) => o && updateConfig({ layout: o.value as "auto" | "stretch" })}
variant="secondary"
size="small"
triggerVariant="settings"
/>
</div>
</Card>
</div>
)
}
@@ -1,30 +1,153 @@
import { Component } from "solid-js"
import { Component, For, Show, createMemo } from "solid-js"
import { Switch } from "@kilocode/kilo-ui/switch"
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 ShareOption {
value: string
label: string
}
const SHARE_OPTIONS: ShareOption[] = [
{ value: "manual", label: "Manual" },
{ value: "auto", label: "Auto" },
{ value: "disabled", label: "Disabled" },
]
const SettingsRow: Component<{ label: string; description: string; last?: boolean; children: any }> = (props) => (
<div
data-slot="settings-row"
style={{
display: "flex",
"align-items": "center",
"justify-content": "space-between",
padding: "8px 0",
"border-bottom": props.last ? "none" : "1px solid var(--border-weak-base)",
}}
>
<div style={{ flex: 1, "min-width": 0, "margin-right": "12px" }}>
<div style={{ "font-weight": "500" }}>{props.label}</div>
<div style={{ "font-size": "11px", color: "var(--text-weak-base, var(--vscode-descriptionForeground))" }}>
{props.description}
</div>
</div>
{props.children}
</div>
)
const ExperimentalTab: Component = () => {
const { config, updateConfig } = useConfig()
const experimental = createMemo(() => config().experimental ?? {})
const updateExperimental = (key: string, value: unknown) => {
updateConfig({
experimental: { ...experimental(), [key]: value },
})
}
return (
<div>
<div
style={{
background: "var(--vscode-editor-background)",
border: "1px solid var(--vscode-panel-border)",
"border-radius": "4px",
padding: "16px",
}}
>
<p
style={{
"font-size": "12px",
color: "var(--vscode-descriptionForeground)",
margin: 0,
"line-height": "1.5",
}}
>
<strong style={{ color: "var(--vscode-foreground)" }}>This section is not implemented yet.</strong> 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.
</p>
</div>
<Card>
{/* Share mode */}
<SettingsRow label="Share Mode" description="How session sharing behaves">
<Select
options={SHARE_OPTIONS}
current={SHARE_OPTIONS.find((o) => 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"
/>
</SettingsRow>
<SettingsRow label="Formatter" description="Enable the automatic code formatter">
<Switch
checked={config().formatter !== false}
onChange={(checked) => updateConfig({ formatter: checked ? {} : false })}
hideLabel
>
Formatter
</Switch>
</SettingsRow>
<SettingsRow label="LSP" description="Enable language server protocol integration">
<Switch
checked={config().lsp !== false}
onChange={(checked) => updateConfig({ lsp: checked ? {} : false })}
hideLabel
>
LSP
</Switch>
</SettingsRow>
<SettingsRow label="Disable Paste Summary" description="Don't summarize large pasted content">
<Switch
checked={experimental().disable_paste_summary ?? false}
onChange={(checked) => updateExperimental("disable_paste_summary", checked)}
hideLabel
>
Disable Paste Summary
</Switch>
</SettingsRow>
<SettingsRow label="Batch Tool" description="Enable batching of multiple tool calls">
<Switch
checked={experimental().batch_tool ?? false}
onChange={(checked) => updateExperimental("batch_tool", checked)}
hideLabel
>
Batch Tool
</Switch>
</SettingsRow>
<SettingsRow label="Continue on Deny" description="Continue the agent loop when a permission is denied">
<Switch
checked={experimental().continue_loop_on_deny ?? false}
onChange={(checked) => updateExperimental("continue_loop_on_deny", checked)}
hideLabel
>
Continue on Deny
</Switch>
</SettingsRow>
{/* MCP timeout */}
<SettingsRow label="MCP Timeout (ms)" description="Timeout for MCP server requests in milliseconds" last>
<TextField
value={String(experimental().mcp_timeout ?? 60000)}
onChange={(val) => {
const num = parseInt(val, 10)
if (!isNaN(num) && num > 0) {
updateExperimental("mcp_timeout", num)
}
}}
/>
</SettingsRow>
</Card>
{/* Tool toggles */}
<Show when={config().tools && Object.keys(config().tools ?? {}).length > 0}>
<h4 style={{ "margin-top": "16px", "margin-bottom": "8px" }}>Tool Toggles</h4>
<Card>
<For each={Object.entries(config().tools ?? {})}>
{([name, enabled], index) => (
<SettingsRow label={name} description="" last={index() >= Object.keys(config().tools ?? {}).length - 1}>
<Switch
checked={enabled}
onChange={(checked) => updateConfig({ tools: { ...config().tools, [name]: checked } })}
hideLabel
>
{name}
</Switch>
</SettingsRow>
)}
</For>
</Card>
</Show>
</div>
)
}
@@ -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 (
<div>
<div
style={{
background: "var(--vscode-editor-background)",
border: "1px solid var(--vscode-panel-border)",
"border-radius": "4px",
padding: "16px",
}}
>
<p
const vscode = useVSCode()
const [agentNotify, setAgentNotify] = createSignal(true)
const [permNotify, setPermNotify] = createSignal(true)
const [errorNotify, setErrorNotify] = createSignal(true)
const [agentSound, setAgentSound] = createSignal("default")
const [permSound, setPermSound] = createSignal("default")
const [errorSound, setErrorSound] = createSignal("default")
const unsubscribe = vscode.onMessage((message: ExtensionMessage) => {
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) => (
<div
data-slot="settings-row"
style={{
display: "flex",
"align-items": "center",
"justify-content": "space-between",
padding: "8px 0",
"border-bottom": props.last ? "none" : "1px solid var(--border-weak-base)",
}}
>
<div style={{ flex: 1, "min-width": 0, "margin-right": "12px" }}>
<div style={{ "font-weight": "500" }}>{props.label}</div>
<div
style={{
"font-size": "12px",
color: "var(--vscode-descriptionForeground)",
margin: 0,
"line-height": "1.5",
"font-size": "11px",
color: "var(--text-weak-base, var(--vscode-descriptionForeground))",
}}
>
<strong style={{ color: "var(--vscode-foreground)" }}>This section is not implemented yet.</strong> 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.
</p>
{props.description}
</div>
</div>
{props.children}
</div>
)
return (
<div>
<Card>
<SettingsRow label="Agent Completion" description="Show notification when agent completes a task">
<Switch
checked={agentNotify()}
onChange={(checked) => {
setAgentNotify(checked)
save("notifications.agent", checked)
}}
hideLabel
>
Agent Completion
</Switch>
</SettingsRow>
<SettingsRow label="Permission Requests" description="Show notification on permission requests">
<Switch
checked={permNotify()}
onChange={(checked) => {
setPermNotify(checked)
save("notifications.permissions", checked)
}}
hideLabel
>
Permission Requests
</Switch>
</SettingsRow>
<SettingsRow label="Errors" description="Show notification on errors" last>
<Switch
checked={errorNotify()}
onChange={(checked) => {
setErrorNotify(checked)
save("notifications.errors", checked)
}}
hideLabel
>
Errors
</Switch>
</SettingsRow>
</Card>
<h4 style={{ "margin-top": "16px", "margin-bottom": "8px" }}>Sounds</h4>
<Card>
<SettingsRow label="Agent Completion Sound" description="Sound to play when agent completes">
<Select
options={SOUND_OPTIONS}
current={SOUND_OPTIONS.find((o) => o.value === agentSound())}
value={(o) => o.value}
label={(o) => o.label}
onSelect={(o) => {
if (o) {
setAgentSound(o.value)
save("sounds.agent", o.value)
}
}}
variant="secondary"
size="small"
triggerVariant="settings"
/>
</SettingsRow>
<SettingsRow label="Permission Request Sound" description="Sound to play on permission requests">
<Select
options={SOUND_OPTIONS}
current={SOUND_OPTIONS.find((o) => 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"
/>
</SettingsRow>
<SettingsRow label="Error Sound" description="Sound to play on errors" last>
<Select
options={SOUND_OPTIONS}
current={SOUND_OPTIONS.find((o) => o.value === errorSound())}
value={(o) => o.value}
label={(o) => o.label}
onSelect={(o) => {
if (o) {
setErrorSound(o.value)
save("sounds.errors", o.value)
}
}}
variant="secondary"
size="small"
triggerVariant="settings"
/>
</SettingsRow>
</Card>
</div>
)
}
@@ -1,30 +1,259 @@
import { Component } from "solid-js"
import { Component, For, createSignal, createMemo } from "solid-js"
import { Select } from "@kilocode/kilo-ui/select"
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"
import { useProvider } from "../../context/provider"
import { ModelSelectorBase } from "../chat/ModelSelector"
import type { ModelSelection } from "../../types/messages"
interface ProviderOption {
value: string
label: string
}
/** Parse a "provider/model" config string into a ModelSelection (or null). */
function parseModelConfig(raw: string | undefined): ModelSelection | null {
if (!raw) {
return null
}
const slash = raw.indexOf("/")
if (slash <= 0) {
return null
}
return { providerID: raw.slice(0, slash), modelID: raw.slice(slash + 1) }
}
const SettingsRow: Component<{ label: string; description: string; last?: boolean; children: any }> = (props) => (
<div
data-slot="settings-row"
style={{
display: "flex",
"align-items": "center",
"justify-content": "space-between",
padding: "8px 0",
"border-bottom": props.last ? "none" : "1px solid var(--border-weak-base)",
}}
>
<div style={{ flex: 1, "min-width": 0, "margin-right": "12px" }}>
<div style={{ "font-weight": "500" }}>{props.label}</div>
<div style={{ "font-size": "11px", color: "var(--text-weak-base, var(--vscode-descriptionForeground))" }}>
{props.description}
</div>
</div>
{props.children}
</div>
)
const ProvidersTab: Component = () => {
const { config, updateConfig } = useConfig()
const provider = useProvider()
const providerOptions = createMemo<ProviderOption[]>(() =>
Object.keys(provider.providers())
.sort()
.map((id) => ({ value: id, label: id })),
)
const [newDisabled, setNewDisabled] = createSignal<ProviderOption | undefined>()
const [newEnabled, setNewEnabled] = createSignal<ProviderOption | undefined>()
const disabledProviders = () => config().disabled_providers ?? []
const enabledProviders = () => config().enabled_providers ?? []
const addToList = (key: "disabled_providers" | "enabled_providers", value: string) => {
const current = key === "disabled_providers" ? [...disabledProviders()] : [...enabledProviders()]
if (value && !current.includes(value)) {
current.push(value)
updateConfig({ [key]: current })
}
}
const removeFromList = (key: "disabled_providers" | "enabled_providers", index: number) => {
const current = key === "disabled_providers" ? [...disabledProviders()] : [...enabledProviders()]
current.splice(index, 1)
updateConfig({ [key]: current })
}
function handleModelSelect(configKey: "model" | "small_model") {
return (providerID: string, modelID: string) => {
if (!providerID || !modelID) {
updateConfig({ [configKey]: undefined })
} else {
updateConfig({ [configKey]: `${providerID}/${modelID}` })
}
}
}
return (
<div>
<div
style={{
background: "var(--vscode-editor-background)",
border: "1px solid var(--vscode-panel-border)",
"border-radius": "4px",
padding: "16px",
}}
>
<p
{/* Model selection */}
<Card>
<SettingsRow label="Default Model" description="Primary model for conversations">
<ModelSelectorBase
value={parseModelConfig(config().model)}
onSelect={handleModelSelect("model")}
placement="bottom-start"
allowClear
clearLabel="Not set (use server default)"
/>
</SettingsRow>
<SettingsRow
label="Small Model"
description="Lightweight model for title generation and other quick tasks"
last
>
<ModelSelectorBase
value={parseModelConfig(config().small_model)}
onSelect={handleModelSelect("small_model")}
placement="bottom-start"
allowClear
clearLabel="Not set (use server default)"
/>
</SettingsRow>
</Card>
{/* Disabled providers */}
<h4 style={{ "margin-top": "16px", "margin-bottom": "8px" }}>Disabled Providers</h4>
<Card>
<div
style={{
"font-size": "12px",
color: "var(--vscode-descriptionForeground)",
margin: 0,
"line-height": "1.5",
"font-size": "11px",
color: "var(--text-weak-base, var(--vscode-descriptionForeground))",
"padding-bottom": "8px",
"border-bottom": "1px solid var(--border-weak-base)",
}}
>
<strong style={{ color: "var(--vscode-foreground)" }}>This section is not implemented yet.</strong> 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.
</p>
</div>
Providers to hide from the provider list
</div>
<div
style={{
display: "flex",
gap: "8px",
"align-items": "center",
padding: "8px 0",
"border-bottom": disabledProviders().length > 0 ? "1px solid var(--border-weak-base)" : "none",
}}
>
<div style={{ flex: 1 }}>
<Select
options={providerOptions().filter((o) => !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…"
/>
</div>
<Button
size="small"
onClick={() => {
if (newDisabled()) {
addToList("disabled_providers", newDisabled()!.value)
setNewDisabled(undefined)
}
}}
>
Add
</Button>
</div>
<For each={disabledProviders()}>
{(id, index) => (
<div
style={{
display: "flex",
"align-items": "center",
"justify-content": "space-between",
padding: "6px 0",
"border-bottom":
index() < disabledProviders().length - 1 ? "1px solid var(--border-weak-base)" : "none",
}}
>
<span style={{ "font-size": "12px" }}>{id}</span>
<IconButton
size="small"
variant="ghost"
icon="close"
onClick={() => removeFromList("disabled_providers", index())}
/>
</div>
)}
</For>
</Card>
{/* Enabled providers (allowlist) */}
<h4 style={{ "margin-top": "16px", "margin-bottom": "8px" }}>Enabled Providers (Allowlist)</h4>
<Card>
<div
style={{
"font-size": "11px",
color: "var(--text-weak-base, var(--vscode-descriptionForeground))",
"padding-bottom": "8px",
"border-bottom": "1px solid var(--border-weak-base)",
}}
>
If set, only these providers will be available (exclusive allowlist)
</div>
<div
style={{
display: "flex",
gap: "8px",
"align-items": "center",
padding: "8px 0",
"border-bottom": enabledProviders().length > 0 ? "1px solid var(--border-weak-base)" : "none",
}}
>
<div style={{ flex: 1 }}>
<Select
options={providerOptions().filter((o) => !enabledProviders().includes(o.value))}
current={newEnabled()}
value={(o) => o.value}
label={(o) => o.label}
onSelect={(o) => setNewEnabled(o)}
variant="secondary"
size="small"
triggerVariant="settings"
placeholder="Select provider…"
/>
</div>
<Button
size="small"
onClick={() => {
if (newEnabled()) {
addToList("enabled_providers", newEnabled()!.value)
setNewEnabled(undefined)
}
}}
>
Add
</Button>
</div>
<For each={enabledProviders()}>
{(id, index) => (
<div
style={{
display: "flex",
"align-items": "center",
"justify-content": "space-between",
padding: "6px 0",
"border-bottom": index() < enabledProviders().length - 1 ? "1px solid var(--border-weak-base)" : "none",
}}
>
<span style={{ "font-size": "12px" }}>{id}</span>
<IconButton
size="small"
variant="ghost"
icon="close"
onClick={() => removeFromList("enabled_providers", index())}
/>
</div>
)}
</For>
</Card>
</div>
)
}
@@ -0,0 +1,83 @@
/**
* Config context
* Manages backend configuration state (permissions, agents, providers, etc.)
* and exposes an updateConfig method to apply partial updates.
*/
import { createContext, useContext, createSignal, onCleanup, ParentComponent, Accessor } from "solid-js"
import { useVSCode } from "./vscode"
import type { Config, ExtensionMessage } from "../types/messages"
interface ConfigContextValue {
config: Accessor<Config>
loading: Accessor<boolean>
updateConfig: (partial: Partial<Config>) => void
}
const ConfigContext = createContext<ConfigContextValue>()
export const ConfigProvider: ParentComponent = (props) => {
const vscode = useVSCode()
const [config, setConfig] = createSignal<Config>({})
const [loading, setLoading] = createSignal(true)
// Register handler immediately (not in onMount) so we never miss
// a configLoaded message that arrives before the DOM mount.
const unsubscribe = vscode.onMessage((message: ExtensionMessage) => {
if (message.type === "configLoaded") {
setConfig(message.config)
setLoading(false)
return
}
if (message.type === "configUpdated") {
setConfig(message.config)
return
}
})
onCleanup(unsubscribe)
// Request config in case the initial push was missed.
// Retry a few times because the extension's httpClient may
// not be ready yet when the first request arrives.
let retries = 0
const maxRetries = 5
const retryMs = 500
vscode.postMessage({ type: "requestConfig" })
const retryTimer = setInterval(() => {
retries++
if (!loading() || retries >= maxRetries) {
clearInterval(retryTimer)
return
}
vscode.postMessage({ type: "requestConfig" })
}, retryMs)
onCleanup(() => clearInterval(retryTimer))
function updateConfig(partial: Partial<Config>) {
// Optimistically update local state
setConfig((prev) => ({ ...prev, ...partial }))
// Send to extension for persistence
vscode.postMessage({ type: "updateConfig", config: partial })
}
const value: ConfigContextValue = {
config,
loading,
updateConfig,
}
return <ConfigContext.Provider value={value}>{props.children}</ConfigContext.Provider>
}
export function useConfig(): ConfigContextValue {
const context = useContext(ConfigContext)
if (!context) {
throw new Error("useConfig must be used within a ConfigProvider")
}
return context
}
@@ -790,6 +790,7 @@ export const dict = {
"settings.aboutKiloCode.extensionName": "إضافة Kilo Code",
"settings.agentBehaviour.subtab.modes": "الأوضاع",
"settings.agentBehaviour.subtab.agents": "Agents",
"settings.agentBehaviour.subtab.mcpServers": "خوادم MCP",
"settings.agentBehaviour.subtab.rules": "القواعد",
"settings.agentBehaviour.subtab.workflows": "سير العمل",
@@ -797,6 +797,7 @@ export const dict = {
"settings.aboutKiloCode.extensionName": "Extensão Kilo Code",
"settings.agentBehaviour.subtab.modes": "Modos",
"settings.agentBehaviour.subtab.agents": "Agents",
"settings.agentBehaviour.subtab.mcpServers": "Servidores MCP",
"settings.agentBehaviour.subtab.rules": "Regras",
"settings.agentBehaviour.subtab.workflows": "Fluxos de trabalho",
@@ -824,6 +824,7 @@ export const dict = {
"settings.aboutKiloCode.extensionName": "Kilo Code ekstenzija",
"settings.agentBehaviour.subtab.modes": "Modovi",
"settings.agentBehaviour.subtab.agents": "Agents",
"settings.agentBehaviour.subtab.mcpServers": "MCP serveri",
"settings.agentBehaviour.subtab.rules": "Pravila",
"settings.agentBehaviour.subtab.workflows": "Tokovi rada",
@@ -796,6 +796,7 @@ export const dict = {
"settings.aboutKiloCode.extensionName": "Kilo Code-udvidelse",
"settings.agentBehaviour.subtab.modes": "Tilstande",
"settings.agentBehaviour.subtab.agents": "Agents",
"settings.agentBehaviour.subtab.mcpServers": "MCP-servere",
"settings.agentBehaviour.subtab.rules": "Regler",
"settings.agentBehaviour.subtab.workflows": "Arbejdsgange",
@@ -802,6 +802,7 @@ export const dict = {
"settings.aboutKiloCode.extensionName": "Kilo Code Erweiterung",
"settings.agentBehaviour.subtab.modes": "Modi",
"settings.agentBehaviour.subtab.agents": "Agents",
"settings.agentBehaviour.subtab.mcpServers": "MCP-Server",
"settings.agentBehaviour.subtab.rules": "Regeln",
"settings.agentBehaviour.subtab.workflows": "Workflows",
@@ -829,6 +829,7 @@ export const dict = {
"settings.aboutKiloCode.extensionName": "Kilo Code Extension",
"settings.agentBehaviour.subtab.modes": "Modes",
"settings.agentBehaviour.subtab.agents": "Agents",
"settings.agentBehaviour.subtab.mcpServers": "MCP Servers",
"settings.agentBehaviour.subtab.rules": "Rules",
"settings.agentBehaviour.subtab.workflows": "Workflows",
@@ -802,6 +802,7 @@ export const dict = {
"settings.aboutKiloCode.extensionName": "Extensión Kilo Code",
"settings.agentBehaviour.subtab.modes": "Modos",
"settings.agentBehaviour.subtab.agents": "Agents",
"settings.agentBehaviour.subtab.mcpServers": "Servidores MCP",
"settings.agentBehaviour.subtab.rules": "Reglas",
"settings.agentBehaviour.subtab.workflows": "Flujos de trabajo",
@@ -806,6 +806,7 @@ export const dict = {
"settings.aboutKiloCode.extensionName": "Extension Kilo Code",
"settings.agentBehaviour.subtab.modes": "Modes",
"settings.agentBehaviour.subtab.agents": "Agents",
"settings.agentBehaviour.subtab.mcpServers": "Serveurs MCP",
"settings.agentBehaviour.subtab.rules": "Règles",
"settings.agentBehaviour.subtab.workflows": "Flux de travail",
@@ -790,6 +790,7 @@ export const dict = {
"settings.aboutKiloCode.extensionName": "Kilo Code拡張機能",
"settings.agentBehaviour.subtab.modes": "モード",
"settings.agentBehaviour.subtab.agents": "Agents",
"settings.agentBehaviour.subtab.mcpServers": "MCPサーバー",
"settings.agentBehaviour.subtab.rules": "ルール",
"settings.agentBehaviour.subtab.workflows": "ワークフロー",
@@ -795,6 +795,7 @@ export const dict = {
"settings.aboutKiloCode.extensionName": "Kilo Code 확장 프로그램",
"settings.agentBehaviour.subtab.modes": "모드",
"settings.agentBehaviour.subtab.agents": "Agents",
"settings.agentBehaviour.subtab.mcpServers": "MCP 서버",
"settings.agentBehaviour.subtab.rules": "규칙",
"settings.agentBehaviour.subtab.workflows": "워크플로",
@@ -799,6 +799,7 @@ export const dict = {
"settings.aboutKiloCode.extensionName": "Kilo Code-utvidelse",
"settings.agentBehaviour.subtab.modes": "Moduser",
"settings.agentBehaviour.subtab.agents": "Agents",
"settings.agentBehaviour.subtab.mcpServers": "MCP-servere",
"settings.agentBehaviour.subtab.rules": "Regler",
"settings.agentBehaviour.subtab.workflows": "Arbeidsflyter",
@@ -798,6 +798,7 @@ export const dict = {
"settings.aboutKiloCode.extensionName": "Rozszerzenie Kilo Code",
"settings.agentBehaviour.subtab.modes": "Tryby",
"settings.agentBehaviour.subtab.agents": "Agents",
"settings.agentBehaviour.subtab.mcpServers": "Serwery MCP",
"settings.agentBehaviour.subtab.rules": "Reguły",
"settings.agentBehaviour.subtab.workflows": "Przepływy pracy",
@@ -802,6 +802,7 @@ export const dict = {
"settings.aboutKiloCode.extensionName": "Расширение Kilo Code",
"settings.agentBehaviour.subtab.modes": "Режимы",
"settings.agentBehaviour.subtab.agents": "Agents",
"settings.agentBehaviour.subtab.mcpServers": "MCP-серверы",
"settings.agentBehaviour.subtab.rules": "Правила",
"settings.agentBehaviour.subtab.workflows": "Рабочие процессы",
@@ -788,6 +788,7 @@ export const dict = {
"settings.aboutKiloCode.extensionName": "ส่วนขยาย Kilo Code",
"settings.agentBehaviour.subtab.modes": "โหมด",
"settings.agentBehaviour.subtab.agents": "Agents",
"settings.agentBehaviour.subtab.mcpServers": "เซิร์ฟเวอร์ MCP",
"settings.agentBehaviour.subtab.rules": "กฎ",
"settings.agentBehaviour.subtab.workflows": "เวิร์กโฟลว์",
@@ -791,6 +791,7 @@ export const dict = {
"settings.aboutKiloCode.extensionName": "Kilo Code 扩展",
"settings.agentBehaviour.subtab.modes": "模式",
"settings.agentBehaviour.subtab.agents": "Agents",
"settings.agentBehaviour.subtab.mcpServers": "MCP 服务器",
"settings.agentBehaviour.subtab.rules": "规则",
"settings.agentBehaviour.subtab.workflows": "工作流",
@@ -788,6 +788,7 @@ export const dict = {
"settings.aboutKiloCode.extensionName": "Kilo Code 擴充功能",
"settings.agentBehaviour.subtab.modes": "模式",
"settings.agentBehaviour.subtab.agents": "Agents",
"settings.agentBehaviour.subtab.mcpServers": "MCP 伺服器",
"settings.agentBehaviour.subtab.rules": "規則",
"settings.agentBehaviour.subtab.workflows": "工作流程",
@@ -207,6 +207,90 @@ export interface ModelSelection {
modelID: string
}
// ============================================
// Backend Config Types (mirrored for webview)
// ============================================
export type PermissionLevel = "allow" | "ask" | "deny"
export type PermissionConfig = Partial<Record<string, PermissionLevel>>
export interface AgentConfig {
model?: string
prompt?: string
temperature?: number
top_p?: number
steps?: number
permission?: PermissionConfig
}
export interface ProviderConfig {
name?: string
api_key?: string
base_url?: string
models?: Record<string, unknown>
}
export interface McpConfig {
command?: string
args?: string[]
env?: Record<string, string>
url?: string
headers?: Record<string, string>
}
export interface CommandConfig {
command: string
description?: string
}
export interface SkillsConfig {
paths?: string[]
urls?: string[]
}
export interface CompactionConfig {
auto?: boolean
prune?: boolean
}
export interface WatcherConfig {
ignore?: string[]
}
export interface ExperimentalConfig {
disable_paste_summary?: boolean
batch_tool?: boolean
primary_tools?: string[]
continue_loop_on_deny?: boolean
mcp_timeout?: number
}
export interface Config {
permission?: PermissionConfig
model?: string
small_model?: string
default_agent?: string
agent?: Record<string, AgentConfig>
provider?: Record<string, ProviderConfig>
disabled_providers?: string[]
enabled_providers?: string[]
mcp?: Record<string, McpConfig>
command?: Record<string, CommandConfig>
instructions?: string[]
skills?: SkillsConfig
snapshot?: boolean
share?: "manual" | "auto" | "disabled"
username?: string
watcher?: WatcherConfig
formatter?: false | Record<string, unknown>
lsp?: false | Record<string, unknown>
compaction?: CompactionConfig
tools?: Record<string, boolean>
layout?: "auto" | "stretch"
experimental?: ExperimentalConfig
}
// ============================================
// Messages FROM extension TO webview
// ============================================
@@ -356,6 +440,28 @@ export interface BrowserSettingsLoadedMessage {
settings: BrowserSettings
}
export interface ConfigLoadedMessage {
type: "configLoaded"
config: Config
}
export interface ConfigUpdatedMessage {
type: "configUpdated"
config: Config
}
export interface NotificationSettingsLoadedMessage {
type: "notificationSettingsLoaded"
settings: {
notifyAgent: boolean
notifyPermissions: boolean
notifyErrors: boolean
soundAgent: string
soundPermissions: string
soundErrors: string
}
}
export type ExtensionMessage =
| ReadyMessage
| ConnectionStateMessage
@@ -382,6 +488,9 @@ export type ExtensionMessage =
| QuestionResolvedMessage
| QuestionErrorMessage
| BrowserSettingsLoadedMessage
| ConfigLoadedMessage
| ConfigUpdatedMessage
| NotificationSettingsLoadedMessage
// ============================================
// Messages FROM webview TO extension
@@ -502,6 +611,19 @@ export interface RequestBrowserSettingsMessage {
type: "requestBrowserSettings"
}
export interface RequestConfigMessage {
type: "requestConfig"
}
export interface UpdateConfigMessage {
type: "updateConfig"
config: Partial<Config>
}
export interface RequestNotificationSettingsMessage {
type: "requestNotificationSettings"
}
export type WebviewMessage =
| SendMessageRequest
| AbortRequest
@@ -526,6 +648,9 @@ export type WebviewMessage =
| RenameSessionRequest
| UpdateSettingRequest
| RequestBrowserSettingsMessage
| RequestConfigMessage
| UpdateConfigMessage
| RequestNotificationSettingsMessage
// ============================================
// VS Code API type