diff --git a/crates/ide-agent/src/lib.rs b/crates/ide-agent/src/lib.rs index af30ddbdb..676eb3036 100644 --- a/crates/ide-agent/src/lib.rs +++ b/crates/ide-agent/src/lib.rs @@ -1937,7 +1937,7 @@ fn permission_title(params: &Value) -> String { if let Some(command) = call.pointer("/rawInput/command").and_then(Value::as_str).filter(|c| !c.is_empty()) { // Network approvals describe the host rather than approving command execution. if !title.is_some_and(|t| t.starts_with("Allow network access to ")) { - return format!("Run {command}"); + return format!("Run {}", shell_script(command).unwrap_or_else(|| command.to_owned())); } } if title.is_none() @@ -1950,6 +1950,45 @@ fn permission_title(params: &Value) -> String { title.unwrap_or("The agent asks for permission").to_owned() } +/// The script of a ` -c ` command line, when the whole line is exactly that. +pub(crate) fn shell_script(command: &str) -> Option { + let (shell, rest) = command.split_once(' ')?; + if !matches!(shell.rsplit('/').next(), Some("sh" | "bash" | "zsh")) { + return None; + } + let quoted = rest.strip_prefix("-lc ").or_else(|| rest.strip_prefix("-c "))?; + let mut out = String::new(); + let mut chars = quoted.chars(); + // Adjacent quoted words, e.g. 'a'\''b' or "a \"b\"". + while let Some(c) = chars.next() { + match c { + '\'' => loop { + match chars.next()? { + '\'' => break, + c => out.push(c), + } + }, + '"' => loop { + match chars.next()? { + '"' => break, + '\\' => match chars.next()? { + c @ ('"' | '\\' | '$' | '`') => out.push(c), + '\n' => {} + c => { + out.push('\\'); + out.push(c); + } + }, + c => out.push(c), + } + }, + '\\' => out.push(chars.next()?), + _ => return None, + } + } + Some(out) +} + /// The questions a permission request asks, and whether they answer by question index. /// The Claude Code adapter carries them as `_meta.questions`, answered as the option id; /// Qwen Code (ACP) as `_meta.qwenQuestions`, answered with `answers` keyed by index beside diff --git a/crates/ide-agent/src/native/codex/translate.rs b/crates/ide-agent/src/native/codex/translate.rs index 2dc0a4152..006048967 100644 --- a/crates/ide-agent/src/native/codex/translate.rs +++ b/crates/ide-agent/src/native/codex/translate.rs @@ -745,50 +745,11 @@ pub(super) fn command_approval_title(params: &Value, item: Option<&Value>) -> St .filter(|c| !c.is_empty()) .or_else(|| item.and_then(|i| i.get("command")).and_then(Value::as_str).filter(|c| !c.is_empty())); match command { - Some(command) => format!("Run {}", shell_script(command).unwrap_or_else(|| command.to_owned())), + Some(command) => format!("Run {}", crate::shell_script(command).unwrap_or_else(|| command.to_owned())), None => "Run a command".to_owned(), } } -/// The script of a ` -c ` command line, when the whole line is exactly that. -fn shell_script(command: &str) -> Option { - let (shell, rest) = command.split_once(' ')?; - if !matches!(shell.rsplit('/').next(), Some("sh" | "bash" | "zsh")) { - return None; - } - let quoted = rest.strip_prefix("-lc ").or_else(|| rest.strip_prefix("-c "))?; - let mut out = String::new(); - let mut chars = quoted.chars(); - // Adjacent quoted words, e.g. 'a'\''b' or "a \"b\"". - while let Some(c) = chars.next() { - match c { - '\'' => loop { - match chars.next()? { - '\'' => break, - c => out.push(c), - } - }, - '"' => loop { - match chars.next()? { - '"' => break, - '\\' => match chars.next()? { - c @ ('"' | '\\' | '$' | '`') => out.push(c), - '\n' => {} - c => { - out.push('\\'); - out.push(c); - } - }, - c => out.push(c), - } - }, - '\\' => out.push(chars.next()?), - _ => return None, - } - } - Some(out) -} - /// The ACP title of a file-change approval, from the item it is about. pub(super) fn file_approval_title(item: Option<&Value>, cwd: Option<&str>) -> String { let changes = item.and_then(|i| i.get("changes")).and_then(Value::as_array).map(Vec::as_slice).unwrap_or_default(); diff --git a/crates/ide-agent/src/tests.rs b/crates/ide-agent/src/tests.rs index 1ee70c274..fd53447ab 100644 --- a/crates/ide-agent/src/tests.rs +++ b/crates/ide-agent/src/tests.rs @@ -17,6 +17,14 @@ fn permission_uses_its_own_command_payload() { assert_eq!(permission_title(&request), format!("Run {command}")); } +#[test] +fn permission_shows_the_script_inside_a_shell_wrapper() { + let wrapped = r#"/bin/zsh -lc "python3 -c 'import pathlib\np = pathlib.Path(\"a.txt\")'""#; + let request = json!({"toolCall": {"toolCallId": "write", "kind": "execute", + "title": "Run python3", "rawInput": {"command": wrapped}}}); + assert_eq!(permission_title(&request), "Run python3 -c 'import pathlib\\np = pathlib.Path(\"a.txt\")'"); +} + /// Says "Hi", edits a file, asks to run a command and reports what it was allowed. #[derive(Default)] struct FakeAgent {