mirror of
https://github.com/anomalyco/opencode.git
synced 2026-03-05 22:24:01 +00:00
Compare commits
10 Commits
opencode/h
...
implement-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
069acaf821 | ||
|
|
9507b0eace | ||
|
|
4da199697b | ||
|
|
9cccaa693a | ||
|
|
bb37e908ad | ||
|
|
d802e28381 | ||
|
|
3bd3904902 | ||
|
|
1a705cbca5 | ||
|
|
e6222529e7 | ||
|
|
2453f40d88 |
4
bun.lock
4
bun.lock
@@ -47,7 +47,7 @@
|
||||
"@thisbeyond/solid-dnd": "0.7.5",
|
||||
"diff": "catalog:",
|
||||
"fuzzysort": "catalog:",
|
||||
"ghostty-web": "0.4.0",
|
||||
"ghostty-web": "github:anomalyco/ghostty-web#main",
|
||||
"luxon": "catalog:",
|
||||
"marked": "catalog:",
|
||||
"marked-shiki": "catalog:",
|
||||
@@ -2958,7 +2958,7 @@
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="],
|
||||
|
||||
"ghostty-web": ["ghostty-web@0.4.0", "", {}, "sha512-0puDBik2qapbD/QQBW9o5ZHfXnZBqZWx/ctBiVtKZ6ZLds4NYb+wZuw1cRLXZk9zYovIQ908z3rvFhexAvc5Hg=="],
|
||||
"ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#4af877d", {}, "anomalyco-ghostty-web-4af877d", "sha512-fbEK8mtr7ar4ySsF+JUGjhaZrane7dKphanN+SxHt5XXI6yLMAh/Hpf6sNCOyyVa2UlGCd7YpXG/T2v2RUAX+A=="],
|
||||
|
||||
"gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="],
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"nodeModules": {
|
||||
"x86_64-linux": "sha256-ZmxeRNy2chc9py4m1iW6B+c/NSccMnVZ0lfni/EMdHw=",
|
||||
"aarch64-linux": "sha256-R+1mxsmAQicerN8ixVy0ff6V8bZ4GH18MHpihvWnaTg=",
|
||||
"aarch64-darwin": "sha256-m+QT20ohlqo9e86qXu67eKthZm6VDRLwlqJ9CNlEV+0=",
|
||||
"x86_64-darwin": "sha256-4GeNPyTT2Hq4rxHGSON23ul5Ud3yFGE0QUVsB03Gidc="
|
||||
"x86_64-linux": "sha256-v83hWzYVg/g4zJiBpGsQ71wTdndPk3BQVZ2mjMApUIQ=",
|
||||
"aarch64-linux": "sha256-inpMwkQqwBFP2wL8w/pTOP7q3fg1aOqvE0wgzVd3/B8=",
|
||||
"aarch64-darwin": "sha256-r42LGrQWqDyIy62mBSU5Nf3M22dJ3NNo7mjN/1h8d8Y=",
|
||||
"x86_64-darwin": "sha256-J6XrrdK5qBK3sQBQOO/B3ZluOnsAf5f65l4q/K1nDTI="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"@thisbeyond/solid-dnd": "0.7.5",
|
||||
"diff": "catalog:",
|
||||
"fuzzysort": "catalog:",
|
||||
"ghostty-web": "0.4.0",
|
||||
"ghostty-web": "github:anomalyco/ghostty-web#main",
|
||||
"luxon": "catalog:",
|
||||
"marked": "catalog:",
|
||||
"marked-shiki": "catalog:",
|
||||
|
||||
@@ -8,7 +8,6 @@ import { SettingsGeneral } from "./settings-general"
|
||||
import { SettingsKeybinds } from "./settings-keybinds"
|
||||
import { SettingsProviders } from "./settings-providers"
|
||||
import { SettingsModels } from "./settings-models"
|
||||
import { SettingsMcp } from "./settings-mcp"
|
||||
|
||||
export const DialogSettings: Component = () => {
|
||||
const language = useLanguage()
|
||||
@@ -46,10 +45,6 @@ export const DialogSettings: Component = () => {
|
||||
<Icon name="models" />
|
||||
{language.t("settings.models.title")}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="mcp">
|
||||
<Icon name="mcp" />
|
||||
{language.t("settings.mcp.title")}
|
||||
</Tabs.Trigger>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -72,9 +67,6 @@ export const DialogSettings: Component = () => {
|
||||
<Tabs.Content value="models" class="no-scrollbar">
|
||||
<SettingsModels />
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="mcp" class="no-scrollbar">
|
||||
<SettingsMcp />
|
||||
</Tabs.Content>
|
||||
</Tabs>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
@@ -1,665 +1,15 @@
|
||||
import type { Config, McpLocalConfig, McpRemoteConfig, McpStatus } from "@opencode-ai/sdk/v2/client"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Icon, type IconProps } from "@opencode-ai/ui/icon"
|
||||
import { Tag } from "@opencode-ai/ui/tag"
|
||||
import { Switch } from "@opencode-ai/ui/switch"
|
||||
import { TextField } from "@opencode-ai/ui/text-field"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { For, Show, createMemo, onMount, type Component } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useGlobalSDK } from "@/context/global-sdk"
|
||||
import { useGlobalSync } from "@/context/global-sync"
|
||||
import { Component } from "solid-js"
|
||||
import { useLanguage } from "@/context/language"
|
||||
|
||||
type Mode = "remote" | "local"
|
||||
type McpMap = NonNullable<Config["mcp"]>
|
||||
type McpEntry = McpMap[string]
|
||||
type McpConfig = McpLocalConfig | McpRemoteConfig
|
||||
type McpState = McpStatus["status"]
|
||||
|
||||
const FEATURED = [
|
||||
{
|
||||
name: "playwright",
|
||||
title: "Playwright",
|
||||
description: "Browser automation tools for testing, scraping, and repros.",
|
||||
icon: "window-cursor",
|
||||
panel: "linear-gradient(135deg, rgba(59, 130, 246, 0.14), rgba(15, 23, 42, 0.04))",
|
||||
glow: "rgba(96, 165, 250, 0.18)",
|
||||
badge: "rgba(37, 99, 235, 0.14)",
|
||||
color: "rgb(37, 99, 235)",
|
||||
config: {
|
||||
type: "local",
|
||||
command: ["npx", "-y", "@playwright/mcp@latest"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "context7",
|
||||
title: "Context7",
|
||||
description: "Fresh framework docs and API references in one remote server.",
|
||||
icon: "code-lines",
|
||||
panel: "linear-gradient(135deg, rgba(14, 165, 233, 0.16), rgba(15, 23, 42, 0.04))",
|
||||
glow: "rgba(56, 189, 248, 0.2)",
|
||||
badge: "rgba(8, 145, 178, 0.14)",
|
||||
color: "rgb(8, 145, 178)",
|
||||
config: {
|
||||
type: "remote",
|
||||
url: "https://mcp.context7.com/mcp",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "linear",
|
||||
title: "Linear",
|
||||
description: "Issue and project workflows from your Linear workspace.",
|
||||
icon: "branch",
|
||||
panel: "linear-gradient(135deg, rgba(124, 58, 237, 0.14), rgba(30, 41, 59, 0.04))",
|
||||
glow: "rgba(139, 92, 246, 0.18)",
|
||||
badge: "rgba(109, 40, 217, 0.14)",
|
||||
color: "rgb(109, 40, 217)",
|
||||
config: {
|
||||
type: "remote",
|
||||
url: "https://mcp.linear.app/sse",
|
||||
oauth: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "sentry",
|
||||
title: "Sentry",
|
||||
description: "Error monitoring, traces, and issue triage in one place.",
|
||||
icon: "warning",
|
||||
panel: "linear-gradient(135deg, rgba(14, 116, 144, 0.16), rgba(15, 23, 42, 0.04))",
|
||||
glow: "rgba(6, 182, 212, 0.16)",
|
||||
badge: "rgba(8, 145, 178, 0.14)",
|
||||
color: "rgb(8, 145, 178)",
|
||||
config: {
|
||||
type: "remote",
|
||||
url: "https://mcp.sentry.dev/mcp",
|
||||
oauth: {},
|
||||
},
|
||||
},
|
||||
] satisfies Array<{
|
||||
name: string
|
||||
title: string
|
||||
description: string
|
||||
icon: IconProps["name"]
|
||||
panel: string
|
||||
glow: string
|
||||
badge: string
|
||||
color: string
|
||||
config: McpConfig
|
||||
}>
|
||||
|
||||
const STATUS = {
|
||||
connected: "mcp.status.connected",
|
||||
failed: "mcp.status.failed",
|
||||
needs_auth: "mcp.status.needs_auth",
|
||||
disabled: "mcp.status.disabled",
|
||||
needs_client_registration: "settings.mcp.status.needs_client_registration",
|
||||
} satisfies Record<McpState, string>
|
||||
|
||||
const empty = (mode: Mode = "remote") => ({
|
||||
mode,
|
||||
name: "",
|
||||
url: "",
|
||||
command: "",
|
||||
headers: "",
|
||||
environment: "",
|
||||
timeout: "",
|
||||
})
|
||||
|
||||
const isConfig = (value: McpEntry | undefined): value is McpConfig =>
|
||||
typeof value === "object" && value !== null && "type" in value
|
||||
|
||||
const split = (value: string) =>
|
||||
value
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
const parseMap = (value: string, allowColon: boolean) => {
|
||||
const out: Record<string, string> = {}
|
||||
|
||||
for (const line of split(value)) {
|
||||
const eq = line.indexOf("=")
|
||||
const cut = !allowColon ? eq : ([line.indexOf(":"), eq].filter((part) => part > 0).sort((a, b) => a - b)[0] ?? -1)
|
||||
|
||||
if (cut < 1) return { error: line }
|
||||
|
||||
const key = line.slice(0, cut).trim()
|
||||
const item = line.slice(cut + 1).trim()
|
||||
if (!key || !item) return { error: line }
|
||||
out[key] = item
|
||||
}
|
||||
|
||||
return { value: Object.keys(out).length > 0 ? out : undefined }
|
||||
}
|
||||
|
||||
const parseCmd = (value: string) =>
|
||||
(value.match(/"[^"]*"|'[^']*'|[^\s]+/g) ?? []).map((part) => {
|
||||
if (part.startsWith('"') && part.endsWith('"')) return part.slice(1, -1)
|
||||
if (part.startsWith("'") && part.endsWith("'")) return part.slice(1, -1)
|
||||
return part
|
||||
})
|
||||
|
||||
export const SettingsMcp: Component = () => {
|
||||
const lang = useLanguage()
|
||||
const sdk = useGlobalSDK()
|
||||
const sync = useGlobalSync()
|
||||
const [state, setState] = createStore({
|
||||
form: empty(),
|
||||
submitting: "",
|
||||
statusLoading: false,
|
||||
status: {} as Record<string, McpStatus>,
|
||||
})
|
||||
|
||||
const busy = createMemo(() => state.submitting.length > 0)
|
||||
|
||||
const items = createMemo(() => {
|
||||
return Object.entries(sync.data.config.mcp ?? {})
|
||||
.filter((item): item is [string, McpConfig] => isConfig(item[1]))
|
||||
.map(([name, config]) => ({ name, config }))
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
})
|
||||
|
||||
const names = createMemo(() => new Set(items().map((item) => item.name)))
|
||||
|
||||
const spin = () => `${lang.t("common.loading")}${lang.t("common.loading.ellipsis")}`
|
||||
|
||||
const kind = (value: Mode) => {
|
||||
if (value === "remote") return lang.t("settings.mcp.type.remote")
|
||||
return lang.t("settings.mcp.type.local")
|
||||
}
|
||||
|
||||
const fail = (description: string) => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: lang.t("common.requestFailed"),
|
||||
description,
|
||||
})
|
||||
}
|
||||
|
||||
const load = () => {
|
||||
setState("statusLoading", true)
|
||||
return sdk.client.mcp
|
||||
.status()
|
||||
.then((x) => {
|
||||
setState("status", x.data ?? {})
|
||||
})
|
||||
.catch(() => undefined)
|
||||
.finally(() => {
|
||||
setState("statusLoading", false)
|
||||
})
|
||||
}
|
||||
|
||||
const save = (next: McpMap, job: string, onSuccess: () => void, title: string, description: string) => {
|
||||
const prev = sync.data.config.mcp
|
||||
setState("submitting", job)
|
||||
sync.set("config", "mcp", next)
|
||||
|
||||
sync
|
||||
.updateConfig({ mcp: next })
|
||||
.then(() => {
|
||||
onSuccess()
|
||||
void load()
|
||||
showToast({
|
||||
variant: "success",
|
||||
icon: "circle-check",
|
||||
title,
|
||||
description,
|
||||
})
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
sync.set("config", "mcp", prev)
|
||||
fail(err instanceof Error ? err.message : String(err))
|
||||
})
|
||||
.finally(() => {
|
||||
setState("submitting", "")
|
||||
})
|
||||
}
|
||||
|
||||
const add = (name: string, config: McpConfig, job: string, reset: boolean) => {
|
||||
const key = name.trim()
|
||||
if (!key) {
|
||||
fail(lang.t("settings.mcp.validation.name"))
|
||||
return
|
||||
}
|
||||
|
||||
if (names().has(key)) {
|
||||
fail(lang.t("settings.mcp.validation.duplicate", { name: key }))
|
||||
return
|
||||
}
|
||||
|
||||
const next = {
|
||||
...(sync.data.config.mcp ?? {}),
|
||||
[key]: config,
|
||||
}
|
||||
|
||||
save(
|
||||
next,
|
||||
job,
|
||||
() => {
|
||||
if (!reset) return
|
||||
setState("form", empty(state.form.mode))
|
||||
},
|
||||
lang.t("settings.mcp.toast.added.title"),
|
||||
lang.t("settings.mcp.toast.added.description", { name: key }),
|
||||
)
|
||||
}
|
||||
|
||||
const addForm = () => {
|
||||
if (busy()) return
|
||||
|
||||
const timeout = state.form.timeout.trim()
|
||||
const wait = timeout ? Number(timeout) : undefined
|
||||
if (wait !== undefined && (!Number.isInteger(wait) || wait <= 0)) {
|
||||
fail(lang.t("settings.mcp.validation.timeout"))
|
||||
return
|
||||
}
|
||||
|
||||
if (state.form.mode === "remote") {
|
||||
const url = state.form.url.trim()
|
||||
if (!url) {
|
||||
fail(lang.t("settings.mcp.validation.url"))
|
||||
return
|
||||
}
|
||||
|
||||
const headers = parseMap(state.form.headers, true)
|
||||
if (headers.error) {
|
||||
fail(lang.t("settings.mcp.validation.headers", { line: headers.error }))
|
||||
return
|
||||
}
|
||||
|
||||
add(
|
||||
state.form.name,
|
||||
{
|
||||
type: "remote",
|
||||
url,
|
||||
...(headers.value ? { headers: headers.value } : {}),
|
||||
...(wait ? { timeout: wait } : {}),
|
||||
},
|
||||
"form",
|
||||
true,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const command = parseCmd(state.form.command.trim())
|
||||
if (command.length === 0) {
|
||||
fail(lang.t("settings.mcp.validation.command"))
|
||||
return
|
||||
}
|
||||
|
||||
const environment = parseMap(state.form.environment, false)
|
||||
if (environment.error) {
|
||||
fail(lang.t("settings.mcp.validation.environment", { line: environment.error }))
|
||||
return
|
||||
}
|
||||
|
||||
add(
|
||||
state.form.name,
|
||||
{
|
||||
type: "local",
|
||||
command,
|
||||
...(environment.value ? { environment: environment.value } : {}),
|
||||
...(wait ? { timeout: wait } : {}),
|
||||
},
|
||||
"form",
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
const addFeatured = (item: (typeof FEATURED)[number]) => {
|
||||
if (busy()) return
|
||||
add(item.name, item.config, `featured:${item.name}`, false)
|
||||
}
|
||||
|
||||
const toggle = (name: string, enabled: boolean) => {
|
||||
if (busy()) return
|
||||
const current = (sync.data.config.mcp ?? {})[name]
|
||||
if (!isConfig(current)) return
|
||||
|
||||
const next = {
|
||||
...(sync.data.config.mcp ?? {}),
|
||||
[name]: {
|
||||
...current,
|
||||
enabled,
|
||||
},
|
||||
}
|
||||
|
||||
save(
|
||||
next,
|
||||
`toggle:${name}`,
|
||||
() => undefined,
|
||||
lang.t("settings.mcp.toast.updated.title"),
|
||||
lang.t("settings.mcp.toast.updated.description", {
|
||||
name,
|
||||
state: enabled ? lang.t("settings.mcp.state.enabled") : lang.t("settings.mcp.state.disabled"),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const remove = (name: string) => {
|
||||
if (busy()) return
|
||||
|
||||
const next = { ...(sync.data.config.mcp ?? {}) }
|
||||
delete next[name]
|
||||
|
||||
save(
|
||||
next,
|
||||
`remove:${name}`,
|
||||
() => undefined,
|
||||
lang.t("settings.mcp.toast.removed.title"),
|
||||
lang.t("settings.mcp.toast.removed.description", { name }),
|
||||
)
|
||||
}
|
||||
|
||||
const label = (name: string) => {
|
||||
const value = state.status[name]?.status
|
||||
if (!value) return
|
||||
return lang.t(STATUS[value])
|
||||
}
|
||||
|
||||
const issue = (name: string) => {
|
||||
const value = state.status[name]
|
||||
if (!value || !("error" in value)) return
|
||||
return value.error
|
||||
}
|
||||
|
||||
const line = (config: McpConfig) => {
|
||||
if (config.type === "remote") return config.url
|
||||
return config.command.join(" ")
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
void load()
|
||||
})
|
||||
// TODO: Replace this placeholder with full MCP settings controls.
|
||||
const language = useLanguage()
|
||||
|
||||
return (
|
||||
<div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
|
||||
<div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
|
||||
<div class="flex flex-col gap-1 pt-6 pb-8 max-w-[720px]">
|
||||
<h2 class="text-16-medium text-text-strong">{lang.t("settings.mcp.title")}</h2>
|
||||
<p class="text-14-regular text-text-weak">{lang.t("settings.mcp.description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-8 max-w-[720px]">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="text-14-medium text-text-strong">{lang.t("settings.mcp.section.featured")}</h3>
|
||||
<p class="text-12-regular text-text-weak">{lang.t("settings.mcp.section.featured.description")}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<For each={FEATURED}>
|
||||
{(item) => {
|
||||
const added = () => names().has(item.name)
|
||||
const pending = () => state.submitting === `featured:${item.name}`
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
class="group relative overflow-hidden rounded-2xl border border-border-weak-base p-4 text-left transition-transform duration-200 disabled:cursor-default"
|
||||
classList={{
|
||||
"hover:-translate-y-0.5": !added() && !busy(),
|
||||
"opacity-60": added(),
|
||||
}}
|
||||
disabled={added() || busy()}
|
||||
onClick={() => addFeatured(item)}
|
||||
>
|
||||
<div class="absolute inset-0" aria-hidden="true">
|
||||
<div class="absolute inset-0" style={{ background: item.panel }} />
|
||||
<div
|
||||
class="absolute -right-6 -top-6 size-24 rounded-full blur-2xl"
|
||||
style={{ background: item.glow }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="relative flex flex-col gap-4">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div
|
||||
class="flex size-11 shrink-0 items-center justify-center rounded-2xl border border-border-weak-base"
|
||||
style={{ background: item.badge, color: item.color }}
|
||||
>
|
||||
<Icon name={item.icon} class="size-5" />
|
||||
</div>
|
||||
<span class="rounded-full bg-surface-base px-2.5 py-1 text-11-medium text-text-weak">
|
||||
{kind(item.config.type)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-14-medium text-text-strong">{item.title}</span>
|
||||
<Show when={added()}>
|
||||
<span class="rounded-full bg-surface-base px-2 py-0.5 text-11-medium text-text-weak">
|
||||
{lang.t("settings.mcp.featured.added")}
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={pending()}>
|
||||
<span class="text-11-regular text-text-weak">{spin()}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<span class="text-12-regular leading-5 text-text-weak">{item.description}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h3 class="text-14-medium text-text-strong">{lang.t("settings.mcp.section.configured")}</h3>
|
||||
<Show when={state.statusLoading}>
|
||||
<span class="text-11-regular text-text-weak">{spin()}</span>
|
||||
</Show>
|
||||
</div>
|
||||
<p class="text-12-regular text-text-weak">{lang.t("settings.mcp.section.configured.description")}</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-surface-raised-base px-4 rounded-lg">
|
||||
<Show
|
||||
when={items().length > 0}
|
||||
fallback={<div class="py-4 text-14-regular text-text-weak">{lang.t("dialog.mcp.empty")}</div>}
|
||||
>
|
||||
<For each={items()}>
|
||||
{(item) => {
|
||||
const current = () => state.status[item.name]?.status
|
||||
const text = () => label(item.name)
|
||||
const problem = () => issue(item.name)
|
||||
const enabled = () => item.config.enabled !== false
|
||||
const pendingToggle = () => state.submitting === `toggle:${item.name}`
|
||||
const pending = () => state.submitting === `remove:${item.name}`
|
||||
|
||||
return (
|
||||
<div class="flex flex-wrap items-start justify-between gap-4 py-4 border-b border-border-weak-base last:border-none">
|
||||
<div class="min-w-0 flex-1 flex flex-col gap-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-14-medium text-text-strong">{item.name}</span>
|
||||
<Tag>{kind(item.config.type)}</Tag>
|
||||
<Show when={text()}>
|
||||
<span
|
||||
class="rounded-full bg-surface-base px-2 py-0.5 text-11-medium"
|
||||
classList={{
|
||||
"text-icon-success-base": current() === "connected",
|
||||
"text-icon-warning-base": current() === "needs_auth",
|
||||
"text-icon-critical-base":
|
||||
current() === "failed" || current() === "needs_client_registration",
|
||||
"text-text-weak": current() === "disabled",
|
||||
}}
|
||||
>
|
||||
{text()}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<span class="text-12-regular text-text-weak break-all">{line(item.config)}</span>
|
||||
|
||||
<Show when={problem()}>
|
||||
<span class="text-12-regular text-icon-critical-base break-all">{problem()}</span>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3" onClick={(e) => e.stopPropagation()}>
|
||||
<div class="flex items-center gap-2 pl-1">
|
||||
<Show when={pendingToggle()}>
|
||||
<span class="text-11-regular text-text-weak">{spin()}</span>
|
||||
</Show>
|
||||
<Switch
|
||||
checked={enabled()}
|
||||
disabled={busy()}
|
||||
onChange={(next) => toggle(item.name, next)}
|
||||
hideLabel
|
||||
>
|
||||
{item.name}
|
||||
</Switch>
|
||||
<span class="text-12-regular text-text-weak">
|
||||
{enabled() ? lang.t("settings.mcp.state.enabled") : lang.t("settings.mcp.state.disabled")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button size="large" variant="ghost" disabled={busy()} onClick={() => remove(item.name)}>
|
||||
{pending() ? spin() : lang.t("settings.mcp.action.remove")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h3 class="text-14-medium text-text-strong">{lang.t("settings.mcp.section.add")}</h3>
|
||||
<p class="text-12-regular text-text-weak">{lang.t("settings.mcp.section.add.description")}</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-border-weak-base bg-surface-raised-base p-4 sm:p-5">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-12-medium text-text-strong">{lang.t("settings.mcp.form.type.label")}</span>
|
||||
<div class="inline-flex w-full gap-1 rounded-xl bg-surface-base p-1 sm:w-auto">
|
||||
<For each={["remote", "local"] as const}>
|
||||
{(mode) => (
|
||||
<button
|
||||
type="button"
|
||||
class="h-9 flex-1 rounded-lg px-3 text-12-medium transition-colors sm:flex-none"
|
||||
classList={{
|
||||
"bg-surface-raised-base text-text-strong": state.form.mode === mode,
|
||||
"text-text-weak": state.form.mode !== mode,
|
||||
}}
|
||||
onClick={() => {
|
||||
if (busy()) return
|
||||
setState("form", "mode", mode)
|
||||
}}
|
||||
>
|
||||
{kind(mode)}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<TextField
|
||||
label={lang.t("settings.mcp.form.name.label")}
|
||||
value={state.form.name}
|
||||
onChange={(value) => setState("form", "name", value)}
|
||||
placeholder={lang.t("settings.mcp.form.name.placeholder")}
|
||||
spellcheck={false}
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
autocapitalize="off"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label={lang.t("settings.mcp.form.timeout.label")}
|
||||
value={state.form.timeout}
|
||||
onChange={(value) => setState("form", "timeout", value)}
|
||||
placeholder={lang.t("settings.mcp.form.timeout.placeholder")}
|
||||
inputMode="numeric"
|
||||
spellcheck={false}
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
autocapitalize="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Show
|
||||
when={state.form.mode === "remote"}
|
||||
fallback={
|
||||
<>
|
||||
<TextField
|
||||
label={lang.t("settings.mcp.form.command.label")}
|
||||
value={state.form.command}
|
||||
onChange={(value) => setState("form", "command", value)}
|
||||
placeholder={lang.t("settings.mcp.form.command.placeholder")}
|
||||
spellcheck={false}
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
autocapitalize="off"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label={lang.t("settings.mcp.form.environment.label")}
|
||||
description={lang.t("settings.mcp.form.environment.description")}
|
||||
value={state.form.environment}
|
||||
onChange={(value) => setState("form", "environment", value)}
|
||||
placeholder="API_KEY={env:API_KEY}"
|
||||
multiline
|
||||
rows={4}
|
||||
spellcheck={false}
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
autocapitalize="off"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TextField
|
||||
label={lang.t("settings.mcp.form.url.label")}
|
||||
value={state.form.url}
|
||||
onChange={(value) => setState("form", "url", value)}
|
||||
placeholder={lang.t("settings.mcp.form.url.placeholder")}
|
||||
spellcheck={false}
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
autocapitalize="off"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label={lang.t("settings.mcp.form.headers.label")}
|
||||
description={lang.t("settings.mcp.form.headers.description")}
|
||||
value={state.form.headers}
|
||||
onChange={(value) => setState("form", "headers", value)}
|
||||
placeholder="Authorization: Bearer {env:API_KEY}"
|
||||
multiline
|
||||
rows={4}
|
||||
spellcheck={false}
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
autocapitalize="off"
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Button
|
||||
size="large"
|
||||
variant="secondary"
|
||||
icon="plus-small"
|
||||
class="w-full justify-center sm:w-auto"
|
||||
disabled={busy()}
|
||||
onClick={addForm}
|
||||
>
|
||||
{state.submitting === "form" ? spin() : lang.t("settings.mcp.action.add")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col h-full overflow-y-auto">
|
||||
<div class="flex flex-col gap-6 p-6 max-w-[600px]">
|
||||
<h2 class="text-16-medium text-text-strong">{language.t("settings.mcp.title")}</h2>
|
||||
<p class="text-14-regular text-text-weak">{language.t("settings.mcp.description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -665,47 +665,6 @@ export const dict = {
|
||||
"settings.commands.description": "ستكون إعدادات الأمر قابلة للتكوين هنا.",
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "ستكون إعدادات MCP قابلة للتكوين هنا.",
|
||||
"settings.mcp.section.featured": "مميّز",
|
||||
"settings.mcp.section.featured.description": "أضف إعدادًا مسبقًا لخوادم MCP الشهيرة بنقرة واحدة.",
|
||||
"settings.mcp.section.configured": "الخوادم المهيأة",
|
||||
"settings.mcp.section.configured.description":
|
||||
"اطلع على خوادم MCP المثبتة، وكيفية اتصالها، وقم بإزالة تلك التي لم تعد بحاجة إليها.",
|
||||
"settings.mcp.section.add": "أضف خادمًا",
|
||||
"settings.mcp.section.add.description": "أنشئ تكوين خادم MCP محلي أو بعيد خاص بك.",
|
||||
"settings.mcp.type.local": "محلي",
|
||||
"settings.mcp.type.remote": "بعيد",
|
||||
"settings.mcp.featured.added": "تمت الإضافة",
|
||||
"settings.mcp.action.add": "أضف خادمًا",
|
||||
"settings.mcp.action.remove": "إزالة",
|
||||
"settings.mcp.state.enabled": "مفعل",
|
||||
"settings.mcp.state.disabled": "معطل",
|
||||
"settings.mcp.form.type.label": "نوع الاتصال",
|
||||
"settings.mcp.form.name.label": "اسم الخادم",
|
||||
"settings.mcp.form.name.placeholder": "my-mcp-server",
|
||||
"settings.mcp.form.url.label": "عنوان URL البعيد",
|
||||
"settings.mcp.form.url.placeholder": "https://mcp.example.com/mcp",
|
||||
"settings.mcp.form.command.label": "الأمر",
|
||||
"settings.mcp.form.command.placeholder": "npx -y @modelcontextprotocol/server-memory",
|
||||
"settings.mcp.form.headers.label": "الرؤوس (Headers)",
|
||||
"settings.mcp.form.headers.description": "اختياري. أضف رأسًا واحدًا في كل سطر بصيغة KEY: value.",
|
||||
"settings.mcp.form.environment.label": "البيئة",
|
||||
"settings.mcp.form.environment.description": "اختياري. أضف متغيرًا واحدًا في كل سطر بصيغة KEY=value.",
|
||||
"settings.mcp.form.timeout.label": "المهلة (مللي ثانية)",
|
||||
"settings.mcp.form.timeout.placeholder": "5000",
|
||||
"settings.mcp.toast.added.title": "تمت إضافة خادم MCP",
|
||||
"settings.mcp.toast.added.description": "تم حفظ {{name}} في إعدادات MCP الخاصة بك.",
|
||||
"settings.mcp.toast.updated.title": "تم تحديث خادم MCP",
|
||||
"settings.mcp.toast.updated.description": "أصبح {{name}} الآن {{state}}.",
|
||||
"settings.mcp.toast.removed.title": "تمت إزالة خادم MCP",
|
||||
"settings.mcp.toast.removed.description": "تمت إزالة {{name}} من إعدادات MCP الخاصة بك.",
|
||||
"settings.mcp.validation.name": "أدخل اسم الخادم قبل الحفظ.",
|
||||
"settings.mcp.validation.duplicate": "{{name}} مهيأ بالفعل.",
|
||||
"settings.mcp.validation.url": "أدخل عنوان URL لخادم MCP البعيد.",
|
||||
"settings.mcp.validation.command": "أدخل الأمر المستخدم لبدء تشغيل خادم MCP المحلي.",
|
||||
"settings.mcp.validation.timeout": "يجب أن تكون المهلة عددًا صحيحًا موجبًا.",
|
||||
"settings.mcp.validation.headers": "تعذر تحليل سطر الرأس: {{line}}",
|
||||
"settings.mcp.validation.environment": "تعذر تحليل سطر البيئة: {{line}}",
|
||||
"settings.mcp.status.needs_client_registration": "يحتاج إلى تسجيل العميل",
|
||||
"settings.permissions.title": "الأذونات",
|
||||
"settings.permissions.description": "تحكم في الأدوات التي يمكن للخادم استخدامها بشكل افتراضي.",
|
||||
"settings.permissions.section.tools": "الأدوات",
|
||||
|
||||
@@ -673,48 +673,6 @@ export const dict = {
|
||||
"settings.commands.description": "Configurações de comandos estarão disponíveis aqui.",
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "Configurações de MCP estarão disponíveis aqui.",
|
||||
"settings.mcp.section.featured": "Destaques",
|
||||
"settings.mcp.section.featured.description":
|
||||
"Adicione uma predefinição refinada para servidores MCP populares com um clique.",
|
||||
"settings.mcp.section.configured": "Servidores configurados",
|
||||
"settings.mcp.section.configured.description":
|
||||
"Veja quais servidores MCP estão instalados, como eles se conectam e remova os que você não precisa mais.",
|
||||
"settings.mcp.section.add": "Adicionar servidor",
|
||||
"settings.mcp.section.add.description": "Crie sua própria configuração de servidor MCP local ou remoto.",
|
||||
"settings.mcp.type.local": "Local",
|
||||
"settings.mcp.type.remote": "Remoto",
|
||||
"settings.mcp.featured.added": "Adicionado",
|
||||
"settings.mcp.action.add": "Adicionar servidor",
|
||||
"settings.mcp.action.remove": "Remover",
|
||||
"settings.mcp.state.enabled": "Habilitado",
|
||||
"settings.mcp.state.disabled": "Desabilitado",
|
||||
"settings.mcp.form.type.label": "Tipo de conexão",
|
||||
"settings.mcp.form.name.label": "Nome do servidor",
|
||||
"settings.mcp.form.name.placeholder": "my-mcp-server",
|
||||
"settings.mcp.form.url.label": "URL remota",
|
||||
"settings.mcp.form.url.placeholder": "https://mcp.example.com/mcp",
|
||||
"settings.mcp.form.command.label": "Comando",
|
||||
"settings.mcp.form.command.placeholder": "npx -y @modelcontextprotocol/server-memory",
|
||||
"settings.mcp.form.headers.label": "Cabeçalhos",
|
||||
"settings.mcp.form.headers.description": "Opcional. Adicione um cabeçalho por linha usando KEY: value.",
|
||||
"settings.mcp.form.environment.label": "Ambiente",
|
||||
"settings.mcp.form.environment.description": "Opcional. Adicione uma variável por linha usando KEY=value.",
|
||||
"settings.mcp.form.timeout.label": "Tempo limite (ms)",
|
||||
"settings.mcp.form.timeout.placeholder": "5000",
|
||||
"settings.mcp.toast.added.title": "Servidor MCP adicionado",
|
||||
"settings.mcp.toast.added.description": "{{name}} foi salvo nas suas configurações do MCP.",
|
||||
"settings.mcp.toast.updated.title": "Servidor MCP atualizado",
|
||||
"settings.mcp.toast.updated.description": "{{name}} está agora {{state}}.",
|
||||
"settings.mcp.toast.removed.title": "Servidor MCP removido",
|
||||
"settings.mcp.toast.removed.description": "{{name}} foi removido das suas configurações do MCP.",
|
||||
"settings.mcp.validation.name": "Insira um nome para o servidor antes de salvar.",
|
||||
"settings.mcp.validation.duplicate": "{{name}} já está configurado.",
|
||||
"settings.mcp.validation.url": "Insira uma URL de servidor MCP remoto.",
|
||||
"settings.mcp.validation.command": "Insira o comando usado para iniciar o servidor MCP local.",
|
||||
"settings.mcp.validation.timeout": "O tempo limite deve ser um número inteiro positivo.",
|
||||
"settings.mcp.validation.headers": "Não foi possível analisar a linha do cabeçalho: {{line}}",
|
||||
"settings.mcp.validation.environment": "Não foi possível analisar a linha de ambiente: {{line}}",
|
||||
"settings.mcp.status.needs_client_registration": "Requer registro do cliente",
|
||||
"settings.permissions.title": "Permissões",
|
||||
"settings.permissions.description": "Controle quais ferramentas o servidor pode usar por padrão.",
|
||||
"settings.permissions.section.tools": "Ferramentas",
|
||||
|
||||
@@ -745,47 +745,6 @@ export const dict = {
|
||||
"settings.commands.description": "Postavke komandi će se ovdje moći podešavati.",
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "MCP postavke će se ovdje moći podešavati.",
|
||||
"settings.mcp.section.featured": "Izdvojeno",
|
||||
"settings.mcp.section.featured.description": "Dodajte gotovu postavku za popularne MCP servere jednim klikom.",
|
||||
"settings.mcp.section.configured": "Konfigurisani serveri",
|
||||
"settings.mcp.section.configured.description":
|
||||
"Pogledajte koji su MCP serveri instalirani, kako se povezuju i uklonite one koji vam više ne trebaju.",
|
||||
"settings.mcp.section.add": "Dodaj server",
|
||||
"settings.mcp.section.add.description": "Kreirajte vlastitu konfiguraciju lokalnog ili udaljenog MCP servera.",
|
||||
"settings.mcp.type.local": "Lokalno",
|
||||
"settings.mcp.type.remote": "Udaljeno",
|
||||
"settings.mcp.featured.added": "Dodano",
|
||||
"settings.mcp.action.add": "Dodaj server",
|
||||
"settings.mcp.action.remove": "Ukloni",
|
||||
"settings.mcp.state.enabled": "Omogućeno",
|
||||
"settings.mcp.state.disabled": "Onemogućeno",
|
||||
"settings.mcp.form.type.label": "Tip konekcije",
|
||||
"settings.mcp.form.name.label": "Naziv servera",
|
||||
"settings.mcp.form.name.placeholder": "my-mcp-server",
|
||||
"settings.mcp.form.url.label": "Udaljeni URL",
|
||||
"settings.mcp.form.url.placeholder": "https://mcp.example.com/mcp",
|
||||
"settings.mcp.form.command.label": "Komanda",
|
||||
"settings.mcp.form.command.placeholder": "npx -y @modelcontextprotocol/server-memory",
|
||||
"settings.mcp.form.headers.label": "Zaglavlja",
|
||||
"settings.mcp.form.headers.description": "Opcionalno. Dodajte jedno zaglavlje po liniji koristeći KEY: value.",
|
||||
"settings.mcp.form.environment.label": "Okruženje",
|
||||
"settings.mcp.form.environment.description": "Opcionalno. Dodajte jednu varijablu po liniji koristeći KEY=value.",
|
||||
"settings.mcp.form.timeout.label": "Istek vremena (ms)",
|
||||
"settings.mcp.form.timeout.placeholder": "5000",
|
||||
"settings.mcp.toast.added.title": "MCP server dodan",
|
||||
"settings.mcp.toast.added.description": "{{name}} je sačuvan u vaše MCP postavke.",
|
||||
"settings.mcp.toast.updated.title": "MCP server ažuriran",
|
||||
"settings.mcp.toast.updated.description": "{{name}} je sada {{state}}.",
|
||||
"settings.mcp.toast.removed.title": "MCP server uklonjen",
|
||||
"settings.mcp.toast.removed.description": "{{name}} je uklonjen iz vaših MCP postavki.",
|
||||
"settings.mcp.validation.name": "Unesite naziv servera prije čuvanja.",
|
||||
"settings.mcp.validation.duplicate": "{{name}} je već konfigurisan.",
|
||||
"settings.mcp.validation.url": "Unesite URL udaljenog MCP servera.",
|
||||
"settings.mcp.validation.command": "Unesite komandu koja se koristi za pokretanje lokalnog MCP servera.",
|
||||
"settings.mcp.validation.timeout": "Istek vremena mora biti pozitivan cijeli broj.",
|
||||
"settings.mcp.validation.headers": "Nije moguće parsirati liniju zaglavlja: {{line}}",
|
||||
"settings.mcp.validation.environment": "Nije moguće parsirati liniju okruženja: {{line}}",
|
||||
"settings.mcp.status.needs_client_registration": "Potrebna registracija klijenta",
|
||||
|
||||
"settings.permissions.title": "Dozvole",
|
||||
"settings.permissions.description": "Kontroliši koje alate server smije koristiti po defaultu.",
|
||||
|
||||
@@ -739,48 +739,6 @@ export const dict = {
|
||||
"settings.commands.description": "Kommandoindstillinger vil kunne konfigureres her.",
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "MCP-indstillinger vil kunne konfigureres her.",
|
||||
"settings.mcp.section.featured": "Udvalgte",
|
||||
"settings.mcp.section.featured.description":
|
||||
"Tilføj en poleret forudindstilling for populære MCP-servere med ét klik.",
|
||||
"settings.mcp.section.configured": "Konfigurerede servere",
|
||||
"settings.mcp.section.configured.description":
|
||||
"Se hvilke MCP-servere der er installeret, hvordan de forbinder, og fjern dem du ikke længere har brug for.",
|
||||
"settings.mcp.section.add": "Tilføj en server",
|
||||
"settings.mcp.section.add.description": "Opret din egen lokale eller fjern MCP-serverkonfiguration.",
|
||||
"settings.mcp.type.local": "Lokal",
|
||||
"settings.mcp.type.remote": "Fjern",
|
||||
"settings.mcp.featured.added": "Tilføjet",
|
||||
"settings.mcp.action.add": "Tilføj server",
|
||||
"settings.mcp.action.remove": "Fjern",
|
||||
"settings.mcp.state.enabled": "Aktiveret",
|
||||
"settings.mcp.state.disabled": "Deaktiveret",
|
||||
"settings.mcp.form.type.label": "Forbindelsestype",
|
||||
"settings.mcp.form.name.label": "Servernavn",
|
||||
"settings.mcp.form.name.placeholder": "my-mcp-server",
|
||||
"settings.mcp.form.url.label": "Fjern-URL",
|
||||
"settings.mcp.form.url.placeholder": "https://mcp.example.com/mcp",
|
||||
"settings.mcp.form.command.label": "Kommando",
|
||||
"settings.mcp.form.command.placeholder": "npx -y @modelcontextprotocol/server-memory",
|
||||
"settings.mcp.form.headers.label": "Headere",
|
||||
"settings.mcp.form.headers.description": "Valgfrit. Tilføj én header pr. linje ved brug af KEY: value.",
|
||||
"settings.mcp.form.environment.label": "Miljø",
|
||||
"settings.mcp.form.environment.description": "Valgfrit. Tilføj én variabel pr. linje ved brug af KEY=value.",
|
||||
"settings.mcp.form.timeout.label": "Timeout (ms)",
|
||||
"settings.mcp.form.timeout.placeholder": "5000",
|
||||
"settings.mcp.toast.added.title": "MCP-server tilføjet",
|
||||
"settings.mcp.toast.added.description": "{{name}} er blevet gemt i dine MCP-indstillinger.",
|
||||
"settings.mcp.toast.updated.title": "MCP-server opdateret",
|
||||
"settings.mcp.toast.updated.description": "{{name}} er nu {{state}}.",
|
||||
"settings.mcp.toast.removed.title": "MCP-server fjernet",
|
||||
"settings.mcp.toast.removed.description": "{{name}} er blevet fjernet fra dine MCP-indstillinger.",
|
||||
"settings.mcp.validation.name": "Indtast et servernavn før du gemmer.",
|
||||
"settings.mcp.validation.duplicate": "{{name}} er allerede konfigureret.",
|
||||
"settings.mcp.validation.url": "Indtast en URL til en fjern MCP-server.",
|
||||
"settings.mcp.validation.command": "Indtast kommandoen, der bruges til at starte den lokale MCP-server.",
|
||||
"settings.mcp.validation.timeout": "Timeout skal være et positivt heltal.",
|
||||
"settings.mcp.validation.headers": "Kunne ikke fortolke header-linje: {{line}}",
|
||||
"settings.mcp.validation.environment": "Kunne ikke fortolke miljø-linje: {{line}}",
|
||||
"settings.mcp.status.needs_client_registration": "Kræver klientregistrering",
|
||||
|
||||
"settings.permissions.title": "Tilladelser",
|
||||
"settings.permissions.description": "Styr hvilke værktøjer serveren kan bruge som standard.",
|
||||
|
||||
@@ -682,47 +682,6 @@ export const dict = {
|
||||
"settings.commands.description": "Befehlseinstellungen können hier konfiguriert werden.",
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "MCP-Einstellungen können hier konfiguriert werden.",
|
||||
"settings.mcp.section.featured": "Vorgestellt",
|
||||
"settings.mcp.section.featured.description": "Fügen Sie mit einem Klick eine Vorlage für beliebte MCP-Server hinzu.",
|
||||
"settings.mcp.section.configured": "Konfigurierte Server",
|
||||
"settings.mcp.section.configured.description":
|
||||
"Sehen Sie, welche MCP-Server installiert sind, wie sie verbunden sind, und entfernen Sie nicht mehr benötigte.",
|
||||
"settings.mcp.section.add": "Server hinzufügen",
|
||||
"settings.mcp.section.add.description": "Erstellen Sie Ihre eigene lokale oder Remote-MCP-Server-Konfiguration.",
|
||||
"settings.mcp.type.local": "Lokal",
|
||||
"settings.mcp.type.remote": "Remote",
|
||||
"settings.mcp.featured.added": "Hinzugefügt",
|
||||
"settings.mcp.action.add": "Server hinzufügen",
|
||||
"settings.mcp.action.remove": "Entfernen",
|
||||
"settings.mcp.state.enabled": "Aktiviert",
|
||||
"settings.mcp.state.disabled": "Deaktiviert",
|
||||
"settings.mcp.form.type.label": "Verbindungstyp",
|
||||
"settings.mcp.form.name.label": "Servername",
|
||||
"settings.mcp.form.name.placeholder": "my-mcp-server",
|
||||
"settings.mcp.form.url.label": "Remote-URL",
|
||||
"settings.mcp.form.url.placeholder": "https://mcp.example.com/mcp",
|
||||
"settings.mcp.form.command.label": "Befehl",
|
||||
"settings.mcp.form.command.placeholder": "npx -y @modelcontextprotocol/server-memory",
|
||||
"settings.mcp.form.headers.label": "Header",
|
||||
"settings.mcp.form.headers.description": "Optional. Fügen Sie einen Header pro Zeile im Format KEY: value hinzu.",
|
||||
"settings.mcp.form.environment.label": "Umgebung",
|
||||
"settings.mcp.form.environment.description": "Optional. Fügen Sie eine Variable pro Zeile im Format KEY=value hinzu.",
|
||||
"settings.mcp.form.timeout.label": "Timeout (ms)",
|
||||
"settings.mcp.form.timeout.placeholder": "5000",
|
||||
"settings.mcp.toast.added.title": "MCP-Server hinzugefügt",
|
||||
"settings.mcp.toast.added.description": "{{name}} wurde in Ihren MCP-Einstellungen gespeichert.",
|
||||
"settings.mcp.toast.updated.title": "MCP-Server aktualisiert",
|
||||
"settings.mcp.toast.updated.description": "{{name}} ist jetzt {{state}}.",
|
||||
"settings.mcp.toast.removed.title": "MCP-Server entfernt",
|
||||
"settings.mcp.toast.removed.description": "{{name}} wurde aus Ihren MCP-Einstellungen entfernt.",
|
||||
"settings.mcp.validation.name": "Geben Sie vor dem Speichern einen Servernamen ein.",
|
||||
"settings.mcp.validation.duplicate": "{{name}} ist bereits konfiguriert.",
|
||||
"settings.mcp.validation.url": "Geben Sie eine Remote-MCP-Server-URL ein.",
|
||||
"settings.mcp.validation.command": "Geben Sie den Befehl zum Starten des lokalen MCP-Servers ein.",
|
||||
"settings.mcp.validation.timeout": "Das Timeout muss eine positive ganze Zahl sein.",
|
||||
"settings.mcp.validation.headers": "Header-Zeile konnte nicht verarbeitet werden: {{line}}",
|
||||
"settings.mcp.validation.environment": "Umgebungsvariablen-Zeile konnte nicht verarbeitet werden: {{line}}",
|
||||
"settings.mcp.status.needs_client_registration": "Client-Registrierung erforderlich",
|
||||
"settings.permissions.title": "Berechtigungen",
|
||||
"settings.permissions.description": "Steuern Sie, welche Tools der Server standardmäßig verwenden darf.",
|
||||
"settings.permissions.section.tools": "Tools",
|
||||
|
||||
@@ -770,48 +770,7 @@ export const dict = {
|
||||
"settings.commands.title": "Commands",
|
||||
"settings.commands.description": "Command settings will be configurable here.",
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "Manage local and remote MCP servers that OpenCode can use across your workspaces.",
|
||||
"settings.mcp.section.featured": "Featured",
|
||||
"settings.mcp.section.featured.description": "Add a polished preset for popular MCP servers in one click.",
|
||||
"settings.mcp.section.configured": "Configured servers",
|
||||
"settings.mcp.section.configured.description":
|
||||
"See which MCP servers are installed, how they connect, and remove the ones you no longer need.",
|
||||
"settings.mcp.section.add": "Add a server",
|
||||
"settings.mcp.section.add.description": "Create your own local or remote MCP server configuration.",
|
||||
"settings.mcp.type.local": "Local",
|
||||
"settings.mcp.type.remote": "Remote",
|
||||
"settings.mcp.featured.added": "Added",
|
||||
"settings.mcp.action.add": "Add server",
|
||||
"settings.mcp.action.remove": "Remove",
|
||||
"settings.mcp.state.enabled": "Enabled",
|
||||
"settings.mcp.state.disabled": "Disabled",
|
||||
"settings.mcp.form.type.label": "Connection type",
|
||||
"settings.mcp.form.name.label": "Server name",
|
||||
"settings.mcp.form.name.placeholder": "my-mcp-server",
|
||||
"settings.mcp.form.url.label": "Remote URL",
|
||||
"settings.mcp.form.url.placeholder": "https://mcp.example.com/mcp",
|
||||
"settings.mcp.form.command.label": "Command",
|
||||
"settings.mcp.form.command.placeholder": "npx -y @modelcontextprotocol/server-memory",
|
||||
"settings.mcp.form.headers.label": "Headers",
|
||||
"settings.mcp.form.headers.description": "Optional. Add one header per line using KEY: value.",
|
||||
"settings.mcp.form.environment.label": "Environment",
|
||||
"settings.mcp.form.environment.description": "Optional. Add one variable per line using KEY=value.",
|
||||
"settings.mcp.form.timeout.label": "Timeout (ms)",
|
||||
"settings.mcp.form.timeout.placeholder": "5000",
|
||||
"settings.mcp.toast.added.title": "MCP server added",
|
||||
"settings.mcp.toast.added.description": "{{name}} has been saved to your MCP settings.",
|
||||
"settings.mcp.toast.updated.title": "MCP server updated",
|
||||
"settings.mcp.toast.updated.description": "{{name}} is now {{state}}.",
|
||||
"settings.mcp.toast.removed.title": "MCP server removed",
|
||||
"settings.mcp.toast.removed.description": "{{name}} has been removed from your MCP settings.",
|
||||
"settings.mcp.validation.name": "Enter a server name before saving.",
|
||||
"settings.mcp.validation.duplicate": "{{name}} is already configured.",
|
||||
"settings.mcp.validation.url": "Enter a remote MCP server URL.",
|
||||
"settings.mcp.validation.command": "Enter the command used to start the local MCP server.",
|
||||
"settings.mcp.validation.timeout": "Timeout must be a positive whole number.",
|
||||
"settings.mcp.validation.headers": "Couldn't parse header line: {{line}}",
|
||||
"settings.mcp.validation.environment": "Couldn't parse environment line: {{line}}",
|
||||
"settings.mcp.status.needs_client_registration": "Needs client registration",
|
||||
"settings.mcp.description": "MCP settings will be configurable here.",
|
||||
|
||||
"settings.permissions.title": "Permissions",
|
||||
"settings.permissions.description": "Control what tools the server can use by default.",
|
||||
|
||||
@@ -751,48 +751,6 @@ export const dict = {
|
||||
"settings.commands.description": "La configuración de comandos estará disponible aquí.",
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "La configuración de MCP estará disponible aquí.",
|
||||
"settings.mcp.section.featured": "Destacados",
|
||||
"settings.mcp.section.featured.description":
|
||||
"Añade un ajuste preestablecido para servidores MCP populares con un solo clic.",
|
||||
"settings.mcp.section.configured": "Servidores configurados",
|
||||
"settings.mcp.section.configured.description":
|
||||
"Consulta qué servidores MCP están instalados, cómo se conectan y elimina los que ya no necesites.",
|
||||
"settings.mcp.section.add": "Añadir un servidor",
|
||||
"settings.mcp.section.add.description": "Crea tu propia configuración de servidor MCP local o remoto.",
|
||||
"settings.mcp.type.local": "Local",
|
||||
"settings.mcp.type.remote": "Remoto",
|
||||
"settings.mcp.featured.added": "Añadido",
|
||||
"settings.mcp.action.add": "Añadir servidor",
|
||||
"settings.mcp.action.remove": "Eliminar",
|
||||
"settings.mcp.state.enabled": "Habilitado",
|
||||
"settings.mcp.state.disabled": "Deshabilitado",
|
||||
"settings.mcp.form.type.label": "Tipo de conexión",
|
||||
"settings.mcp.form.name.label": "Nombre del servidor",
|
||||
"settings.mcp.form.name.placeholder": "my-mcp-server",
|
||||
"settings.mcp.form.url.label": "URL remota",
|
||||
"settings.mcp.form.url.placeholder": "https://mcp.example.com/mcp",
|
||||
"settings.mcp.form.command.label": "Comando",
|
||||
"settings.mcp.form.command.placeholder": "npx -y @modelcontextprotocol/server-memory",
|
||||
"settings.mcp.form.headers.label": "Encabezados",
|
||||
"settings.mcp.form.headers.description": "Opcional. Añade un encabezado por línea usando KEY: value.",
|
||||
"settings.mcp.form.environment.label": "Entorno",
|
||||
"settings.mcp.form.environment.description": "Opcional. Añade una variable por línea usando KEY=value.",
|
||||
"settings.mcp.form.timeout.label": "Tiempo de espera (ms)",
|
||||
"settings.mcp.form.timeout.placeholder": "5000",
|
||||
"settings.mcp.toast.added.title": "Servidor MCP añadido",
|
||||
"settings.mcp.toast.added.description": "{{name}} se ha guardado en tu configuración de MCP.",
|
||||
"settings.mcp.toast.updated.title": "Servidor MCP actualizado",
|
||||
"settings.mcp.toast.updated.description": "{{name}} está ahora {{state}}.",
|
||||
"settings.mcp.toast.removed.title": "Servidor MCP eliminado",
|
||||
"settings.mcp.toast.removed.description": "{{name}} se ha eliminado de tu configuración de MCP.",
|
||||
"settings.mcp.validation.name": "Introduce un nombre de servidor antes de guardar.",
|
||||
"settings.mcp.validation.duplicate": "{{name}} ya está configurado.",
|
||||
"settings.mcp.validation.url": "Introduce una URL de servidor MCP remoto.",
|
||||
"settings.mcp.validation.command": "Introduce el comando utilizado para iniciar el servidor MCP local.",
|
||||
"settings.mcp.validation.timeout": "El tiempo de espera debe ser un número entero positivo.",
|
||||
"settings.mcp.validation.headers": "No se pudo analizar la línea de encabezado: {{line}}",
|
||||
"settings.mcp.validation.environment": "No se pudo analizar la línea de entorno: {{line}}",
|
||||
"settings.mcp.status.needs_client_registration": "Requiere registro de cliente",
|
||||
|
||||
"settings.permissions.title": "Permisos",
|
||||
"settings.permissions.description": "Controla qué herramientas puede usar el servidor por defecto.",
|
||||
|
||||
@@ -679,48 +679,6 @@ export const dict = {
|
||||
"settings.commands.description": "Les paramètres des commandes seront configurables ici.",
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "Les paramètres MCP seront configurables ici.",
|
||||
"settings.mcp.section.featured": "En vedette",
|
||||
"settings.mcp.section.featured.description":
|
||||
"Ajoutez un préréglage soigné pour les serveurs MCP populaires en un clic.",
|
||||
"settings.mcp.section.configured": "Serveurs configurés",
|
||||
"settings.mcp.section.configured.description":
|
||||
"Consultez les serveurs MCP installés, leur mode de connexion, et supprimez ceux dont vous n'avez plus besoin.",
|
||||
"settings.mcp.section.add": "Ajouter un serveur",
|
||||
"settings.mcp.section.add.description": "Créez votre propre configuration de serveur MCP local ou distant.",
|
||||
"settings.mcp.type.local": "Local",
|
||||
"settings.mcp.type.remote": "Distant",
|
||||
"settings.mcp.featured.added": "Ajouté",
|
||||
"settings.mcp.action.add": "Ajouter un serveur",
|
||||
"settings.mcp.action.remove": "Supprimer",
|
||||
"settings.mcp.state.enabled": "Activé",
|
||||
"settings.mcp.state.disabled": "Désactivé",
|
||||
"settings.mcp.form.type.label": "Type de connexion",
|
||||
"settings.mcp.form.name.label": "Nom du serveur",
|
||||
"settings.mcp.form.name.placeholder": "my-mcp-server",
|
||||
"settings.mcp.form.url.label": "URL distante",
|
||||
"settings.mcp.form.url.placeholder": "https://mcp.example.com/mcp",
|
||||
"settings.mcp.form.command.label": "Commande",
|
||||
"settings.mcp.form.command.placeholder": "npx -y @modelcontextprotocol/server-memory",
|
||||
"settings.mcp.form.headers.label": "En-têtes",
|
||||
"settings.mcp.form.headers.description": "Facultatif. Ajoutez un en-tête par ligne au format KEY: value.",
|
||||
"settings.mcp.form.environment.label": "Environnement",
|
||||
"settings.mcp.form.environment.description": "Facultatif. Ajoutez une variable par ligne au format KEY=value.",
|
||||
"settings.mcp.form.timeout.label": "Délai d'attente (ms)",
|
||||
"settings.mcp.form.timeout.placeholder": "5000",
|
||||
"settings.mcp.toast.added.title": "Serveur MCP ajouté",
|
||||
"settings.mcp.toast.added.description": "{{name}} a été enregistré dans vos paramètres MCP.",
|
||||
"settings.mcp.toast.updated.title": "Serveur MCP mis à jour",
|
||||
"settings.mcp.toast.updated.description": "{{name}} est désormais {{state}}.",
|
||||
"settings.mcp.toast.removed.title": "Serveur MCP supprimé",
|
||||
"settings.mcp.toast.removed.description": "{{name}} a été supprimé de vos paramètres MCP.",
|
||||
"settings.mcp.validation.name": "Saisissez un nom de serveur avant d'enregistrer.",
|
||||
"settings.mcp.validation.duplicate": "{{name}} est déjà configuré.",
|
||||
"settings.mcp.validation.url": "Saisissez une URL de serveur MCP distant.",
|
||||
"settings.mcp.validation.command": "Saisissez la commande utilisée pour démarrer le serveur MCP local.",
|
||||
"settings.mcp.validation.timeout": "Le délai d'attente doit être un nombre entier positif.",
|
||||
"settings.mcp.validation.headers": "Impossible d'analyser la ligne d'en-tête : {{line}}",
|
||||
"settings.mcp.validation.environment": "Impossible d'analyser la ligne d'environnement : {{line}}",
|
||||
"settings.mcp.status.needs_client_registration": "Nécessite l'enregistrement du client",
|
||||
"settings.permissions.title": "Permissions",
|
||||
"settings.permissions.description": "Contrôlez les outils que le serveur peut utiliser par défaut.",
|
||||
"settings.permissions.section.tools": "Outils",
|
||||
|
||||
@@ -670,48 +670,6 @@ export const dict = {
|
||||
"settings.commands.description": "コマンド設定はここで構成できます。",
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "MCP設定はここで構成できます。",
|
||||
"settings.mcp.section.featured": "おすすめ",
|
||||
"settings.mcp.section.featured.description":
|
||||
"人気のMCPサーバー向けに調整されたプリセットをワンクリックで追加できます。",
|
||||
"settings.mcp.section.configured": "設定済みのサーバー",
|
||||
"settings.mcp.section.configured.description":
|
||||
"インストールされているMCPサーバーや接続方法を確認したり、不要になったサーバーを削除したりできます。",
|
||||
"settings.mcp.section.add": "サーバーを追加",
|
||||
"settings.mcp.section.add.description": "ローカルまたはリモートの独自のMCPサーバー設定を作成します。",
|
||||
"settings.mcp.type.local": "ローカル",
|
||||
"settings.mcp.type.remote": "リモート",
|
||||
"settings.mcp.featured.added": "追加済み",
|
||||
"settings.mcp.action.add": "サーバーを追加",
|
||||
"settings.mcp.action.remove": "削除",
|
||||
"settings.mcp.state.enabled": "有効",
|
||||
"settings.mcp.state.disabled": "無効",
|
||||
"settings.mcp.form.type.label": "接続タイプ",
|
||||
"settings.mcp.form.name.label": "サーバー名",
|
||||
"settings.mcp.form.name.placeholder": "my-mcp-server",
|
||||
"settings.mcp.form.url.label": "リモートURL",
|
||||
"settings.mcp.form.url.placeholder": "https://mcp.example.com/mcp",
|
||||
"settings.mcp.form.command.label": "コマンド",
|
||||
"settings.mcp.form.command.placeholder": "npx -y @modelcontextprotocol/server-memory",
|
||||
"settings.mcp.form.headers.label": "ヘッダー",
|
||||
"settings.mcp.form.headers.description": "任意。1行につき1つのヘッダーを KEY: value の形式で追加してください。",
|
||||
"settings.mcp.form.environment.label": "環境変数",
|
||||
"settings.mcp.form.environment.description": "任意。1行につき1つの変数を KEY=value の形式で追加してください。",
|
||||
"settings.mcp.form.timeout.label": "タイムアウト (ms)",
|
||||
"settings.mcp.form.timeout.placeholder": "5000",
|
||||
"settings.mcp.toast.added.title": "MCPサーバーを追加しました",
|
||||
"settings.mcp.toast.added.description": "{{name}} をMCP設定に保存しました。",
|
||||
"settings.mcp.toast.updated.title": "MCPサーバーを更新しました",
|
||||
"settings.mcp.toast.updated.description": "{{name}} は現在{{state}}です。",
|
||||
"settings.mcp.toast.removed.title": "MCPサーバーを削除しました",
|
||||
"settings.mcp.toast.removed.description": "MCP設定から {{name}} を削除しました。",
|
||||
"settings.mcp.validation.name": "保存する前にサーバー名を入力してください。",
|
||||
"settings.mcp.validation.duplicate": "{{name}} は既に設定されています。",
|
||||
"settings.mcp.validation.url": "リモートMCPサーバーのURLを入力してください。",
|
||||
"settings.mcp.validation.command": "ローカルMCPサーバーの起動コマンドを入力してください。",
|
||||
"settings.mcp.validation.timeout": "タイムアウトには正の整数を指定してください。",
|
||||
"settings.mcp.validation.headers": "ヘッダー行を解析できませんでした: {{line}}",
|
||||
"settings.mcp.validation.environment": "環境変数の行を解析できませんでした: {{line}}",
|
||||
"settings.mcp.status.needs_client_registration": "クライアント登録が必要です",
|
||||
"settings.permissions.title": "権限",
|
||||
"settings.permissions.description": "サーバーがデフォルトで使用できるツールを制御します。",
|
||||
"settings.permissions.section.tools": "ツール",
|
||||
|
||||
@@ -670,49 +670,6 @@ export const dict = {
|
||||
"settings.commands.description": "명령어 설정은 여기서 구성할 수 있습니다.",
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "MCP 설정은 여기서 구성할 수 있습니다.",
|
||||
"settings.mcp.section.featured": "추천",
|
||||
"settings.mcp.section.featured.description": "클릭 한 번으로 인기 있는 MCP 서버에 대한 프리셋을 추가하세요.",
|
||||
"settings.mcp.section.configured": "구성된 서버",
|
||||
"settings.mcp.section.configured.description":
|
||||
"설치된 MCP 서버와 연결 방식을 확인하고, 더 이상 필요하지 않은 서버를 제거하세요.",
|
||||
"settings.mcp.section.add": "서버 추가",
|
||||
"settings.mcp.section.add.description": "로컬 또는 원격 MCP 서버 구성을 직접 생성하세요.",
|
||||
"settings.mcp.type.local": "로컬",
|
||||
"settings.mcp.type.remote": "원격",
|
||||
"settings.mcp.featured.added": "추가됨",
|
||||
"settings.mcp.action.add": "서버 추가",
|
||||
"settings.mcp.action.remove": "제거",
|
||||
"settings.mcp.state.enabled": "활성화됨",
|
||||
"settings.mcp.state.disabled": "비활성화됨",
|
||||
"settings.mcp.form.type.label": "연결 유형",
|
||||
"settings.mcp.form.name.label": "서버 이름",
|
||||
"settings.mcp.form.name.placeholder": "my-mcp-server",
|
||||
"settings.mcp.form.url.label": "원격 URL",
|
||||
"settings.mcp.form.url.placeholder": "https://mcp.example.com/mcp",
|
||||
"settings.mcp.form.command.label": "명령",
|
||||
"settings.mcp.form.command.placeholder": "npx -y @modelcontextprotocol/server-memory",
|
||||
"settings.mcp.form.headers.label": "헤더",
|
||||
"settings.mcp.form.headers.description":
|
||||
"선택 사항입니다. KEY: value 형식을 사용하여 한 줄에 하나의 헤더를 추가하세요.",
|
||||
"settings.mcp.form.environment.label": "환경 변수",
|
||||
"settings.mcp.form.environment.description":
|
||||
"선택 사항입니다. KEY=value 형식을 사용하여 한 줄에 하나의 변수를 추가하세요.",
|
||||
"settings.mcp.form.timeout.label": "시간 초과 (ms)",
|
||||
"settings.mcp.form.timeout.placeholder": "5000",
|
||||
"settings.mcp.toast.added.title": "MCP 서버 추가됨",
|
||||
"settings.mcp.toast.added.description": "{{name}} 서버가 MCP 설정에 저장되었습니다.",
|
||||
"settings.mcp.toast.updated.title": "MCP 서버 업데이트됨",
|
||||
"settings.mcp.toast.updated.description": "{{name}}이(가) 이제 {{state}} 상태입니다.",
|
||||
"settings.mcp.toast.removed.title": "MCP 서버 제거됨",
|
||||
"settings.mcp.toast.removed.description": "{{name}} 서버가 MCP 설정에서 제거되었습니다.",
|
||||
"settings.mcp.validation.name": "저장하기 전에 서버 이름을 입력하세요.",
|
||||
"settings.mcp.validation.duplicate": "{{name}}은(는) 이미 구성되어 있습니다.",
|
||||
"settings.mcp.validation.url": "원격 MCP 서버 URL을 입력하세요.",
|
||||
"settings.mcp.validation.command": "로컬 MCP 서버를 시작하는 데 사용되는 명령을 입력하세요.",
|
||||
"settings.mcp.validation.timeout": "시간 초과는 양의 정수여야 합니다.",
|
||||
"settings.mcp.validation.headers": "헤더 줄을 구문 분석할 수 없습니다: {{line}}",
|
||||
"settings.mcp.validation.environment": "환경 변수 줄을 구문 분석할 수 없습니다: {{line}}",
|
||||
"settings.mcp.status.needs_client_registration": "클라이언트 등록 필요",
|
||||
"settings.permissions.title": "권한",
|
||||
"settings.permissions.description": "서버가 기본적으로 사용할 수 있는 도구를 제어합니다.",
|
||||
"settings.permissions.section.tools": "도구",
|
||||
|
||||
@@ -747,47 +747,6 @@ export const dict = {
|
||||
"settings.commands.description": "Kommandoinnstillinger vil kunne konfigureres her.",
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "MCP-innstillinger vil kunne konfigureres her.",
|
||||
"settings.mcp.section.featured": "Utvalgte",
|
||||
"settings.mcp.section.featured.description": "Legg til et ferdig oppsett for populære MCP-servere med ett klikk.",
|
||||
"settings.mcp.section.configured": "Konfigurerte servere",
|
||||
"settings.mcp.section.configured.description":
|
||||
"Se hvilke MCP-servere som er installert, hvordan de kobler til, og fjern de du ikke lenger trenger.",
|
||||
"settings.mcp.section.add": "Legg til en server",
|
||||
"settings.mcp.section.add.description": "Opprett din egen konfigurasjon for lokal eller ekstern MCP-server.",
|
||||
"settings.mcp.type.local": "Lokal",
|
||||
"settings.mcp.type.remote": "Ekstern",
|
||||
"settings.mcp.featured.added": "Lagt til",
|
||||
"settings.mcp.action.add": "Legg til server",
|
||||
"settings.mcp.action.remove": "Fjern",
|
||||
"settings.mcp.state.enabled": "Aktivert",
|
||||
"settings.mcp.state.disabled": "Deaktivert",
|
||||
"settings.mcp.form.type.label": "Tilkoblingstype",
|
||||
"settings.mcp.form.name.label": "Servernavn",
|
||||
"settings.mcp.form.name.placeholder": "my-mcp-server",
|
||||
"settings.mcp.form.url.label": "Ekstern URL",
|
||||
"settings.mcp.form.url.placeholder": "https://mcp.example.com/mcp",
|
||||
"settings.mcp.form.command.label": "Kommando",
|
||||
"settings.mcp.form.command.placeholder": "npx -y @modelcontextprotocol/server-memory",
|
||||
"settings.mcp.form.headers.label": "Headere",
|
||||
"settings.mcp.form.headers.description": "Valgfritt. Legg til én header per linje med KEY: value.",
|
||||
"settings.mcp.form.environment.label": "Miljø",
|
||||
"settings.mcp.form.environment.description": "Valgfritt. Legg til én variabel per linje med KEY=value.",
|
||||
"settings.mcp.form.timeout.label": "Tidsavbrudd (ms)",
|
||||
"settings.mcp.form.timeout.placeholder": "5000",
|
||||
"settings.mcp.toast.added.title": "MCP-server lagt til",
|
||||
"settings.mcp.toast.added.description": "{{name}} er lagret i dine MCP-innstillinger.",
|
||||
"settings.mcp.toast.updated.title": "MCP-server oppdatert",
|
||||
"settings.mcp.toast.updated.description": "{{name}} er nå {{state}}.",
|
||||
"settings.mcp.toast.removed.title": "MCP-server fjernet",
|
||||
"settings.mcp.toast.removed.description": "{{name}} er fjernet fra dine MCP-innstillinger.",
|
||||
"settings.mcp.validation.name": "Skriv inn et servernavn før du lagrer.",
|
||||
"settings.mcp.validation.duplicate": "{{name}} er allerede konfigurert.",
|
||||
"settings.mcp.validation.url": "Skriv inn URL for ekstern MCP-server.",
|
||||
"settings.mcp.validation.command": "Skriv inn kommandoen for å starte den lokale MCP-serveren.",
|
||||
"settings.mcp.validation.timeout": "Tidsavbrudd må være et positivt heltall.",
|
||||
"settings.mcp.validation.headers": "Kunne ikke tolke header-linje: {{line}}",
|
||||
"settings.mcp.validation.environment": "Kunne ikke tolke miljøvariabel-linje: {{line}}",
|
||||
"settings.mcp.status.needs_client_registration": "Krever klientregistrering",
|
||||
|
||||
"settings.permissions.title": "Tillatelser",
|
||||
"settings.permissions.description": "Kontroller hvilke verktøy serveren kan bruke som standard.",
|
||||
|
||||
@@ -672,48 +672,6 @@ export const dict = {
|
||||
"settings.commands.description": "Ustawienia poleceń będą tutaj konfigurowalne.",
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "Ustawienia MCP będą tutaj konfigurowalne.",
|
||||
"settings.mcp.section.featured": "Wyróżnione",
|
||||
"settings.mcp.section.featured.description":
|
||||
"Dodaj dopracowany zestaw dla popularnych serwerów MCP jednym kliknięciem.",
|
||||
"settings.mcp.section.configured": "Skonfigurowane serwery",
|
||||
"settings.mcp.section.configured.description":
|
||||
"Zobacz, które serwery MCP są zainstalowane, jak się łączą, i usuń te, których już nie potrzebujesz.",
|
||||
"settings.mcp.section.add": "Dodaj serwer",
|
||||
"settings.mcp.section.add.description": "Utwórz własną konfigurację lokalnego lub zdalnego serwera MCP.",
|
||||
"settings.mcp.type.local": "Lokalny",
|
||||
"settings.mcp.type.remote": "Zdalny",
|
||||
"settings.mcp.featured.added": "Dodano",
|
||||
"settings.mcp.action.add": "Dodaj serwer",
|
||||
"settings.mcp.action.remove": "Usuń",
|
||||
"settings.mcp.state.enabled": "Włączony",
|
||||
"settings.mcp.state.disabled": "Wyłączony",
|
||||
"settings.mcp.form.type.label": "Typ połączenia",
|
||||
"settings.mcp.form.name.label": "Nazwa serwera",
|
||||
"settings.mcp.form.name.placeholder": "my-mcp-server",
|
||||
"settings.mcp.form.url.label": "Zdalny adres URL",
|
||||
"settings.mcp.form.url.placeholder": "https://mcp.example.com/mcp",
|
||||
"settings.mcp.form.command.label": "Polecenie",
|
||||
"settings.mcp.form.command.placeholder": "npx -y @modelcontextprotocol/server-memory",
|
||||
"settings.mcp.form.headers.label": "Nagłówki",
|
||||
"settings.mcp.form.headers.description": "Opcjonalne. Dodaj jeden nagłówek w wierszu używając formatu KEY: value.",
|
||||
"settings.mcp.form.environment.label": "Środowisko",
|
||||
"settings.mcp.form.environment.description": "Opcjonalne. Dodaj jedną zmienną w wierszu używając formatu KEY=value.",
|
||||
"settings.mcp.form.timeout.label": "Limit czasu (ms)",
|
||||
"settings.mcp.form.timeout.placeholder": "5000",
|
||||
"settings.mcp.toast.added.title": "Dodano serwer MCP",
|
||||
"settings.mcp.toast.added.description": "{{name}} został zapisany w ustawieniach MCP.",
|
||||
"settings.mcp.toast.updated.title": "Zaktualizowano serwer MCP",
|
||||
"settings.mcp.toast.updated.description": "{{name}} jest teraz {{state}}.",
|
||||
"settings.mcp.toast.removed.title": "Usunięto serwer MCP",
|
||||
"settings.mcp.toast.removed.description": "{{name}} został usunięty z ustawień MCP.",
|
||||
"settings.mcp.validation.name": "Wprowadź nazwę serwera przed zapisaniem.",
|
||||
"settings.mcp.validation.duplicate": "{{name}} jest już skonfigurowany.",
|
||||
"settings.mcp.validation.url": "Wprowadź adres URL zdalnego serwera MCP.",
|
||||
"settings.mcp.validation.command": "Wprowadź polecenie służące do uruchomienia lokalnego serwera MCP.",
|
||||
"settings.mcp.validation.timeout": "Limit czasu musi być dodatnią liczbą całkowitą.",
|
||||
"settings.mcp.validation.headers": "Nie udało się przetworzyć wiersza nagłówka: {{line}}",
|
||||
"settings.mcp.validation.environment": "Nie udało się przetworzyć wiersza środowiska: {{line}}",
|
||||
"settings.mcp.status.needs_client_registration": "Wymaga rejestracji klienta",
|
||||
"settings.permissions.title": "Uprawnienia",
|
||||
"settings.permissions.description": "Kontroluj, jakich narzędzi serwer może używać domyślnie.",
|
||||
"settings.permissions.section.tools": "Narzędzia",
|
||||
|
||||
@@ -747,49 +747,6 @@ export const dict = {
|
||||
"settings.commands.description": "Настройки команд будут доступны здесь.",
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "Настройки MCP будут доступны здесь.",
|
||||
"settings.mcp.section.featured": "Рекомендуемые",
|
||||
"settings.mcp.section.featured.description": "Добавляйте готовые настройки для популярных серверов MCP в один клик.",
|
||||
"settings.mcp.section.configured": "Настроенные серверы",
|
||||
"settings.mcp.section.configured.description":
|
||||
"Просматривайте установленные серверы MCP, способы их подключения и удаляйте ненужные.",
|
||||
"settings.mcp.section.add": "Добавить сервер",
|
||||
"settings.mcp.section.add.description": "Создайте собственную конфигурацию локального или удаленного сервера MCP.",
|
||||
"settings.mcp.type.local": "Локальный",
|
||||
"settings.mcp.type.remote": "Удаленный",
|
||||
"settings.mcp.featured.added": "Добавлено",
|
||||
"settings.mcp.action.add": "Добавить сервер",
|
||||
"settings.mcp.action.remove": "Удалить",
|
||||
"settings.mcp.state.enabled": "Включено",
|
||||
"settings.mcp.state.disabled": "Отключено",
|
||||
"settings.mcp.form.type.label": "Тип подключения",
|
||||
"settings.mcp.form.name.label": "Имя сервера",
|
||||
"settings.mcp.form.name.placeholder": "my-mcp-server",
|
||||
"settings.mcp.form.url.label": "URL удаленного сервера",
|
||||
"settings.mcp.form.url.placeholder": "https://mcp.example.com/mcp",
|
||||
"settings.mcp.form.command.label": "Команда",
|
||||
"settings.mcp.form.command.placeholder": "npx -y @modelcontextprotocol/server-memory",
|
||||
"settings.mcp.form.headers.label": "Заголовки",
|
||||
"settings.mcp.form.headers.description":
|
||||
"Необязательно. Добавьте по одному заголовку на строку в формате KEY: value.",
|
||||
"settings.mcp.form.environment.label": "Окружение",
|
||||
"settings.mcp.form.environment.description":
|
||||
"Необязательно. Добавьте по одной переменной на строку в формате KEY=value.",
|
||||
"settings.mcp.form.timeout.label": "Тайм-аут (мс)",
|
||||
"settings.mcp.form.timeout.placeholder": "5000",
|
||||
"settings.mcp.toast.added.title": "Сервер MCP добавлен",
|
||||
"settings.mcp.toast.added.description": "{{name}} сохранен в настройках MCP.",
|
||||
"settings.mcp.toast.updated.title": "Сервер MCP обновлен",
|
||||
"settings.mcp.toast.updated.description": "{{name}} теперь {{state}}.",
|
||||
"settings.mcp.toast.removed.title": "Сервер MCP удален",
|
||||
"settings.mcp.toast.removed.description": "{{name}} удален из настроек MCP.",
|
||||
"settings.mcp.validation.name": "Введите имя сервера перед сохранением.",
|
||||
"settings.mcp.validation.duplicate": "{{name}} уже настроен.",
|
||||
"settings.mcp.validation.url": "Введите URL удаленного сервера MCP.",
|
||||
"settings.mcp.validation.command": "Введите команду для запуска локального сервера MCP.",
|
||||
"settings.mcp.validation.timeout": "Тайм-аут должен быть положительным целым числом.",
|
||||
"settings.mcp.validation.headers": "Не удалось разобрать строку заголовка: {{line}}",
|
||||
"settings.mcp.validation.environment": "Не удалось разобрать строку окружения: {{line}}",
|
||||
"settings.mcp.status.needs_client_registration": "Требуется регистрация клиента",
|
||||
|
||||
"settings.permissions.title": "Разрешения",
|
||||
"settings.permissions.description": "Контролируйте какие инструменты сервер может использовать по умолчанию.",
|
||||
|
||||
@@ -738,47 +738,6 @@ export const dict = {
|
||||
"settings.commands.description": "การตั้งค่าคำสั่งจะสามารถกำหนดค่าได้ที่นี่",
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "การตั้งค่า MCP จะสามารถกำหนดค่าได้ที่นี่",
|
||||
"settings.mcp.section.featured": "แนะนำ",
|
||||
"settings.mcp.section.featured.description": "เพิ่มค่าที่ตั้งไว้ล่วงหน้าสำหรับเซิร์ฟเวอร์ MCP ยอดนิยมได้ในคลิกเดียว",
|
||||
"settings.mcp.section.configured": "เซิร์ฟเวอร์ที่กำหนดค่าแล้ว",
|
||||
"settings.mcp.section.configured.description":
|
||||
"ดูว่าเซิร์ฟเวอร์ MCP ใดติดตั้งอยู่ เชื่อมต่ออย่างไร และลบสิ่งที่ไม่ต้องการออก",
|
||||
"settings.mcp.section.add": "เพิ่มเซิร์ฟเวอร์",
|
||||
"settings.mcp.section.add.description": "สร้างการกำหนดค่าเซิร์ฟเวอร์ MCP แบบ Local หรือ Remote ของคุณเอง",
|
||||
"settings.mcp.type.local": "Local",
|
||||
"settings.mcp.type.remote": "Remote",
|
||||
"settings.mcp.featured.added": "เพิ่มแล้ว",
|
||||
"settings.mcp.action.add": "เพิ่มเซิร์ฟเวอร์",
|
||||
"settings.mcp.action.remove": "ลบ",
|
||||
"settings.mcp.state.enabled": "เปิดใช้งาน",
|
||||
"settings.mcp.state.disabled": "ปิดใช้งาน",
|
||||
"settings.mcp.form.type.label": "ประเภทการเชื่อมต่อ",
|
||||
"settings.mcp.form.name.label": "ชื่อเซิร์ฟเวอร์",
|
||||
"settings.mcp.form.name.placeholder": "my-mcp-server",
|
||||
"settings.mcp.form.url.label": "URL ระยะไกล",
|
||||
"settings.mcp.form.url.placeholder": "https://mcp.example.com/mcp",
|
||||
"settings.mcp.form.command.label": "คำสั่ง",
|
||||
"settings.mcp.form.command.placeholder": "npx -y @modelcontextprotocol/server-memory",
|
||||
"settings.mcp.form.headers.label": "Headers",
|
||||
"settings.mcp.form.headers.description": "ไม่บังคับ เพิ่มหนึ่ง header ต่อบรรทัดโดยใช้ KEY: value",
|
||||
"settings.mcp.form.environment.label": "Environment",
|
||||
"settings.mcp.form.environment.description": "ไม่บังคับ เพิ่มหนึ่งตัวแปรต่อบรรทัดโดยใช้ KEY=value",
|
||||
"settings.mcp.form.timeout.label": "Timeout (ms)",
|
||||
"settings.mcp.form.timeout.placeholder": "5000",
|
||||
"settings.mcp.toast.added.title": "เพิ่มเซิร์ฟเวอร์ MCP แล้ว",
|
||||
"settings.mcp.toast.added.description": "บันทึก {{name}} ลงในค่าติดตั้ง MCP ของคุณแล้ว",
|
||||
"settings.mcp.toast.updated.title": "อัปเดตเซิร์ฟเวอร์ MCP แล้ว",
|
||||
"settings.mcp.toast.updated.description": "ขณะนี้ {{name}} อยู่ในสถานะ {{state}}",
|
||||
"settings.mcp.toast.removed.title": "ลบเซิร์ฟเวอร์ MCP แล้ว",
|
||||
"settings.mcp.toast.removed.description": "ลบ {{name}} ออกจากค่าติดตั้ง MCP ของคุณแล้ว",
|
||||
"settings.mcp.validation.name": "ป้อนชื่อเซิร์ฟเวอร์ก่อนบันทึก",
|
||||
"settings.mcp.validation.duplicate": "มีการกำหนดค่า {{name}} ไว้แล้ว",
|
||||
"settings.mcp.validation.url": "ป้อน URL ของเซิร์ฟเวอร์ MCP ระยะไกล",
|
||||
"settings.mcp.validation.command": "ป้อนคำสั่งที่ใช้เริ่มต้นเซิร์ฟเวอร์ MCP แบบ Local",
|
||||
"settings.mcp.validation.timeout": "Timeout ต้องเป็นจำนวนเต็มบวก",
|
||||
"settings.mcp.validation.headers": "ไม่สามารถแยกวิเคราะห์บรรทัด header: {{line}}",
|
||||
"settings.mcp.validation.environment": "ไม่สามารถแยกวิเคราะห์บรรทัด environment: {{line}}",
|
||||
"settings.mcp.status.needs_client_registration": "ต้องลงทะเบียนไคลเอนต์",
|
||||
|
||||
"settings.permissions.title": "สิทธิ์",
|
||||
"settings.permissions.description": "ควบคุมเครื่องมือที่เซิร์ฟเวอร์สามารถใช้โดยค่าเริ่มต้น",
|
||||
|
||||
@@ -759,50 +759,6 @@ export const dict = {
|
||||
"settings.commands.description": "Komut ayarları burada yapılandırılabilecek.",
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "MCP ayarları burada yapılandırılabilecek.",
|
||||
"settings.mcp.section.featured": "Öne Çıkanlar",
|
||||
"settings.mcp.section.featured.description":
|
||||
"Tek tıkla popüler MCP sunucuları için cilalanmış bir hazır ayar ekleyin.",
|
||||
"settings.mcp.section.configured": "Yapılandırılmış sunucular",
|
||||
"settings.mcp.section.configured.description":
|
||||
"Hangi MCP sunucularının yüklü olduğunu ve nasıl bağlandıklarını görün; artık ihtiyaç duymadıklarınızı kaldırın.",
|
||||
"settings.mcp.section.add": "Sunucu ekle",
|
||||
"settings.mcp.section.add.description": "Kendi yerel veya uzak MCP sunucu yapılandırmanızı oluşturun.",
|
||||
"settings.mcp.type.local": "Yerel",
|
||||
"settings.mcp.type.remote": "Uzak",
|
||||
"settings.mcp.featured.added": "Eklendi",
|
||||
"settings.mcp.action.add": "Sunucu ekle",
|
||||
"settings.mcp.action.remove": "Kaldır",
|
||||
"settings.mcp.state.enabled": "Etkin",
|
||||
"settings.mcp.state.disabled": "Devre Dışı",
|
||||
"settings.mcp.form.type.label": "Bağlantı türü",
|
||||
"settings.mcp.form.name.label": "Sunucu adı",
|
||||
"settings.mcp.form.name.placeholder": "my-mcp-server",
|
||||
"settings.mcp.form.url.label": "Uzak URL",
|
||||
"settings.mcp.form.url.placeholder": "https://mcp.example.com/mcp",
|
||||
"settings.mcp.form.command.label": "Komut",
|
||||
"settings.mcp.form.command.placeholder": "npx -y @modelcontextprotocol/server-memory",
|
||||
"settings.mcp.form.headers.label": "Başlıklar",
|
||||
"settings.mcp.form.headers.description":
|
||||
"İsteğe bağlı. Her satıra bir başlık olacak şekilde KEY: value biçiminde ekleyin.",
|
||||
"settings.mcp.form.environment.label": "Ortam",
|
||||
"settings.mcp.form.environment.description":
|
||||
"İsteğe bağlı. Her satıra bir değişken olacak şekilde KEY=value biçiminde ekleyin.",
|
||||
"settings.mcp.form.timeout.label": "Zaman aşımı (ms)",
|
||||
"settings.mcp.form.timeout.placeholder": "5000",
|
||||
"settings.mcp.toast.added.title": "MCP sunucusu eklendi",
|
||||
"settings.mcp.toast.added.description": "{{name}}, MCP ayarlarınıza kaydedildi.",
|
||||
"settings.mcp.toast.updated.title": "MCP sunucusu güncellendi",
|
||||
"settings.mcp.toast.updated.description": "{{name}} artık {{state}}.",
|
||||
"settings.mcp.toast.removed.title": "MCP sunucusu kaldırıldı",
|
||||
"settings.mcp.toast.removed.description": "{{name}}, MCP ayarlarınızdan kaldırıldı.",
|
||||
"settings.mcp.validation.name": "Kaydetmeden önce bir sunucu adı girin.",
|
||||
"settings.mcp.validation.duplicate": "{{name}} zaten yapılandırılmış.",
|
||||
"settings.mcp.validation.url": "Bir uzak MCP sunucu URL'si girin.",
|
||||
"settings.mcp.validation.command": "Yerel MCP sunucusunu başlatmak için kullanılan komutu girin.",
|
||||
"settings.mcp.validation.timeout": "Zaman aşımı pozitif bir tam sayı olmalıdır.",
|
||||
"settings.mcp.validation.headers": "Başlık satırı ayrıştırılamadı: {{line}}",
|
||||
"settings.mcp.validation.environment": "Ortam satırı ayrıştırılamadı: {{line}}",
|
||||
"settings.mcp.status.needs_client_registration": "İstemci kaydı gerekiyor",
|
||||
|
||||
"settings.permissions.title": "İzinler",
|
||||
"settings.permissions.description": "Sunucunun varsayılan olarak hangi araçları kullanabileceğini kontrol edin.",
|
||||
|
||||
@@ -738,46 +738,6 @@ export const dict = {
|
||||
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "MCP 设置将在此处可配置。",
|
||||
"settings.mcp.section.featured": "精选",
|
||||
"settings.mcp.section.featured.description": "一键添加经过精心优化的热门 MCP 服务器预设。",
|
||||
"settings.mcp.section.configured": "已配置的服务器",
|
||||
"settings.mcp.section.configured.description": "查看已安装的 MCP 服务器及其连接方式,并移除不再需要的服务器。",
|
||||
"settings.mcp.section.add": "添加服务器",
|
||||
"settings.mcp.section.add.description": "创建您自己的本地或远程 MCP 服务器配置。",
|
||||
"settings.mcp.type.local": "本地",
|
||||
"settings.mcp.type.remote": "远程",
|
||||
"settings.mcp.featured.added": "已添加",
|
||||
"settings.mcp.action.add": "添加服务器",
|
||||
"settings.mcp.action.remove": "移除",
|
||||
"settings.mcp.state.enabled": "已启用",
|
||||
"settings.mcp.state.disabled": "已禁用",
|
||||
"settings.mcp.form.type.label": "连接类型",
|
||||
"settings.mcp.form.name.label": "服务器名称",
|
||||
"settings.mcp.form.name.placeholder": "my-mcp-server",
|
||||
"settings.mcp.form.url.label": "远程 URL",
|
||||
"settings.mcp.form.url.placeholder": "https://mcp.example.com/mcp",
|
||||
"settings.mcp.form.command.label": "命令",
|
||||
"settings.mcp.form.command.placeholder": "npx -y @modelcontextprotocol/server-memory",
|
||||
"settings.mcp.form.headers.label": "请求头",
|
||||
"settings.mcp.form.headers.description": "可选。每行添加一个请求头,格式为 KEY: value。",
|
||||
"settings.mcp.form.environment.label": "环境变量",
|
||||
"settings.mcp.form.environment.description": "可选。每行添加一个变量,格式为 KEY=value。",
|
||||
"settings.mcp.form.timeout.label": "超时 (ms)",
|
||||
"settings.mcp.form.timeout.placeholder": "5000",
|
||||
"settings.mcp.toast.added.title": "MCP 服务器已添加",
|
||||
"settings.mcp.toast.added.description": "{{name}} 已保存到您的 MCP 设置中。",
|
||||
"settings.mcp.toast.updated.title": "MCP 服务器已更新",
|
||||
"settings.mcp.toast.updated.description": "{{name}} 现已{{state}}。",
|
||||
"settings.mcp.toast.removed.title": "MCP 服务器已移除",
|
||||
"settings.mcp.toast.removed.description": "{{name}} 已从您的 MCP 设置中移除。",
|
||||
"settings.mcp.validation.name": "保存前请输入服务器名称。",
|
||||
"settings.mcp.validation.duplicate": "{{name}} 已配置。",
|
||||
"settings.mcp.validation.url": "请输入远程 MCP 服务器 URL。",
|
||||
"settings.mcp.validation.command": "请输入用于启动本地 MCP 服务器的命令。",
|
||||
"settings.mcp.validation.timeout": "超时必须为正整数。",
|
||||
"settings.mcp.validation.headers": "无法解析请求头行:{{line}}",
|
||||
"settings.mcp.validation.environment": "无法解析环境行:{{line}}",
|
||||
"settings.mcp.status.needs_client_registration": "需要客户端注册",
|
||||
|
||||
"settings.permissions.title": "权限",
|
||||
"settings.permissions.description": "控制服务器默认可以使用哪些工具。",
|
||||
|
||||
@@ -731,46 +731,6 @@ export const dict = {
|
||||
"settings.commands.description": "命令設定將在此處可設定。",
|
||||
"settings.mcp.title": "MCP",
|
||||
"settings.mcp.description": "MCP 設定將在此處可設定。",
|
||||
"settings.mcp.section.featured": "精選",
|
||||
"settings.mcp.section.featured.description": "一鍵新增熱門 MCP 伺服器的精選預設設定。",
|
||||
"settings.mcp.section.configured": "已設定的伺服器",
|
||||
"settings.mcp.section.configured.description": "查看已安裝的 MCP 伺服器及其連線方式,並移除不再需要的伺服器。",
|
||||
"settings.mcp.section.add": "新增伺服器",
|
||||
"settings.mcp.section.add.description": "建立您自己的本機或遠端 MCP 伺服器設定。",
|
||||
"settings.mcp.type.local": "本機",
|
||||
"settings.mcp.type.remote": "遠端",
|
||||
"settings.mcp.featured.added": "已新增",
|
||||
"settings.mcp.action.add": "新增伺服器",
|
||||
"settings.mcp.action.remove": "移除",
|
||||
"settings.mcp.state.enabled": "已啟用",
|
||||
"settings.mcp.state.disabled": "已停用",
|
||||
"settings.mcp.form.type.label": "連線類型",
|
||||
"settings.mcp.form.name.label": "伺服器名稱",
|
||||
"settings.mcp.form.name.placeholder": "my-mcp-server",
|
||||
"settings.mcp.form.url.label": "遠端 URL",
|
||||
"settings.mcp.form.url.placeholder": "https://mcp.example.com/mcp",
|
||||
"settings.mcp.form.command.label": "指令",
|
||||
"settings.mcp.form.command.placeholder": "npx -y @modelcontextprotocol/server-memory",
|
||||
"settings.mcp.form.headers.label": "標頭",
|
||||
"settings.mcp.form.headers.description": "選用。每行新增一個標頭,格式為 KEY: value。",
|
||||
"settings.mcp.form.environment.label": "環境變數",
|
||||
"settings.mcp.form.environment.description": "選用。每行新增一個變數,格式為 KEY=value。",
|
||||
"settings.mcp.form.timeout.label": "逾時 (ms)",
|
||||
"settings.mcp.form.timeout.placeholder": "5000",
|
||||
"settings.mcp.toast.added.title": "已新增 MCP 伺服器",
|
||||
"settings.mcp.toast.added.description": "{{name}} 已儲存至您的 MCP 設定。",
|
||||
"settings.mcp.toast.updated.title": "MCP 伺服器已更新",
|
||||
"settings.mcp.toast.updated.description": "{{name}} 現已{{state}}。",
|
||||
"settings.mcp.toast.removed.title": "已移除 MCP 伺服器",
|
||||
"settings.mcp.toast.removed.description": "{{name}} 已從您的 MCP 設定中移除。",
|
||||
"settings.mcp.validation.name": "儲存前請輸入伺服器名稱。",
|
||||
"settings.mcp.validation.duplicate": "{{name}} 已經設定過了。",
|
||||
"settings.mcp.validation.url": "請輸入遠端 MCP 伺服器 URL。",
|
||||
"settings.mcp.validation.command": "請輸入用於啟動本機 MCP 伺服器的指令。",
|
||||
"settings.mcp.validation.timeout": "逾時必須是正整數。",
|
||||
"settings.mcp.validation.headers": "無法解析標頭行:{{line}}",
|
||||
"settings.mcp.validation.environment": "無法解析環境變數行:{{line}}",
|
||||
"settings.mcp.status.needs_client_registration": "需要用戶端註冊",
|
||||
|
||||
"settings.permissions.title": "權限",
|
||||
"settings.permissions.description": "控制伺服器預設可以使用哪些工具。",
|
||||
|
||||
@@ -22,6 +22,5 @@
|
||||
}
|
||||
},
|
||||
"include": ["src", "package.json"],
|
||||
"exclude": ["dist", "ts-dist"],
|
||||
"references": [{ "path": "../sdk/js" }]
|
||||
"exclude": ["dist", "ts-dist"]
|
||||
}
|
||||
|
||||
@@ -129,14 +129,40 @@ export function Session() {
|
||||
.toSorted((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
||||
})
|
||||
const messages = createMemo(() => sync.data.message[route.sessionID] ?? [])
|
||||
const localPermissions = createMemo(() => sync.data.permission[route.sessionID] ?? [])
|
||||
const localQuestions = createMemo(() => sync.data.question[route.sessionID] ?? [])
|
||||
const childSessions = createMemo(() => {
|
||||
if (session()?.parentID) return []
|
||||
return children().filter((x) => x.id !== route.sessionID)
|
||||
})
|
||||
const permissions = createMemo(() => {
|
||||
if (session()?.parentID) return []
|
||||
return children().flatMap((x) => sync.data.permission[x.id] ?? [])
|
||||
const child = childSessions().flatMap((x) => sync.data.permission[x.id] ?? [])
|
||||
return [...localPermissions(), ...child]
|
||||
})
|
||||
const questions = createMemo(() => {
|
||||
if (session()?.parentID) return []
|
||||
return children().flatMap((x) => sync.data.question[x.id] ?? [])
|
||||
const child = childSessions().flatMap((x) => sync.data.question[x.id] ?? [])
|
||||
return [...localQuestions(), ...child]
|
||||
})
|
||||
const activeSubagents = createMemo(() =>
|
||||
childSessions().flatMap((item) => {
|
||||
const status = sync.data.session_status?.[item.id]
|
||||
if (status?.type !== "busy" && status?.type !== "retry") return []
|
||||
const count = (sync.data.message[item.id] ?? [])
|
||||
.flatMap((message) => sync.data.part[message.id] ?? [])
|
||||
.filter(
|
||||
(part) => part.type === "tool" && (part.state.status === "completed" || part.state.status === "error"),
|
||||
).length
|
||||
return [
|
||||
{
|
||||
session: item,
|
||||
status,
|
||||
count,
|
||||
},
|
||||
]
|
||||
}),
|
||||
)
|
||||
|
||||
const pending = createMemo(() => {
|
||||
return messages().findLast((x) => x.role === "assistant" && !x.time.completed)?.id
|
||||
@@ -1150,6 +1176,29 @@ export function Session() {
|
||||
</For>
|
||||
</scrollbox>
|
||||
<box flexShrink={0}>
|
||||
<Show when={activeSubagents().length > 0}>
|
||||
<box paddingLeft={3} paddingBottom={1} gap={0}>
|
||||
<text fg={theme.text}>
|
||||
<span style={{ fg: theme.textMuted }}>Subagents</span> {activeSubagents().length} running
|
||||
<span style={{ fg: theme.textMuted }}> · {keybind.print("session_child_cycle")} open</span>
|
||||
</text>
|
||||
<For each={activeSubagents()}>
|
||||
{(item) => (
|
||||
<text
|
||||
fg={theme.textMuted}
|
||||
onMouseUp={() => {
|
||||
navigate({
|
||||
type: "session",
|
||||
sessionID: item.session.id,
|
||||
})
|
||||
}}
|
||||
>
|
||||
↳ {Locale.truncate(item.session.title, 36)} · {item.count} toolcalls
|
||||
</text>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={permissions().length > 0}>
|
||||
<PermissionPrompt request={permissions()[0]} />
|
||||
</Show>
|
||||
@@ -1157,7 +1206,7 @@ export function Session() {
|
||||
<QuestionPrompt request={questions()[0]} />
|
||||
</Show>
|
||||
<Prompt
|
||||
visible={!session()?.parentID && permissions().length === 0 && questions().length === 0}
|
||||
visible={!session()?.parentID && localPermissions().length === 0 && localQuestions().length === 0}
|
||||
ref={(r) => {
|
||||
prompt = r
|
||||
promptRef.set(r)
|
||||
@@ -1166,7 +1215,7 @@ export function Session() {
|
||||
r.set(route.initialPrompt)
|
||||
}
|
||||
}}
|
||||
disabled={permissions().length > 0 || questions().length > 0}
|
||||
disabled={localPermissions().length > 0 || localQuestions().length > 0}
|
||||
onSubmit={() => {
|
||||
toBottom()
|
||||
}}
|
||||
@@ -1625,11 +1674,14 @@ function InlineTool(props: {
|
||||
spinner?: boolean
|
||||
children: JSX.Element
|
||||
part: ToolPart
|
||||
onClick?: () => void
|
||||
}) {
|
||||
const [margin, setMargin] = createSignal(0)
|
||||
const { theme } = useTheme()
|
||||
const ctx = use()
|
||||
const sync = useSync()
|
||||
const renderer = useRenderer()
|
||||
const [hover, setHover] = createSignal(false)
|
||||
|
||||
const permission = createMemo(() => {
|
||||
const callID = sync.data.permission[ctx.sessionID]?.at(0)?.tool?.callID
|
||||
@@ -1639,6 +1691,7 @@ function InlineTool(props: {
|
||||
|
||||
const fg = createMemo(() => {
|
||||
if (permission()) return theme.warning
|
||||
if (hover() && props.onClick) return theme.text
|
||||
if (props.complete) return theme.textMuted
|
||||
return theme.text
|
||||
})
|
||||
@@ -1656,6 +1709,12 @@ function InlineTool(props: {
|
||||
<box
|
||||
marginTop={margin()}
|
||||
paddingLeft={3}
|
||||
onMouseOver={() => props.onClick && setHover(true)}
|
||||
onMouseOut={() => setHover(false)}
|
||||
onMouseUp={() => {
|
||||
if (renderer.getSelection()?.getSelectedText()) return
|
||||
props.onClick?.()
|
||||
}}
|
||||
renderBefore={function () {
|
||||
const el = this as BoxRenderable
|
||||
const parent = el.parent
|
||||
@@ -1943,10 +2002,8 @@ function WebSearch(props: ToolProps<any>) {
|
||||
}
|
||||
|
||||
function Task(props: ToolProps<typeof TaskTool>) {
|
||||
const { theme } = useTheme()
|
||||
const keybind = useKeybind()
|
||||
const { navigate } = useRoute()
|
||||
const local = useLocal()
|
||||
const sync = useSync()
|
||||
|
||||
onMount(() => {
|
||||
@@ -1964,9 +2021,47 @@ function Task(props: ToolProps<typeof TaskTool>) {
|
||||
)
|
||||
})
|
||||
|
||||
const current = createMemo(() => tools().findLast((x) => (x.state as any).title))
|
||||
|
||||
const isRunning = createMemo(() => props.part.state.status === "running")
|
||||
const current = createMemo(() => tools().findLast((x) => x.state.status !== "pending"))
|
||||
const background = createMemo(() => props.metadata.background === true)
|
||||
const status = createMemo(() => {
|
||||
const sessionID = props.metadata.sessionId
|
||||
if (!sessionID) return
|
||||
return sync.data.session_status?.[sessionID]
|
||||
})
|
||||
const counts = createMemo(() => {
|
||||
const all = tools()
|
||||
const done = all.filter((item) => item.state.status === "completed" || item.state.status === "error").length
|
||||
return {
|
||||
all: all.length,
|
||||
done,
|
||||
}
|
||||
})
|
||||
const childRunning = createMemo(() => status()?.type === "busy" || status()?.type === "retry")
|
||||
const latest = createMemo(() => {
|
||||
const user = messages().findLast((msg) => msg.role === "user")
|
||||
const assistant = messages().findLast((msg) => msg.role === "assistant")
|
||||
return {
|
||||
user,
|
||||
assistant,
|
||||
}
|
||||
})
|
||||
const terminal = createMemo(() => {
|
||||
const assistant = latest().assistant
|
||||
if (!assistant) return false
|
||||
const user = latest().user
|
||||
if (user && user.id > assistant.id) return false
|
||||
if (assistant.error) return true
|
||||
return !!assistant.finish && !["tool-calls", "unknown"].includes(assistant.finish)
|
||||
})
|
||||
const backgroundRunning = createMemo(() => background() && childRunning())
|
||||
const failed = createMemo(() => !!background() && terminal() && !!latest().assistant?.error)
|
||||
const statusLabel = createMemo(() => {
|
||||
if (backgroundRunning()) return "running in background"
|
||||
if (!terminal()) return "background task pending sync"
|
||||
if (failed()) return "background task failed"
|
||||
return "background task finished"
|
||||
})
|
||||
const isRunning = createMemo(() => props.part.state.status === "running" || childRunning())
|
||||
|
||||
const duration = createMemo(() => {
|
||||
const first = messages().find((x) => x.role === "user")?.time.created
|
||||
@@ -1977,16 +2072,21 @@ function Task(props: ToolProps<typeof TaskTool>) {
|
||||
|
||||
const content = createMemo(() => {
|
||||
if (!props.input.description) return ""
|
||||
let content = [`Task ${props.input.description}`]
|
||||
const toolLabel = `${childRunning() ? counts().done : counts().all} toolcalls`
|
||||
const content = [`Task ${props.input.description}`]
|
||||
|
||||
if (background()) content.push(`↳ ${statusLabel()}`)
|
||||
|
||||
if (isRunning() && tools().length > 0) {
|
||||
// content[0] += ` · ${tools().length} toolcalls`
|
||||
if (current()) content.push(`↳ ${Locale.titlecase(current()!.tool)} ${(current()!.state as any).title}`)
|
||||
else content.push(`↳ ${tools().length} toolcalls`)
|
||||
const title = current() && (current()!.state as any).title
|
||||
if (title) content.push(`↳ ${Locale.titlecase(current()!.tool)} ${title}`)
|
||||
else content.push(`↳ ${toolLabel}`)
|
||||
}
|
||||
|
||||
if (props.part.state.status === "completed") {
|
||||
content.push(`└ ${tools().length} toolcalls · ${Locale.duration(duration())}`)
|
||||
content.push(`└ ${toolLabel} · ${Locale.duration(duration())}`)
|
||||
} else if (props.metadata.sessionId) {
|
||||
content.push(`└ ${keybind.print("session_child_cycle")} view subagents`)
|
||||
}
|
||||
|
||||
return content.join("\n")
|
||||
@@ -1999,6 +2099,11 @@ function Task(props: ToolProps<typeof TaskTool>) {
|
||||
complete={props.input.description}
|
||||
pending="Delegating..."
|
||||
part={props.part}
|
||||
onClick={() => {
|
||||
if (props.metadata.sessionId) {
|
||||
navigate({ type: "session", sessionID: props.metadata.sessionId })
|
||||
}
|
||||
}}
|
||||
>
|
||||
{content()}
|
||||
</InlineTool>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { GrepTool } from "./grep"
|
||||
import { BatchTool } from "./batch"
|
||||
import { ReadTool } from "./read"
|
||||
import { TaskTool } from "./task"
|
||||
import { TaskStatusTool } from "./task_status"
|
||||
import { TodoWriteTool, TodoReadTool } from "./todo"
|
||||
import { WebFetchTool } from "./webfetch"
|
||||
import { WriteTool } from "./write"
|
||||
@@ -110,6 +111,7 @@ export namespace ToolRegistry {
|
||||
EditTool,
|
||||
WriteTool,
|
||||
TaskTool,
|
||||
TaskStatusTool,
|
||||
WebFetchTool,
|
||||
TodoWriteTool,
|
||||
// TodoReadTool,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { MessageV2 } from "../session/message-v2"
|
||||
import { Identifier } from "../id/id"
|
||||
import { Agent } from "../agent/agent"
|
||||
import { SessionPrompt } from "../session/prompt"
|
||||
import { SessionStatus } from "../session/status"
|
||||
import { iife } from "@/util/iife"
|
||||
import { defer } from "@/util/defer"
|
||||
import { Config } from "../config/config"
|
||||
@@ -22,8 +23,93 @@ const parameters = z.object({
|
||||
)
|
||||
.optional(),
|
||||
command: z.string().describe("The command that triggered this task").optional(),
|
||||
background: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("When true, launch the subagent in the background and return immediately"),
|
||||
})
|
||||
|
||||
function output(sessionID: string, text: string) {
|
||||
return [
|
||||
`task_id: ${sessionID} (for resuming to continue this task if needed)`,
|
||||
"",
|
||||
"<task_result>",
|
||||
text,
|
||||
"</task_result>",
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
function backgroundOutput(sessionID: string) {
|
||||
return [
|
||||
`task_id: ${sessionID} (for polling this task with task_status)`,
|
||||
"state: running",
|
||||
"",
|
||||
"<task_result>",
|
||||
"Background task started. Continue your current work and call task_status when you need the result.",
|
||||
"</task_result>",
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
function backgroundMessage(input: {
|
||||
sessionID: string
|
||||
description: string
|
||||
state: "completed" | "error"
|
||||
text: string
|
||||
}) {
|
||||
const tag = input.state === "completed" ? "task_result" : "task_error"
|
||||
const title =
|
||||
input.state === "completed"
|
||||
? `Background task completed: ${input.description}`
|
||||
: `Background task failed: ${input.description}`
|
||||
return [title, `task_id: ${input.sessionID}`, `state: ${input.state}`, `<${tag}>`, input.text, `</${tag}>`].join("\n")
|
||||
}
|
||||
|
||||
function errorText(error: unknown) {
|
||||
if (error instanceof Error) return error.message
|
||||
return String(error)
|
||||
}
|
||||
|
||||
function resultTaskID(input: unknown) {
|
||||
if (!input || typeof input !== "object") return
|
||||
const taskID = Reflect.get(input, "task_id")
|
||||
if (typeof taskID === "string") return taskID
|
||||
}
|
||||
|
||||
function polled(input: { message: MessageV2.WithParts; taskID: string }) {
|
||||
if (input.message.info.role !== "assistant") return false
|
||||
return input.message.parts.some((part) => {
|
||||
if (part.type !== "tool") return false
|
||||
if (part.tool !== "task_status") return false
|
||||
if (part.state.status !== "completed") return false
|
||||
return resultTaskID(part.state.input) === input.taskID
|
||||
})
|
||||
}
|
||||
|
||||
async function latestUser(sessionID: string) {
|
||||
const [message] = await Session.messages({
|
||||
sessionID,
|
||||
limit: 1,
|
||||
})
|
||||
if (!message) return
|
||||
if (message.info.role !== "user") return
|
||||
return message.info.id
|
||||
}
|
||||
|
||||
async function continueParent(input: { parentID: string; userID: string; taskID: string }) {
|
||||
const message =
|
||||
SessionStatus.get(input.parentID).type === "idle"
|
||||
? undefined
|
||||
: await SessionPrompt.loop({
|
||||
sessionID: input.parentID,
|
||||
}).catch(() => undefined)
|
||||
if (message && polled({ message, taskID: input.taskID })) return
|
||||
if (SessionStatus.get(input.parentID).type !== "idle") return
|
||||
if ((await latestUser(input.parentID)) !== input.userID) return
|
||||
await SessionPrompt.loop({
|
||||
sessionID: input.parentID,
|
||||
})
|
||||
}
|
||||
|
||||
export const TaskTool = Tool.define("task", async (ctx) => {
|
||||
const agents = await Agent.list().then((x) => x.filter((a) => a.mode !== "primary"))
|
||||
|
||||
@@ -103,62 +189,110 @@ export const TaskTool = Tool.define("task", async (ctx) => {
|
||||
const msg = await MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID })
|
||||
if (msg.info.role !== "assistant") throw new Error("Not an assistant message")
|
||||
|
||||
const model = agent.model ?? {
|
||||
const parentModel = {
|
||||
modelID: msg.info.modelID,
|
||||
providerID: msg.info.providerID,
|
||||
}
|
||||
const model = agent.model ?? parentModel
|
||||
const background = params.background === true
|
||||
const metadata = {
|
||||
sessionId: session.id,
|
||||
model,
|
||||
...(background ? { background: true } : {}),
|
||||
}
|
||||
|
||||
ctx.metadata({
|
||||
title: params.description,
|
||||
metadata: {
|
||||
sessionId: session.id,
|
||||
model,
|
||||
},
|
||||
metadata,
|
||||
})
|
||||
|
||||
const messageID = Identifier.ascending("message")
|
||||
const run = async () => {
|
||||
const promptParts = await SessionPrompt.resolvePromptParts(params.prompt)
|
||||
const result = await SessionPrompt.prompt({
|
||||
messageID: Identifier.ascending("message"),
|
||||
sessionID: session.id,
|
||||
model: {
|
||||
modelID: model.modelID,
|
||||
providerID: model.providerID,
|
||||
},
|
||||
agent: agent.name,
|
||||
tools: {
|
||||
todowrite: false,
|
||||
todoread: false,
|
||||
...(hasTaskPermission ? {} : { task: false }),
|
||||
...Object.fromEntries((config.experimental?.primary_tools ?? []).map((t) => [t, false])),
|
||||
},
|
||||
parts: promptParts,
|
||||
})
|
||||
return result.parts.findLast((x) => x.type === "text")?.text ?? ""
|
||||
}
|
||||
|
||||
if (background) {
|
||||
const inject = (state: "completed" | "error", text: string) =>
|
||||
SessionPrompt.prompt({
|
||||
sessionID: ctx.sessionID,
|
||||
noReply: true,
|
||||
model: {
|
||||
modelID: parentModel.modelID,
|
||||
providerID: parentModel.providerID,
|
||||
},
|
||||
agent: ctx.agent,
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
synthetic: true,
|
||||
text: backgroundMessage({
|
||||
sessionID: session.id,
|
||||
description: params.description,
|
||||
state,
|
||||
text,
|
||||
}),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
void run()
|
||||
.then((text) =>
|
||||
inject("completed", text)
|
||||
.then((message) =>
|
||||
continueParent({
|
||||
parentID: ctx.sessionID,
|
||||
userID: message.info.id,
|
||||
taskID: session.id,
|
||||
}),
|
||||
)
|
||||
.catch(() => {}),
|
||||
)
|
||||
.catch((error) =>
|
||||
inject("error", errorText(error))
|
||||
.then((message) =>
|
||||
continueParent({
|
||||
parentID: ctx.sessionID,
|
||||
userID: message.info.id,
|
||||
taskID: session.id,
|
||||
}),
|
||||
)
|
||||
.catch(() => {}),
|
||||
)
|
||||
|
||||
return {
|
||||
title: params.description,
|
||||
metadata,
|
||||
output: backgroundOutput(session.id),
|
||||
}
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
SessionPrompt.cancel(session.id)
|
||||
}
|
||||
ctx.abort.addEventListener("abort", cancel)
|
||||
using _ = defer(() => ctx.abort.removeEventListener("abort", cancel))
|
||||
const promptParts = await SessionPrompt.resolvePromptParts(params.prompt)
|
||||
|
||||
const result = await SessionPrompt.prompt({
|
||||
messageID,
|
||||
sessionID: session.id,
|
||||
model: {
|
||||
modelID: model.modelID,
|
||||
providerID: model.providerID,
|
||||
},
|
||||
agent: agent.name,
|
||||
tools: {
|
||||
todowrite: false,
|
||||
todoread: false,
|
||||
...(hasTaskPermission ? {} : { task: false }),
|
||||
...Object.fromEntries((config.experimental?.primary_tools ?? []).map((t) => [t, false])),
|
||||
},
|
||||
parts: promptParts,
|
||||
})
|
||||
|
||||
const text = result.parts.findLast((x) => x.type === "text")?.text ?? ""
|
||||
|
||||
const output = [
|
||||
`task_id: ${session.id} (for resuming to continue this task if needed)`,
|
||||
"",
|
||||
"<task_result>",
|
||||
text,
|
||||
"</task_result>",
|
||||
].join("\n")
|
||||
const text = await run()
|
||||
|
||||
return {
|
||||
title: params.description,
|
||||
metadata: {
|
||||
sessionId: session.id,
|
||||
model,
|
||||
},
|
||||
output,
|
||||
metadata,
|
||||
output: output(session.id, text),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -17,11 +17,13 @@ When NOT to use the Task tool:
|
||||
|
||||
Usage notes:
|
||||
1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
|
||||
2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result. The output includes a task_id you can reuse later to continue the same subagent session.
|
||||
3. Each agent invocation starts with a fresh context unless you provide task_id to resume the same subagent session (which continues with its previous messages and tool outputs). When starting fresh, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
|
||||
4. The agent's outputs should generally be trusted
|
||||
5. Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent. Tell it how to verify its work if possible (e.g., relevant test commands).
|
||||
6. If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.
|
||||
2. By default, task waits for completion and returns the result immediately, along with a task_id you can reuse later to continue the same subagent session.
|
||||
3. Set background=true to launch asynchronously. In background mode, continue your current work without waiting.
|
||||
4. For background runs, use task_status(task_id=..., wait=false) to poll, or wait=true to block until done (optionally with timeout_ms).
|
||||
5. Each agent invocation starts with a fresh context unless you provide task_id to resume the same subagent session (which continues with its previous messages and tool outputs). When starting fresh, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
|
||||
6. The agent's outputs should generally be trusted
|
||||
7. Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent. Tell it how to verify its work if possible (e.g., relevant test commands).
|
||||
8. If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.
|
||||
|
||||
Example usage (NOTE: The agents below are fictional examples for illustration only - use the actual agents listed above):
|
||||
|
||||
|
||||
163
packages/opencode/src/tool/task_status.ts
Normal file
163
packages/opencode/src/tool/task_status.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import z from "zod"
|
||||
import { Tool } from "./tool"
|
||||
import DESCRIPTION from "./task_status.txt"
|
||||
import { Identifier } from "../id/id"
|
||||
import { Session } from "../session"
|
||||
import { SessionStatus } from "../session/status"
|
||||
import { MessageV2 } from "../session/message-v2"
|
||||
|
||||
type State = "running" | "completed" | "error"
|
||||
|
||||
const DEFAULT_TIMEOUT = 60_000
|
||||
const POLL_MS = 300
|
||||
|
||||
const parameters = z.object({
|
||||
task_id: Identifier.schema("session").describe("The task_id returned by the task tool"),
|
||||
wait: z.boolean().optional().describe("When true, wait until the task reaches a terminal state or timeout"),
|
||||
timeout_ms: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.describe("Maximum milliseconds to wait when wait=true (default: 60000)"),
|
||||
})
|
||||
|
||||
function format(input: { taskID: string; state: State; text: string }) {
|
||||
return [`task_id: ${input.taskID}`, `state: ${input.state}`, "", "<task_result>", input.text, "</task_result>"].join(
|
||||
"\n",
|
||||
)
|
||||
}
|
||||
|
||||
function errorText(error: NonNullable<MessageV2.Assistant["error"]>) {
|
||||
const data = error.data as Record<string, unknown> | undefined
|
||||
const message = data?.message
|
||||
if (typeof message === "string" && message) return message
|
||||
return error.name
|
||||
}
|
||||
|
||||
async function inspect(taskID: string) {
|
||||
const status = SessionStatus.get(taskID)
|
||||
if (status.type === "busy" || status.type === "retry") {
|
||||
return {
|
||||
state: "running" as const,
|
||||
text: status.type === "retry" ? `Task is retrying: ${status.message}` : "Task is still running.",
|
||||
}
|
||||
}
|
||||
|
||||
let latestUser: MessageV2.User | undefined
|
||||
let latestAssistant:
|
||||
| {
|
||||
info: MessageV2.Assistant
|
||||
parts: MessageV2.Part[]
|
||||
}
|
||||
| undefined
|
||||
for await (const item of MessageV2.stream(taskID)) {
|
||||
if (!latestUser && item.info.role === "user") latestUser = item.info
|
||||
if (!latestAssistant && item.info.role === "assistant") {
|
||||
latestAssistant = {
|
||||
info: item.info,
|
||||
parts: item.parts,
|
||||
}
|
||||
}
|
||||
if (latestUser && latestAssistant) break
|
||||
}
|
||||
|
||||
if (!latestAssistant) {
|
||||
return {
|
||||
state: "running" as const,
|
||||
text: "Task has started but has not produced output yet.",
|
||||
}
|
||||
}
|
||||
|
||||
if (latestUser && latestUser.id > latestAssistant.info.id) {
|
||||
return {
|
||||
state: "running" as const,
|
||||
text: "Task is starting.",
|
||||
}
|
||||
}
|
||||
|
||||
const text = latestAssistant.parts.findLast((part) => part.type === "text")?.text ?? ""
|
||||
if (latestAssistant.info.error) {
|
||||
const summary = errorText(latestAssistant.info.error)
|
||||
return {
|
||||
state: "error" as const,
|
||||
text: text || summary,
|
||||
}
|
||||
}
|
||||
|
||||
const done = latestAssistant.info.finish && !["tool-calls", "unknown"].includes(latestAssistant.info.finish)
|
||||
if (done) {
|
||||
return {
|
||||
state: "completed" as const,
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
state: "running" as const,
|
||||
text: text || "Task is still running.",
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms: number, abort: AbortSignal) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (abort.aborted) {
|
||||
reject(new Error("Task status polling aborted"))
|
||||
return
|
||||
}
|
||||
|
||||
const onAbort = () => {
|
||||
clearTimeout(timer)
|
||||
reject(new Error("Task status polling aborted"))
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
abort.removeEventListener("abort", onAbort)
|
||||
resolve()
|
||||
}, ms)
|
||||
|
||||
abort.addEventListener("abort", onAbort, { once: true })
|
||||
})
|
||||
}
|
||||
|
||||
export const TaskStatusTool = Tool.define("task_status", {
|
||||
description: DESCRIPTION,
|
||||
parameters,
|
||||
async execute(params, ctx) {
|
||||
await Session.get(params.task_id)
|
||||
|
||||
let result = await inspect(params.task_id)
|
||||
if (!params.wait || result.state !== "running") {
|
||||
return {
|
||||
title: "Task status",
|
||||
metadata: {
|
||||
task_id: params.task_id,
|
||||
state: result.state,
|
||||
timed_out: false,
|
||||
},
|
||||
output: format({ taskID: params.task_id, state: result.state, text: result.text }),
|
||||
}
|
||||
}
|
||||
|
||||
const timeout = params.timeout_ms ?? DEFAULT_TIMEOUT
|
||||
const end = Date.now() + timeout
|
||||
while (Date.now() < end) {
|
||||
const left = end - Date.now()
|
||||
await sleep(Math.min(POLL_MS, left), ctx.abort)
|
||||
result = await inspect(params.task_id)
|
||||
if (result.state !== "running") break
|
||||
}
|
||||
|
||||
const done = result.state !== "running"
|
||||
const text = done ? result.text : `Timed out after ${timeout}ms while waiting for task completion.`
|
||||
return {
|
||||
title: "Task status",
|
||||
metadata: {
|
||||
task_id: params.task_id,
|
||||
state: result.state,
|
||||
timed_out: !done,
|
||||
},
|
||||
output: format({ taskID: params.task_id, state: result.state, text }),
|
||||
}
|
||||
},
|
||||
})
|
||||
13
packages/opencode/src/tool/task_status.txt
Normal file
13
packages/opencode/src/tool/task_status.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
Poll the status of a subagent task launched with the task tool.
|
||||
|
||||
Use this to check background tasks started with `task(background=true)`.
|
||||
|
||||
Parameters:
|
||||
- `task_id` (required): the task session id returned by the task tool
|
||||
- `wait` (optional): when true, wait for completion
|
||||
- `timeout_ms` (optional): max wait duration in milliseconds when `wait=true`
|
||||
|
||||
Returns compact, parseable output:
|
||||
- `task_id`
|
||||
- `state` (`running`, `completed`, or `error`)
|
||||
- `<task_result>...</task_result>` containing final output, error summary, or current progress text
|
||||
231
packages/opencode/test/tool/task_status.test.ts
Normal file
231
packages/opencode/test/tool/task_status.test.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Session } from "../../src/session"
|
||||
import { Identifier } from "../../src/id/id"
|
||||
import { SessionStatus } from "../../src/session/status"
|
||||
import { TaskStatusTool } from "../../src/tool/task_status"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
|
||||
const ctx = {
|
||||
sessionID: "session_test",
|
||||
messageID: "message_test",
|
||||
callID: "call_test",
|
||||
agent: "build",
|
||||
abort: AbortSignal.any([]),
|
||||
messages: [],
|
||||
metadata: () => {},
|
||||
ask: async () => {},
|
||||
}
|
||||
|
||||
async function user(sessionID: string) {
|
||||
await Session.updateMessage({
|
||||
id: Identifier.ascending("message"),
|
||||
sessionID,
|
||||
role: "user",
|
||||
time: {
|
||||
created: Date.now(),
|
||||
},
|
||||
agent: "build",
|
||||
model: {
|
||||
providerID: "test-provider",
|
||||
modelID: "test-model",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function assistant(input: { sessionID: string; text: string; error?: string }) {
|
||||
const msg = await Session.updateMessage({
|
||||
id: Identifier.ascending("message"),
|
||||
sessionID: input.sessionID,
|
||||
role: "assistant",
|
||||
time: {
|
||||
created: Date.now(),
|
||||
completed: Date.now(),
|
||||
},
|
||||
parentID: Identifier.ascending("message"),
|
||||
modelID: "test-model",
|
||||
providerID: "test-provider",
|
||||
mode: "build",
|
||||
agent: "build",
|
||||
path: {
|
||||
cwd: process.cwd(),
|
||||
root: process.cwd(),
|
||||
},
|
||||
cost: 0,
|
||||
tokens: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
reasoning: 0,
|
||||
cache: {
|
||||
read: 0,
|
||||
write: 0,
|
||||
},
|
||||
},
|
||||
finish: "stop",
|
||||
...(input.error
|
||||
? {
|
||||
error: new MessageV2.APIError({
|
||||
message: input.error,
|
||||
isRetryable: false,
|
||||
}).toObject(),
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
|
||||
await Session.updatePart({
|
||||
id: Identifier.ascending("part"),
|
||||
sessionID: input.sessionID,
|
||||
messageID: msg.id,
|
||||
type: "text",
|
||||
text: input.text,
|
||||
})
|
||||
}
|
||||
|
||||
describe("tool.task_status", () => {
|
||||
test("returns running while session status is busy", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const tool = await TaskStatusTool.init()
|
||||
|
||||
SessionStatus.set(session.id, { type: "busy" })
|
||||
const result = await tool.execute({ task_id: session.id }, ctx)
|
||||
|
||||
expect(result.output).toContain("state: running")
|
||||
SessionStatus.set(session.id, { type: "idle" })
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("returns completed with final task output", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const tool = await TaskStatusTool.init()
|
||||
|
||||
await assistant({
|
||||
sessionID: session.id,
|
||||
text: "all done",
|
||||
})
|
||||
|
||||
const result = await tool.execute({ task_id: session.id }, ctx)
|
||||
expect(result.output).toContain("state: completed")
|
||||
expect(result.output).toContain("all done")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("wait=true blocks until terminal status", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const tool = await TaskStatusTool.init()
|
||||
|
||||
SessionStatus.set(session.id, { type: "busy" })
|
||||
const transition = Bun.sleep(150).then(async () => {
|
||||
SessionStatus.set(session.id, { type: "idle" })
|
||||
await assistant({
|
||||
sessionID: session.id,
|
||||
text: "finished later",
|
||||
})
|
||||
})
|
||||
|
||||
const result = await tool.execute(
|
||||
{
|
||||
task_id: session.id,
|
||||
wait: true,
|
||||
timeout_ms: 4_000,
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
|
||||
await transition
|
||||
expect(result.output).toContain("state: completed")
|
||||
expect(result.output).toContain("finished later")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("returns error when child run fails", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const tool = await TaskStatusTool.init()
|
||||
|
||||
await assistant({
|
||||
sessionID: session.id,
|
||||
text: "",
|
||||
error: "child failed",
|
||||
})
|
||||
|
||||
const result = await tool.execute({ task_id: session.id }, ctx)
|
||||
expect(result.output).toContain("state: error")
|
||||
expect(result.output).toContain("child failed")
|
||||
expect(result.metadata.state).toBe("error")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("wait=true times out with timed_out metadata", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const tool = await TaskStatusTool.init()
|
||||
|
||||
SessionStatus.set(session.id, { type: "busy" })
|
||||
const result = await tool.execute(
|
||||
{
|
||||
task_id: session.id,
|
||||
wait: true,
|
||||
timeout_ms: 80,
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
|
||||
expect(result.output).toContain("Timed out after 80ms")
|
||||
expect(result.metadata.timed_out).toBe(true)
|
||||
expect(result.metadata.state).toBe("running")
|
||||
SessionStatus.set(session.id, { type: "idle" })
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("returns running for resumed task with a newer user turn", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const session = await Session.create({})
|
||||
const tool = await TaskStatusTool.init()
|
||||
|
||||
await user(session.id)
|
||||
await assistant({
|
||||
sessionID: session.id,
|
||||
text: "old done",
|
||||
})
|
||||
await user(session.id)
|
||||
|
||||
const result = await tool.execute({ task_id: session.id }, ctx)
|
||||
expect(result.output).toContain("state: running")
|
||||
expect(result.output).toContain("Task is starting.")
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -12,18 +12,9 @@
|
||||
".": "./src/index.ts",
|
||||
"./client": "./src/client.ts",
|
||||
"./server": "./src/server.ts",
|
||||
"./v2": {
|
||||
"types": "./dist/v2/index.d.ts",
|
||||
"default": "./src/v2/index.ts"
|
||||
},
|
||||
"./v2/client": {
|
||||
"types": "./dist/v2/client.d.ts",
|
||||
"default": "./src/v2/client.ts"
|
||||
},
|
||||
"./v2/gen/client": {
|
||||
"types": "./dist/v2/gen/client/index.d.ts",
|
||||
"default": "./src/v2/gen/client/index.ts"
|
||||
},
|
||||
"./v2": "./src/v2/index.ts",
|
||||
"./v2/client": "./src/v2/client.ts",
|
||||
"./v2/gen/client": "./src/v2/gen/client/index.ts",
|
||||
"./v2/server": "./src/v2/server.ts"
|
||||
},
|
||||
"files": [
|
||||
@@ -36,8 +27,5 @@
|
||||
"typescript": "catalog:",
|
||||
"@typescript/native-preview": "catalog:"
|
||||
},
|
||||
"dependencies": {},
|
||||
"publishConfig": {
|
||||
"directory": "dist"
|
||||
}
|
||||
"dependencies": {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user