mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
fix(core/agents): skip missing agent CLIs by default (fixes #512)
This commit is contained in:
@@ -479,6 +479,7 @@ impl Stream for ResponseStream {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::model_family::find_family_for_model;
|
||||
use code_apply_patch::APPLY_PATCH_TOOL_INSTRUCTIONS;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::agent_defaults::agent_model_spec;
|
||||
use crate::agent_defaults::enabled_agent_model_specs;
|
||||
use crate::config_types::AgentConfig;
|
||||
use crate::config_types::SubagentCommandConfig;
|
||||
@@ -7,11 +8,68 @@ use crate::config_types::SubagentCommandConfig;
|
||||
// the user documentation in `docs/slash-commands.md` so the list stays in sync
|
||||
// with the UI and behavior.
|
||||
|
||||
fn command_exists(cmd: &str) -> bool {
|
||||
if cmd.contains(std::path::MAIN_SEPARATOR) || cmd.contains('/') || cmd.contains('\\') {
|
||||
return std::fs::metadata(cmd).map(|m| m.is_file()).unwrap_or(false);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
which::which(cmd).map(|p| p.is_file()).unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let Some(path_os) = std::env::var_os("PATH") else {
|
||||
return false;
|
||||
};
|
||||
for dir in std::env::split_paths(&path_os) {
|
||||
if dir.as_os_str().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let candidate = dir.join(cmd);
|
||||
if let Ok(meta) = std::fs::metadata(&candidate) {
|
||||
if meta.is_file() && (meta.permissions().mode() & 0o111 != 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the list of enabled agent names from the configuration
|
||||
pub fn get_enabled_agents(agents: &[AgentConfig]) -> Vec<String> {
|
||||
fn command_for_check(command: &str) -> &str {
|
||||
command.split_whitespace().next().unwrap_or(command)
|
||||
}
|
||||
|
||||
fn agent_is_runnable(agent: &AgentConfig) -> bool {
|
||||
let spec = agent_model_spec(&agent.name).or_else(|| agent_model_spec(&agent.command));
|
||||
if let Some(spec) = spec {
|
||||
if matches!(spec.family, "code" | "codex" | "cloud") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
let cmd = agent.command.trim();
|
||||
let cmd = if cmd.is_empty() { agent.name.trim() } else { cmd };
|
||||
let cmd = command_for_check(cmd);
|
||||
if !cmd.is_empty() && command_exists(cmd) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(spec) = spec {
|
||||
return command_exists(spec.cli);
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
agents
|
||||
.iter()
|
||||
.filter(|agent| agent.enabled)
|
||||
.filter(|agent| agent.enabled && agent_is_runnable(agent))
|
||||
.map(|agent| agent.name.clone())
|
||||
.collect()
|
||||
}
|
||||
@@ -20,6 +78,9 @@ pub fn get_enabled_agents(agents: &[AgentConfig]) -> Vec<String> {
|
||||
fn get_default_models() -> Vec<String> {
|
||||
enabled_agent_model_specs()
|
||||
.into_iter()
|
||||
.filter(|spec| {
|
||||
matches!(spec.family, "code" | "codex" | "cloud") || command_exists(spec.cli)
|
||||
})
|
||||
.map(|spec| spec.slug.to_string())
|
||||
.collect()
|
||||
}
|
||||
@@ -233,6 +294,30 @@ pub fn handle_slash_command(input: &str, agents: Option<&[AgentConfig]>) -> Opti
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_models_skip_missing_agent_clis() {
|
||||
let orig_path = std::env::var_os("PATH");
|
||||
unsafe {
|
||||
std::env::set_var("PATH", "");
|
||||
}
|
||||
|
||||
let defaults = get_default_models();
|
||||
assert!(defaults.iter().any(|v| v == "code-gpt-5.2"));
|
||||
assert!(!defaults.iter().any(|v| v == "qwen-3-coder"));
|
||||
assert!(!defaults.iter().any(|v| v == "gemini-3-flash"));
|
||||
assert!(!defaults.iter().any(|v| v == "claude-sonnet-4.5"));
|
||||
|
||||
if let Some(path) = orig_path {
|
||||
unsafe {
|
||||
std::env::set_var("PATH", path);
|
||||
}
|
||||
} else {
|
||||
unsafe {
|
||||
std::env::remove_var("PATH");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slash_command_parsing() {
|
||||
// Test /plan command
|
||||
|
||||
@@ -21011,7 +21011,7 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
let build_editor = || {
|
||||
AgentEditorView::new(
|
||||
name.clone(),
|
||||
true,
|
||||
builtin,
|
||||
if ro.is_empty() { None } else { Some(ro.clone()) },
|
||||
if wr.is_empty() { None } else { Some(wr.clone()) },
|
||||
None,
|
||||
@@ -23484,7 +23484,7 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
};
|
||||
agent_rows.push(AgentOverviewRow {
|
||||
name: cfg.name.clone(),
|
||||
enabled: cfg.enabled,
|
||||
enabled: cfg.enabled && installed,
|
||||
installed,
|
||||
description: Self::agent_description_for(
|
||||
&cfg.name,
|
||||
@@ -23509,7 +23509,7 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
};
|
||||
agent_rows.push(AgentOverviewRow {
|
||||
name: cfg.name.clone(),
|
||||
enabled: cfg.enabled,
|
||||
enabled: cfg.enabled && installed,
|
||||
installed,
|
||||
description: Self::agent_description_for(
|
||||
&cfg.name,
|
||||
@@ -23530,7 +23530,7 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
};
|
||||
agent_rows.push(AgentOverviewRow {
|
||||
name: name.clone(),
|
||||
enabled: true,
|
||||
enabled: installed,
|
||||
installed,
|
||||
description: Self::agent_description_for(name, Some(&cmd), None),
|
||||
});
|
||||
@@ -29521,6 +29521,42 @@ use code_core::protocol::OrderMeta;
|
||||
assert!(chat.processed_auto_review_agents.contains("agent-blank"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_agent_clis_start_disabled_in_overview() {
|
||||
let orig_path = std::env::var_os("PATH");
|
||||
unsafe {
|
||||
std::env::set_var("PATH", "");
|
||||
}
|
||||
|
||||
let mut harness = ChatWidgetHarness::new();
|
||||
let chat = harness.chat();
|
||||
|
||||
let (rows, _commands) = chat.collect_agents_overview_rows();
|
||||
let qwen = rows
|
||||
.iter()
|
||||
.find(|row| row.name == "qwen-3-coder")
|
||||
.expect("qwen row present");
|
||||
assert!(!qwen.installed);
|
||||
assert!(!qwen.enabled);
|
||||
|
||||
let code = rows
|
||||
.iter()
|
||||
.find(|row| row.name == "code-gpt-5.2")
|
||||
.expect("code row present");
|
||||
assert!(code.installed);
|
||||
assert!(code.enabled);
|
||||
|
||||
if let Some(path) = orig_path {
|
||||
unsafe {
|
||||
std::env::set_var("PATH", path);
|
||||
}
|
||||
} else {
|
||||
unsafe {
|
||||
std::env::remove_var("PATH");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skipped_auto_review_with_findings_defers_to_next_turn() {
|
||||
let _rt = enter_test_runtime_guard();
|
||||
|
||||
Reference in New Issue
Block a user