From 5e0e5e2a981bdde6e5fe2c43a0854ffbf9732d70 Mon Sep 17 00:00:00 2001 From: harryfan1985 Date: Sat, 30 May 2026 13:58:27 +0800 Subject: [PATCH] feat(acp): add Oh My Pi (omp) as a built-in ACP agent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrate OMP via BitFun's existing ACP client subsystem instead of a bespoke runtime — OMP speaks the Agent Client Protocol natively through `omp acp`, so it reuses the entire battle-tested ACP path (subprocess spawn, JSON-RPC, tool routing, permissions, session persistence, model selection, PATH probing). - builtin_clients.rs: new preset { id: "omp", command: "omp", args: ["acp"] }. Native ACP, no adapter (like opencode). Makes OMP appear automatically in the per-workspace ACP session menu (which lists clients dynamically). - OMP is user-managed: it targets the bun runtime and ships via its own installer (`bun install -g @oh-my-pi/pi-coding-agent` or `curl -fsSL https://omp.sh/install | sh`), which BitFun's npm-based installer cannot provide. So `install_package` becomes Option and is None for omp; BitFun only detects `omp` on PATH and launches it. The other presets keep their npm installers (now Some(...)). - AcpAgentsConfig.tsx: add OMP to the settings catalog; treat it as native ACP (NATIVE_ACP_PRESET_IDS, no adapter) and as self-managed (SELF_MANAGED_INSTALL_PRESET_IDS) so the one-click "Install CLI" action is hidden — the row just reflects PATH-detected install state. Verified: cargo test -p bitfun-acp (45 passed, incl. omp_is_a_native_acp_preset asserting no adapter + no installer); vitest AcpAgentsConfig + workspaceAcpMenuClients (10 passed); tsc clean. Co-Authored-By: Claude Opus 4.8 --- src/crates/acp/src/client/builtin_clients.rs | 44 +++++++++++++++++-- src/crates/acp/src/client/requirements.rs | 2 +- .../config/components/AcpAgentsConfig.tsx | 25 +++++++++-- 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/src/crates/acp/src/client/builtin_clients.rs b/src/crates/acp/src/client/builtin_clients.rs index 375114621..e709a1476 100644 --- a/src/crates/acp/src/client/builtin_clients.rs +++ b/src/crates/acp/src/client/builtin_clients.rs @@ -7,7 +7,10 @@ pub(crate) struct BuiltinAcpClientPreset { pub(crate) command: &'static str, pub(crate) args: &'static [&'static str], pub(crate) tool_command: &'static str, - pub(crate) install_package: &'static str, + /// npm package BitFun can install on the user's behalf. `None` means the + /// agent is user-managed (BitFun only provides the integration, the user + /// installs the CLI themselves) — the UI then shows no one-click installer. + pub(crate) install_package: Option<&'static str>, pub(crate) adapter_package: Option<&'static str>, pub(crate) adapter_bin: Option<&'static str>, } @@ -18,7 +21,22 @@ const BUILTIN_ACP_CLIENT_PRESETS: &[BuiltinAcpClientPreset] = &[ command: "opencode", args: &["acp"], tool_command: "opencode", - install_package: "opencode-ai", + install_package: Some("opencode-ai"), + adapter_package: None, + adapter_bin: None, + }, + // Oh My Pi (omp) — a terminal coding agent that speaks ACP natively via + // `omp acp` (no adapter needed, like opencode). User-managed: omp targets + // the bun runtime (installed via `bun install -g @oh-my-pi/pi-coding-agent` + // or `curl -fsSL https://omp.sh/install | sh`), which BitFun's npm-based + // installer cannot provide — so install_package is None and BitFun only + // detects `omp` on PATH and launches it. https://github.com/can1357/oh-my-pi + BuiltinAcpClientPreset { + id: "omp", + command: "omp", + args: &["acp"], + tool_command: "omp", + install_package: None, adapter_package: None, adapter_bin: None, }, @@ -27,7 +45,7 @@ const BUILTIN_ACP_CLIENT_PRESETS: &[BuiltinAcpClientPreset] = &[ command: "npx", args: &["--yes", "@zed-industries/claude-code-acp@latest"], tool_command: "claude", - install_package: "@anthropic-ai/claude-code", + install_package: Some("@anthropic-ai/claude-code"), adapter_package: Some("@zed-industries/claude-code-acp"), adapter_bin: Some("claude-code-acp"), }, @@ -36,7 +54,7 @@ const BUILTIN_ACP_CLIENT_PRESETS: &[BuiltinAcpClientPreset] = &[ command: "npx", args: &["--yes", "@zed-industries/codex-acp@latest"], tool_command: "codex", - install_package: "@openai/codex", + install_package: Some("@openai/codex"), adapter_package: Some("@zed-industries/codex-acp"), adapter_bin: Some("codex-acp"), }, @@ -85,4 +103,22 @@ mod tests { vec!["--yes", "@zed-industries/claude-code-acp@latest"] ); } + + #[test] + fn omp_is_a_native_acp_preset() { + let preset = builtin_acp_client_preset("omp").expect("omp preset registered"); + assert_eq!(preset.command, "omp"); + assert_eq!(preset.args, &["acp"]); + assert_eq!(preset.tool_command, "omp"); + // Native ACP — no adapter package/bin, like opencode. + assert!(preset.adapter_package.is_none()); + assert!(preset.adapter_bin.is_none()); + // User-managed: BitFun provides no installer for omp. + assert!(preset.install_package.is_none()); + + let config = default_config_for_builtin_client("omp").expect("omp config"); + assert!(config.enabled); + assert_eq!(config.command, "omp"); + assert_eq!(config.args, vec!["acp"]); + } } diff --git a/src/crates/acp/src/client/requirements.rs b/src/crates/acp/src/client/requirements.rs index 8789628a2..82fd29f71 100644 --- a/src/crates/acp/src/client/requirements.rs +++ b/src/crates/acp/src/client/requirements.rs @@ -34,7 +34,7 @@ pub(crate) fn acp_requirement_spec<'a>( if let Some(preset) = builtin_acp_client_preset(client_id) { return AcpRequirementSpec { tool_command: preset.tool_command, - install_package: Some(preset.install_package), + install_package: preset.install_package, adapter: match (preset.adapter_package, preset.adapter_bin) { (Some(package), Some(bin)) => Some(AcpAdapterSpec { package, bin }), _ => None, diff --git a/src/web-ui/src/infrastructure/config/components/AcpAgentsConfig.tsx b/src/web-ui/src/infrastructure/config/components/AcpAgentsConfig.tsx index f51cea670..19f4297ce 100644 --- a/src/web-ui/src/infrastructure/config/components/AcpAgentsConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/AcpAgentsConfig.tsx @@ -60,6 +60,15 @@ interface AcpClientPreset { args: string[]; } +// Presets that speak ACP natively and therefore need no separate adapter +// package (their CLI binary is launched directly). +const NATIVE_ACP_PRESET_IDS = new Set(['opencode', 'omp']); + +// Presets BitFun cannot install on the user's behalf — the agent must be +// installed manually (e.g. omp targets bun and ships via its own installer). +// The UI hides the one-click "Install CLI" action for these. +const SELF_MANAGED_INSTALL_PRESET_IDS = new Set(['omp']); + const PRESETS: AcpClientPreset[] = [ { id: 'opencode', @@ -68,6 +77,13 @@ const PRESETS: AcpClientPreset[] = [ command: 'opencode', args: ['acp'], }, + { + id: 'omp', + name: 'Oh My Pi', + description: 'Native ACP coding agent (omp acp).', + command: 'omp', + args: ['acp'], + }, { id: 'claude-code', name: 'Claude Code', @@ -388,7 +404,7 @@ const AcpAgentsConfig: React.FC = () => { enabled, toolInstalled: probe?.tool.installed, adapterInstalled: probe?.adapter?.installed, - requiresAdapter: Boolean(probe?.adapter || preset.id !== 'opencode'), + requiresAdapter: Boolean(probe?.adapter || !NATIVE_ACP_PRESET_IDS.has(preset.id)), probePending, probe, }); @@ -1031,7 +1047,9 @@ const AcpAgentsConfig: React.FC = () => { const installing = installingClientIds.has(preset.id); const configuring = installingClientIds.has(preset.id); const showSelect = hasConfigEntry && (status === 'enabled' || status === 'ready'); - const canInstallCli = status === 'not_installed' && issueKind !== 'connection_failed'; + const canInstallCli = status === 'not_installed' + && issueKind !== 'connection_failed' + && !SELF_MANAGED_INSTALL_PRESET_IDS.has(preset.id); const canConfigureAcp = !requiresAdapter ? false : issueKind === 'adapter_missing' || (status === 'partial' && issueKind === 'config_invalid'); @@ -1351,7 +1369,8 @@ const AcpAgentsConfig: React.FC = () => { probe: row.requirementProbe, requiresAdapter: row.requiresAdapter, }); - const canInstallCli = row.preset && row.status === 'not_installed' && row.issueKind === 'cli_missing'; + const canInstallCli = row.preset && row.status === 'not_installed' && row.issueKind === 'cli_missing' + && !SELF_MANAGED_INSTALL_PRESET_IDS.has(row.preset.id); const canViewError = row.status === 'invalid' || row.status === 'partial' || row.issueKind === 'connection_failed' || row.issueKind === 'permission_denied'