From 3cd962c6fae0f7320159e97f346d23b13a08e905 Mon Sep 17 00:00:00 2001 From: Nathan Lutz Date: Tue, 25 Aug 2026 14:09:41 -0700 Subject: [PATCH 1/2] feat(desktop): add GitHub Copilot CLI harness Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Nathan Lutz --- VISION.md | 2 +- desktop/src-tauri/src/commands/agent_auth.rs | 33 +++++++++++++ .../src-tauri/src/managed_agents/discovery.rs | 49 ++++++++++++++++++- .../discovery/auth_status_cache.rs | 11 ++++- .../discovery/runtime_metadata.rs | 12 ++++- .../src/managed_agents/discovery/tests.rs | 48 +++++++++++++++++- desktop/src/features/agents/AGENTS.md | 4 ++ .../agents/ui/personaRuntimeModel.test.mjs | 1 + .../features/agents/ui/personaRuntimeModel.ts | 4 +- desktop/src/testing/e2eBridge.ts | 23 ++++++++- .../e2e/harness-catalog-screenshots.spec.ts | 24 ++++++++- 11 files changed, 200 insertions(+), 11 deletions(-) diff --git a/VISION.md b/VISION.md index 900e5a9475b..bd9eb5dcd7d 100644 --- a/VISION.md +++ b/VISION.md @@ -219,7 +219,7 @@ Greenfield. Agent swarms build in parallel, integrating at the event store bound |-|------| | ✅ | Core relay, auth, pub/sub, search, audit | | ✅ | MCP server — full feature surface | -| ✅ | ACP agent harness — goose, codex, claude code | +| ✅ | ACP agent harness — Goose, Codex, Claude Code, GitHub Copilot CLI | | ✅ | Desktop client (Tauri) — Stream, Home, Forum, DMs, Agents, Workflows, Search, Settings, Profiles, Presence | | ✅ | Channel features — messaging, threads, reactions, canvases, media uploads, editing, deletion, typing indicators, NIP-29, soft-delete | | ✅ | Workflow engine — YAML-as-code, execution traces, message/reaction/schedule/webhook triggers | diff --git a/desktop/src-tauri/src/commands/agent_auth.rs b/desktop/src-tauri/src/commands/agent_auth.rs index bba0f338602..73a4ab38eae 100644 --- a/desktop/src-tauri/src/commands/agent_auth.rs +++ b/desktop/src-tauri/src/commands/agent_auth.rs @@ -12,6 +12,7 @@ use serde::{Deserialize, Serialize}; use crate::managed_agents::{ default_agent_workdir, known_acp_runtime_exact, normalize_agent_args, resolve_command, + AuthStatus, }; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -77,6 +78,38 @@ fn discover_acp_auth_methods_blocking(runtime_id: &str) -> Result AuthStatus { + match run_buzz_acp_auth_command(runtime_id, ["models", "--json"]) { + Ok(output) if output.status.success() => AuthStatus::LoggedIn, + Ok(output) => { + let stderr = String::from_utf8_lossy(&output.stderr); + match crate::managed_agents::readiness::cli_probe::classify_probe_output( + stderr.as_bytes(), + false, + ) { + crate::managed_agents::readiness::cli_probe::ProbeOutcome::LoggedIn => { + AuthStatus::LoggedIn + } + crate::managed_agents::readiness::cli_probe::ProbeOutcome::LoggedOut => { + AuthStatus::LoggedOut + } + crate::managed_agents::readiness::cli_probe::ProbeOutcome::ConfigInvalid { + stderr_excerpt, + } => AuthStatus::ConfigInvalid { + diagnostic: stderr_excerpt, + }, + } + } + Err(_) => AuthStatus::Unknown, + } +} + fn connect_acp_runtime_blocking( request: &ConnectAcpRuntimeRequest, ) -> Result { diff --git a/desktop/src-tauri/src/managed_agents/discovery.rs b/desktop/src-tauri/src/managed_agents/discovery.rs index 78592357c9b..77a4191425e 100644 --- a/desktop/src-tauri/src/managed_agents/discovery.rs +++ b/desktop/src-tauri/src/managed_agents/discovery.rs @@ -31,6 +31,7 @@ pub(crate) use runtime_metadata::KnownAcpRuntime; const GOOSE_AVATAR_URL: &str = "https://goose-docs.ai/img/logo_dark.png"; const CLAUDE_CODE_AVATAR_URL: &str = "https://anthropic.gallerycdn.vsassets.io/extensions/anthropic/claude-code/2.1.77/1773707456892/Microsoft.VisualStudio.Services.Icons.Default"; const CODEX_AVATAR_URL: &str = "https://openai.gallerycdn.vsassets.io/extensions/openai/chatgpt/26.5313.41514/1773706730621/Microsoft.VisualStudio.Services.Icons.Default"; +const COPILOT_CLI_AVATAR_URL: &str = "https://github.com/github.png"; const BUZZ_AGENT_AVATAR_URL: &str = "https://raw.githubusercontent.com/block/buzz/refs/heads/main/crates/buzz-agent/buzz-agent.png"; fn common_binary_paths() -> &'static [PathBuf] { @@ -186,6 +187,39 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[ // Verified: `codex login status` exits 0 when logged in, non-zero otherwise. auth_probe_args: Some(&["codex", "login", "status"]), }, + KnownAcpRuntime { + id: "copilot", + label: "GitHub Copilot CLI", + commands: &["copilot"], + aliases: &["github-copilot", "gh-copilot"], + avatar_url: COPILOT_CLI_AVATAR_URL, + mcp_command: None, + mcp_hooks: false, + underlying_cli: Some("copilot"), + cli_install_commands: &[], + cli_install_commands_windows: &[], + adapter_install_commands: &[], + cli_install_instructions_url: "https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli", + adapter_install_instructions_url: "", + cli_install_hint: "Install GitHub Copilot CLI, then sign in with your GitHub account. Buzz uses its built-in ACP server directly.", + adapter_install_hint: "", + skill_dir: None, + supports_acp_model_switching: true, + model_env_var: None, + provider_env_var: None, + provider_locked: false, + default_env: &[], + config_file_path: Some("~/.copilot/config.json"), + config_file_format: Some("json"), + supports_acp_native_config: false, + thinking_env_var: None, + max_tokens_env_var: None, + context_limit_env_var: None, + max_rounds_env_var: None, + required_normalized_fields: &[], + login_hint: Some("Run `copilot login` to authenticate."), + auth_probe_args: None, + }, KnownAcpRuntime { id: "buzz-agent", label: "Buzz Agent", @@ -451,6 +485,7 @@ pub fn try_record_agent_command( fn default_agent_args(command: &str) -> Option> { match normalize_command_identity(command).as_str() { "goose" => Some(vec!["acp".to_string()]), + "copilot" | "github-copilot" | "gh-copilot" => Some(vec!["--acp".to_string()]), "codex" | "codex-acp" | "claude-agent-acp" | "claude-code-acp" | "claude-code" | "claudecode" | "buzz-agent" => Some(Vec::new()), _ => None, @@ -458,12 +493,22 @@ fn default_agent_args(command: &str) -> Option> { } pub fn normalize_agent_args(command: &str, agent_args: Vec) -> Vec { - let normalized = agent_args + let mut normalized = agent_args .into_iter() .map(|arg| arg.trim().to_string()) .filter(|arg| !arg.is_empty()) .collect::>(); + if matches!( + normalize_command_identity(command).as_str(), + "copilot" | "github-copilot" | "gh-copilot" + ) { + if !normalized.iter().any(|arg| arg == "--acp") { + normalized.insert(0, "--acp".to_string()); + } + return normalized; + } + let Some(default_args) = default_agent_args(command) else { return normalized; }; @@ -1297,7 +1342,7 @@ pub fn discover_acp_runtimes_from( if partial.entry.auth_status == AuthStatus::Unknown { partial.entry.auth_status = if partial.entry.availability == AcpAvailabilityStatus::Available - && partial.runtime.auth_probe_args.is_none() + && !auth_status_cache::runtime_is_probeable(partial.runtime) { AuthStatus::NotApplicable } else { diff --git a/desktop/src-tauri/src/managed_agents/discovery/auth_status_cache.rs b/desktop/src-tauri/src/managed_agents/discovery/auth_status_cache.rs index cae0d7e2c94..6fd3132cdb7 100644 --- a/desktop/src-tauri/src/managed_agents/discovery/auth_status_cache.rs +++ b/desktop/src-tauri/src/managed_agents/discovery/auth_status_cache.rs @@ -39,6 +39,10 @@ pub(super) fn get(runtime_id: &str) -> AuthStatus { .unwrap_or(AuthStatus::Unknown) } +pub(super) fn runtime_is_probeable(runtime: &super::KnownAcpRuntime) -> bool { + runtime.id == "copilot" || runtime.auth_probe_args.is_some() +} + #[cfg(test)] pub(crate) fn len() -> usize { cache().lock().map(|g| g.len()).unwrap_or(0) @@ -62,6 +66,11 @@ pub(super) fn resolve_auth_statuses(partials: &mut [super::PartialEntry], force: if partial.entry.availability != AcpAvailabilityStatus::Available { return None; } + if partial.runtime.id == "copilot" { + let handle = + std::thread::spawn(|| crate::commands::probe_acp_runtime_auth("copilot")); + return Some((idx, handle)); + } let probe_args = partial.runtime.auth_probe_args?; // Need the resolved binary path for the CLI (e.g. the actual `claude` binary). let binary_path = super::resolve_command(probe_args[0])?; @@ -84,7 +93,7 @@ pub(super) fn resolve_auth_statuses(partials: &mut [super::PartialEntry], force: } else { for partial in partials.iter_mut() { if partial.entry.availability != AcpAvailabilityStatus::Available - || partial.runtime.auth_probe_args.is_none() + || !runtime_is_probeable(partial.runtime) { continue; } diff --git a/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs b/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs index 34edecdcd9c..24271e35fd0 100644 --- a/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs +++ b/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs @@ -85,7 +85,7 @@ impl KnownAcpRuntime { #[cfg(test)] mod tests { - use super::super::known_acp_runtime_exact; + use super::super::{auth_status_cache, known_acp_runtime_exact}; #[test] fn vendor_metadata_distinguishes_cli_and_adapter_guidance() { @@ -122,5 +122,15 @@ mod tests { ); assert!(codex.adapter_install_instructions_url.contains("codex-acp")); assert!(codex.cli_install_hint.contains("Codex CLI")); + + let copilot = known_acp_runtime_exact("copilot").unwrap(); + assert_eq!(copilot.commands, &["copilot"]); + assert!(copilot.adapter_install_commands.is_empty()); + assert!(copilot.cli_install_hint.contains("built-in ACP server")); + assert!(copilot + .cli_install_instructions_url + .contains("docs.github.com")); + assert!(copilot.supports_acp_model_switching); + assert!(auth_status_cache::runtime_is_probeable(copilot)); } } diff --git a/desktop/src-tauri/src/managed_agents/discovery/tests.rs b/desktop/src-tauri/src/managed_agents/discovery/tests.rs index 2d1db692932..6d7b40f9c07 100644 --- a/desktop/src-tauri/src/managed_agents/discovery/tests.rs +++ b/desktop/src-tauri/src/managed_agents/discovery/tests.rs @@ -7,7 +7,8 @@ use super::{ effective_agent_command, find_nvm_default_bin, is_login_shell_path_uninit, is_safe_nvm_tag, managed_agent_avatar_url, normalize_agent_args, parse_semver_tag, probe_codex_acp_version, record_agent_command, refresh_login_shell_path, try_record_agent_command, - BUZZ_AGENT_AVATAR_URL, CLAUDE_CODE_AVATAR_URL, CODEX_AVATAR_URL, GOOSE_AVATAR_URL, + BUZZ_AGENT_AVATAR_URL, CLAUDE_CODE_AVATAR_URL, CODEX_AVATAR_URL, COPILOT_CLI_AVATAR_URL, + GOOSE_AVATAR_URL, }; use crate::managed_agents::AcpAvailabilityStatus; @@ -69,6 +70,51 @@ fn normalizes_claude_and_codex_args_to_empty() { ); } +#[test] +fn normalizes_copilot_args_to_native_stdio_acp_mode() { + let expected = vec!["--acp".to_string()]; + assert_eq!(normalize_agent_args("copilot", Vec::new()), expected); + assert_eq!( + normalize_agent_args(r"C:\Tools\copilot.exe", Vec::new()), + vec!["--acp".to_string()] + ); + assert_eq!( + normalize_agent_args("gh-copilot", Vec::new()), + vec!["--acp".to_string()] + ); + assert_eq!( + normalize_agent_args("copilot", vec!["--allow-all-tools".to_string()]), + vec!["--acp".to_string(), "--allow-all-tools".to_string()] + ); + assert_eq!( + normalize_agent_args( + "copilot", + vec![ + "--acp".to_string(), + "--model".to_string(), + "gpt-5.4".to_string() + ] + ), + vec![ + "--acp".to_string(), + "--model".to_string(), + "gpt-5.4".to_string() + ] + ); +} + +#[test] +fn resolves_copilot_avatar_for_command_and_alias() { + assert_eq!( + managed_agent_avatar_url("copilot"), + Some(COPILOT_CLI_AVATAR_URL.to_string()) + ); + assert_eq!( + managed_agent_avatar_url("GitHub Copilot"), + Some(COPILOT_CLI_AVATAR_URL.to_string()) + ); +} + #[test] fn resolves_buzz_agent_avatar() { assert_eq!( diff --git a/desktop/src/features/agents/AGENTS.md b/desktop/src/features/agents/AGENTS.md index 7822211541b..cd18a66913e 100644 --- a/desktop/src/features/agents/AGENTS.md +++ b/desktop/src/features/agents/AGENTS.md @@ -301,3 +301,7 @@ matches the code is worse than no rule; a new pattern that isn't written down here will be broken by the next agent that never learns it existed. Reviewers: treat a config-behavior diff without a matching AGENTS.md diff (or an explicit "no rules changed" note) as incomplete. + +GitHub Copilot CLI follows the existing catalog-owned capability model. Its +provider submission classification only extends the pre-catalog fallback for +CLI-login runtimes; no configuration rule changes. diff --git a/desktop/src/features/agents/ui/personaRuntimeModel.test.mjs b/desktop/src/features/agents/ui/personaRuntimeModel.test.mjs index 967bd3e1290..aa9fb949ebf 100644 --- a/desktop/src/features/agents/ui/personaRuntimeModel.test.mjs +++ b/desktop/src/features/agents/ui/personaRuntimeModel.test.mjs @@ -195,6 +195,7 @@ test("resolveRuntimeProviderCapability classifies known CLI-login runtimes as lo // The core fix: a not-yet-loaded catalog must not force these to "unknown". assert.equal(resolveRuntimeProviderCapability("claude", false), "locked"); assert.equal(resolveRuntimeProviderCapability("codex", false), "locked"); + assert.equal(resolveRuntimeProviderCapability("copilot", false), "locked"); assert.equal(resolveRuntimeProviderCapability(" claude ", false), "locked"); }); diff --git a/desktop/src/features/agents/ui/personaRuntimeModel.ts b/desktop/src/features/agents/ui/personaRuntimeModel.ts index 20d789c4ebd..afe982759b6 100644 --- a/desktop/src/features/agents/ui/personaRuntimeModel.ts +++ b/desktop/src/features/agents/ui/personaRuntimeModel.ts @@ -18,7 +18,7 @@ export type ProviderRuntimeCapability = "capable" | "locked" | "unknown"; * provider. To avoid that, we resolve capability STATICALLY for known ids: * * - buzz-agent / goose → "capable" (`isProviderCapable`, id-based). - * - claude / codex → "locked" (CLI-login runtimes; no LLM provider selection). + * - claude / codex / copilot → "locked" (CLI-login runtimes; no LLM provider selection). * - anything else (custom, empty, genuinely unknown) → "unknown". * * `isProviderCapable` is the caller-supplied {@link @@ -33,7 +33,7 @@ export function resolveRuntimeProviderCapability( return "capable"; } const id = runtimeId.trim(); - if (id === "claude" || id === "codex") { + if (id === "claude" || id === "codex" || id === "copilot") { return "locked"; } return "unknown"; diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index 0bd7bc6eecc..dc180d1d486 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -8148,6 +8148,27 @@ async function handleDiscoverAcpRuntimes( source: "builtin", login_hint: undefined, }, + { + id: "copilot", + label: "GitHub Copilot CLI", + avatar_url: "", + availability: "available", + command: "copilot", + binary_path: "/usr/local/bin/copilot", + default_args: ["--acp"], + mcp_command: null, + install_hint: + "Install GitHub Copilot CLI, then sign in with your GitHub account. Buzz uses its built-in ACP server directly.", + install_instructions_url: + "https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli", + can_auto_install: false, + requires_external_cli: true, + underlying_cli_path: "/usr/local/bin/copilot", + node_required: false, + auth_status: { status: "logged_in" }, + source: "builtin", + login_hint: undefined, + }, { id: "buzz-agent", label: "Buzz Agent", @@ -8163,7 +8184,7 @@ async function handleDiscoverAcpRuntimes( requires_external_cli: false, underlying_cli_path: null, node_required: false, - auth_status: { status: "not_applicable" }, + auth_status: { status: "logged_in" }, source: "builtin", login_hint: undefined, }, diff --git a/desktop/tests/e2e/harness-catalog-screenshots.spec.ts b/desktop/tests/e2e/harness-catalog-screenshots.spec.ts index c5613f41f2a..851e061adc3 100644 --- a/desktop/tests/e2e/harness-catalog-screenshots.spec.ts +++ b/desktop/tests/e2e/harness-catalog-screenshots.spec.ts @@ -25,7 +25,7 @@ const CATALOG = [ can_auto_install: false, underlying_cli_path: null, node_required: false, - auth_status: { status: "not_applicable" }, + auth_status: { status: "logged_in" }, }, { id: "claude", @@ -79,6 +79,26 @@ const CATALOG = [ auth_status: { status: "not_applicable" }, source: "preset", }, + { + id: "copilot", + label: "GitHub Copilot CLI", + avatar_url: "", + availability: "available", + command: "copilot", + binary_path: "/usr/local/bin/copilot", + default_args: ["--acp"], + mcp_command: null, + install_hint: + "Buzz uses GitHub Copilot CLI's built-in ACP server directly.", + install_instructions_url: + "https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli", + can_auto_install: false, + requires_external_cli: true, + underlying_cli_path: "/usr/local/bin/copilot", + node_required: false, + auth_status: { status: "logged_in" }, + source: "builtin", + }, { id: "omp", label: "Oh My Pi", @@ -163,7 +183,7 @@ test("after: consolidated harnesses panel + catalog dialog", async ({ // 3. Ready entry detail (Ready state, no install action). Ready entries // sit in the "Installed" accordion, collapsed by default — expand it. await page.getByTestId("harness-catalog-section-installed").click(); - await page.getByTestId("harness-catalog-list-item-claude").click(); + await page.getByTestId("harness-catalog-list-item-copilot").click(); await page.waitForTimeout(300); await page.getByTestId("harness-catalog-dialog").screenshot({ path: `${SHOTS}/after-catalog-ready-detail.png`, From e80953037253610c662ce116687089e43a56d67c Mon Sep 17 00:00:00 2001 From: Nathan Lutz Date: Wed, 26 Aug 2026 09:58:31 -0700 Subject: [PATCH 2/2] fix(desktop): address Copilot harness review feedback Pin the GitHub avatar URL, document auth probe caching, log probe startup failures, and remove unrelated capitalization churn. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Nathan Lutz --- VISION.md | 2 +- desktop/src-tauri/src/commands/agent_auth.rs | 16 ++++++++++++---- .../src-tauri/src/managed_agents/discovery.rs | 2 +- .../managed_agents/discovery/runtime_metadata.rs | 4 ++++ 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/VISION.md b/VISION.md index bd9eb5dcd7d..880c97d92cc 100644 --- a/VISION.md +++ b/VISION.md @@ -219,7 +219,7 @@ Greenfield. Agent swarms build in parallel, integrating at the event store bound |-|------| | ✅ | Core relay, auth, pub/sub, search, audit | | ✅ | MCP server — full feature surface | -| ✅ | ACP agent harness — Goose, Codex, Claude Code, GitHub Copilot CLI | +| ✅ | ACP agent harness — goose, codex, claude code, GitHub Copilot CLI | | ✅ | Desktop client (Tauri) — Stream, Home, Forum, DMs, Agents, Workflows, Search, Settings, Profiles, Presence | | ✅ | Channel features — messaging, threads, reactions, canvases, media uploads, editing, deletion, typing indicators, NIP-29, soft-delete | | ✅ | Workflow engine — YAML-as-code, execution traces, message/reaction/schedule/webhook triggers | diff --git a/desktop/src-tauri/src/commands/agent_auth.rs b/desktop/src-tauri/src/commands/agent_auth.rs index 73a4ab38eae..f3b6b3a15e6 100644 --- a/desktop/src-tauri/src/commands/agent_auth.rs +++ b/desktop/src-tauri/src/commands/agent_auth.rs @@ -81,9 +81,10 @@ fn discover_acp_auth_methods_blocking(runtime_id: &str) -> Result AuthStatus { match run_buzz_acp_auth_command(runtime_id, ["models", "--json"]) { Ok(output) if output.status.success() => AuthStatus::LoggedIn, @@ -106,7 +107,14 @@ pub(crate) fn probe_acp_runtime_auth(runtime_id: &str) -> AuthStatus { }, } } - Err(_) => AuthStatus::Unknown, + Err(error) => { + tracing::debug!( + runtime_id, + error = %error, + "ACP runtime auth probe could not start" + ); + AuthStatus::Unknown + } } } diff --git a/desktop/src-tauri/src/managed_agents/discovery.rs b/desktop/src-tauri/src/managed_agents/discovery.rs index 77a4191425e..dbbd9742763 100644 --- a/desktop/src-tauri/src/managed_agents/discovery.rs +++ b/desktop/src-tauri/src/managed_agents/discovery.rs @@ -31,7 +31,7 @@ pub(crate) use runtime_metadata::KnownAcpRuntime; const GOOSE_AVATAR_URL: &str = "https://goose-docs.ai/img/logo_dark.png"; const CLAUDE_CODE_AVATAR_URL: &str = "https://anthropic.gallerycdn.vsassets.io/extensions/anthropic/claude-code/2.1.77/1773707456892/Microsoft.VisualStudio.Services.Icons.Default"; const CODEX_AVATAR_URL: &str = "https://openai.gallerycdn.vsassets.io/extensions/openai/chatgpt/26.5313.41514/1773706730621/Microsoft.VisualStudio.Services.Icons.Default"; -const COPILOT_CLI_AVATAR_URL: &str = "https://github.com/github.png"; +const COPILOT_CLI_AVATAR_URL: &str = "https://avatars.githubusercontent.com/u/9919?s=200&v=4"; const BUZZ_AGENT_AVATAR_URL: &str = "https://raw.githubusercontent.com/block/buzz/refs/heads/main/crates/buzz-agent/buzz-agent.png"; fn common_binary_paths() -> &'static [PathBuf] { diff --git a/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs b/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs index 24271e35fd0..8a811f2d9ae 100644 --- a/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs +++ b/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs @@ -125,6 +125,10 @@ mod tests { let copilot = known_acp_runtime_exact("copilot").unwrap(); assert_eq!(copilot.commands, &["copilot"]); + assert_eq!( + copilot.avatar_url, + "https://avatars.githubusercontent.com/u/9919?s=200&v=4" + ); assert!(copilot.adapter_install_commands.is_empty()); assert!(copilot.cli_install_hint.contains("built-in ACP server")); assert!(copilot