diff --git a/cmux-tui/crates/cmux-tui/src/plugin_manager.rs b/cmux-tui/crates/cmux-tui/src/plugin_manager.rs index 8c9c584aec74..19febe0915ee 100644 --- a/cmux-tui/crates/cmux-tui/src/plugin_manager.rs +++ b/cmux-tui/crates/cmux-tui/src/plugin_manager.rs @@ -55,6 +55,15 @@ impl PluginKind { Self::Agent => "agent_plugin_", } } + + /// Resource noun named by `validation.invalid` errors about the selected + /// plugin, matching the `sidebar_plugin.*` operations in the resource spec. + fn resource_field(self) -> &'static str { + match self { + Self::Sidebar => "sidebar_plugin", + Self::Agent => "agent_plugin", + } + } } /// A userland plugin build must not hold the CLI forever. @@ -353,12 +362,13 @@ fn update_command( let mut plugin = resolve_installed_plugin(&positionals[1], kind)?; let source = git_text(&plugin.dir, ["remote", "get-url", "origin"]).ok_or_else(|| { ManagerError::validation( - Some("plugin"), + Some(kind.resource_field()), format!("plugin {} has no readable origin remote", plugin.name), ) })?; - validate_git_source(&source) - .map_err(|error| ManagerError::validation(Some("plugin"), error.to_string()))?; + validate_git_source(&source).map_err(|error| { + ManagerError::validation(Some(kind.resource_field()), error.to_string()) + })?; // Build and validate a fresh clone before touching the active install. // Updating in place would let a failed pull or build leave the selected @@ -630,18 +640,20 @@ fn resolve_installed_plugin( let selector = forced_name.unwrap_or(selector); let by_id = forced_name.is_none() && selector.starts_with(kind.id_prefix()); if by_id { - validate_plugin_id_for(selector, kind) - .map_err(|error| ManagerError::validation(Some("plugin"), error.to_string()))?; + validate_plugin_id_for(selector, kind).map_err(|error| { + ManagerError::validation(Some(kind.resource_field()), error.to_string()) + })?; } else { - validate_plugin_name(selector) - .map_err(|error| ManagerError::validation(Some("plugin"), error.to_string()))?; + validate_plugin_name(selector).map_err(|error| { + ManagerError::validation(Some(kind.resource_field()), error.to_string()) + })?; } installed_plugins(kind)? .into_iter() .find(|plugin| if by_id { plugin.id == selector } else { plugin.name == selector }) .ok_or_else(|| { ManagerError::validation( - Some("plugin"), + Some(kind.resource_field()), format!("plugin {selector:?} is not installed"), ) }) diff --git a/cmux-tui/crates/cmux-tui/tests/resource_cli_v2.rs b/cmux-tui/crates/cmux-tui/tests/resource_cli_v2.rs index 46e13976cebe..67e1f6b14218 100644 --- a/cmux-tui/crates/cmux-tui/tests/resource_cli_v2.rs +++ b/cmux-tui/crates/cmux-tui/tests/resource_cli_v2.rs @@ -826,6 +826,21 @@ fn local_plugin_jsonl_never_connects_to_the_session_socket() { assert_eq!(invalid["retryable"], false); assert_eq!(invalid["details"]["field"], "sidebar_plugin"); assert!(invalid["details"]["reason"].is_string()); + + // Agent plugins share the manager but name their own resource noun. + let invalid_agent = Command::new(bin()) + .args(["--json", "--socket"]) + .arg(dir.join("missing.sock")) + .args(["agent", "plugin", "use", "Bad"]) + .env("XDG_DATA_HOME", &data) + .env("XDG_CONFIG_HOME", &config) + .env_remove("CMUX_TUI_SOCKET") + .output() + .unwrap(); + assert_eq!(invalid_agent.status.code(), Some(1)); + let invalid_agent = parse_single_json(&invalid_agent.stderr); + assert_eq!(invalid_agent["code"], "validation.invalid"); + assert_eq!(invalid_agent["details"]["field"], "agent_plugin"); fs::remove_dir_all(dir).unwrap(); }