diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs index babbaffe2..c8d219a82 100644 --- a/desktop/scripts/check-file-sizes.mjs +++ b/desktop/scripts/check-file-sizes.mjs @@ -121,7 +121,11 @@ const overrides = new Map([ // record_provider param + applies persona_field_with_record_fallback. +5 lines. // global-agent-config: spawn_agent_child loads global config and merges as // lowest env layer (+8 lines). Queued to split. - ["src-tauri/src/managed_agents/runtime.rs", 2216], + // acp-dead-machinery retirement: codex adapter-availability spawn stamp + + // the availability_drift half of needs_restart deleted (the bundled bridge + // can't drift out-of-band); ratcheted to bank the deletions (main's + // team-instructions spawn-hash growth stays). + ["src-tauri/src/managed_agents/runtime.rs", 2087], // config-bridge setup-payload env-boundary fix adds readiness wiring in // spawn_agent_child; load-bearing security fix, queued to split. ["src-tauri/src/managed_agents/config_bridge/reader.rs", 1016], @@ -205,7 +209,9 @@ const overrides = new Map([ // split with the rest of this file. // bundled-adapter-doctor-copy: adapter_bundled field on // RawAcpRuntimeCatalogEntry + mapper passthrough (+3 lines). - ["src/shared/api/tauri.ts", 1343], + // acp-dead-machinery retirement: node_required wire field deleted; + // ratcheted 1343 -> 1341. + ["src/shared/api/tauri.ts", 1341], // doctor-npm-eacces-preflight: hint field added to InstallStepResult (+1 line). // doctor-install-reliability: AuthStatus tagged union + nodeRequired/authStatus/ // loginHint fields on AcpRuntimeCatalogEntry (+14 lines). Load-bearing new feature. @@ -227,7 +233,9 @@ const overrides = new Map([ // AcpRuntimeCatalogEntry (+2 lines). // bundled-cli-probes: "cli_missing" availability retired; +4 doc lines on // AcpAvailabilityStatus explaining why the state no longer exists. - ["src/shared/api/types.ts", 1075], + // acp-dead-machinery retirement: nodeRequired field deleted; + // ratcheted 1075 -> 1073. + ["src/shared/api/types.ts", 1073], // readiness-gate: PersonaDialog.tsx threads computeLocalModeGate + // requiredCredentialEnvKeys + RequiredFieldLabel so the "New agent" dialog // shows required markers and credential amber rows (parity with @@ -294,7 +302,10 @@ const overrides = new Map([ // bundled-cli-probes: resolve_probe_binary (bundled-CLI-first probe // resolution) + classify_runtime/underlying_cli doc rewrites for the // CliMissing retirement (+8 lines). - ["src-tauri/src/managed_agents/discovery.rs", 1267], + // acp-dead-machinery retirement: adapter-availability cache + + // availability_drift, runtime_needs_npm/is_npm_global_install, and the + // node_required computation deleted; ratcheted 1267 -> 1166. + ["src-tauri/src/managed_agents/discovery.rs", 1166], // rebase over codex-acp-package-swap: its version-probe tests union with the // doctor-install-reliability nvm/login-shell/semver tests — each side alone // stayed under the 1000 default; the union exceeds it. @@ -470,7 +481,12 @@ const overrides = new Map([ // bundle-acps: adapter_verification_step post-install gate + its test // quartet (+120 lines, partly offset by the version-gate retirement's // deletions in this file). Queued to split. - ["src-tauri/src/commands/agent_discovery.rs", 1576], + // acp-dead-machinery retirement: npm EACCES preflight (resolve_npm_prefix, + // npm_preflight_check, npm_eacces_hint) + its Phase-2 branches, test + // groups, and the availability_drift tests deleted; ratcheted + // 1576 -> 1184 to bank the deletions (main's Windows install-shell + // machinery and tests stay). + ["src-tauri/src/commands/agent_discovery.rs", 1184], // draft-persistence predicate: submit-time `loadDraft` check + inline comment // + deps-array entry in submitMessage closes the never-persisted-boundary // defect (Thufir Pass-3 finding). Load-bearing correctness fix; queued to diff --git a/desktop/src-tauri/src/commands/agent_discovery.rs b/desktop/src-tauri/src/commands/agent_discovery.rs index 32710dad9..814e08ae7 100644 --- a/desktop/src-tauri/src/commands/agent_discovery.rs +++ b/desktop/src-tauri/src/commands/agent_discovery.rs @@ -4,9 +4,9 @@ use tauri::State; use crate::{ app_state::AppState, managed_agents::{ - command_availability, is_npm_global_install, AcpRuntimeCatalogEntry, - DiscoverManagedAgentPrereqsRequest, InstallRuntimeResult, InstallStepResult, - ManagedAgentPrereqsInfo, RelayAgentInfo, DEFAULT_ACP_COMMAND, + command_availability, AcpRuntimeCatalogEntry, DiscoverManagedAgentPrereqsRequest, + InstallRuntimeResult, InstallStepResult, ManagedAgentPrereqsInfo, RelayAgentInfo, + DEFAULT_ACP_COMMAND, }, nostr_convert, relay::query_relay, @@ -55,8 +55,8 @@ pub(crate) fn plan_adapter_install<'c>( /// Returns `None` when there is nothing to verify (`commands` is empty) or any /// adapter command resolves. Otherwise returns a failed synthetic "verify" /// step whose hint points at reinstalling Buzz when the adapter is bundled -/// (`bundled`, i.e. the catalog carries no install commands) or at the install -/// step output otherwise. +/// (`bundled`, see [`runtime_adapter_is_bundled`]) or at the install step +/// output otherwise. fn adapter_verification_step( commands: &[&str], label: &str, @@ -74,7 +74,7 @@ fn adapter_verification_step( } else { format!( "The {label} ACP adapter still could not be found after the install steps completed. \ - Check the step output above and your npm global prefix." + Check the step output above." ) }; Some(InstallStepResult { @@ -88,6 +88,14 @@ fn adapter_verification_step( }) } +/// A runtime's adapter ships with the Buzz desktop app when its catalog entry +/// carries no install commands at all — neither CLI nor adapter. Goose has a +/// curl CLI installer (and its CLI *is* its adapter), so a failed goose verify +/// must not claim the adapter is bundled and point at reinstalling Buzz. +fn runtime_adapter_is_bundled(runtime: &crate::managed_agents::KnownAcpRuntime) -> bool { + runtime.cli_install_commands.is_empty() && runtime.adapter_install_commands.is_empty() +} + #[tauri::command] pub async fn discover_acp_providers() -> Result, String> { tokio::task::spawn_blocking(|| { @@ -184,11 +192,6 @@ fn install_acp_runtime_blocking(runtime_id: &str) -> Result Result Result usize { index } -// ── npm EACCES preflight ────────────────────────────────────────────────────── - -/// Guidance text for the EACCES / unwritable-prefix case. -fn npm_eacces_guidance(command: &str) -> String { - format!( - "npm's global install directory isn't writable by your user.\n\ -\n\ -Fix (no sudo):\n\ - 1. Run: npm config set prefix ~/.npm-global\n\ - 2. Add to ~/.zprofile: export PATH=\"$HOME/.npm-global/bin:$PATH\"\n\ - 3. Restart Buzz, then click Install again.\n\ -\n\ -Or install manually, then click Refresh:\n\ - sudo {command}" - ) -} - -/// Guidance text shown when npm / Node.js is not found in the login-shell PATH. -const NPM_MISSING_HINT: &str = "Node.js / npm was not found. Install Node.js \ -(https://nodejs.org or your version manager), restart Buzz, then click Install again.\n\ -If npm works in your terminal, make sure your Node version manager is initialized in \ -~/.zprofile (not only ~/.zshrc) — Buzz resolves tools via non-interactive login shells."; - -/// Result of probing `npm prefix -g` in the hermit-stripped login shell. -#[cfg(unix)] -enum NpmPrefix { - /// npm responded with a parseable prefix path. - Found(std::path::PathBuf), - /// npm was not found, the spawn failed, the command returned a non-zero - /// exit, or the output could not be parsed. - Unavailable, - /// The probe exceeded the 30-second deadline (e.g. a version-manager init - /// that blocks on `/dev/tty`). The install should proceed so the stderr - /// classifier remains the backstop. - TimedOut, -} - -/// Spawn the same login shell used by `run_install_command` and run -/// `npm prefix -g` to discover where npm would install global packages. -#[cfg(unix)] -fn resolve_npm_prefix() -> NpmPrefix { - let mut cmd = match install_shell_command("npm prefix -g") { - Ok(cmd) => cmd, - Err(_) => return NpmPrefix::Unavailable, - }; - let mut child = match cmd - .stdin(std::process::Stdio::null()) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) - .spawn() - { - Ok(c) => c, - Err(_) => return NpmPrefix::Unavailable, - }; - - // Drain stdout/stderr on background threads to prevent pipe-buffer deadlock. - let stdout_pipe = child.stdout.take(); - let stderr_pipe = child.stderr.take(); - let stdout_thread = std::thread::spawn(move || { - let mut buf = Vec::new(); - if let Some(mut pipe) = stdout_pipe { - let _ = pipe.read_to_end(&mut buf); - } - buf - }); - let stderr_thread = std::thread::spawn(move || { - // Drain stderr so the child doesn't block on a full pipe. - if let Some(mut pipe) = stderr_pipe { - let _ = std::io::copy(&mut pipe, &mut std::io::sink()); - } - }); - - let child_pid = child.id(); - let (tx, rx) = std::sync::mpsc::channel(); - let wait_thread = std::thread::spawn(move || { - let status = child.wait(); - let _ = tx.send(status); - }); - - // 30-second timeout — plenty for `npm prefix -g`; intentionally shorter - // than the 5-minute install budget in `run_install_command`. - let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30); - let raw_bytes: Option> = loop { - let remaining = deadline.saturating_duration_since(std::time::Instant::now()); - if remaining.is_zero() { - // Timed out: send SIGTERM, clean up threads, signal the caller to - // fall through to the install path rather than abort. - unsafe { libc::kill(child_pid as i32, libc::SIGTERM) }; - drop(rx); - let _ = wait_thread.join(); - let _ = stdout_thread.join(); - let _ = stderr_thread.join(); - eprintln!( - "buzz: npm prefix probe timed out after 30s; \ - proceeding to install (stderr classifier is the backstop)" - ); - return NpmPrefix::TimedOut; - } - match rx.recv_timeout(std::time::Duration::from_millis(200).min(remaining)) { - Ok(Ok(status)) => { - let _ = wait_thread.join(); - let stdout = stdout_thread.join().unwrap_or_default(); - let _ = stderr_thread.join(); - break if status.success() { Some(stdout) } else { None }; - } - Ok(Err(_)) | Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => { - let _ = wait_thread.join(); - let _ = stdout_thread.join(); - let _ = stderr_thread.join(); - break None; - } - Err(std::sync::mpsc::RecvTimeoutError::Timeout) => continue, - } - }; - - let bytes = match raw_bytes { - Some(b) => b, - None => return NpmPrefix::Unavailable, - }; - let raw = String::from_utf8_lossy(&bytes).into_owned(); - // Version managers can print banner lines before the real prefix — take the - // last non-empty line to skip any preamble. - let prefix = match raw.lines().rfind(|l| !l.trim().is_empty()) { - Some(l) => l.trim().to_string(), - None => return NpmPrefix::Unavailable, - }; - if prefix.is_empty() { - return NpmPrefix::Unavailable; - } - NpmPrefix::Found(std::path::PathBuf::from(prefix)) -} - -/// Check write access to a file-system path using the POSIX `access(2)` syscall. -#[cfg(unix)] -fn unix_is_writable(path: &std::path::Path) -> bool { - use std::os::unix::ffi::OsStrExt; - let bytes = path.as_os_str().as_bytes(); - let Ok(c_path) = std::ffi::CString::new(bytes) else { - return false; - }; - // SAFETY: `access` is a pure read-only syscall; we pass a valid NUL-terminated - // path and a standard flag constant. This mirrors the existing `setsid`/`kill` - // usage in this file. - unsafe { libc::access(c_path.as_ptr(), libc::W_OK) == 0 } -} - -/// Returns true when the directory where npm would write global packages is -/// writable by the current process user. -/// -/// On non-unix platforms always returns `true` — the EACCES preflight is a -/// no-op there; the stderr classifier still applies. -fn npm_install_target_is_writable(prefix: &std::path::Path) -> bool { - #[cfg(unix)] - { - // Probe the most specific candidate that exists; fall back up the tree. - for candidate in &[ - prefix.join("lib/node_modules"), - prefix.join("lib"), - prefix.to_path_buf(), - ] { - if candidate.exists() { - return unix_is_writable(candidate); - } - } - // Nothing exists — npm couldn't create it either. - unix_is_writable(prefix) - } - #[cfg(not(unix))] - { - let _ = prefix; - true - } -} - -/// Inspect `stderr` for known npm EACCES patterns and return actionable -/// guidance if matched, or `None` when the error is unrelated. -fn npm_eacces_hint(stderr: &str, command: &str) -> Option { - if stderr.contains("EACCES: permission denied") || stderr.contains("npm error EACCES") { - Some(npm_eacces_guidance(command)) - } else { - None - } -} - -/// Run the npm preflight before executing an npm global install command. -/// Returns `Some(failed InstallStepResult)` to abort, or `None` to proceed. -fn npm_preflight_check(step: &str, command: &str) -> Option { - #[cfg(unix)] - { - match resolve_npm_prefix() { - NpmPrefix::Unavailable => Some(InstallStepResult { - step: step.to_string(), - command: command.to_string(), - success: false, - stdout: String::new(), - stderr: String::new(), - exit_code: None, - hint: Some(NPM_MISSING_HINT.to_string()), - }), - NpmPrefix::Found(prefix) if !npm_install_target_is_writable(&prefix) => { - Some(InstallStepResult { - step: step.to_string(), - command: command.to_string(), - success: false, - stdout: String::new(), - stderr: format!( - "npm global prefix '{}' is not writable by the current user.", - prefix.display() - ), - exit_code: None, - hint: Some(npm_eacces_guidance(command)), - }) - } - // `Found` + writable, or `TimedOut` — proceed; let the install run and - // the stderr classifier serve as the backstop. - _ => None, - } - } - #[cfg(not(unix))] - { - let _ = (step, command); - None - } -} - -// ── end npm preflight ───────────────────────────────────────────────────────── - #[tauri::command] pub async fn discover_managed_agent_prereqs( input: DiscoverManagedAgentPrereqsRequest, @@ -1123,124 +883,6 @@ pub async fn list_relay_agents(state: State<'_, AppState>) -> Result Option { pub fn clear_resolve_cache() { let mut guard = resolve_cache().lock().unwrap_or_else(|e| e.into_inner()); guard.clear(); - // Also invalidate the adapter-availability cache so a freshly-installed - // adapter is reflected the next time the summary builder checks the badge. - clear_adapter_availability_cache(); -} - -// ── Adapter availability cache (Phase-2 badge fallback) ───────────────────── -// -// `build_managed_agent_summary` needs to compare the spawn-time adapter -// availability against the *current* availability without re-running command -// resolution on every poll cycle. This cache stores the last availability -// status of the codex-acp binary at its resolved path. It is warmed by -// `discover_acp_runtimes` (which already resolves), so the badge path reads -// warm data, and is invalidated by `clear_resolve_cache` -// (called on every Doctor install and every `discover_acp_providers` call). - -fn adapter_availability_cache() -> &'static std::sync::Mutex> { - use std::sync::{Mutex, OnceLock}; - static CACHE: OnceLock>> = OnceLock::new(); - CACHE.get_or_init(|| Mutex::new(None)) -} - -fn clear_adapter_availability_cache() { - if let Ok(mut guard) = adapter_availability_cache().lock() { - *guard = None; - } -} - -/// Cache the current codex-acp adapter availability status. -/// -/// Called by `discover_acp_runtimes` after it probes the codex adapter so the -/// badge path has a warm value without re-probing. -pub(crate) fn cache_adapter_availability(status: AcpAvailabilityStatus) { - if let Ok(mut guard) = adapter_availability_cache().lock() { - *guard = Some(status); - } -} - -/// Return the most recently cached codex-acp adapter availability, or -/// `None` if no discovery has run yet. -/// -/// This is a **read from cache only** — it never spawns a subprocess. The -/// value is populated by `discover_acp_runtimes` and invalidated by -/// `clear_resolve_cache`. When the cache is cold, returning `None` defers -/// the drift check until discovery has produced a real value, preventing -/// a fabricated `AdapterMissing` stamp from triggering a false restart badge -/// on a newly restarted process. -pub(crate) fn adapter_availability_cached() -> Option { - adapter_availability_cache() - .lock() - .ok() - .and_then(|g| g.clone()) -} - -/// Pure predicate: does the stamped adapter availability differ from the -/// current cached availability? -/// -/// Returns `false` whenever either side is `None` (unknown) — "no data" is -/// not evidence of drift. This is extracted for unit testing without global -/// state and used by `build_managed_agent_summary`. -pub(crate) fn availability_drift( - stamped: Option<&AcpAvailabilityStatus>, - current: Option, -) -> bool { - match (stamped, current) { - (Some(s), Some(c)) => *s != c, - _ => false, - } } /// Return all candidate basenames for `command` on the current platform. @@ -943,24 +876,6 @@ pub(crate) fn find_command(command: &str) -> Option { resolve_command(command) } -/// Returns true when the runtime has at least one adapter install step that -/// is an npm global install. Used to determine whether Node.js is required. -fn runtime_needs_npm(runtime: &KnownAcpRuntime) -> bool { - runtime - .adapter_install_commands - .iter() - .any(|cmd| is_npm_global_install(cmd)) -} - -/// Returns `true` when `cmd` is an `npm install -g` invocation. -/// -/// Used by Doctor to determine whether Node.js is required before running an -/// install step, and by the npm EACCES preflight in the install command path. -pub(crate) fn is_npm_global_install(cmd: &str) -> bool { - let t = cmd.trim_start(); - t.starts_with("npm install -g ") || t.starts_with("npm i -g ") -} - /// Resolve the binary for a CLI auth probe (`claude`, `codex`): the pinned /// CLI vendored inside the bundled bridge wins — it is the exact binary agent /// sessions run and reads the same credential store — falling back to the @@ -1136,13 +1051,6 @@ pub fn discover_acp_runtimes() -> Vec { let (availability, command, binary_path) = classify_runtime(adapter_result, runtime.underlying_cli, underlying_cli_found); - // Warm the adapter-availability cache for the badge fallback. - // The cache is scoped to the codex runtime; other runtimes leave it - // unchanged. Invalidated by `clear_resolve_cache`. - if runtime.id == "codex" { - cache_adapter_availability(availability.clone()); - } - let underlying_cli_path = runtime .underlying_cli .and_then(find_command) @@ -1172,14 +1080,6 @@ pub fn discover_acp_runtimes() -> Vec { } }; - // node_required: an npm adapter step is pending AND node/npm are absent. - let node_required = matches!( - availability, - AcpAvailabilityStatus::AdapterMissing | AcpAvailabilityStatus::NotInstalled - ) && runtime_needs_npm(runtime) - && resolve_command("npm").is_none() - && resolve_command("node").is_none(); - PartialEntry { runtime, entry: AcpRuntimeCatalogEntry { @@ -1196,7 +1096,6 @@ pub fn discover_acp_runtimes() -> Vec { install_instructions_url: runtime.install_instructions_url.to_string(), can_auto_install, underlying_cli_path, - node_required, // Filled in by the probe phase below. auth_status: AuthStatus::Unknown, login_hint: None, diff --git a/desktop/src-tauri/src/managed_agents/process_lifecycle.rs b/desktop/src-tauri/src/managed_agents/process_lifecycle.rs index 1ef3eae01..473861ba6 100644 --- a/desktop/src-tauri/src/managed_agents/process_lifecycle.rs +++ b/desktop/src-tauri/src/managed_agents/process_lifecycle.rs @@ -135,7 +135,6 @@ pub fn finish_spawn( log_path: std::path::PathBuf, spawn_config_hash: u64, setup_mode: bool, - adapter_availability: Option, agent_name: &str, ) -> super::ManagedAgentProcess { let job = create_job_for_child(child.id()); @@ -150,7 +149,6 @@ pub fn finish_spawn( log_path, spawn_config_hash, setup_mode, - adapter_availability, job, } } diff --git a/desktop/src-tauri/src/managed_agents/runtime.rs b/desktop/src-tauri/src/managed_agents/runtime.rs index a61987392..bc014889b 100644 --- a/desktop/src-tauri/src/managed_agents/runtime.rs +++ b/desktop/src-tauri/src/managed_agents/runtime.rs @@ -1321,31 +1321,20 @@ pub fn build_managed_agent_summary( // at launch; recompute from current disk state and flag drift. Only a // tracked live process can drift — stopped agents spawn fresh, and // adopted (runtime_pid-only) processes have no stamped hash to compare. - // - // Additionally, for runtimes with an adapter version gate (codex only), - // check whether the cached adapter availability has drifted from the value - // stamped at spawn. This catches out-of-band adapter changes (manual - // npm install/downgrade) that Phase-1 auto-restart doesn't cover. The - // cache is read-only here — no subprocess is spawned. let needs_restart = runtimes.get(&record.pubkey).is_some_and(|runtime| { use tauri::Manager; let state = app.state::(); let global_for_hash = crate::managed_agents::load_global_agent_config(app).unwrap_or_default(); let teams_for_hash = crate::managed_agents::load_teams(app).unwrap_or_default(); - let hash_drift = runtime.spawn_config_hash + runtime.spawn_config_hash != crate::managed_agents::spawn_hash::spawn_config_hash( record, personas, &teams_for_hash, &crate::relay::relay_ws_url_with_override(&state), &global_for_hash, - ); - let availability_drift = super::availability_drift( - runtime.adapter_availability.as_ref(), - super::adapter_availability_cached(), - ); - hash_drift || availability_drift + ) }); // Resolve the effective harness the same way, then derive args/mcp from it, @@ -1891,20 +1880,6 @@ pub fn spawn_agent_child( &global, ); - // Stamp the adapter availability for runtimes with a version gate (codex - // only). The summary builder compares this against the current cached value - // to detect out-of-band adapter changes after spawn (Phase-2 badge fallback). - // Non-codex runtimes get `None` — nothing changes for them. - // When the cache is cold (e.g. Doctor just installed and cleared the cache), - // `adapter_availability_cached()` returns `None`, so the stamp is `None` and - // the drift check is skipped until discovery warms the cache — preventing a - // false restart badge immediately after auto-restart. - let spawned_adapter_availability = if runtime_meta.is_some_and(|r| r.id == "codex") { - super::adapter_availability_cached() - } else { - None - }; - let _ = super::write_agent_pid_file(app, &record.pubkey, child.id()); // Windows: assign the harness to a Job Object so its whole tree dies with @@ -1915,7 +1890,6 @@ pub fn spawn_agent_child( log_path, spawn_config_hash, spawned_setup_mode, - spawned_adapter_availability, &record.name, )); #[cfg(not(windows))] @@ -1924,7 +1898,6 @@ pub fn spawn_agent_child( log_path, spawn_config_hash, setup_mode: spawned_setup_mode, - adapter_availability: spawned_adapter_availability, }) } diff --git a/desktop/src-tauri/src/managed_agents/types.rs b/desktop/src-tauri/src/managed_agents/types.rs index 45333a2a8..e6c42823f 100644 --- a/desktop/src-tauri/src/managed_agents/types.rs +++ b/desktop/src-tauri/src/managed_agents/types.rs @@ -435,12 +435,6 @@ pub struct ManagedAgentProcess { /// `install_acp_runtime` to target only stuck agents for auto-restart, /// excluding healthy in-pool agents. pub setup_mode: bool, - /// Adapter availability status stamped at spawn time for runtimes with a - /// version gate (currently codex only; `None` for all others). Runtime-only - /// — never persisted. The summary builder compares this against the current - /// cached availability and sets `needs_restart` on drift, catching out-of- - /// band adapter changes that Phase-1 auto-restart doesn't cover. - pub adapter_availability: Option, /// Win32 Job Object owning the harness + its entire process tree. Closing /// the handle (via `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`) kills the whole /// tree — the Windows mirror of the Unix process-group teardown. `None` @@ -581,9 +575,6 @@ pub struct AcpRuntimeCatalogEntry { /// true when at least one automated install step is available pub can_auto_install: bool, pub underlying_cli_path: Option, - /// true when an npm adapter step is pending but Node.js / npm is absent. - /// The UI hides the Install button and shows a Node.js install callout. - pub node_required: bool, /// Login/authentication status for CLI-based runtimes. pub auth_status: AuthStatus, /// Hint for completing authentication, shown when `auth_status` is not `logged_in`. diff --git a/desktop/src/features/onboarding/ui/agentReadiness.test.mjs b/desktop/src/features/onboarding/ui/agentReadiness.test.mjs index 66b11b6be..33eb195dd 100644 --- a/desktop/src/features/onboarding/ui/agentReadiness.test.mjs +++ b/desktop/src/features/onboarding/ui/agentReadiness.test.mjs @@ -19,7 +19,6 @@ function makeRuntime(overrides = {}) { installInstructionsUrl: "https://example.com", canAutoInstall: false, underlyingCliPath: null, - nodeRequired: false, loginHint: null, ...overrides, }; diff --git a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx index 2649b4520..7e78409b7 100644 --- a/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx +++ b/desktop/src/features/settings/ui/DoctorSettingsPanel.tsx @@ -83,7 +83,7 @@ function InstallActions({ onInstall: () => void; runtime: AcpRuntimeCatalogEntry; }) { - const showInstall = runtime.canAutoInstall && !runtime.nodeRequired; + const showInstall = runtime.canAutoInstall; return (
@@ -117,46 +117,6 @@ function InstallActions({ ); } -/** - * Node.js callout when required, or the install actions when it is not. - * Used for both `adapter_missing` and `not_installed` availability states. - */ -function NodeRequiredOrInstall({ - hasError, - isInstalling, - onInstall, - runtime, -}: { - hasError: boolean; - isInstalling: boolean; - onInstall: () => void; - runtime: AcpRuntimeCatalogEntry; -}) { - if (runtime.nodeRequired) { - return ( -

- Node.js is required to install this adapter.{" "} - - , then click Re-run. -

- ); - } - return ( - - ); -} - function RuntimeRow({ installError, installSuccess, @@ -262,7 +222,7 @@ function RuntimeRow({

{runtime.installHint}

- {runtime.installHint}

- { await row.screenshot({ path: `${SHOTS}/03-auth-config-error.png` }); }); - /** - * 04 — adapter_missing runtime with node_required: true: the amber "Node.js - * is required…" callout replaces the Install button so the user cannot - * inadvertently trigger a doomed npm install. + /* + * 04-node-required retired with the per-row `node_required` install gate: + * no runtime carries npm adapter install commands anymore, so the amber + * "Node.js is required…" callout has no trigger. The Node.js requirement + * for the bundled bridges is covered by 07-node-runtime-warn below. */ - test("04-node-required", async ({ page }) => { - await installMockBridge(page, { - acpRuntimesCatalog: [ - GOOSE_AVAILABLE, - CLAUDE_AVAILABLE_LOGGED_IN, - { - ...CODEX_NOT_INSTALLED, - availability: "adapter_missing", - underlying_cli_path: "/usr/local/bin/codex", - node_required: true, - install_hint: - "Install the Codex ACP adapter: npm install -g @zed-industries/codex-acp", - }, - BUZZ_AGENT_AVAILABLE, - ], - }); - - await page.goto("/", { waitUntil: "domcontentloaded" }); - await openSettings(page, "doctor"); - - const row = page.getByTestId("doctor-runtime-codex"); - await expect(row).toBeVisible({ timeout: 10_000 }); - await expect(row).toContainText("Node.js is required"); - // Exact-name match so "Install Node.js" (inside the callout) is not counted. - await expect( - row.getByRole("button", { name: "Install", exact: true }), - ).toHaveCount(0); - - await row.scrollIntoViewIfNeeded(); - await waitForAnimations(page); - await row.screenshot({ path: `${SHOTS}/04-node-required.png` }); - }); /** * 05 — a failed install renders a "Retry" button; clicking Retry succeeds. @@ -261,7 +226,6 @@ test.describe("Doctor panel state screenshots", () => { { ...CODEX_NOT_INSTALLED, can_auto_install: true, - node_required: false, }, BUZZ_AGENT_AVAILABLE, ],