From b8d325b7e229cf637b154bcb49f8733837b8e580 Mon Sep 17 00:00:00 2001 From: idevlab Date: Mon, 21 Sep 2026 14:16:40 +0800 Subject: [PATCH 1/2] feat: support custom ACP agents --- apps/desktop/src/bridge.ts | 32 +- apps/desktop/src/i18n/strings.ts | 41 +- .../desktop/src/settings/ProviderSettings.tsx | 287 ++++++++++++- apps/desktop/src/settings/SettingsPage.tsx | 12 + .../tests/providerSettingsRendered.test.tsx | 149 ++++++- .../src/plugins/app/plugins/foundation.rs | 39 +- crates/core/src/plugins/app/service.rs | 4 +- crates/core/src/provider_lifecycle.rs | 380 +++++++++++++++++- .../2026-09-21-custom-acp-agents/intent.md | 38 ++ .../2026-09-21-custom-acp-agents/plan.md | 39 ++ .../2026-09-21-custom-acp-agents/spec.md | 53 +++ .../verification.md | 71 ++++ website/guide/providers.md | 18 + website/zh/guide/providers.md | 15 + 14 files changed, 1154 insertions(+), 24 deletions(-) create mode 100644 docs/sdlc/changes/2026-09-21-custom-acp-agents/intent.md create mode 100644 docs/sdlc/changes/2026-09-21-custom-acp-agents/plan.md create mode 100644 docs/sdlc/changes/2026-09-21-custom-acp-agents/spec.md create mode 100644 docs/sdlc/changes/2026-09-21-custom-acp-agents/verification.md diff --git a/apps/desktop/src/bridge.ts b/apps/desktop/src/bridge.ts index 9fa7145b..b1ae5bb7 100644 --- a/apps/desktop/src/bridge.ts +++ b/apps/desktop/src/bridge.ts @@ -211,6 +211,7 @@ export async function syncDeviceDataNow(): Promise { export interface ProviderInfo { id: string; display_name: string; + custom: boolean; available: boolean; enabled: boolean; needs_node: boolean; @@ -237,6 +238,14 @@ export interface ProviderRuntimeConfiguration extends ProviderRuntimeOverride { effective_args: string[]; } +export interface CustomProviderConfiguration { + id: string; + display_name: string; + command: string; + args: string[]; + forwarded_environment: string[]; +} + export interface ProviderManagementInfo { installed: boolean; version: string | null; @@ -336,9 +345,10 @@ function normalizeBrowserUseSettings( type ProviderInfoWire = Omit< ProviderInfo, - "capabilities" | "enabled" | "management" | "configuration" + "capabilities" | "custom" | "enabled" | "management" | "configuration" > & { capabilities?: ProviderCapability[] | null; + custom?: boolean | null; enabled?: boolean | null; management?: ProviderManagementInfo | null; configuration?: ProviderRuntimeConfiguration | null; @@ -370,6 +380,7 @@ export function normalizeProviderInfo( ): ProviderInfo { return { ...provider, + custom: provider.custom ?? false, enabled: provider.enabled ?? true, capabilities: provider.capabilities ?? [], configuration: @@ -1769,6 +1780,7 @@ const fallbackProvider = ( ): ProviderInfo => ({ id, display_name, + custom: false, available: false, enabled: true, needs_node, @@ -1937,6 +1949,24 @@ export async function configureProvider( return providers.map(normalizeProviderInfo); } +export async function registerCustomProvider( + configuration: CustomProviderConfiguration +): Promise { + if (!inDesktop) + throw new Error( + "Custom provider registration is available in the C2 desktop app" + ); + await call("providers.register", { configuration }); +} + +export async function removeCustomProvider(provider: string): Promise { + if (!inDesktop) + throw new Error( + "Custom provider removal is available in the C2 desktop app" + ); + await call("providers.remove", { provider }); +} + export async function installProvider( provider: string ): Promise { diff --git a/apps/desktop/src/i18n/strings.ts b/apps/desktop/src/i18n/strings.ts index f11c9c5f..e4dd4599 100644 --- a/apps/desktop/src/i18n/strings.ts +++ b/apps/desktop/src/i18n/strings.ts @@ -1421,7 +1421,26 @@ export const en = { "settings.providers": "Providers", "settings.restoreDefaults": "Restore defaults", "settings.providersHint": - "Enable providers for new sessions, install their local runtimes, and keep installed versions current.", + "Enable providers for new sessions, manage built-in runtimes, or add any local Agent that speaks ACP over stdio.", + "settings.customProviderAddAction": "Add ACP Agent", + "settings.customProviderTitle": "Custom ACP Agent", + "settings.customProviderHint": + "C2 launches this local command directly and uses the same ACP session and permission flow as built-in providers.", + "settings.customProviderId": "Agent ID", + "settings.customProviderIdHint": + "Starts with a lowercase letter; use lowercase letters, numbers, dots, dashes, or underscores.", + "settings.customProviderCommandHint": + "Executable or absolute path. The command must speak ACP over stdin and stdout; shell expressions are not evaluated.", + "settings.customProviderArgumentsHint": + "Optional; one exact launch argument per line.", + "settings.customProviderAdd": "Add Agent", + "settings.customProviderAdding": "Adding…", + "settings.customProviderCancel": "Cancel", + "settings.customProviderAdded": "Added {provider} for new sessions.", + "settings.customProviderRemove": "Remove Agent", + "settings.customProviderRemoveConfirm": + "Remove {provider} from C2? Existing sessions and provider-owned files will not be deleted.", + "settings.customProviderRemoved": "Removed {provider} from C2.", "settings.providerChecked": "Checked just now", "settings.providerChecking": "Checking…", "settings.providerRefresh": "Refresh", @@ -4294,7 +4313,25 @@ export const zhCN: Record = { "settings.providers": "供应商", "settings.restoreDefaults": "恢复默认", "settings.providersHint": - "管理新会话可用的 Provider,安装本地运行时并保持版本更新。", + "管理新会话可用的 Provider、内置运行时,或添加任何通过 stdio 支持 ACP 的本地 Agent。", + "settings.customProviderAddAction": "添加 ACP Agent", + "settings.customProviderTitle": "自定义 ACP Agent", + "settings.customProviderHint": + "C2 会直接启动这个本地命令,并复用与内置 Provider 相同的 ACP 会话和权限流程。", + "settings.customProviderId": "Agent ID", + "settings.customProviderIdHint": + "以小写字母开头;仅使用小写字母、数字、点、短横线或下划线。", + "settings.customProviderCommandHint": + "填写可执行程序名或绝对路径。命令必须通过 stdin/stdout 使用 ACP;不会执行 shell 表达式。", + "settings.customProviderArgumentsHint": "可选;每行填写一个完整的启动参数。", + "settings.customProviderAdd": "添加 Agent", + "settings.customProviderAdding": "正在添加…", + "settings.customProviderCancel": "取消", + "settings.customProviderAdded": "已为新会话添加 {provider}。", + "settings.customProviderRemove": "移除 Agent", + "settings.customProviderRemoveConfirm": + "从 C2 中移除 {provider}?已有会话和 Agent 自己的文件不会被删除。", + "settings.customProviderRemoved": "已从 C2 移除 {provider}。", "settings.providerChecked": "刚刚检查", "settings.providerChecking": "正在检查…", "settings.providerRefresh": "刷新", diff --git a/apps/desktop/src/settings/ProviderSettings.tsx b/apps/desktop/src/settings/ProviderSettings.tsx index 718aebe0..ae5a91d3 100644 --- a/apps/desktop/src/settings/ProviderSettings.tsx +++ b/apps/desktop/src/settings/ProviderSettings.tsx @@ -4,7 +4,13 @@ import { SearchField } from "@/components/business/search-field"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Field, FieldDescription, FieldLabel } from "@/components/ui/field"; -import { ChevronDown, Download, RefreshCw } from "@/components/ui/icons"; +import { + ChevronDown, + Download, + Plus, + RefreshCw, + Trash2, +} from "@/components/ui/icons"; import { Input } from "@/components/ui/input"; import { Spinner } from "@/components/ui/spinner"; import { Switch } from "@/components/ui/switch"; @@ -12,12 +18,16 @@ import { Textarea } from "@/components/ui/textarea"; import { cn } from "@/lib/utils"; import { + confirmNative, configureProvider, installProvider, + registerCustomProvider, + removeCustomProvider, setProviderEnabled, upgradeProvider, } from "../bridge"; import type { + CustomProviderConfiguration, ProviderInfo, ProviderRuntimeConfiguration, ProviderRuntimeOverride, @@ -38,7 +48,14 @@ const CAPABILITY_LABELS = { interface ProviderOperation { id: string; - action: "install" | "upgrade" | "enable" | "configure" | "refresh"; + action: + | "install" + | "upgrade" + | "enable" + | "configure" + | "refresh" + | "register" + | "remove"; } function runtimeConfiguration( @@ -273,24 +290,181 @@ function ProviderRuntimeEditor({ )}
+ {!provider.custom && ( + + )} + +
+ + ); +} + +function CustomProviderForm({ + disabled, + saving, + onCancel, + onSave, +}: { + disabled: boolean; + saving: boolean; + onCancel: () => void; + onSave: (configuration: CustomProviderConfiguration) => Promise; +}) { + const t = useT(); + const [id, setId] = useState(""); + const [displayName, setDisplayName] = useState(""); + const [command, setCommand] = useState(""); + const [args, setArgs] = useState(""); + const [forwardedEnvironment, setForwardedEnvironment] = useState(""); + const ready = + id.trim() !== "" && displayName.trim() !== "" && command.trim() !== ""; + + return ( +
+
+

+ {t("settings.customProviderTitle")} +

+

+ {t("settings.customProviderHint")} +

+
+
+ + + {t("settings.customProviderId")} + + setId(event.target.value)} + /> + + {t("settings.customProviderIdHint")} + + + + + {t("settings.providerDisplayName")} + + setDisplayName(event.target.value)} + /> + +
+ + + {t("settings.providerRuntimeCommand")} + + setCommand(event.target.value)} + /> + + {t("settings.customProviderCommandHint")} + + +
+ + + {t("settings.providerRuntimeArguments")} + +