mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-10-24 01:24:18 +08:00
Merge pull request #272 from Kilo-Org/mark/migrate-settings-plan
feat: implement settings migration from desktop app
This commit is contained in:
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user