mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
fix(agents): enable external copilot command
This commit is contained in:
@@ -19,6 +19,7 @@ use tokio::time::Duration as TokioDuration;
|
||||
use std::thread;
|
||||
|
||||
use crate::agent_defaults::{agent_model_spec, default_params_for};
|
||||
use shlex::split as shlex_split;
|
||||
use crate::config_types::AgentConfig;
|
||||
use crate::openai_tools::JsonSchema;
|
||||
use crate::openai_tools::OpenAiTool;
|
||||
@@ -702,12 +703,19 @@ async fn execute_model_with_permissions(
|
||||
model.to_lowercase()
|
||||
};
|
||||
|
||||
let (command_base, command_extra_args) = split_command_and_args(&command);
|
||||
let command_for_spawn = if command_base.is_empty() {
|
||||
command.clone()
|
||||
} else {
|
||||
command_base.clone()
|
||||
};
|
||||
|
||||
// Special case: for the built‑in Codex agent, prefer invoking the currently
|
||||
// running executable with the `exec` subcommand rather than relying on a
|
||||
// `codex` binary to be present on PATH. This improves portability,
|
||||
// especially on Windows where global shims may be missing.
|
||||
let model_lower = model.to_lowercase();
|
||||
let command_lower = command.to_ascii_lowercase();
|
||||
let command_lower = command_for_spawn.to_ascii_lowercase();
|
||||
fn is_known_family(s: &str) -> bool {
|
||||
matches!(s, "claude" | "gemini" | "qwen" | "codex" | "code" | "cloud")
|
||||
}
|
||||
@@ -728,7 +736,7 @@ async fn execute_model_with_permissions(
|
||||
|
||||
if matches!(family, "code" | "codex" | "cloud") {
|
||||
if config.is_none() {
|
||||
if !command_exists(&command) {
|
||||
if !command_exists(&command_for_spawn) {
|
||||
use_current_exe = true;
|
||||
}
|
||||
} else if let Some(ref cfg) = config {
|
||||
@@ -744,7 +752,7 @@ async fn execute_model_with_permissions(
|
||||
Err(e) => return Err(format!("Failed to resolve current executable: {}", e)),
|
||||
}
|
||||
} else {
|
||||
Command::new(command.clone())
|
||||
Command::new(command_for_spawn.clone())
|
||||
};
|
||||
|
||||
// Set working directory if provided
|
||||
@@ -761,7 +769,7 @@ async fn execute_model_with_permissions(
|
||||
}
|
||||
}
|
||||
|
||||
let mut final_args: Vec<String> = Vec::new();
|
||||
let mut final_args: Vec<String> = command_extra_args;
|
||||
|
||||
if let Some(ref cfg) = config {
|
||||
if read_only {
|
||||
@@ -828,14 +836,17 @@ async fn execute_model_with_permissions(
|
||||
final_args.extend(spec_model_args.iter().cloned());
|
||||
final_args.push(prompt.to_string());
|
||||
}
|
||||
_ => { return Err(format!("Unknown model: {}", model)); }
|
||||
_ => {
|
||||
final_args.extend(spec_model_args.iter().cloned());
|
||||
final_args.push(prompt.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Proactively check for presence of external command before spawn when not
|
||||
// using the current executable fallback. This avoids confusing OS errors
|
||||
// like "program not found" and lets us surface a cleaner message.
|
||||
if !(family == "codex" || family == "code" || (family == "cloud" && config.is_none()))
|
||||
&& !command_exists(&command)
|
||||
&& !command_exists(&command_for_spawn)
|
||||
{
|
||||
return Err(format!("Required agent '{}' is not installed or not in PATH", command));
|
||||
}
|
||||
@@ -913,7 +924,7 @@ async fn execute_model_with_permissions(
|
||||
std::env::current_exe().map_err(|e| format!("Failed to resolve current executable: {}", e))?
|
||||
} else {
|
||||
// Use program name; PATH resolution will be handled by spawn helper with provided env.
|
||||
std::path::PathBuf::from(&command)
|
||||
std::path::PathBuf::from(&command_for_spawn)
|
||||
};
|
||||
let args = final_args.clone();
|
||||
|
||||
@@ -1014,6 +1025,26 @@ fn strip_model_flags(args: &mut Vec<String>) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn split_command_and_args(command: &str) -> (String, Vec<String>) {
|
||||
let trimmed = command.trim();
|
||||
if trimmed.is_empty() {
|
||||
return (String::new(), Vec::new());
|
||||
}
|
||||
if let Some(tokens) = shlex_split(trimmed) {
|
||||
if let Some((first, rest)) = tokens.split_first() {
|
||||
return (first.clone(), rest.to_vec());
|
||||
}
|
||||
}
|
||||
|
||||
let tokens: Vec<String> = trimmed.split_whitespace().map(|s| s.to_string()).collect();
|
||||
if tokens.is_empty() {
|
||||
(String::new(), Vec::new())
|
||||
} else {
|
||||
let head = tokens[0].clone();
|
||||
(head, tokens[1..].to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
const AGENT_SMOKE_TEST_PROMPT: &str = "Reply only with the string \"ok\". Do not include any other words.";
|
||||
const AGENT_SMOKE_TEST_EXPECTED: &str = "ok";
|
||||
const AGENT_SMOKE_TEST_TIMEOUT: TokioDuration = TokioDuration::from_secs(20);
|
||||
|
||||
@@ -63,6 +63,7 @@ use crate::EnvironmentContextEmission;
|
||||
use crate::AuthManager;
|
||||
use crate::CodexAuth;
|
||||
use crate::agent_tool::AgentStatusUpdatePayload;
|
||||
use crate::split_command_and_args;
|
||||
use crate::git_worktree;
|
||||
use crate::protocol::ApprovedCommandMatchKind;
|
||||
use crate::protocol::WebSearchBeginEvent;
|
||||
@@ -8309,7 +8310,14 @@ pub(crate) async fn handle_run_agent(
|
||||
|
||||
// Helper: derive the command to check for a given model/config pair.
|
||||
fn resolve_command_for_check(model: &str, cfg: Option<&crate::config_types::AgentConfig>) -> (String, bool) {
|
||||
if let Some(c) = cfg { return (c.command.clone(), false); }
|
||||
if let Some(c) = cfg {
|
||||
let (base, _) = split_command_and_args(&c.command);
|
||||
let trimmed = base.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return (trimmed.to_string(), false);
|
||||
}
|
||||
return (c.command.clone(), false);
|
||||
}
|
||||
let m = model.to_lowercase();
|
||||
match m.as_str() {
|
||||
// Built-in: always available via current_exe fallback.
|
||||
|
||||
@@ -116,6 +116,7 @@ pub mod util;
|
||||
pub use apply_patch::CODEX_APPLY_PATCH_ARG1;
|
||||
pub use command_safety::is_safe_command;
|
||||
pub use agent_tool::smoke_test_agent_blocking;
|
||||
pub use agent_tool::split_command_and_args;
|
||||
pub use safety::get_platform_sandbox;
|
||||
pub use housekeeping::run_housekeeping_if_due;
|
||||
pub use housekeeping::CleanupOutcome;
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::app_event_sender::AppEventSender;
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::agent_install_helpers::macos_brew_formula_for_command;
|
||||
|
||||
use super::bottom_pane_view::BottomPaneView;
|
||||
use super::bottom_pane_view::{BottomPaneView, ConditionalUpdate};
|
||||
use super::form_text_field::{FormTextField, InputFilter};
|
||||
use super::BottomPane;
|
||||
|
||||
@@ -118,6 +118,24 @@ impl AgentEditorView {
|
||||
true
|
||||
}
|
||||
|
||||
fn paste_into_field(field: &mut FormTextField, text: &str) -> bool {
|
||||
let before = field.text().len();
|
||||
field.handle_paste(text.to_string());
|
||||
field.text().len() != before
|
||||
}
|
||||
|
||||
fn paste_into_current_field(&mut self, text: &str) -> bool {
|
||||
match self.field {
|
||||
FIELD_NAME => Self::paste_into_field(&mut self.name_field, text),
|
||||
FIELD_COMMAND => Self::paste_into_field(&mut self.command_field, text),
|
||||
FIELD_READ_ONLY => Self::paste_into_field(&mut self.params_ro, text),
|
||||
FIELD_WRITE => Self::paste_into_field(&mut self.params_wr, text),
|
||||
FIELD_DESCRIPTION => Self::paste_into_field(&mut self.description_field, text),
|
||||
FIELD_INSTRUCTIONS => Self::paste_into_field(&mut self.instr, text),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_key_internal(&mut self, key_event: KeyEvent) -> bool {
|
||||
let last_field_idx = FIELD_CANCEL;
|
||||
match key_event {
|
||||
@@ -555,6 +573,14 @@ impl<'a> BottomPaneView<'a> for AgentEditorView {
|
||||
let _ = self.handle_key_internal(key_event);
|
||||
}
|
||||
|
||||
fn handle_paste(&mut self, text: String) -> ConditionalUpdate {
|
||||
if self.paste_into_current_field(&text) {
|
||||
ConditionalUpdate::NeedsRedraw
|
||||
} else {
|
||||
ConditionalUpdate::NoRedraw
|
||||
}
|
||||
}
|
||||
|
||||
fn is_complete(&self) -> bool { self.complete }
|
||||
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::chatwidget::BackgroundOrderTicket;
|
||||
use crate::user_approval_widget::{ApprovalRequest, UserApprovalWidget};
|
||||
use crate::thread_spawner;
|
||||
pub(crate) use bottom_pane_view::BottomPaneView;
|
||||
pub(crate) use bottom_pane_view::ConditionalUpdate;
|
||||
use crate::util::buffer::fill_rect;
|
||||
use code_core::protocol::TokenUsage;
|
||||
use code_file_search::FileMatch;
|
||||
|
||||
@@ -55,6 +55,7 @@ use code_login::AuthMode;
|
||||
use code_protocol::mcp_protocol::AuthMode as McpAuthMode;
|
||||
use code_protocol::protocol::SessionSource;
|
||||
use code_protocol::num_format::format_with_separators;
|
||||
use code_core::split_command_and_args;
|
||||
|
||||
|
||||
mod diff_handlers;
|
||||
@@ -7001,6 +7002,9 @@ impl ChatWidget<'_> {
|
||||
// dispatch_command() removed — command routing is handled at the App layer via AppEvent::DispatchCommand
|
||||
|
||||
pub(crate) fn handle_paste(&mut self, text: String) {
|
||||
if settings_handlers::handle_settings_paste(self, text.clone()) {
|
||||
return;
|
||||
}
|
||||
// Check if the pasted text is a file path to an image
|
||||
let trimmed = text.trim();
|
||||
|
||||
@@ -17616,6 +17620,7 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
let key = pending.key();
|
||||
let attempt = pending.clone();
|
||||
self.pending_agent_updates.insert(key, pending);
|
||||
self.refresh_settings_overview_rows();
|
||||
let tx = self.app_event_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let cfg = attempt.cfg.clone();
|
||||
@@ -18705,6 +18710,15 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
}
|
||||
}
|
||||
|
||||
fn command_for_check(command: &str) -> String {
|
||||
let (base, _) = split_command_and_args(command);
|
||||
if base.trim().is_empty() {
|
||||
command.trim().to_string()
|
||||
} else {
|
||||
base
|
||||
}
|
||||
}
|
||||
|
||||
fn is_builtin_agent(name: &str, command: &str) -> bool {
|
||||
name.eq_ignore_ascii_case("code")
|
||||
|| name.eq_ignore_ascii_case("codex")
|
||||
@@ -18725,6 +18739,14 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
extras.push(agent.name.to_ascii_lowercase());
|
||||
}
|
||||
}
|
||||
let mut pending_agents: HashMap<String, AgentConfig> = HashMap::new();
|
||||
for pending in self.pending_agent_updates.values() {
|
||||
let lower = pending.cfg.name.to_ascii_lowercase();
|
||||
pending_agents.insert(lower.clone(), pending.cfg.clone());
|
||||
if !ordered.iter().any(|name| name.eq_ignore_ascii_case(&lower)) {
|
||||
extras.push(lower);
|
||||
}
|
||||
}
|
||||
extras.sort();
|
||||
for extra in extras {
|
||||
if !ordered.iter().any(|name| name.eq_ignore_ascii_case(&extra)) {
|
||||
@@ -18733,19 +18755,46 @@ Have we met every part of this goal and is there no further work to do?"#
|
||||
}
|
||||
|
||||
for name in ordered.iter() {
|
||||
let name_lower = name.to_ascii_lowercase();
|
||||
if let Some(cfg) = self
|
||||
.config
|
||||
.agents
|
||||
.iter()
|
||||
.find(|a| a.name.eq_ignore_ascii_case(name))
|
||||
{
|
||||
let builtin = is_builtin_agent(&cfg.name, &cfg.command);
|
||||
let spec_cli = agent_model_spec(&cfg.name)
|
||||
.or_else(|| agent_model_spec(&cfg.command))
|
||||
.map(|spec| spec.cli);
|
||||
let command_to_check = command_for_check(&cfg.command);
|
||||
let installed = if builtin {
|
||||
true
|
||||
} else if command_exists(&command_to_check) {
|
||||
true
|
||||
} else if let Some(cli) = spec_cli {
|
||||
command_exists(cli)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
agent_rows.push(AgentOverviewRow {
|
||||
name: cfg.name.clone(),
|
||||
enabled: cfg.enabled,
|
||||
installed,
|
||||
description: Self::agent_description_for(
|
||||
&cfg.name,
|
||||
Some(&cfg.command),
|
||||
cfg.description.as_deref(),
|
||||
),
|
||||
});
|
||||
} else if let Some(cfg) = pending_agents.get(&name_lower) {
|
||||
let builtin = is_builtin_agent(&cfg.name, &cfg.command);
|
||||
let spec_cli = agent_model_spec(&cfg.name)
|
||||
.or_else(|| agent_model_spec(&cfg.command))
|
||||
.map(|spec| spec.cli);
|
||||
let command_to_check = command_for_check(&cfg.command);
|
||||
let installed = if builtin {
|
||||
true
|
||||
} else if command_exists(&cfg.command) {
|
||||
} else if command_exists(&command_to_check) {
|
||||
true
|
||||
} else if let Some(cli) = spec_cli {
|
||||
command_exists(cli)
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
use super::ChatWidget;
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
||||
|
||||
pub(super) fn handle_settings_paste(chat: &mut ChatWidget<'_>, text: String) -> bool {
|
||||
if chat.settings.overlay.is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let overlay = match chat.settings.overlay.as_mut() {
|
||||
Some(overlay) => overlay,
|
||||
None => return false,
|
||||
};
|
||||
|
||||
if overlay.is_menu_active() || overlay.is_help_visible() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(content) = overlay.active_content_mut() {
|
||||
if content.handle_paste(text) {
|
||||
chat.request_redraw();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Handle key presses for the full-screen settings overlay. Returns true when the
|
||||
/// key has been consumed (overlay stays modal while active).
|
||||
pub(super) fn handle_settings_key(chat: &mut ChatWidget<'_>, key_event: KeyEvent) -> bool {
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::bottom_pane::{
|
||||
agents_settings_view::SubagentEditorView,
|
||||
AutoDriveSettingsView,
|
||||
BottomPaneView,
|
||||
ConditionalUpdate,
|
||||
settings_panel::{render_panel, PanelFrameStyle},
|
||||
GithubSettingsView,
|
||||
McpSettingsView,
|
||||
@@ -36,6 +37,9 @@ pub(crate) trait SettingsContent {
|
||||
fn handle_key(&mut self, key: KeyEvent) -> bool;
|
||||
fn is_complete(&self) -> bool;
|
||||
fn on_close(&mut self) {}
|
||||
fn handle_paste(&mut self, _text: String) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -784,6 +788,15 @@ impl SettingsContent for AgentsSettingsContent {
|
||||
fn is_complete(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn handle_paste(&mut self, text: String) -> bool {
|
||||
match &mut self.pane {
|
||||
AgentsPane::Agent(view) => {
|
||||
matches!(view.handle_paste(text), ConditionalUpdate::NeedsRedraw)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct LimitsSettingsContent {
|
||||
|
||||
Reference in New Issue
Block a user