Merge branch 'main' into feat/config-permission-protection

This commit is contained in:
Marian Alexandru Alecu
2026-03-27 09:17:23 +02:00
committed by GitHub
50 changed files with 894 additions and 114 deletions
+16 -16
View File
@@ -27,7 +27,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "7.1.5",
"version": "7.1.8",
"dependencies": {
"@kilocode/kilo-i18n": "workspace:*",
"@kilocode/kilo-ui": "workspace:*",
@@ -79,7 +79,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "7.1.5",
"version": "7.1.8",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -112,7 +112,7 @@
},
"packages/desktop-electron": {
"name": "@opencode-ai/desktop-electron",
"version": "7.1.5",
"version": "7.1.8",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -142,7 +142,7 @@
},
"packages/kilo-docs": {
"name": "@kilocode/kilo-docs",
"version": "7.1.5",
"version": "7.1.8",
"dependencies": {
"@docsearch/css": "^4",
"@docsearch/js": "^4",
@@ -171,7 +171,7 @@
},
"packages/kilo-gateway": {
"name": "@kilocode/kilo-gateway",
"version": "7.1.5",
"version": "7.1.8",
"dependencies": {
"@ai-sdk/anthropic": "2.0.65",
"@ai-sdk/openai": "2.0.101",
@@ -206,7 +206,7 @@
},
"packages/kilo-i18n": {
"name": "@kilocode/kilo-i18n",
"version": "7.1.5",
"version": "7.1.8",
"devDependencies": {
"@tsconfig/node22": "catalog:",
"@types/bun": "catalog:",
@@ -216,7 +216,7 @@
},
"packages/kilo-telemetry": {
"name": "@kilocode/kilo-telemetry",
"version": "7.1.5",
"version": "7.1.8",
"dependencies": {
"@kilocode/kilo-gateway": "workspace:*",
"@opentelemetry/api": "1.9.0",
@@ -236,7 +236,7 @@
},
"packages/kilo-ui": {
"name": "@kilocode/kilo-ui",
"version": "7.1.5",
"version": "7.1.8",
"dependencies": {
"@kobalte/core": "0.13.11",
"@opencode-ai/util": "workspace:*",
@@ -271,7 +271,7 @@
},
"packages/kilo-vscode": {
"name": "kilo-code",
"version": "7.1.5",
"version": "7.1.8",
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0",
"@kilocode/kilo-i18n": "workspace:*",
@@ -324,7 +324,7 @@
},
"packages/opencode": {
"name": "@kilocode/cli",
"version": "7.1.5",
"version": "7.1.8",
"bin": {
"kilo": "./bin/kilo",
"kilocode": "./bin/kilo",
@@ -446,7 +446,7 @@
},
"packages/plugin": {
"name": "@kilocode/plugin",
"version": "7.1.5",
"version": "7.1.8",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"zod": "catalog:",
@@ -460,14 +460,14 @@
},
"packages/script": {
"name": "@opencode-ai/script",
"version": "7.1.5",
"version": "7.1.8",
"devDependencies": {
"@types/bun": "catalog:",
},
},
"packages/sdk/js": {
"name": "@kilocode/sdk",
"version": "7.1.5",
"version": "7.1.8",
"devDependencies": {
"@hey-api/openapi-ts": "0.90.10",
"@tsconfig/node22": "catalog:",
@@ -478,7 +478,7 @@
},
"packages/storybook": {
"name": "@opencode-ai/storybook",
"version": "7.1.5",
"version": "7.1.8",
"devDependencies": {
"@opencode-ai/ui": "workspace:*",
"@solidjs/meta": "catalog:",
@@ -501,7 +501,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "7.1.5",
"version": "7.1.8",
"dependencies": {
"@kilocode/sdk": "workspace:*",
"@kobalte/core": "catalog:",
@@ -548,7 +548,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "7.1.5",
"version": "7.1.8",
"dependencies": {
"zod": "catalog:",
},
+1 -1
View File
@@ -112,6 +112,6 @@
"@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch",
"ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch"
},
"version": "7.1.5",
"version": "7.1.8",
"peerDependencies": {}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "7.1.5",
"version": "7.1.8",
"description": "",
"type": "module",
"exports": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop-electron",
"private": true,
"version": "7.1.5",
"version": "7.1.8",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "7.1.5",
"version": "7.1.8",
"type": "module",
"license": "MIT",
"scripts": {
+6 -6
View File
@@ -1,7 +1,7 @@
id = "kilo"
name = "Kilo"
description = "The open source coding agent."
version = "7.1.5"
version = "7.1.8"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/Kilo-Org/kilocode"
@@ -11,26 +11,26 @@ name = "Kilo"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.5/opencode-darwin-arm64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.8/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.5/opencode-darwin-x64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.8/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.5/opencode-linux-arm64.tar.gz"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.8/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.5/opencode-linux-x64.tar.gz"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.8/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.5/opencode-windows-x64.zip"
archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.8/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-docs",
"version": "7.1.5",
"version": "7.1.8",
"private": true,
"scripts": {
"dev": "next dev --webpack --port 3002",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-gateway",
"version": "7.1.5",
"version": "7.1.8",
"type": "module",
"license": "MIT",
"description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-i18n",
"version": "7.1.5",
"version": "7.1.8",
"type": "module",
"license": "MIT",
"description": "Kilo-specific i18n translations and overrides",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/kilo-telemetry",
"version": "7.1.5",
"version": "7.1.8",
"type": "module",
"license": "MIT",
"description": "Telemetry for Kilo CLI - PostHog analytics integration",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/kilo-ui",
"version": "7.1.5",
"version": "7.1.8",
"type": "module",
"license": "MIT",
"exports": {
@@ -1306,6 +1306,15 @@ function useToolReveal(pending: () => boolean, animate?: () => boolean) {
function WebfetchMeta(props: { url: string; animate?: boolean }) {
let ref: HTMLSpanElement | undefined
useToolFade(() => ref, { wipe: true, animate: props.animate })
const data = useData()
const open = (event: MouseEvent) => {
event.stopPropagation()
event.preventDefault()
const handler = data.openUrl
if (handler) return handler(props.url)
window.open(props.url, "_blank", "noopener,noreferrer")
}
return (
<span ref={ref} data-slot="webfetch-meta">
@@ -1316,11 +1325,11 @@ function WebfetchMeta(props: { url: string; animate?: boolean }) {
href={props.url}
target="_blank"
rel="noopener noreferrer"
onClick={(event) => event.stopPropagation()}
onClick={open}
>
{props.url}
</a>
<div data-component="tool-action">
<div data-component="tool-action" onClick={open} style={{ cursor: "pointer" }}>
<Icon name="square-arrow-top-right" size="small" />
</div>
</span>
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2025 Kilo Code
Copyright (c) 2025 opencode
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+2 -1
View File
@@ -2,7 +2,7 @@
"name": "kilo-code",
"displayName": "Kilo Code: AI Coding Agent, Copilot, and Autocomplete",
"description": "Open Source AI coding agent that generates code from natural language, automates tasks, and runs terminal commands. Features inline autocomplete, browser automation, automated refactoring, and custom modes for planning, coding, and debugging. Supports 500+ AI models including Claude (Anthropic), Gemini, Grok, GPT, Codex and GLM.",
"version": "7.1.5",
"version": "7.1.8",
"icon": "assets/icons/logo-outline-black.png",
"galleryBanner": {
"color": "#FFFFFF",
@@ -17,6 +17,7 @@
"engines": {
"vscode": "^1.105.1"
},
"license": "MIT",
"author": {
"name": "Kilo Code"
},
@@ -0,0 +1,158 @@
import { describe, it, expect } from "bun:test"
import { deepMerge, stripNulls, ConfigState } from "../../webview-ui/src/utils/config-utils"
import type { Config } from "../../webview-ui/src/types/messages"
// ---------------------------------------------------------------------------
// Pure helpers
// ---------------------------------------------------------------------------
describe("deepMerge", () => {
it("overrides scalar values", () => {
const target: Config = { snapshot: true }
const source: Partial<Config> = { snapshot: false }
expect(deepMerge(target, source)).toEqual({ snapshot: false })
})
it("merges nested objects recursively", () => {
const target: Config = { agent: { code: { temperature: 0.5 } } }
const source: Partial<Config> = { agent: { code: { steps: 10 } } }
const result = deepMerge(target, source)
expect(result.agent?.code?.temperature).toBe(0.5)
expect(result.agent?.code?.steps).toBe(10)
})
it("preserves keys not present in source", () => {
const target: Config = { snapshot: true, username: "alice" }
const source: Partial<Config> = { snapshot: false }
expect(deepMerge(target, source)).toEqual({ snapshot: false, username: "alice" })
})
it("replaces arrays instead of merging them", () => {
const target: Config = { instructions: ["a", "b"] }
const source: Partial<Config> = { instructions: ["c"] }
expect(deepMerge(target, source)).toEqual({ instructions: ["c"] })
})
})
describe("stripNulls", () => {
it("removes null values", () => {
const cfg = { snapshot: true, username: null } as unknown as Config
expect(stripNulls(cfg)).toEqual({ snapshot: true })
})
it("removes undefined values", () => {
const cfg = { snapshot: true, username: undefined } as unknown as Config
expect(stripNulls(cfg)).toEqual({ snapshot: true })
})
it("strips nulls recursively in nested objects", () => {
const cfg = { agent: { code: { temperature: 0.5, prompt: null } } } as unknown as Config
expect(stripNulls(cfg)).toEqual({ agent: { code: { temperature: 0.5 } } })
})
})
// ---------------------------------------------------------------------------
// Config state machine — reproduces the actual message-handler flow
// ---------------------------------------------------------------------------
describe("ConfigState", () => {
it("configLoaded sets config when no draft is pending", () => {
const s = new ConfigState()
s.handleConfigLoaded({ snapshot: true, username: "alice" })
expect(s.config).toEqual({ snapshot: true, username: "alice" })
expect(s.loading).toBe(false)
})
describe("configLoaded while draft is pending (the reported bug)", () => {
it("preserves the user's pending toggle change", () => {
const s = new ConfigState()
// 1. Server sends initial config
s.handleConfigLoaded({ snapshot: true, username: "alice" })
expect(s.config.snapshot).toBe(true)
// 2. User toggles snapshot off (but hasn't saved yet)
s.updateConfig({ snapshot: false })
expect(s.config.snapshot).toBe(false)
expect(s.dirty).toBe(true)
// 3. A configLoaded push arrives from the extension (e.g. SSE event,
// tab switch, or another webview triggers a config reload).
// The server still has snapshot: true.
s.handleConfigLoaded({ snapshot: true, username: "alice" })
// BUG (old code): config.snapshot would be reset to true here
// FIX: the draft is re-applied, so the user's toggle stays false
expect(s.config.snapshot).toBe(false)
expect(s.config.username).toBe("alice")
expect(s.dirty).toBe(true)
})
it("preserves nested draft changes across configLoaded pushes", () => {
const s = new ConfigState()
s.handleConfigLoaded({ agent: { code: { temperature: 0.7 } } })
s.updateConfig({ agent: { code: { steps: 5 } } })
// Server pushes a reload — temperature may have changed server-side
s.handleConfigLoaded({ agent: { code: { temperature: 0.9 } } })
expect(s.config.agent?.code?.steps).toBe(5)
expect(s.config.agent?.code?.temperature).toBe(0.9)
})
})
describe("configUpdated while draft is pending", () => {
it("preserves draft when update comes from another source", () => {
const s = new ConfigState()
s.handleConfigLoaded({ snapshot: true, username: "alice" })
s.updateConfig({ snapshot: false })
// Another webview (e.g. PermissionDock) saves a different setting
s.handleConfigUpdated({ snapshot: true, username: "bob" })
expect(s.config.snapshot).toBe(false) // draft preserved
expect(s.config.username).toBe("bob") // server update applied
})
it("clears draft when update confirms our save", () => {
const s = new ConfigState()
s.handleConfigLoaded({ snapshot: true })
s.updateConfig({ snapshot: false })
s.saveConfig()
expect(s.saving).toBe(true)
// Server confirms the write
s.handleConfigUpdated({ snapshot: false })
expect(s.config.snapshot).toBe(false)
expect(s.dirty).toBe(false)
expect(s.saving).toBe(false)
expect(Object.keys(s.draft).length).toBe(0)
})
})
it("configLoaded is ignored while save is in-flight", () => {
const s = new ConfigState()
s.handleConfigLoaded({ snapshot: true })
s.updateConfig({ snapshot: false })
s.saveConfig()
// A stale configLoaded arrives during the write round-trip
s.handleConfigLoaded({ snapshot: true })
// Config must not revert — the save is still in flight
expect(s.config.snapshot).toBe(false)
})
it("discardConfig restores server state", () => {
const s = new ConfigState()
s.handleConfigLoaded({ snapshot: true, username: "alice" })
s.updateConfig({ snapshot: false })
expect(s.dirty).toBe(true)
s.discardConfig()
expect(s.config.snapshot).toBe(true)
expect(s.dirty).toBe(false)
})
})
@@ -109,6 +109,10 @@ export const DataBridge: Component<{ children: any }> = (props) => {
vscode.postMessage({ type: "openFile", filePath, line, column })
}
const openUrl = (url: string) => {
vscode.postMessage({ type: "openExternal", url })
}
const directory = () => {
const dir = server.workspaceDirectory()
if (!dir) return ""
@@ -124,6 +128,7 @@ export const DataBridge: Component<{ children: any }> = (props) => {
onQuestionReply={reply}
onQuestionReject={reject}
onOpenFile={open}
onOpenUrl={openUrl}
>
{props.children}
</DataProvider>
@@ -13,6 +13,7 @@ import { useLanguage } from "../../context/language"
import type { AgentInfo, SkillInfo } from "../../types/messages"
import ModeEditView from "./ModeEditView"
import ModeCreateView from "./ModeCreateView"
import McpEditView from "./McpEditView"
import WorkflowsTab from "./agent-behaviour/WorkflowsTab"
import { parseImport, MAX_IMPORT_SIZE } from "./mode-io"
import type { ImportError } from "./mode-io"
@@ -71,6 +72,9 @@ const AgentBehaviourTab: Component = () => {
const [agentView, setAgentView] = createSignal<AgentView>("list")
const [editingAgent, setEditingAgent] = createSignal<string>("")
// MCP view state
const [editingMcp, setEditingMcp] = createSignal<string>("")
// Fetch skills whenever the skills subtab becomes active
createEffect(() => {
if (activeSubtab() === "skills") {
@@ -489,6 +493,19 @@ const AgentBehaviourTab: Component = () => {
setExpanded((prev) => ({ ...prev, [name]: !prev[name] }))
}
if (editingMcp()) {
return (
<McpEditView
name={editingMcp()}
onBack={() => setEditingMcp("")}
onRemove={(name) => {
confirmRemoveMcp(name)
setEditingMcp("")
}}
/>
)
}
return (
<div>
<Show
@@ -548,15 +565,26 @@ const AgentBehaviourTab: Component = () => {
{mcp.url ? "remote" : "stdio"}
</span>
</div>
<IconButton
size="small"
variant="ghost"
icon="close"
onClick={(e: MouseEvent) => {
e.stopPropagation()
confirmRemoveMcp(name)
}}
/>
<div style={{ display: "flex", gap: "4px" }}>
<IconButton
size="small"
variant="ghost"
icon="close"
onClick={(e: MouseEvent) => {
e.stopPropagation()
confirmRemoveMcp(name)
}}
/>
<IconButton
size="small"
variant="ghost"
icon="chevron-right"
onClick={(e: MouseEvent) => {
e.stopPropagation()
setEditingMcp(name)
}}
/>
</div>
</div>
{/* Expandable detail */}
@@ -916,11 +944,12 @@ const AgentBehaviourTab: Component = () => {
<button
onClick={() => {
setActiveSubtab(subtab.id)
// Reset agent view when switching subtabs
// Reset views when switching subtabs
if (subtab.id === "agents") {
setAgentView("list")
setEditingAgent("")
}
setEditingMcp("")
}}
style={{
padding: "8px 16px",
@@ -0,0 +1,221 @@
import { Component, Show, createMemo, createSignal, For } from "solid-js"
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"
import { useLanguage } from "../../context/language"
import type { McpConfig } from "../../types/messages"
import SettingsRow from "./SettingsRow"
interface Props {
name: string
onBack: () => void
onRemove: (name: string) => void
}
const McpEditView: Component<Props> = (props) => {
const language = useLanguage()
const { config, updateConfig } = useConfig()
const cfg = createMemo<McpConfig>(() => config().mcp?.[props.name] ?? {})
const [envKey, setEnvKey] = createSignal("")
const [envVal, setEnvVal] = createSignal("")
const update = (partial: Partial<McpConfig>) => {
const existing = config().mcp ?? {}
const current = existing[props.name] ?? {}
updateConfig({
mcp: { ...existing, [props.name]: { ...current, ...partial } },
})
}
const transport = () => cfg().type ?? (cfg().url ? "remote" : "local")
const cmd = () => {
const c = cfg().command
if (Array.isArray(c)) return c[0] ?? ""
return c ?? ""
}
const args = () => {
const c = cfg().command
if (Array.isArray(c)) return c.slice(1).join("\n")
return ""
}
const env = createMemo(() => Object.entries(cfg().environment ?? cfg().env ?? {}))
const addEnv = () => {
const key = envKey().trim()
const val = envVal().trim()
if (!key) return
const existing = cfg().environment ?? cfg().env ?? {}
update({ environment: { ...existing, [key]: val } })
setEnvKey("")
setEnvVal("")
}
const removeEnv = (key: string) => {
const existing = { ...(cfg().environment ?? cfg().env ?? {}) }
delete existing[key]
update({ environment: existing })
}
return (
<div>
<div
style={{
display: "flex",
"align-items": "center",
"justify-content": "space-between",
"margin-bottom": "16px",
}}
>
<div style={{ display: "flex", "align-items": "center" }}>
<IconButton size="small" variant="ghost" icon="arrow-left" onClick={props.onBack} />
<span style={{ "font-weight": "600", "font-size": "14px", "margin-left": "8px" }}>
{language.t("settings.agentBehaviour.editMcp")} {props.name}
</span>
</div>
<IconButton size="small" variant="ghost" icon="close" onClick={() => props.onRemove(props.name)} />
</div>
{/* Transport info */}
<Card style={{ "margin-bottom": "12px" }}>
<div
style={{
"font-size": "12px",
color: "var(--text-weak-base, var(--vscode-descriptionForeground))",
padding: "4px 0",
}}
>
{transport() === "local"
? language.t("settings.agentBehaviour.editMcp.transportLocal")
: language.t("settings.agentBehaviour.editMcp.transportRemote")}
</div>
</Card>
{/* Command / URL */}
<Show when={transport() === "local"}>
<Card style={{ "margin-bottom": "12px" }}>
<div data-slot="settings-row-label-title" style={{ "margin-bottom": "8px" }}>
{language.t("settings.agentBehaviour.addMcp.command")}
</div>
<TextField
value={cmd()}
placeholder={language.t("settings.agentBehaviour.addMcp.command.placeholder")}
onChange={(val) => {
const existing = cfg().command
const rest = Array.isArray(existing) ? existing.slice(1) : []
update({ command: [val.trim(), ...rest] })
}}
/>
</Card>
<Card style={{ "margin-bottom": "12px" }}>
<div data-slot="settings-row-label-title" style={{ "margin-bottom": "4px" }}>
{language.t("settings.agentBehaviour.addMcp.args")}
</div>
<div data-slot="settings-row-label-subtitle" style={{ "margin-bottom": "8px" }}>
{language.t("settings.agentBehaviour.addMcp.args.help")}
</div>
<TextField
value={args()}
placeholder={language.t("settings.agentBehaviour.addMcp.args.placeholder")}
multiline
onChange={(val) => {
const parts = val.split(/\n/).filter(Boolean)
update({ command: [cmd(), ...parts] })
}}
/>
</Card>
</Show>
<Show when={transport() === "remote"}>
<Card style={{ "margin-bottom": "12px" }}>
<div data-slot="settings-row-label-title" style={{ "margin-bottom": "8px" }}>
{language.t("settings.agentBehaviour.addMcp.url")}
</div>
<TextField
value={cfg().url ?? ""}
placeholder={language.t("settings.agentBehaviour.addMcp.url.placeholder")}
onChange={(val) => update({ url: val.trim() || undefined })}
/>
</Card>
</Show>
{/* Environment variables (local servers only) */}
<Show when={transport() === "local"}>
<Card style={{ "margin-bottom": "12px" }}>
<div data-slot="settings-row-label-title" style={{ "margin-bottom": "4px" }}>
{language.t("settings.agentBehaviour.editMcp.env")}
</div>
<div data-slot="settings-row-label-subtitle" style={{ "margin-bottom": "8px" }}>
{language.t("settings.agentBehaviour.editMcp.env.help")}
</div>
<div
style={{
display: "flex",
gap: "8px",
"align-items": "center",
padding: "8px 0",
"border-bottom": env().length > 0 ? "1px solid var(--border-weak-base)" : "none",
}}
>
<div style={{ flex: 1 }}>
<TextField value={envKey()} placeholder="KEY" onChange={(val) => setEnvKey(val)} />
</div>
<div style={{ flex: 1 }}>
<TextField
value={envVal()}
placeholder="value"
onChange={(val) => setEnvVal(val)}
onKeyDown={(e: KeyboardEvent) => {
if (e.key === "Enter") addEnv()
}}
/>
</div>
<Button variant="secondary" onClick={addEnv}>
{language.t("common.add")}
</Button>
</div>
<For each={env()}>
{([key, val], index) => (
<div
style={{
display: "flex",
"align-items": "center",
"justify-content": "space-between",
padding: "6px 0",
"border-bottom": index() < env().length - 1 ? "1px solid var(--border-weak-base)" : "none",
}}
>
<span
style={{
"font-family": "var(--vscode-editor-font-family, monospace)",
"font-size": "12px",
}}
>
{key}={val}
</span>
<IconButton size="small" variant="ghost" icon="close" onClick={() => removeEnv(key)} />
</div>
)}
</For>
</Card>
</Show>
<div style={{ display: "flex", "justify-content": "flex-end" }}>
<Button variant="ghost" onClick={props.onBack}>
{language.t("settings.agentBehaviour.editMode.back")}
</Button>
</div>
</div>
)
}
export default McpEditView
@@ -11,6 +11,7 @@
import { createContext, useContext, createSignal, onCleanup, ParentComponent, Accessor } from "solid-js"
import { useVSCode } from "./vscode"
import type { Config, ExtensionMessage } from "../types/messages"
import { deepMerge, stripNulls, resolveConfig } from "../utils/config-utils"
interface ConfigContextValue {
config: Accessor<Config>
@@ -21,37 +22,6 @@ interface ConfigContextValue {
discardConfig: () => void
}
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === "object" && !Array.isArray(value)
}
/** Deep merge two objects, with source values overriding target values. */
function deepMerge(target: Config, source: Partial<Config>): Config {
const result: Record<string, unknown> = { ...target }
for (const [key, value] of Object.entries(source)) {
if (isRecord(value) && isRecord(result[key])) {
result[key] = deepMerge(result[key] as Config, value as Partial<Config>)
} else {
result[key] = value
}
}
return result as Config
}
/** Recursively remove keys whose value is null (null = "deleted"). */
function stripNulls(obj: Config): Config {
const result: Record<string, unknown> = {}
for (const [key, value] of Object.entries(obj)) {
if (value === null || value === undefined) continue
if (isRecord(value)) {
result[key] = stripNulls(value as Config)
} else {
result[key] = value
}
}
return result as Config
}
export const ConfigContext = createContext<ConfigContextValue>()
export const ConfigProvider: ParentComponent = (props) => {
@@ -74,7 +44,9 @@ export const ConfigProvider: ParentComponent = (props) => {
// Skip if a save is in-flight — a stale configLoaded must not overwrite
// the optimistically-updated state while the write is being confirmed.
if (saving) return
setConfig(message.config)
// Re-apply the draft on top so pending changes (e.g. a toggled switch the
// user hasn't saved yet) stay visible instead of snapping back.
setConfig(resolveConfig(message.config, draft(), isDirty()))
setSaved(message.config)
setLoading(false)
return
@@ -90,11 +62,7 @@ export const ConfigProvider: ParentComponent = (props) => {
} else {
// configUpdated from a different source (e.g. PermissionDock save).
// Re-apply the draft on top so pending settings changes are preserved.
if (isDirty()) {
setConfig(stripNulls(deepMerge(message.config, draft())))
} else {
setConfig(message.config)
}
setConfig(resolveConfig(message.config, draft(), isDirty()))
}
setSaved(message.config)
return
@@ -1086,6 +1086,19 @@ export const dict = {
"settings.agentBehaviour.removeMcp.title": "إزالة خادم MCP",
"settings.agentBehaviour.removeMcp.confirm": 'هل تريد إزالة خادم MCP "{{name}}"؟ سيؤدي هذا إلى إزالته من الإعدادات.',
"settings.agentBehaviour.removeMcp.button": "إزالة",
"settings.agentBehaviour.editMcp": "تعديل خادم MCP",
"settings.agentBehaviour.editMcp.transportLocal": "خادم محلي (نقل stdio)",
"settings.agentBehaviour.editMcp.transportRemote": "خادم بعيد (نقل SSE/HTTP)",
"settings.agentBehaviour.editMcp.env": "متغيرات البيئة",
"settings.agentBehaviour.editMcp.env.help": "المتغيرات التي يتم تمريرها إلى عملية خادم MCP.",
"settings.agentBehaviour.addMcp.command": "الأمر",
"settings.agentBehaviour.addMcp.command.placeholder": "e.g. npx",
"settings.agentBehaviour.addMcp.args": "الوسائط",
"settings.agentBehaviour.addMcp.args.help":
"وسيطة واحدة لكل سطر. يتم الحفاظ على المسارات التي تحتوي على مسافات كما هي.",
"settings.agentBehaviour.addMcp.args.placeholder": "e.g.\n-y\n@modelcontextprotocol/server-filesystem\n/tmp",
"settings.agentBehaviour.addMcp.url": "عنوان URL للخادم",
"settings.agentBehaviour.addMcp.url.placeholder": "e.g. http://localhost:3000/sse",
"settings.agentBehaviour.skillPaths": "مسارات مجلدات المهارات",
"settings.agentBehaviour.skillUrls": "عناوين URL للمهارات",
"settings.agentBehaviour.removeSkill.title": "إزالة المهارة",
@@ -1102,6 +1102,19 @@ export const dict = {
"settings.agentBehaviour.removeMcp.confirm":
'Remover o servidor MCP "{{name}}"? Isso o removerá da sua configuração.',
"settings.agentBehaviour.removeMcp.button": "Remover",
"settings.agentBehaviour.editMcp": "Editar Servidor MCP",
"settings.agentBehaviour.editMcp.transportLocal": "Servidor local (transporte stdio)",
"settings.agentBehaviour.editMcp.transportRemote": "Servidor remoto (transporte SSE/HTTP)",
"settings.agentBehaviour.editMcp.env": "Variáveis de Ambiente",
"settings.agentBehaviour.editMcp.env.help": "Variáveis passadas para o processo do servidor MCP.",
"settings.agentBehaviour.addMcp.command": "Comando",
"settings.agentBehaviour.addMcp.command.placeholder": "e.g. npx",
"settings.agentBehaviour.addMcp.args": "Argumentos",
"settings.agentBehaviour.addMcp.args.help":
"Um argumento por linha. Caminhos com espaços são preservados como estão.",
"settings.agentBehaviour.addMcp.args.placeholder": "e.g.\n-y\n@modelcontextprotocol/server-filesystem\n/tmp",
"settings.agentBehaviour.addMcp.url": "URL do Servidor",
"settings.agentBehaviour.addMcp.url.placeholder": "e.g. http://localhost:3000/sse",
"settings.agentBehaviour.skillPaths": "Caminhos de pastas de habilidades",
"settings.agentBehaviour.skillUrls": "URLs de habilidades",
"settings.agentBehaviour.removeSkill.title": "Remover habilidade",
@@ -1101,6 +1101,18 @@ export const dict = {
"settings.agentBehaviour.removeMcp.confirm":
'Ukloniti MCP server "{{name}}"? Ovo će ga ukloniti iz vaše konfiguracije.',
"settings.agentBehaviour.removeMcp.button": "Ukloni",
"settings.agentBehaviour.editMcp": "Uredi MCP Server",
"settings.agentBehaviour.editMcp.transportLocal": "Lokalni server (stdio transport)",
"settings.agentBehaviour.editMcp.transportRemote": "Udaljeni server (SSE/HTTP transport)",
"settings.agentBehaviour.editMcp.env": "Varijable okruženja",
"settings.agentBehaviour.editMcp.env.help": "Varijable proslijeđene procesu MCP servera.",
"settings.agentBehaviour.addMcp.command": "Naredba",
"settings.agentBehaviour.addMcp.command.placeholder": "e.g. npx",
"settings.agentBehaviour.addMcp.args": "Argumenti",
"settings.agentBehaviour.addMcp.args.help": "Jedan argument po liniji. Putanje s razmacima se čuvaju kakve jesu.",
"settings.agentBehaviour.addMcp.args.placeholder": "e.g.\n-y\n@modelcontextprotocol/server-filesystem\n/tmp",
"settings.agentBehaviour.addMcp.url": "URL servera",
"settings.agentBehaviour.addMcp.url.placeholder": "e.g. http://localhost:3000/sse",
"settings.agentBehaviour.skillPaths": "Putanje mapa vještina",
"settings.agentBehaviour.skillUrls": "URL-ovi vještina",
"settings.agentBehaviour.removeSkill.title": "Ukloni vještinu",
@@ -1095,6 +1095,18 @@ export const dict = {
"settings.agentBehaviour.removeMcp.confirm":
'Vil du fjerne MCP-serveren "{{name}}"? Dette vil fjerne den fra din konfiguration.',
"settings.agentBehaviour.removeMcp.button": "Fjern",
"settings.agentBehaviour.editMcp": "Rediger MCP-server",
"settings.agentBehaviour.editMcp.transportLocal": "Lokal server (stdio-transport)",
"settings.agentBehaviour.editMcp.transportRemote": "Fjernserver (SSE/HTTP-transport)",
"settings.agentBehaviour.editMcp.env": "Miljøvariabler",
"settings.agentBehaviour.editMcp.env.help": "Variabler der sendes til MCP-serverprocessen.",
"settings.agentBehaviour.addMcp.command": "Kommando",
"settings.agentBehaviour.addMcp.command.placeholder": "e.g. npx",
"settings.agentBehaviour.addMcp.args": "Argumenter",
"settings.agentBehaviour.addMcp.args.help": "Ét argument per linje. Stier med mellemrum bevares som de er.",
"settings.agentBehaviour.addMcp.args.placeholder": "e.g.\n-y\n@modelcontextprotocol/server-filesystem\n/tmp",
"settings.agentBehaviour.addMcp.url": "Server-URL",
"settings.agentBehaviour.addMcp.url.placeholder": "e.g. http://localhost:3000/sse",
"settings.agentBehaviour.skillPaths": "Skill-mappestier",
"settings.agentBehaviour.skillUrls": "Skill-URL'er",
"settings.agentBehaviour.removeSkill.title": "Fjern færdighed",
@@ -1115,6 +1115,19 @@ export const dict = {
"settings.agentBehaviour.removeMcp.confirm":
'MCP-Server "{{name}}" entfernen? Dadurch wird er aus Ihrer Konfiguration entfernt.',
"settings.agentBehaviour.removeMcp.button": "Entfernen",
"settings.agentBehaviour.editMcp": "MCP-Server bearbeiten",
"settings.agentBehaviour.editMcp.transportLocal": "Lokaler Server (stdio-Transport)",
"settings.agentBehaviour.editMcp.transportRemote": "Remote-Server (SSE/HTTP-Transport)",
"settings.agentBehaviour.editMcp.env": "Umgebungsvariablen",
"settings.agentBehaviour.editMcp.env.help": "Variablen, die an den MCP-Serverprozess übergeben werden.",
"settings.agentBehaviour.addMcp.command": "Befehl",
"settings.agentBehaviour.addMcp.command.placeholder": "e.g. npx",
"settings.agentBehaviour.addMcp.args": "Argumente",
"settings.agentBehaviour.addMcp.args.help":
"Ein Argument pro Zeile. Pfade mit Leerzeichen werden unverändert übernommen.",
"settings.agentBehaviour.addMcp.args.placeholder": "e.g.\n-y\n@modelcontextprotocol/server-filesystem\n/tmp",
"settings.agentBehaviour.addMcp.url": "Server-URL",
"settings.agentBehaviour.addMcp.url.placeholder": "e.g. http://localhost:3000/sse",
"settings.agentBehaviour.skillPaths": "Skill-Ordnerpfade",
"settings.agentBehaviour.skillUrls": "Skill-URLs",
"settings.agentBehaviour.removeSkill.title": "Skill entfernen",
@@ -1083,6 +1083,18 @@ export const dict = {
"settings.agentBehaviour.mcpDetail.args": "Arguments",
"settings.agentBehaviour.mcpDetail.env": "Environment",
"settings.agentBehaviour.mcpDetail.disabled": "This server is disabled.",
"settings.agentBehaviour.editMcp": "Edit MCP Server",
"settings.agentBehaviour.editMcp.transportLocal": "Local server (stdio transport)",
"settings.agentBehaviour.editMcp.transportRemote": "Remote server (SSE/HTTP transport)",
"settings.agentBehaviour.editMcp.env": "Environment Variables",
"settings.agentBehaviour.editMcp.env.help": "Variables passed to the MCP server process.",
"settings.agentBehaviour.addMcp.command": "Command",
"settings.agentBehaviour.addMcp.command.placeholder": "e.g. npx",
"settings.agentBehaviour.addMcp.args": "Arguments",
"settings.agentBehaviour.addMcp.args.help": "One argument per line. Paths with spaces are preserved as-is.",
"settings.agentBehaviour.addMcp.args.placeholder": "e.g.\n-y\n@modelcontextprotocol/server-filesystem\n/tmp",
"settings.agentBehaviour.addMcp.url": "Server URL",
"settings.agentBehaviour.addMcp.url.placeholder": "e.g. http://localhost:3000/sse",
"settings.agentBehaviour.mcpEmpty": "No MCP servers configured. Edit the opencode config file to add MCP servers.",
"settings.agentBehaviour.workflowsPlaceholder": "Workflows are managed via workflow files in your workspace.",
"settings.agentBehaviour.workflows.description":
@@ -1106,6 +1106,18 @@ export const dict = {
"settings.agentBehaviour.removeMcp.confirm":
'¿Eliminar el servidor MCP "{{name}}"? Esto lo eliminará de su configuración.',
"settings.agentBehaviour.removeMcp.button": "Eliminar",
"settings.agentBehaviour.editMcp": "Editar servidor MCP",
"settings.agentBehaviour.editMcp.transportLocal": "Servidor local (transporte stdio)",
"settings.agentBehaviour.editMcp.transportRemote": "Servidor remoto (transporte SSE/HTTP)",
"settings.agentBehaviour.editMcp.env": "Variables de entorno",
"settings.agentBehaviour.editMcp.env.help": "Variables pasadas al proceso del servidor MCP.",
"settings.agentBehaviour.addMcp.command": "Comando",
"settings.agentBehaviour.addMcp.command.placeholder": "e.g. npx",
"settings.agentBehaviour.addMcp.args": "Argumentos",
"settings.agentBehaviour.addMcp.args.help": "Un argumento por línea. Las rutas con espacios se conservan tal cual.",
"settings.agentBehaviour.addMcp.args.placeholder": "e.g.\n-y\n@modelcontextprotocol/server-filesystem\n/tmp",
"settings.agentBehaviour.addMcp.url": "URL del servidor",
"settings.agentBehaviour.addMcp.url.placeholder": "e.g. http://localhost:3000/sse",
"settings.agentBehaviour.skillPaths": "Rutas de carpetas de habilidades",
"settings.agentBehaviour.skillUrls": "URLs de habilidades",
"settings.agentBehaviour.removeSkill.title": "Eliminar habilidad",
@@ -1119,6 +1119,19 @@ export const dict = {
"settings.agentBehaviour.removeMcp.confirm":
'Supprimer le serveur MCP "{{name}}" ? Cela le supprimera de votre configuration.',
"settings.agentBehaviour.removeMcp.button": "Supprimer",
"settings.agentBehaviour.editMcp": "Modifier le serveur MCP",
"settings.agentBehaviour.editMcp.transportLocal": "Serveur local (transport stdio)",
"settings.agentBehaviour.editMcp.transportRemote": "Serveur distant (transport SSE/HTTP)",
"settings.agentBehaviour.editMcp.env": "Variables d'environnement",
"settings.agentBehaviour.editMcp.env.help": "Variables transmises au processus du serveur MCP.",
"settings.agentBehaviour.addMcp.command": "Commande",
"settings.agentBehaviour.addMcp.command.placeholder": "e.g. npx",
"settings.agentBehaviour.addMcp.args": "Arguments",
"settings.agentBehaviour.addMcp.args.help":
"Un argument par ligne. Les chemins avec des espaces sont conservés tels quels.",
"settings.agentBehaviour.addMcp.args.placeholder": "e.g.\n-y\n@modelcontextprotocol/server-filesystem\n/tmp",
"settings.agentBehaviour.addMcp.url": "URL du serveur",
"settings.agentBehaviour.addMcp.url.placeholder": "e.g. http://localhost:3000/sse",
"settings.agentBehaviour.skillPaths": "Chemins des dossiers de compétences",
"settings.agentBehaviour.skillUrls": "URLs de compétences",
"settings.agentBehaviour.removeSkill.title": "Supprimer la compétence",
@@ -1096,6 +1096,18 @@ export const dict = {
"settings.agentBehaviour.removeMcp.title": "MCPサーバーを削除",
"settings.agentBehaviour.removeMcp.confirm": 'MCPサーバー "{{name}}" を削除しますか?設定から削除されます。',
"settings.agentBehaviour.removeMcp.button": "削除",
"settings.agentBehaviour.editMcp": "MCPサーバーを編集",
"settings.agentBehaviour.editMcp.transportLocal": "ローカルサーバー(stdio トランスポート)",
"settings.agentBehaviour.editMcp.transportRemote": "リモートサーバー(SSE/HTTP トランスポート)",
"settings.agentBehaviour.editMcp.env": "環境変数",
"settings.agentBehaviour.editMcp.env.help": "MCPサーバープロセスに渡される変数。",
"settings.agentBehaviour.addMcp.command": "コマンド",
"settings.agentBehaviour.addMcp.command.placeholder": "e.g. npx",
"settings.agentBehaviour.addMcp.args": "引数",
"settings.agentBehaviour.addMcp.args.help": "1行に1つの引数。スペースを含むパスはそのまま保持されます。",
"settings.agentBehaviour.addMcp.args.placeholder": "e.g.\n-y\n@modelcontextprotocol/server-filesystem\n/tmp",
"settings.agentBehaviour.addMcp.url": "サーバーURL",
"settings.agentBehaviour.addMcp.url.placeholder": "e.g. http://localhost:3000/sse",
"settings.agentBehaviour.skillPaths": "スキルフォルダパス",
"settings.agentBehaviour.skillUrls": "スキルURL",
"settings.agentBehaviour.removeSkill.title": "スキルを削除",
@@ -1090,6 +1090,18 @@ export const dict = {
"settings.agentBehaviour.removeMcp.title": "MCP 서버 제거",
"settings.agentBehaviour.removeMcp.confirm": 'MCP 서버 "{{name}}"을(를) 제거하시겠습니까? 구성에서 제거됩니다.',
"settings.agentBehaviour.removeMcp.button": "제거",
"settings.agentBehaviour.editMcp": "MCP 서버 편집",
"settings.agentBehaviour.editMcp.transportLocal": "로컬 서버 (stdio 전송)",
"settings.agentBehaviour.editMcp.transportRemote": "원격 서버 (SSE/HTTP 전송)",
"settings.agentBehaviour.editMcp.env": "환경 변수",
"settings.agentBehaviour.editMcp.env.help": "MCP 서버 프로세스에 전달되는 변수입니다.",
"settings.agentBehaviour.addMcp.command": "명령어",
"settings.agentBehaviour.addMcp.command.placeholder": "e.g. npx",
"settings.agentBehaviour.addMcp.args": "인수",
"settings.agentBehaviour.addMcp.args.help": "한 줄에 하나의 인수. 공백이 포함된 경로는 그대로 유지됩니다.",
"settings.agentBehaviour.addMcp.args.placeholder": "e.g.\n-y\n@modelcontextprotocol/server-filesystem\n/tmp",
"settings.agentBehaviour.addMcp.url": "서버 URL",
"settings.agentBehaviour.addMcp.url.placeholder": "e.g. http://localhost:3000/sse",
"settings.agentBehaviour.skillPaths": "스킬 폴더 경로",
"settings.agentBehaviour.skillUrls": "스킬 URL",
"settings.agentBehaviour.removeSkill.title": "스킬 제거",
@@ -1097,6 +1097,19 @@ export const dict = {
"settings.agentBehaviour.mcpDetail.args": "Argumenten",
"settings.agentBehaviour.mcpDetail.env": "Omgeving",
"settings.agentBehaviour.mcpDetail.disabled": "Deze server is uitgeschakeld.",
"settings.agentBehaviour.editMcp": "MCP-server bewerken",
"settings.agentBehaviour.editMcp.transportLocal": "Lokale server (stdio-transport)",
"settings.agentBehaviour.editMcp.transportRemote": "Externe server (SSE/HTTP-transport)",
"settings.agentBehaviour.editMcp.env": "Omgevingsvariabelen",
"settings.agentBehaviour.editMcp.env.help": "Variabelen die worden doorgegeven aan het MCP-serverproces.",
"settings.agentBehaviour.addMcp.command": "Opdracht",
"settings.agentBehaviour.addMcp.command.placeholder": "e.g. npx",
"settings.agentBehaviour.addMcp.args": "Argumenten",
"settings.agentBehaviour.addMcp.args.help":
"Eén argument per regel. Paden met spaties worden ongewijzigd overgenomen.",
"settings.agentBehaviour.addMcp.args.placeholder": "e.g.\n-y\n@modelcontextprotocol/server-filesystem\n/tmp",
"settings.agentBehaviour.addMcp.url": "Server-URL",
"settings.agentBehaviour.addMcp.url.placeholder": "e.g. http://localhost:3000/sse",
"settings.agentBehaviour.mcpEmpty":
"Geen MCP-servers geconfigureerd. Bewerk het opencode configuratiebestand om MCP-servers toe te voegen.",
"settings.agentBehaviour.workflowsPlaceholder": "Workflows worden beheerd via workflowbestanden in je workspace.",
@@ -1098,6 +1098,18 @@ export const dict = {
"settings.agentBehaviour.removeMcp.confirm":
'Vil du fjerne MCP-serveren "{{name}}"? Dette vil fjerne den fra konfigurasjonen din.',
"settings.agentBehaviour.removeMcp.button": "Fjern",
"settings.agentBehaviour.editMcp": "Rediger MCP-server",
"settings.agentBehaviour.editMcp.transportLocal": "Lokal server (stdio-transport)",
"settings.agentBehaviour.editMcp.transportRemote": "Ekstern server (SSE/HTTP-transport)",
"settings.agentBehaviour.editMcp.env": "Miljøvariabler",
"settings.agentBehaviour.editMcp.env.help": "Variabler som sendes til MCP-serverprosessen.",
"settings.agentBehaviour.addMcp.command": "Kommando",
"settings.agentBehaviour.addMcp.command.placeholder": "e.g. npx",
"settings.agentBehaviour.addMcp.args": "Argumenter",
"settings.agentBehaviour.addMcp.args.help": "Ett argument per linje. Stier med mellomrom bevares som de er.",
"settings.agentBehaviour.addMcp.args.placeholder": "e.g.\n-y\n@modelcontextprotocol/server-filesystem\n/tmp",
"settings.agentBehaviour.addMcp.url": "Server-URL",
"settings.agentBehaviour.addMcp.url.placeholder": "e.g. http://localhost:3000/sse",
"settings.agentBehaviour.skillPaths": "Ferdighetsmappe-stier",
"settings.agentBehaviour.skillUrls": "Ferdighets-URLer",
"settings.agentBehaviour.removeSkill.title": "Fjern ferdighet",
@@ -1100,6 +1100,18 @@ export const dict = {
"settings.agentBehaviour.removeMcp.confirm":
'Usunąć serwer MCP "{{name}}"? Spowoduje to usunięcie go z konfiguracji.',
"settings.agentBehaviour.removeMcp.button": "Usuń",
"settings.agentBehaviour.editMcp": "Edytuj serwer MCP",
"settings.agentBehaviour.editMcp.transportLocal": "Serwer lokalny (transport stdio)",
"settings.agentBehaviour.editMcp.transportRemote": "Serwer zdalny (transport SSE/HTTP)",
"settings.agentBehaviour.editMcp.env": "Zmienne środowiskowe",
"settings.agentBehaviour.editMcp.env.help": "Zmienne przekazywane do procesu serwera MCP.",
"settings.agentBehaviour.addMcp.command": "Polecenie",
"settings.agentBehaviour.addMcp.command.placeholder": "e.g. npx",
"settings.agentBehaviour.addMcp.args": "Argumenty",
"settings.agentBehaviour.addMcp.args.help": "Jeden argument na linię. Ścieżki ze spacjami są zachowywane bez zmian.",
"settings.agentBehaviour.addMcp.args.placeholder": "e.g.\n-y\n@modelcontextprotocol/server-filesystem\n/tmp",
"settings.agentBehaviour.addMcp.url": "URL serwera",
"settings.agentBehaviour.addMcp.url.placeholder": "e.g. http://localhost:3000/sse",
"settings.agentBehaviour.skillPaths": "Ścieżki folderów umiejętności",
"settings.agentBehaviour.skillUrls": "Adresy URL umiejętności",
"settings.agentBehaviour.removeSkill.title": "Usuń umiejętność",
@@ -1103,6 +1103,18 @@ export const dict = {
"settings.agentBehaviour.removeMcp.title": "Удалить сервер MCP",
"settings.agentBehaviour.removeMcp.confirm": 'Удалить сервер MCP "{{name}}"? Это удалит его из вашей конфигурации.',
"settings.agentBehaviour.removeMcp.button": "Удалить",
"settings.agentBehaviour.editMcp": "Редактировать сервер MCP",
"settings.agentBehaviour.editMcp.transportLocal": "Локальный сервер (транспорт stdio)",
"settings.agentBehaviour.editMcp.transportRemote": "Удалённый сервер (транспорт SSE/HTTP)",
"settings.agentBehaviour.editMcp.env": "Переменные окружения",
"settings.agentBehaviour.editMcp.env.help": "Переменные, передаваемые процессу сервера MCP.",
"settings.agentBehaviour.addMcp.command": "Команда",
"settings.agentBehaviour.addMcp.command.placeholder": "e.g. npx",
"settings.agentBehaviour.addMcp.args": "Аргументы",
"settings.agentBehaviour.addMcp.args.help": "Один аргумент на строку. Пути с пробелами сохраняются как есть.",
"settings.agentBehaviour.addMcp.args.placeholder": "e.g.\n-y\n@modelcontextprotocol/server-filesystem\n/tmp",
"settings.agentBehaviour.addMcp.url": "URL сервера",
"settings.agentBehaviour.addMcp.url.placeholder": "e.g. http://localhost:3000/sse",
"settings.agentBehaviour.skillPaths": "Пути папок навыков",
"settings.agentBehaviour.skillUrls": "URL навыков",
"settings.agentBehaviour.removeSkill.title": "Удалить навык",
@@ -1087,6 +1087,18 @@ export const dict = {
"settings.agentBehaviour.removeMcp.confirm":
'ต้องการลบเซิร์ฟเวอร์ MCP "{{name}}" หรือไม่? การดำเนินการนี้จะลบออกจากการกำหนดค่าของคุณ',
"settings.agentBehaviour.removeMcp.button": "ลบ",
"settings.agentBehaviour.editMcp": "แก้ไขเซิร์ฟเวอร์ MCP",
"settings.agentBehaviour.editMcp.transportLocal": "เซิร์ฟเวอร์ภายใน (การขนส่ง stdio)",
"settings.agentBehaviour.editMcp.transportRemote": "เซิร์ฟเวอร์ระยะไกล (การขนส่ง SSE/HTTP)",
"settings.agentBehaviour.editMcp.env": "ตัวแปรสภาพแวดล้อม",
"settings.agentBehaviour.editMcp.env.help": "ตัวแปรที่ส่งไปยังกระบวนการเซิร์ฟเวอร์ MCP",
"settings.agentBehaviour.addMcp.command": "คำสั่ง",
"settings.agentBehaviour.addMcp.command.placeholder": "e.g. npx",
"settings.agentBehaviour.addMcp.args": "อาร์กิวเมนต์",
"settings.agentBehaviour.addMcp.args.help": "หนึ่งอาร์กิวเมนต์ต่อบรรทัด เส้นทางที่มีช่องว่างจะถูกเก็บรักษาตามเดิม",
"settings.agentBehaviour.addMcp.args.placeholder": "e.g.\n-y\n@modelcontextprotocol/server-filesystem\n/tmp",
"settings.agentBehaviour.addMcp.url": "URL เซิร์ฟเวอร์",
"settings.agentBehaviour.addMcp.url.placeholder": "e.g. http://localhost:3000/sse",
"settings.agentBehaviour.skillPaths": "เส้นทางโฟลเดอร์ทักษะ",
"settings.agentBehaviour.skillUrls": "URL ทักษะ",
"settings.agentBehaviour.removeSkill.title": "ลบทักษะ",
@@ -1096,6 +1096,18 @@ export const dict = {
"settings.agentBehaviour.mcpDetail.args": "Argümanlar",
"settings.agentBehaviour.mcpDetail.env": "Ortam",
"settings.agentBehaviour.mcpDetail.disabled": "Bu sunucu devre dışı.",
"settings.agentBehaviour.editMcp": "MCP Sunucusunu Düzenle",
"settings.agentBehaviour.editMcp.transportLocal": "Yerel sunucu (stdio taşıma)",
"settings.agentBehaviour.editMcp.transportRemote": "Uzak sunucu (SSE/HTTP taşıma)",
"settings.agentBehaviour.editMcp.env": "Ortam Değişkenleri",
"settings.agentBehaviour.editMcp.env.help": "MCP sunucu sürecine aktarılan değişkenler.",
"settings.agentBehaviour.addMcp.command": "Komut",
"settings.agentBehaviour.addMcp.command.placeholder": "e.g. npx",
"settings.agentBehaviour.addMcp.args": "Argümanlar",
"settings.agentBehaviour.addMcp.args.help": "Satır başına bir argüman. Boşluklu yollar olduğu gibi korunur.",
"settings.agentBehaviour.addMcp.args.placeholder": "e.g.\n-y\n@modelcontextprotocol/server-filesystem\n/tmp",
"settings.agentBehaviour.addMcp.url": "Sunucu URL'si",
"settings.agentBehaviour.addMcp.url.placeholder": "e.g. http://localhost:3000/sse",
"settings.agentBehaviour.mcpEmpty":
"Yapılandırılmış MCP sunucusu yok. MCP sunucuları eklemek için opencode yapılandırma dosyasını düzenleyin.",
"settings.agentBehaviour.workflowsPlaceholder": "İş akışları çalışma alanınızdaki iş akışı dosyaları ile yönetilir.",
@@ -1076,6 +1076,18 @@ export const dict = {
"settings.agentBehaviour.removeMcp.title": "移除 MCP 服务器",
"settings.agentBehaviour.removeMcp.confirm": '移除 MCP 服务器 "{{name}}" 吗?这将从您的配置中移除它。',
"settings.agentBehaviour.removeMcp.button": "移除",
"settings.agentBehaviour.editMcp": "编辑 MCP 服务器",
"settings.agentBehaviour.editMcp.transportLocal": "本地服务器(stdio 传输)",
"settings.agentBehaviour.editMcp.transportRemote": "远程服务器(SSE/HTTP 传输)",
"settings.agentBehaviour.editMcp.env": "环境变量",
"settings.agentBehaviour.editMcp.env.help": "传递给 MCP 服务器进程的变量。",
"settings.agentBehaviour.addMcp.command": "命令",
"settings.agentBehaviour.addMcp.command.placeholder": "e.g. npx",
"settings.agentBehaviour.addMcp.args": "参数",
"settings.agentBehaviour.addMcp.args.help": "每行一个参数。包含空格的路径将按原样保留。",
"settings.agentBehaviour.addMcp.args.placeholder": "e.g.\n-y\n@modelcontextprotocol/server-filesystem\n/tmp",
"settings.agentBehaviour.addMcp.url": "服务器 URL",
"settings.agentBehaviour.addMcp.url.placeholder": "e.g. http://localhost:3000/sse",
"settings.agentBehaviour.skillPaths": "技能文件夹路径",
"settings.agentBehaviour.skillUrls": "技能 URL",
"settings.agentBehaviour.removeSkill.title": "移除技能",
@@ -1078,6 +1078,18 @@ export const dict = {
"settings.agentBehaviour.removeMcp.title": "移除 MCP 伺服器",
"settings.agentBehaviour.removeMcp.confirm": '要移除 MCP 伺服器 "{{name}}" 嗎?這將從您的設定中移除它。',
"settings.agentBehaviour.removeMcp.button": "移除",
"settings.agentBehaviour.editMcp": "編輯 MCP 伺服器",
"settings.agentBehaviour.editMcp.transportLocal": "本機伺服器(stdio 傳輸)",
"settings.agentBehaviour.editMcp.transportRemote": "遠端伺服器(SSE/HTTP 傳輸)",
"settings.agentBehaviour.editMcp.env": "環境變數",
"settings.agentBehaviour.editMcp.env.help": "傳遞給 MCP 伺服器處理程序的變數。",
"settings.agentBehaviour.addMcp.command": "命令",
"settings.agentBehaviour.addMcp.command.placeholder": "e.g. npx",
"settings.agentBehaviour.addMcp.args": "引數",
"settings.agentBehaviour.addMcp.args.help": "每行一個引數。包含空格的路徑將按原樣保留。",
"settings.agentBehaviour.addMcp.args.placeholder": "e.g.\n-y\n@modelcontextprotocol/server-filesystem\n/tmp",
"settings.agentBehaviour.addMcp.url": "伺服器 URL",
"settings.agentBehaviour.addMcp.url.placeholder": "e.g. http://localhost:3000/sse",
"settings.agentBehaviour.skillPaths": "Skill 資料夾路徑",
"settings.agentBehaviour.skillUrls": "Skill URL",
"settings.agentBehaviour.removeSkill.title": "移除技能",
@@ -0,0 +1,96 @@
import type { Config } from "../types/messages"
export function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === "object" && !Array.isArray(value)
}
/** Deep merge two objects, with source values overriding target values. */
export function deepMerge(target: Config, source: Partial<Config>): Config {
const result: Record<string, unknown> = { ...target }
for (const [key, value] of Object.entries(source)) {
if (isRecord(value) && isRecord(result[key])) {
result[key] = deepMerge(result[key] as Config, value as Partial<Config>)
} else {
result[key] = value
}
}
return result as Config
}
/** Recursively remove keys whose value is null (null = "deleted"). */
export function stripNulls(obj: Config): Config {
const result: Record<string, unknown> = {}
for (const [key, value] of Object.entries(obj)) {
if (value === null || value === undefined) continue
if (isRecord(value)) {
result[key] = stripNulls(value as Config)
} else {
result[key] = value
}
}
return result as Config
}
/**
* Resolve the visible config when a configLoaded/configUpdated message arrives.
* If the user has pending draft changes, re-apply the draft on top of the
* incoming server config so pending toggles don't snap back.
*/
export function resolveConfig(server: Config, draft: Partial<Config>, dirty: boolean): Config {
if (dirty) return stripNulls(deepMerge(server, draft))
return server
}
/**
* Plain-object config state machine mirrors the SolidJS ConfigProvider
* logic without signals so the message-handling behavior is unit-testable.
*/
export class ConfigState {
config: Config = {}
saved: Config = {}
draft: Partial<Config> = {}
dirty = false
saving = false
loading = true
/** Accumulate a partial change (same as the toggle click path). */
updateConfig(partial: Partial<Config>) {
this.config = stripNulls(deepMerge(this.config, partial))
this.draft = deepMerge(this.draft as Config, partial)
this.dirty = true
}
/** Handle an incoming configLoaded push from the extension. */
handleConfigLoaded(server: Config) {
if (this.saving) return
this.config = resolveConfig(server, this.draft, this.dirty)
this.saved = server
this.loading = false
}
/** Handle an incoming configUpdated push from the extension. */
handleConfigUpdated(server: Config) {
if (this.saving) {
this.saving = false
this.draft = {}
this.dirty = false
this.config = server
} else {
this.config = resolveConfig(server, this.draft, this.dirty)
}
this.saved = server
}
/** Send the draft to the backend. */
saveConfig() {
if (Object.keys(this.draft).length === 0) return
this.saving = true
}
/** Discard pending changes. */
discardConfig() {
this.config = this.saved
this.draft = {}
this.dirty = false
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "7.1.5",
"version": "7.1.8",
"name": "@kilocode/cli",
"type": "module",
"license": "MIT",
@@ -0,0 +1,62 @@
import { $ } from "bun"
import path from "path"
import fs from "fs/promises"
import { Filesystem } from "../util/filesystem"
import { Log } from "../util/log"
import { Global } from "../global"
import { Instance } from "../project/instance"
const log = Log.create({ service: "snapshot" })
/** Worktree-scoped snapshot git dir (isolated per worktree). */
export function gitdir() {
const project = Instance.project
const workhash = Bun.hash(Instance.worktree).toString(36)
return path.join(Global.Path.data, "snapshot", project.id, workhash)
}
/** Original project-scoped snapshot git dir (pre-isolation layout). */
function legacydir() {
const project = Instance.project
return path.join(Global.Path.data, "snapshot", project.id)
}
/** Initialize a bare snapshot git repo if it doesn't exist yet. */
export async function ensureGit(git: string) {
if (!(await fs.mkdir(git, { recursive: true }))) return
await $`git init`
.env({
...process.env,
GIT_DIR: git,
GIT_WORK_TREE: Instance.worktree,
})
.quiet()
.nothrow()
await $`git --git-dir ${git} config core.autocrlf false`.quiet().nothrow()
await $`git --git-dir ${git} config core.longpaths true`.quiet().nothrow()
await $`git --git-dir ${git} config core.symlinks true`.quiet().nothrow()
await $`git --git-dir ${git} config core.fsmonitor false`.quiet().nothrow()
log.info("initialized")
}
/** Point the worktree repo's alternates at the legacy object store. */
export async function syncAlternates(git: string) {
const legacy = legacydir()
if (legacy === git) return
const objects = path.join(legacy, "objects")
const exists = await fs
.stat(objects)
.then(() => true)
.catch(() => false)
const target = path.join(git, "objects", "info", "alternates")
await fs.mkdir(path.join(git, "objects", "info"), { recursive: true })
await Filesystem.write(target, exists ? objects + "\n" : "")
}
/** Return a ready-to-use snapshot git dir (init + alternates). */
export async function prepare() {
const git = gitdir()
await ensureGit(git)
await syncAlternates(git)
return git
}
+8 -24
View File
@@ -9,6 +9,7 @@ import z from "zod"
import { Config } from "../config/config"
import { Instance } from "../project/instance"
import { Scheduler } from "../scheduler"
import * as KiloSnapshot from "../kilocode/snapshot" // kilocode_change
export namespace Snapshot {
const log = Log.create({ service: "snapshot" })
@@ -55,23 +56,7 @@ export namespace Snapshot {
if (Instance.project.vcs !== "git" || Flag.KILO_CLIENT === "acp") return
const cfg = await Config.get()
if (cfg.snapshot === false) return
const git = gitdir()
if (await fs.mkdir(git, { recursive: true })) {
await $`git init`
.env({
...process.env,
GIT_DIR: git,
GIT_WORK_TREE: Instance.worktree,
})
.quiet()
.nothrow()
// Configure git to not convert line endings on Windows
await $`git --git-dir ${git} config core.autocrlf false`.quiet().nothrow()
await $`git --git-dir ${git} config core.longpaths true`.quiet().nothrow()
await $`git --git-dir ${git} config core.symlinks true`.quiet().nothrow()
await $`git --git-dir ${git} config core.fsmonitor false`.quiet().nothrow()
log.info("initialized")
}
const git = await KiloSnapshot.prepare() // kilocode_change
await add(git)
const hash = await $`git --git-dir ${git} --work-tree ${Instance.worktree} write-tree`
.quiet()
@@ -89,7 +74,7 @@ export namespace Snapshot {
export type Patch = z.infer<typeof Patch>
export async function patch(hash: string): Promise<Patch> {
const git = gitdir()
const git = await KiloSnapshot.prepare() // kilocode_change
await add(git)
const result =
await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --name-only ${hash} -- .`
@@ -117,7 +102,7 @@ export namespace Snapshot {
export async function restore(snapshot: string) {
log.info("restore", { commit: snapshot })
const git = gitdir()
const git = await KiloSnapshot.prepare() // kilocode_change
const result =
await $`git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} read-tree ${snapshot} && git -c core.longpaths=true -c core.symlinks=true --git-dir ${git} --work-tree ${Instance.worktree} checkout-index -a -f`
.quiet()
@@ -136,8 +121,8 @@ export namespace Snapshot {
export async function revert(patches: Patch[]) {
const files = new Set<string>()
const git = gitdir()
for (const item of patches) {
const git = await KiloSnapshot.prepare() // kilocode_change
for (const file of item.files) {
if (files.has(file)) continue
log.info("reverting", { file, hash: item.hash })
@@ -168,7 +153,7 @@ export namespace Snapshot {
}
export async function diff(hash: string) {
const git = gitdir()
const git = await KiloSnapshot.prepare() // kilocode_change
await add(git)
const result =
await $`git -c core.autocrlf=false -c core.longpaths=true -c core.symlinks=true -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff ${hash} -- .`
@@ -203,7 +188,7 @@ export namespace Snapshot {
})
export type FileDiff = z.infer<typeof FileDiff>
export async function diffFull(from: string, to: string): Promise<FileDiff[]> {
const git = gitdir()
const git = await KiloSnapshot.prepare() // kilocode_change
const result: FileDiff[] = []
const status = new Map<string, "added" | "deleted" | "modified">()
@@ -266,8 +251,7 @@ export namespace Snapshot {
}
function gitdir() {
const project = Instance.project
return path.join(Global.Path.data, "snapshot", project.id)
return KiloSnapshot.gitdir() // kilocode_change
}
async function add(git: string) {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/plugin",
"version": "7.1.5",
"version": "7.1.8",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -8,7 +8,7 @@
"exports": {
".": "./src/index.ts"
},
"version": "7.1.5",
"version": "7.1.8",
"dependencies": {},
"peerDependencies": {}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@kilocode/sdk",
"version": "7.1.5",
"version": "7.1.8",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -26,7 +26,7 @@
"typescript": "catalog:",
"vite": "catalog:"
},
"version": "7.1.5",
"version": "7.1.8",
"dependencies": {},
"peerDependencies": {}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "7.1.5",
"version": "7.1.8",
"type": "module",
"license": "MIT",
"exports": {
+4
View File
@@ -28,6 +28,8 @@ export type SessionHrefFn = (sessionID: string) => string
export type OpenFileFn = (filePath: string, line?: number, column?: number) => void // kilocode_change
export type OpenUrlFn = (url: string) => void // kilocode_change
export const { use: useData, provider: DataProvider } = createSimpleContext({
name: "Data",
init: (props: {
@@ -36,6 +38,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
onNavigateToSession?: NavigateToSessionFn
onSessionHref?: SessionHrefFn
onOpenFile?: OpenFileFn // kilocode_change
onOpenUrl?: OpenUrlFn // kilocode_change
}) => {
return {
get store() {
@@ -47,6 +50,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
navigateToSession: props.onNavigateToSession,
sessionHref: props.onSessionHref,
openFile: props.onOpenFile, // kilocode_change
openUrl: props.onOpenUrl, // kilocode_change
}
},
})
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/util",
"version": "7.1.5",
"version": "7.1.8",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kilocode/upstream-merge",
"version": "7.1.5",
"version": "7.1.8",
"private": true,
"type": "module",
"description": "Scripts for automating upstream opencode merges into Kilo",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "7.1.5",
"version": "7.1.8",
"publisher": "sst-dev",
"repository": {
"type": "git",