diff --git a/Cargo.lock b/Cargo.lock index 13ba889c6..d2da17ea1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,37 @@ dependencies = [ "subtle", ] +[[package]] +name = "agent-client-protocol" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2659b1089101b15db31137710159421cb44785ecdb5ba784be3b4a6f8cb8a475" +dependencies = [ + "agent-client-protocol-schema", + "anyhow", + "async-broadcast", + "async-trait", + "derive_more 2.1.1", + "futures", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "agent-client-protocol-schema" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44bc1fef9c32f03bce2ab44af35b6f483bfd169bf55cc59beeb2e3b1a00ae4d1" +dependencies = [ + "anyhow", + "derive_more 2.1.1", + "schemars 1.2.1", + "serde", + "serde_json", + "strum 0.27.2", +] + [[package]] name = "ahash" version = "0.8.12" @@ -501,6 +532,18 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-channel" version = "2.5.0" @@ -943,7 +986,7 @@ version = "3.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ec27229c38ed0eb3c0feee3d2c1d6a4379ae44f418a29a658890e062d8f365" dependencies = [ - "darling 0.21.3", + "darling 0.23.0", "ident_case", "prettyplease", "proc-macro2", @@ -1455,6 +1498,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -2516,7 +2568,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" dependencies = [ - "derive_more-impl", + "derive_more-impl 1.0.0", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl 2.1.1", ] [[package]] @@ -2531,6 +2592,20 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.114", + "unicode-xid", +] + [[package]] name = "dialoguer" version = "0.11.0" @@ -4633,7 +4708,7 @@ dependencies = [ "prost-types", "rand 0.9.2", "snafu", - "strum", + "strum 0.26.3", "tokio", "tracing", "xxhash-rust", @@ -7550,7 +7625,7 @@ dependencies = [ "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -8239,6 +8314,7 @@ name = "spacebot" version = "0.1.15" dependencies = [ "aes-gcm", + "agent-client-protocol", "anyhow", "arc-swap", "arrow-array", @@ -8302,6 +8378,7 @@ dependencies = [ "tokio-stream", "tokio-test", "tokio-tungstenite 0.28.0", + "tokio-util", "toml 0.8.23", "toml_edit 0.22.27", "tower-http", @@ -8654,7 +8731,16 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros", + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", ] [[package]] @@ -8670,6 +8756,18 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "subtle" version = "2.6.1" @@ -8955,7 +9053,7 @@ checksum = "84992abeed3ae42e8401b25d266d12bcba1def0abe59d22f6b9781167545f71e" dependencies = [ "aquamarine", "bytes", - "derive_more", + "derive_more 1.0.0", "dptree", "either", "futures", @@ -8981,7 +9079,7 @@ dependencies = [ "bitflags 2.10.0", "bytes", "chrono", - "derive_more", + "derive_more 1.0.0", "either", "futures", "log", @@ -9319,6 +9417,7 @@ checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "pin-project-lite", "tokio", diff --git a/Cargo.toml b/Cargo.toml index d11347941..2d4ade2c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,7 @@ regex = "1.11" # Async utilities futures = "0.3" +tokio-util = { version = "0.7", features = ["compat"] } pin-project = "1" # Schema validation @@ -146,6 +147,7 @@ pdf-extract = "0.10.0" open = "5.3.3" urlencoding = "2.1.3" moka = "0.12.13" +agent-client-protocol = "0.9.4" [features] metrics = ["dep:prometheus"] diff --git a/README.md b/README.md index 0387d04ec..2ac32bb5e 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Workers come loaded with tools for real work: - **Shell** — run arbitrary commands with configurable timeouts - **File** — read, write, and list files with auto-created directories - **Exec** — run specific programs with arguments and environment variables -- **[OpenCode](https://opencode.ai)** — spawn a full coding agent as a persistent worker with codebase exploration, LSP awareness, and deep context management +- **Code workers (OpenCode or ACP)** — delegate coding tasks to either [OpenCode](https://opencode.ai) or an [ACP-compatible](https://agentclientprotocol.com/) agent backend, with persistent interactive sessions for multi-step code work - **Browser** — headless Chrome automation with an accessibility-tree ref system. Navigate, click, type, screenshot, manage tabs — the LLM addresses elements by short refs (`e0`, `e1`) instead of fragile CSS selectors - **[Brave](https://brave.com/search/api/) web search** — search the web with freshness filters, localization, and configurable result count @@ -291,7 +291,7 @@ Workers are pluggable. Any process that accepts a task and reports status can be **Built-in workers** come with shell, file, exec, and browser tools out of the box. They can write code, run commands, manage files, browse the web — enough to build a whole project from scratch. -**[OpenCode](https://opencode.ai) workers** are a built-in integration that spawns a full OpenCode coding agent as a persistent subprocess. OpenCode brings its own codebase exploration, LSP awareness, and context management — purpose-built for deep coding sessions. When a user asks for a complex refactor or a new feature, the channel can spawn an OpenCode worker that maintains a rich understanding of the codebase across the entire session. Both built-in and OpenCode workers support interactive follow-ups. +**Code workers** provide two external coding backends: **[OpenCode](https://opencode.ai)** (persistent OpenCode subprocess with codebase-aware sessions) and **[ACP workers](https://agentclientprotocol.com/)** (communication via stdio with a variety of coding agents). Code workers bring their own codebase exploration, LSP awareness, and context management — purpose-built for deep coding sessions. When a user asks for a complex refactor or a new feature, the channel can spawn a code worker that maintains a rich understanding of the codebase across the entire session. Both built-in and code workers support interactive follow-ups. ### The Compactor @@ -482,7 +482,7 @@ No server dependencies. Single binary. All data lives in embedded databases in a | [Discord Setup](docs/content/docs/(messaging)/discord-setup.mdx) | Discord bot setup guide | | [Browser](docs/content/docs/(features)/browser.mdx) | Headless Chrome for workers | | [MCP](docs/content/docs/(features)/mcp.mdx) | External tool servers via Model Context Protocol | -| [OpenCode](docs/content/docs/(features)/opencode.mdx) | OpenCode as a worker backend | +| [Code Workers](docs/content/docs/(features)/code-workers.mdx) | OpenCode and ACP coding worker backends | | [Philosophy](docs/content/docs/(core)/philosophy.mdx) | Why Rust | --- diff --git a/docs/content/docs/(features)/opencode.mdx b/docs/content/docs/(features)/code-workers.mdx similarity index 66% rename from docs/content/docs/(features)/opencode.mdx rename to docs/content/docs/(features)/code-workers.mdx index 00f9403e9..f9e113b2c 100644 --- a/docs/content/docs/(features)/opencode.mdx +++ b/docs/content/docs/(features)/code-workers.mdx @@ -1,15 +1,69 @@ --- -title: OpenCode -description: Spawn full coding agents as worker processes via the OpenCode integration. +title: Code Workers +description: Spawn full coding agents as worker processes via OpenCode or ACP integration. --- -# OpenCode +# Code Workers -Spacebot can spawn [OpenCode](https://opencode.ai) as a worker backend. Instead of running a Rig agent with shell/file/exec tools, an OpenCode worker delegates to a persistent OpenCode subprocess that has its own tool suite, codebase exploration, and context management. +For multi-file coding tasks, Spacebot can delegate to one of two external worker backends: -Use OpenCode workers for multi-file coding tasks. Use builtin workers for one-shot commands, file operations, and non-coding work. +- **ACP workers** via [Agent Client Protocol](https://agentclientprotocol.com/) over stdio +- **OpenCode workers** via a persistent OpenCode HTTP/SSE subprocess -## Enabling +Most setups use one or the other, but it is possible to use both at the same time. Start with the backend that matches your local tooling. + +## Choosing a Backend + +- Use **ACP** if you already run (or have a subscription to) a [CLI coding agent with ACP support](https://agentclientprotocol.com/get-started/agents). +- Use **OpenCode** if you want Spacebot-managed OpenCode sessions with server pooling and richer OpenCode-native behavior. +- Keep using **built-in** workers for quick shell/file/exec tasks and non-coding operations. + +Both ACP and OpenCode workers are invoked through `spawn_worker`, support interactive follow-ups via `route`, and publish status updates into the channel status block. When neither external backend is selected, Spacebot uses the built-in worker. + +## ACP (Agent Client Protocol) + +Spacebot supports **ACP-backed workers** via [Agent Client Protocol](https://agentclientprotocol.com/). ACP workers run compatible coding agents over stdio and expose them through the same `spawn_worker` tool path. + +Use ACP workers for compatibility with pre-existing agent CLIs, such as Claude Code, Codex, or Gemini CLI. + +### Enable ACP profiles + +Configure one or more profiles under `defaults.acp`: + +```toml +[defaults.acp.opencode] +enabled = true +command = "opencode" +args = ["acp"] +timeout = 300 + +[defaults.acp.codex] +enabled = true +command = "codex-acp" +timeout = 300 +``` + +### Use ACP from `spawn_worker` + +```json +{ + "task": "Implement the auth flow", + "worker_type": "acp", + "directory": "/code/myapp", + "acp_id": "codex", + "interactive": true +} +``` + +If multiple ACP workers are enabled, provide `acp_id`. If only one is enabled, Spacebot auto-selects it. + +## OpenCode + +Spacebot can also spawn [OpenCode](https://opencode.ai) as a worker backend. An OpenCode worker delegates to a persistent OpenCode subprocess that has its own tool suite, codebase exploration, and context management. + +Use OpenCode workers for multi-file coding tasks where persistent session context and server reuse are valuable. + +### Enable OpenCode OpenCode is disabled by default. Enable it in your config: @@ -25,9 +79,18 @@ The `path` field supports `env:VAR_NAME` resolution: path = "env:OPENCODE_PATH" ``` -Once enabled, the `spawn_worker` tool gains a `worker_type` parameter. The channel LLM decides whether to use `"builtin"` (default) or `"opencode"` based on the task. +### Use OpenCode from `spawn_worker` + +```json +{ + "task": "Refactor the auth module", + "worker_type": "opencode", + "directory": "/code/myapp", + "interactive": true +} +``` -## How It Works +### How It Works ``` Channel: "spawn_worker: refactor the auth module, worker_type: opencode, directory: /code/myapp" @@ -41,7 +104,7 @@ Channel: "spawn_worker: refactor the auth module, worker_type: opencode, directo The OpenCode worker runs its own agent loop internally. Spacebot monitors it via SSE and translates tool events into status updates visible to the channel. -## Server Pool +### Server Pool OpenCode runs as `opencode serve --port ` — a persistent HTTP server per working directory. Spacebot manages a pool of these servers. @@ -53,7 +116,7 @@ OpenCode runs as `opencode serve --port ` — a persistent HTTP server per **Auto-restart**: If a server dies, the pool restarts it automatically (up to `max_restart_retries` times, default: 5). -## Communication Protocol +### Communication Protocol All communication is localhost HTTP: @@ -80,7 +143,7 @@ Spacebot subscribes to the SSE stream and processes: - **Question asked** — auto-selects first option - **Retry status** — reports rate limit retries -## OpenCode vs Builtin Workers +### OpenCode vs Builtin Workers | | Builtin Worker | OpenCode Worker | |---|---|---| @@ -97,7 +160,7 @@ The channel system prompt includes a decision guide when OpenCode is enabled: - Need to read a file? Builtin worker. - Need to write or modify code across multiple files? OpenCode worker. -## Permissions +### Permissions OpenCode has its own permission system for dangerous operations. Spacebot controls the defaults: @@ -112,7 +175,7 @@ With all permissions set to `"allow"`, OpenCode suppresses most permission promp These settings are passed to OpenCode via the `OPENCODE_CONFIG_CONTENT` environment variable. LSP and formatter are disabled for headless operation. -## Interactive Sessions +### Interactive Sessions OpenCode workers support the same interactive pattern as builtin workers: @@ -127,7 +190,7 @@ route: worker_id=abc, message="now add the database layer" The OpenCode session accumulates context across follow-ups, so subsequent messages benefit from everything the agent learned during earlier work. -## Model Override +### Model Override You can override the model used by OpenCode workers: @@ -141,7 +204,7 @@ coding = "anthropic/claude-sonnet-4-20250514" When the worker spawns, the routing config determines the model. The model string is split into `provider_id/model_id` and passed to OpenCode's prompt API. -## Full Configuration +### Full Configuration ```toml [defaults.opencode] @@ -157,21 +220,21 @@ bash = "allow" webfetch = "allow" ``` -## Architecture +### Architecture ``` ┌─────────────┐ HTTP/SSE ┌──────────────────┐ -│ Spacebot │ ←───────────────→ │ OpenCode Server │ -│ (worker.rs) │ │ (port 12345) │ +│ Spacebot │ ←───────────────→ │ OpenCode Server │ +│ (worker.rs) │ │ (port 12345) │ └─────────────┘ └──────────────────┘ │ │ │ ProcessEvent::WorkerStatus │ opencode agent loop │ ProcessEvent::WorkerComplete │ (bash, edit, read, etc.) ↓ ↓ ┌─────────────┐ ┌──────────────────┐ -│ Channel │ │ Working Dir │ -│ (status │ │ /code/myapp │ -│ block) │ └──────────────────┘ +│ Channel │ │ Working Dir │ +│ (status │ │ /code/myapp │ +│ block) │ └──────────────────┘ └─────────────┘ ``` diff --git a/docs/content/docs/(features)/meta.json b/docs/content/docs/(features)/meta.json index 0c4bb20b8..ba5c770c4 100644 --- a/docs/content/docs/(features)/meta.json +++ b/docs/content/docs/(features)/meta.json @@ -1,4 +1,4 @@ { "title": "Features", - "pages": ["workers", "opencode", "tools", "mcp", "browser", "cron", "skills", "ingestion"] + "pages": ["workers", "code-workers", "tools", "mcp", "browser", "cron", "skills", "ingestion"] } diff --git a/docs/content/docs/(features)/workers.mdx b/docs/content/docs/(features)/workers.mdx index 3e0a1096a..abd9015e5 100644 --- a/docs/content/docs/(features)/workers.mdx +++ b/docs/content/docs/(features)/workers.mdx @@ -177,8 +177,8 @@ mode = "enabled" writable_paths = [] ``` -## OpenCode Workers +## Code Workers -Workers can also be backed by an OpenCode subprocess instead of the built-in Rig agent. OpenCode workers are full coding agents with their own tool suite, codebase exploration, and context management. +Workers can also be backed by an OpenCode or ACP subprocess instead of the built-in Rig agent. Code workers are full coding agents with their own tool suite, codebase exploration, and context management. -See [OpenCode](/docs/opencode) for details. +See [Code Workers](/docs/code-workers) for backend options and configuration. diff --git a/interface/src/api/client.ts b/interface/src/api/client.ts index 2683cc1e3..284afd092 100644 --- a/interface/src/api/client.ts +++ b/interface/src/api/client.ts @@ -981,6 +981,20 @@ export interface OpenCodeSettingsUpdate { permissions?: Partial; } +export interface AcpSettings { + enabled: boolean; + command: string; + args: string[]; + timeout: number; +} + +export interface AcpSettingsUpdate { + enabled?: boolean; + command?: string; + args?: string[]; + timeout?: number; +} + export interface GlobalSettingsResponse { brave_search_key: string | null; api_enabled: boolean; @@ -988,6 +1002,7 @@ export interface GlobalSettingsResponse { api_bind: string; worker_log_mode: string; opencode: OpenCodeSettings; + acp: Record; } export interface GlobalSettingsUpdate { @@ -997,6 +1012,7 @@ export interface GlobalSettingsUpdate { api_bind?: string; worker_log_mode?: string; opencode?: OpenCodeSettingsUpdate; + acp?: Record; } export interface GlobalSettingsUpdateResponse { diff --git a/interface/src/routes/Settings.tsx b/interface/src/routes/Settings.tsx index 3df6b4d15..16761e89d 100644 --- a/interface/src/routes/Settings.tsx +++ b/interface/src/routes/Settings.tsx @@ -7,11 +7,11 @@ import { ChannelSettingCard, DisabledChannelCard } from "@/components/ChannelSet import { ModelSelect } from "@/components/ModelSelect"; import { ProviderIcon } from "@/lib/providerIcons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faSearch } from "@fortawesome/free-solid-svg-icons"; +import { faPlus, faSearch } from "@fortawesome/free-solid-svg-icons"; import { parse as parseToml } from "smol-toml"; -type SectionId = "providers" | "channels" | "api-keys" | "server" | "opencode" | "worker-logs" | "updates" | "config-file"; +type SectionId = "providers" | "channels" | "api-keys" | "server" | "code-workers" | "worker-logs" | "updates" | "config-file"; const SECTIONS = [ { @@ -39,10 +39,10 @@ const SECTIONS = [ description: "API server configuration", }, { - id: "opencode" as const, - label: "OpenCode", + id: "code-workers" as const, + label: "Code Workers", group: "system" as const, - description: "OpenCode worker integration", + description: "OpenCode and ACP worker configuration", }, { id: "worker-logs" as const, @@ -268,12 +268,12 @@ export function Settings() { enabled: activeSection === "providers", }); - // Fetch global settings (only when on api-keys, server, or worker-logs tabs) + // Fetch global settings (only when on api-keys, server, code-workers, or worker-logs tabs) const { data: globalSettings, isLoading: globalSettingsLoading } = useQuery({ queryKey: ["global-settings"], queryFn: api.globalSettings, staleTime: 5_000, - enabled: activeSection === "api-keys" || activeSection === "server" || activeSection === "opencode" || activeSection === "worker-logs", + enabled: activeSection === "api-keys" || activeSection === "server" || activeSection === "code-workers" || activeSection === "worker-logs", }); const updateMutation = useMutation({ @@ -660,8 +660,8 @@ export function Settings() { ) : activeSection === "server" ? ( - ) : activeSection === "opencode" ? ( - + ) : activeSection === "code-workers" ? ( + ) : activeSection === "worker-logs" ? ( ) : activeSection === "updates" ? ( @@ -1223,7 +1223,10 @@ const PERMISSION_OPTIONS = [ { value: "deny", label: "Deny", description: "Tool is completely disabled" }, ]; -function OpenCodeSection({ settings, isLoading }: GlobalSettingsSectionProps) { +function CodeWorkersSection({ settings, isLoading }: GlobalSettingsSectionProps) { + const createWorkerUid = () => + globalThis.crypto?.randomUUID?.() ?? `acp-${Date.now()}-${Math.random().toString(16).slice(2)}`; + const queryClient = useQueryClient(); const [enabled, setEnabled] = useState(settings?.opencode?.enabled ?? false); const [path, setPath] = useState(settings?.opencode?.path ?? "opencode"); @@ -1233,6 +1236,14 @@ function OpenCodeSection({ settings, isLoading }: GlobalSettingsSectionProps) { const [editPerm, setEditPerm] = useState(settings?.opencode?.permissions?.edit ?? "allow"); const [bashPerm, setBashPerm] = useState(settings?.opencode?.permissions?.bash ?? "allow"); const [webfetchPerm, setWebfetchPerm] = useState(settings?.opencode?.permissions?.webfetch ?? "allow"); + const [acpWorkers, setAcpWorkers] = useState>([]); const [message, setMessage] = useState<{ text: string; type: "success" | "error" } | null>(null); useEffect(() => { @@ -1246,7 +1257,19 @@ function OpenCodeSection({ settings, isLoading }: GlobalSettingsSectionProps) { setBashPerm(settings.opencode.permissions.bash); setWebfetchPerm(settings.opencode.permissions.webfetch); } - }, [settings?.opencode]); + + const rows = Object.entries(settings?.acp ?? {}) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([id, config]) => ({ + uid: createWorkerUid(), + id, + enabled: config.enabled, + command: config.command, + args: config.args.join(", "), + timeout: config.timeout.toString(), + })); + setAcpWorkers(rows); + }, [settings?.opencode, settings?.acp]); const updateMutation = useMutation({ mutationFn: api.updateGlobalSettings, @@ -1280,6 +1303,43 @@ function OpenCodeSection({ settings, isLoading }: GlobalSettingsSectionProps) { return; } + const acpPayload: Record = {}; + const seenIds = new Set(); + for (const worker of acpWorkers) { + const id = worker.id.trim(); + if (!id) { + setMessage({ text: "ACP worker ID cannot be empty", type: "error" }); + return; + } + if (seenIds.has(id)) { + setMessage({ text: `Duplicate ACP worker ID: ${id}`, type: "error" }); + return; + } + seenIds.add(id); + + const timeoutValue = parseInt(worker.timeout, 10); + if (isNaN(timeoutValue) || timeoutValue < 1) { + setMessage({ text: `ACP timeout must be at least 1 for ${id}`, type: "error" }); + return; + } + + const command = worker.command.trim(); + if (!command) { + setMessage({ text: `ACP command cannot be empty for ${id}`, type: "error" }); + return; + } + + acpPayload[id] = { + enabled: worker.enabled, + command, + args: worker.args + .split(",") + .map((value) => value.trim()) + .filter((value) => value.length > 0), + timeout: timeoutValue, + }; + } + updateMutation.mutate({ opencode: { enabled, @@ -1293,15 +1353,53 @@ function OpenCodeSection({ settings, isLoading }: GlobalSettingsSectionProps) { webfetch: webfetchPerm, }, }, + acp: acpPayload, }); }; + const addAcpWorker = () => { + let counter = 1; + let candidateId = `worker-${counter}`; + while (acpWorkers.some((worker) => worker.id === candidateId)) { + counter += 1; + candidateId = `worker-${counter}`; + } + + setAcpWorkers([ + ...acpWorkers, + { + uid: createWorkerUid(), + id: candidateId, + enabled: true, + command: "", + args: "", + timeout: "300", + }, + ]); + }; + + const updateAcpWorker = ( + index: number, + field: "id" | "enabled" | "command" | "args" | "timeout", + value: string | boolean, + ) => { + setAcpWorkers((current) => + current.map((worker, workerIndex) => + workerIndex === index ? { ...worker, [field]: value } : worker, + ), + ); + }; + + const removeAcpWorker = (index: number) => { + setAcpWorkers((current) => current.filter((_, workerIndex) => workerIndex !== index)); + }; + return (
-

OpenCode Workers

+

Code Workers

- Spawn OpenCode coding agents as worker subprocesses. Requires the opencode binary on PATH or a custom path below. + Configure coding worker backends available to spawn_worker: OpenCode and ACP profiles.

@@ -1312,116 +1410,193 @@ function OpenCodeSection({ settings, isLoading }: GlobalSettingsSectionProps) {
) : (
- {/* Enable toggle */} -
- -
+
+

OpenCode

+

+ Spawn OpenCode coding agents as worker subprocesses. +

- {enabled && ( - <> - {/* Binary path */} +
-
- {/* Pool settings */} -
- Server Pool -

- Controls how many OpenCode server processes can run concurrently -

-
- - - -
-
+ {enabled && ( + <> +
+ +
- {/* Permissions */} -
- Permissions -

- Control which tools OpenCode workers can use -

-
- {([ - { label: "File Edit", value: editPerm, setter: setEditPerm }, - { label: "Shell / Bash", value: bashPerm, setter: setBashPerm }, - { label: "Web Fetch", value: webfetchPerm, setter: setWebfetchPerm }, - ] as const).map(({ label, value, setter }) => ( -
- {label} - +
+ Server Pool +

+ Controls how many OpenCode server processes can run concurrently +

+
+ + +
- ))} -
-
- - )} +
+ +
+ Permissions +

+ Control which tools OpenCode workers can use +

+
+ {([ + { label: "File Edit", value: editPerm, setter: setEditPerm }, + { label: "Shell / Bash", value: bashPerm, setter: setBashPerm }, + { label: "Web Fetch", value: webfetchPerm, setter: setWebfetchPerm }, + ] as const).map(({ label, value, setter }) => ( +
+ {label} + +
+ ))} +
+
+ + )} +
+
+ +
+
+

ACP

+

+ Each entry maps to [defaults.acp.<id>]. Use worker_type: "acp" and optional acp_id. +

+
+ +
+ {acpWorkers.length === 0 ? ( +

No ACP workers configured.

+ ) : ( + acpWorkers.map((worker, index) => ( +
+
+ + + +
+ +
+ +
+ + +
+
+
+ )) + )} +
+ +
+ +
+