diff --git a/Cargo.lock b/Cargo.lock index 503fb48ea5..0dba427eda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5364,9 +5364,9 @@ dependencies = [ [[package]] name = "rmcp" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534fd1cd0601e798ac30545ff2b7f4a62c6f14edd4aaed1cc5eb1e85f69f09af" +checksum = "583d060e99feb3a3683fb48a1e4bf5f8d4a50951f429726f330ee5ff548837f8" dependencies = [ "base64 0.22.1", "chrono", @@ -5393,9 +5393,9 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba777eb0e5f53a757e36f0e287441da0ab766564ba7201600eeb92a4753022e" +checksum = "421d8b0ba302f479214889486f9550e63feca3af310f1190efcf6e2016802693" dependencies = [ "darling 0.21.3", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 2d80bc05f4..1739aa74e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -130,7 +130,7 @@ winreg = "0.55.0" schemars = "1.0.4" jsonschema = "0.30.0" zip = "2.2.0" -rmcp = { version = "0.7.0", features = ["client", "transport-sse-client-reqwest", "reqwest", "transport-streamable-http-client-reqwest", "transport-child-process", "tower", "auth"] } +rmcp = { version = "0.8.0", features = ["client", "transport-sse-client-reqwest", "reqwest", "transport-streamable-http-client-reqwest", "transport-child-process", "tower", "auth"] } [workspace.lints.rust] future_incompatible = "warn" diff --git a/crates/chat-cli/src/cli/agent/root_command_args.rs b/crates/chat-cli/src/cli/agent/root_command_args.rs index 0f02028e50..20d4e62803 100644 --- a/crates/chat-cli/src/cli/agent/root_command_args.rs +++ b/crates/chat-cli/src/cli/agent/root_command_args.rs @@ -116,13 +116,8 @@ impl AgentArgs { Some(AgentSubcommands::Create { name, directory, from }) => { let mut agents = Agents::load(os, None, true, &mut stderr, mcp_enabled).await.0; let path_with_file_name = create_agent(os, &mut agents, name.clone(), directory, from).await?; - let editor_cmd = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string()); - let mut cmd = std::process::Command::new(editor_cmd); - let status = cmd.arg(&path_with_file_name).status()?; - if !status.success() { - bail!("Editor process did not exit with success"); - } + crate::util::editor::launch_editor(&path_with_file_name)?; let Ok(content) = os.fs.read(&path_with_file_name).await else { bail!( @@ -148,13 +143,7 @@ impl AgentArgs { let _agents = Agents::load(os, None, true, &mut stderr, mcp_enabled).await.0; let (_agent, path_with_file_name) = Agent::get_agent_by_name(os, &name).await?; - let editor_cmd = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string()); - let mut cmd = std::process::Command::new(editor_cmd); - - let status = cmd.arg(&path_with_file_name).status()?; - if !status.success() { - bail!("Editor process did not exit with success"); - } + crate::util::editor::launch_editor(&path_with_file_name)?; let Ok(content) = os.fs.read(&path_with_file_name).await else { bail!( diff --git a/crates/chat-cli/src/cli/chat/cli/profile.rs b/crates/chat-cli/src/cli/chat/cli/profile.rs index 83a7b634cd..90c5679713 100644 --- a/crates/chat-cli/src/cli/chat/cli/profile.rs +++ b/crates/chat-cli/src/cli/chat/cli/profile.rs @@ -195,13 +195,9 @@ impl AgentSubcommand { let path_with_file_name = create_agent(os, &mut agents, name.clone(), directory, from) .await .map_err(|e| ChatError::Custom(Cow::Owned(e.to_string())))?; - let editor_cmd = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string()); - let mut cmd = std::process::Command::new(editor_cmd); - let status = cmd.arg(&path_with_file_name).status()?; - if !status.success() { - return Err(ChatError::Custom("Editor process did not exit with success".into())); - } + crate::util::editor::launch_editor(&path_with_file_name) + .map_err(|e| ChatError::Custom(Cow::Owned(e.to_string())))?; let new_agent = Agent::load( os, @@ -253,13 +249,8 @@ impl AgentSubcommand { .await .map_err(|e| ChatError::Custom(Cow::Owned(e.to_string())))?; - let editor_cmd = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string()); - let mut cmd = std::process::Command::new(editor_cmd); - - let status = cmd.arg(&path_with_file_name).status()?; - if !status.success() { - return Err(ChatError::Custom("Editor process did not exit with success".into())); - } + crate::util::editor::launch_editor(&path_with_file_name) + .map_err(|e| ChatError::Custom(Cow::Owned(e.to_string())))?; let updated_agent = Agent::load( os, diff --git a/crates/chat-cli/src/cli/chat/tools/delegate.rs b/crates/chat-cli/src/cli/chat/tools/delegate.rs index 991b999452..f0f8005ad4 100644 --- a/crates/chat-cli/src/cli/chat/tools/delegate.rs +++ b/crates/chat-cli/src/cli/chat/tools/delegate.rs @@ -326,12 +326,17 @@ pub async fn spawn_agent_process(os: &Os, agent: &str, task: &str) -> Result, @@ -45,7 +46,7 @@ pub struct SettingsArgs { key: Option, /// value value: Option, - /// Delete a value + /// Delete a key (No value needed) #[arg(long, short)] delete: bool, /// Format of the output @@ -87,11 +88,26 @@ impl SettingsArgs { }, None => { let Some(key) = &self.key else { + if self.delete { + return Err(eyre::eyre!( + "the argument {} requires a {}\n Usage: q settings {} {}", + "'--delete'".yellow(), + "".green(), + "--delete".yellow(), + "".green() + )); + } return Ok(ExitCode::SUCCESS); }; let key = Setting::try_from(key.as_str())?; match (&self.value, self.delete) { + (Some(_), true) => Err(eyre::eyre!( + "the argument {} cannot be used with {}\n Usage: q settings {} {key}", + "'--delete'".yellow(), + "'[VALUE]'".yellow(), + "--delete".yellow() + )), (None, false) => match os.database.settings.get(key) { Some(value) => { match self.format { @@ -147,9 +163,34 @@ impl SettingsArgs { Ok(ExitCode::SUCCESS) }, - _ => Ok(ExitCode::SUCCESS), } }, } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_delete_with_value_error() { + let mut os = Os::new().await.unwrap(); + + let settings_args = SettingsArgs { + cmd: None, + key: Some("chat.defaultAgent".to_string()), + value: Some("test_value".to_string()), + delete: true, + format: OutputFormat::Plain, + }; + + let result = settings_args.execute(&mut os).await; + + assert!(result.is_err()); + let error_msg = result.unwrap_err().to_string(); + assert!(error_msg.contains("the argument")); + assert!(error_msg.contains("--delete")); + assert!(error_msg.contains("Usage:")); + } +} diff --git a/crates/chat-cli/src/util/editor.rs b/crates/chat-cli/src/util/editor.rs new file mode 100644 index 0000000000..a7aa9baa6e --- /dev/null +++ b/crates/chat-cli/src/util/editor.rs @@ -0,0 +1,39 @@ +use std::path::Path; +use std::process::Command; + +/// Launch the user's preferred editor with the given file path. +/// +/// This function properly parses the EDITOR environment variable to handle +/// editors that require arguments (e.g., "emacsclient -nw"). +/// +/// # Arguments +/// * `file_path` - Path to the file to open in the editor +/// +/// # Returns +/// * `Ok(())` if the editor was launched successfully and exited with success +/// * `Err` if the editor failed to launch or exited with an error +pub fn launch_editor(file_path: &Path) -> eyre::Result<()> { + let editor_cmd = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string()); + + // Parse the editor command to handle arguments + let mut parts = shlex::split(&editor_cmd).ok_or_else(|| eyre::eyre!("Failed to parse EDITOR command"))?; + + if parts.is_empty() { + eyre::bail!("EDITOR environment variable is empty"); + } + + let editor_bin = parts.remove(0); + + let mut cmd = Command::new(editor_bin); + for arg in parts { + cmd.arg(arg); + } + + let status = cmd.arg(file_path).status()?; + + if !status.success() { + eyre::bail!("Editor process did not exit with success"); + } + + Ok(()) +} diff --git a/crates/chat-cli/src/util/mod.rs b/crates/chat-cli/src/util/mod.rs index 48d8c94c97..b0b9333143 100644 --- a/crates/chat-cli/src/util/mod.rs +++ b/crates/chat-cli/src/util/mod.rs @@ -1,5 +1,6 @@ pub mod consts; pub mod directories; +pub mod editor; pub mod knowledge_store; pub mod open; pub mod pattern_matching;