use std::borrow::Cow;
use std::path::Path;
pub fn prefixed(prefix: &str, window_name: &str) -> String {
format!("{}{}", prefix, window_name)
}
pub fn is_posix_shell(shell: &str) -> bool {
let shell_name = Path::new(shell)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("sh");
matches!(shell_name, "bash" | "zsh" | "sh" | "dash" | "ksh" | "ash")
}
pub fn rewrite_agent_command(
command: &str,
prompt_file: &Path,
working_dir: &Path,
effective_agent: Option<&str>,
shell: &str,
type_override: Option<&str>,
) -> Option<String> {
let agent_command = effective_agent?;
let trimmed_command = command.trim();
if trimmed_command.is_empty() {
return None;
}
let (pane_token, pane_rest) = crate::config::split_first_token(trimmed_command)?;
let (config_token, _) = crate::config::split_first_token(agent_command)?;
let resolved_pane_path = crate::config::resolve_executable_path(pane_token)
.unwrap_or_else(|| pane_token.to_string());
let resolved_config_path = crate::config::resolve_executable_path(config_token)
.unwrap_or_else(|| config_token.to_string());
let pane_stem = Path::new(&resolved_pane_path).file_stem();
let config_stem = Path::new(&resolved_config_path).file_stem();
if pane_stem != config_stem {
return None;
}
let relative = prompt_file.strip_prefix(working_dir).unwrap_or(prompt_file);
let prompt_path = relative.to_string_lossy();
let rest = pane_rest.trim_start();
let profile = super::agent::resolve_profile_with_type(effective_agent, type_override);
let mut inner_cmd = pane_token.to_string();
if let Some(subcmd) = profile.default_subcommand()
&& needs_default_subcommand(rest, subcmd)
{
inner_cmd.push(' ');
inner_cmd.push_str(subcmd);
}
if !rest.is_empty() {
inner_cmd.push(' ');
inner_cmd.push_str(rest);
}
inner_cmd.push(' ');
inner_cmd.push_str(&profile.prompt_argument(&prompt_path));
if is_posix_shell(shell) {
Some(format!(" {}", inner_cmd))
} else {
Some(format!(" {}", wrap_for_non_posix_shell(&inner_cmd)))
}
}
pub struct ResolvedCommand {
pub command: String,
pub prompt_injected: bool,
pub effective_agent: Option<String>,
}
pub fn resolve_pane_command(
pane_command: Option<&str>,
run_commands: bool,
prompt_file_path: Option<&Path>,
working_dir: &Path,
effective_agent: Option<&str>,
shell: &str,
type_override: Option<&str>,
) -> Option<ResolvedCommand> {
let raw_command = pane_command?;
let (command, pane_effective_agent) = if raw_command == "<agent>" {
let agent = effective_agent?;
(agent, effective_agent)
} else if super::agent::is_known_agent(raw_command) {
(raw_command, Some(raw_command))
} else {
(raw_command, effective_agent)
};
if !run_commands {
return None;
}
let result = adjust_command(
command,
prompt_file_path,
working_dir,
pane_effective_agent,
shell,
type_override,
);
let prompt_injected = matches!(result, Cow::Owned(_));
Some(ResolvedCommand {
command: result.into_owned(),
prompt_injected,
effective_agent: pane_effective_agent.map(|s| s.to_string()),
})
}
pub fn adjust_command<'a>(
command: &'a str,
prompt_file_path: Option<&Path>,
working_dir: &Path,
effective_agent: Option<&str>,
shell: &str,
type_override: Option<&str>,
) -> Cow<'a, str> {
if let Some(prompt_path) = prompt_file_path
&& let Some(rewritten) = rewrite_agent_command(
command,
prompt_path,
working_dir,
effective_agent,
shell,
type_override,
)
{
return Cow::Owned(rewritten);
}
let profile = super::agent::resolve_profile_with_type(effective_agent, type_override);
if let Some(subcmd) = profile.default_subcommand()
&& let Some((token, rest_with_leading)) = crate::config::split_first_token(command)
{
let resolved =
crate::config::resolve_executable_path(token).unwrap_or_else(|| token.to_string());
let stem = Path::new(&resolved)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("");
if stem == profile.name() {
let rest = rest_with_leading.trim_start();
if needs_default_subcommand(rest, subcmd) {
return if rest.is_empty() {
Cow::Owned(format!("{} {}", token, subcmd))
} else {
Cow::Owned(format!("{} {} {}", token, subcmd, rest))
};
}
}
}
Cow::Borrowed(command)
}
fn needs_default_subcommand(rest: &str, subcmd: &str) -> bool {
match rest.split_whitespace().next() {
None => true, Some(first) if first == subcmd => false, Some(first) if first.starts_with('-') => true, Some(_) => false, }
}
pub fn escape_for_double_quotes(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('$', "\\$")
.replace('`', "\\`")
}
pub fn escape_for_sh_c_inner_single_quote(s: &str) -> String {
let single_escaped = s.replace('\'', "'\\''");
escape_for_double_quotes(&single_escaped)
}
pub fn wrap_for_non_posix_shell(command: &str) -> String {
let escaped = command.replace('\'', "'\\''");
format!("sh -c '{}'", escaped)
}
pub fn inject_skip_permissions_flag(command: &str, flag: &str) -> String {
let trimmed = command.trim_start();
let leading_spaces = &command[..command.len() - trimmed.len()];
if trimmed.starts_with("sh -c '") && trimmed.ends_with('\'') {
let inner = &trimmed[7..trimmed.len() - 1];
let inner_unescaped = inner.replace("'\\''", "'");
let injected = inject_flag_after_agent_executable(&inner_unescaped, flag);
let re_escaped = injected.replace('\'', "'\\''");
return format!("{}sh -c '{}'", leading_spaces, re_escaped);
}
format!(
"{}{}",
leading_spaces,
inject_flag_after_agent_executable(trimmed, flag)
)
}
fn inject_flag_after_agent_executable(command: &str, flag: &str) -> String {
let exe_token = super::agent::find_executable_token(command);
if exe_token.is_empty() {
return format!("{} {}", command, flag);
}
let exe_start = exe_token.as_ptr() as usize - command.as_ptr() as usize;
let exe_end = exe_start + exe_token.len();
let before = &command[..exe_end];
let after = &command[exe_end..];
if after.is_empty() {
format!("{} {}", before, flag)
} else {
format!("{} {}{}", before, flag, after)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_prefixed() {
assert_eq!(prefixed("wm-", "feature"), "wm-feature");
assert_eq!(prefixed("", "feature"), "feature");
assert_eq!(prefixed("prefix-", ""), "prefix-");
}
#[test]
fn test_is_posix_shell_bash() {
assert!(is_posix_shell("/bin/bash"));
assert!(is_posix_shell("/usr/bin/bash"));
}
#[test]
fn test_is_posix_shell_zsh() {
assert!(is_posix_shell("/bin/zsh"));
assert!(is_posix_shell("/usr/local/bin/zsh"));
}
#[test]
fn test_is_posix_shell_sh() {
assert!(is_posix_shell("/bin/sh"));
}
#[test]
fn test_is_posix_shell_nushell() {
assert!(!is_posix_shell("/opt/homebrew/bin/nu"));
assert!(!is_posix_shell("/usr/bin/nu"));
}
#[test]
fn test_is_posix_shell_fish() {
assert!(!is_posix_shell("/usr/bin/fish"));
assert!(!is_posix_shell("/opt/homebrew/bin/fish"));
}
#[test]
fn test_rewrite_claude_command_posix() {
let prompt_file = PathBuf::from("/tmp/worktree/PROMPT.md");
let working_dir = PathBuf::from("/tmp/worktree");
let result = rewrite_agent_command(
"claude",
&prompt_file,
&working_dir,
Some("claude"),
"/bin/zsh",
None,
);
assert_eq!(result, Some(" claude -- \"$(cat PROMPT.md)\"".to_string()));
}
#[test]
fn test_rewrite_gemini_command_posix() {
let prompt_file = PathBuf::from("/tmp/worktree/PROMPT.md");
let working_dir = PathBuf::from("/tmp/worktree");
let result = rewrite_agent_command(
"gemini",
&prompt_file,
&working_dir,
Some("gemini"),
"/bin/bash",
None,
);
assert_eq!(result, Some(" gemini -i \"$(cat PROMPT.md)\"".to_string()));
}
#[test]
fn test_rewrite_opencode_command_posix() {
let prompt_file = PathBuf::from("/tmp/worktree/PROMPT.md");
let working_dir = PathBuf::from("/tmp/worktree");
let result = rewrite_agent_command(
"opencode",
&prompt_file,
&working_dir,
Some("opencode"),
"/bin/zsh",
None,
);
assert_eq!(
result,
Some(" opencode --prompt \"$(cat PROMPT.md)\"".to_string())
);
}
#[test]
fn test_rewrite_kiro_bare_command_posix() {
let prompt_file = PathBuf::from("/tmp/worktree/PROMPT.md");
let working_dir = PathBuf::from("/tmp/worktree");
let result = rewrite_agent_command(
"kiro-cli",
&prompt_file,
&working_dir,
Some("kiro-cli"),
"/bin/zsh",
None,
);
assert_eq!(
result,
Some(" kiro-cli chat \"$(cat PROMPT.md)\"".to_string())
);
}
#[test]
fn test_rewrite_kiro_with_chat_subcommand() {
let prompt_file = PathBuf::from("/tmp/worktree/PROMPT.md");
let working_dir = PathBuf::from("/tmp/worktree");
let result = rewrite_agent_command(
"kiro-cli chat",
&prompt_file,
&working_dir,
Some("kiro-cli chat"),
"/bin/zsh",
None,
);
assert_eq!(
result,
Some(" kiro-cli chat \"$(cat PROMPT.md)\"".to_string())
);
}
#[test]
fn test_rewrite_kiro_with_chat_and_flags() {
let prompt_file = PathBuf::from("/tmp/worktree/PROMPT.md");
let working_dir = PathBuf::from("/tmp/worktree");
let result = rewrite_agent_command(
"kiro-cli chat --model sonnet",
&prompt_file,
&working_dir,
Some("kiro-cli chat --model sonnet"),
"/bin/zsh",
None,
);
assert_eq!(
result,
Some(" kiro-cli chat --model sonnet \"$(cat PROMPT.md)\"".to_string())
);
}
#[test]
fn test_rewrite_command_with_args_posix() {
let prompt_file = PathBuf::from("/tmp/worktree/PROMPT.md");
let working_dir = PathBuf::from("/tmp/worktree");
let result = rewrite_agent_command(
"claude --verbose",
&prompt_file,
&working_dir,
Some("claude"),
"/bin/bash",
None,
);
assert_eq!(
result,
Some(" claude --verbose -- \"$(cat PROMPT.md)\"".to_string())
);
}
#[test]
fn test_rewrite_claude_command_nushell() {
let prompt_file = PathBuf::from("/tmp/worktree/PROMPT.md");
let working_dir = PathBuf::from("/tmp/worktree");
let result = rewrite_agent_command(
"claude",
&prompt_file,
&working_dir,
Some("claude"),
"/opt/homebrew/bin/nu",
None,
);
assert_eq!(
result,
Some(" sh -c 'claude -- \"$(cat PROMPT.md)\"'".to_string())
);
}
#[test]
fn test_rewrite_mismatched_agent() {
let prompt_file = PathBuf::from("/tmp/worktree/PROMPT.md");
let working_dir = PathBuf::from("/tmp/worktree");
let result = rewrite_agent_command(
"claude",
&prompt_file,
&working_dir,
Some("gemini"),
"/bin/zsh",
None,
);
assert_eq!(result, None);
}
#[test]
fn test_rewrite_empty_command() {
let prompt_file = PathBuf::from("/tmp/worktree/PROMPT.md");
let working_dir = PathBuf::from("/tmp/worktree");
let result = rewrite_agent_command(
"",
&prompt_file,
&working_dir,
Some("claude"),
"/bin/zsh",
None,
);
assert_eq!(result, None);
}
#[test]
fn test_escape_for_double_quotes_simple() {
assert_eq!(escape_for_double_quotes("hello"), "hello");
assert_eq!(escape_for_double_quotes("foo bar"), "foo bar");
}
#[test]
fn test_escape_for_double_quotes_special_chars() {
assert_eq!(escape_for_double_quotes("$HOME"), "\\$HOME");
assert_eq!(escape_for_double_quotes("a\"b"), "a\\\"b");
assert_eq!(escape_for_double_quotes("$(cmd)"), "\\$(cmd)");
assert_eq!(escape_for_double_quotes("`cmd`"), "\\`cmd\\`");
}
#[test]
fn test_escape_for_double_quotes_backslash() {
assert_eq!(escape_for_double_quotes("a\\b"), "a\\\\b");
assert_eq!(escape_for_double_quotes("\\$HOME"), "\\\\\\$HOME");
}
#[test]
fn test_escape_for_double_quotes_combined() {
assert_eq!(
escape_for_double_quotes("echo \"$HOME\" `pwd`"),
"echo \\\"\\$HOME\\\" \\`pwd\\`"
);
}
#[test]
fn test_escape_for_sh_c_inner_single_quote_simple() {
assert_eq!(escape_for_sh_c_inner_single_quote("/bin/bash"), "/bin/bash");
}
#[test]
fn test_escape_for_sh_c_inner_single_quote_with_single_quote() {
assert_eq!(
escape_for_sh_c_inner_single_quote("/bin/user's shell"),
"/bin/user'\\\\''s shell"
);
}
#[test]
fn test_escape_for_sh_c_inner_single_quote_with_dollar() {
assert_eq!(
escape_for_sh_c_inner_single_quote("/path/$dir/shell"),
"/path/\\$dir/shell"
);
}
#[test]
fn test_escape_for_sh_c_inner_single_quote_combined() {
assert_eq!(
escape_for_sh_c_inner_single_quote("it's $HOME"),
"it'\\\\''s \\$HOME"
);
}
#[test]
fn test_wrap_for_non_posix_shell_simple() {
assert_eq!(wrap_for_non_posix_shell("echo hello"), "sh -c 'echo hello'");
}
#[test]
fn test_wrap_for_non_posix_shell_with_single_quote() {
assert_eq!(
wrap_for_non_posix_shell("echo 'quoted'"),
"sh -c 'echo '\\''quoted'\\'''"
);
}
#[test]
fn test_wrap_for_non_posix_shell_with_dollar() {
assert_eq!(wrap_for_non_posix_shell("echo $HOME"), "sh -c 'echo $HOME'");
}
#[test]
fn test_wrap_for_non_posix_shell_complex() {
assert_eq!(
wrap_for_non_posix_shell("claude -- \"$(cat PROMPT.md)\""),
"sh -c 'claude -- \"$(cat PROMPT.md)\"'"
);
}
#[test]
fn test_inject_skip_permissions_with_prompt() {
let result = inject_skip_permissions_flag(
" claude -- \"$(cat PROMPT.md)\"",
"--dangerously-skip-permissions",
);
assert_eq!(
result,
" claude --dangerously-skip-permissions -- \"$(cat PROMPT.md)\""
);
}
#[test]
fn test_inject_skip_permissions_with_existing_args() {
let result = inject_skip_permissions_flag(
" claude --verbose -- \"$(cat PROMPT.md)\"",
"--dangerously-skip-permissions",
);
assert_eq!(
result,
" claude --dangerously-skip-permissions --verbose -- \"$(cat PROMPT.md)\""
);
}
#[test]
fn test_inject_skip_permissions_bare_command() {
let result = inject_skip_permissions_flag("claude", "--dangerously-skip-permissions");
assert_eq!(result, "claude --dangerously-skip-permissions");
}
#[test]
fn test_inject_skip_permissions_non_posix_shell() {
let result = inject_skip_permissions_flag(
" sh -c 'claude -- \"$(cat PROMPT.md)\"'",
"--dangerously-skip-permissions",
);
assert_eq!(
result,
" sh -c 'claude --dangerously-skip-permissions -- \"$(cat PROMPT.md)\"'"
);
}
#[test]
fn test_inject_skip_permissions_env_wrapped() {
let result = inject_skip_permissions_flag(
" env -u FOO claude -- \"$(cat PROMPT.md)\"",
"--dangerously-skip-permissions",
);
assert_eq!(
result,
" env -u FOO claude --dangerously-skip-permissions -- \"$(cat PROMPT.md)\""
);
}
#[test]
fn test_inject_skip_permissions_env_with_assignments() {
let result =
inject_skip_permissions_flag("env FOO=bar claude", "--dangerously-skip-permissions");
assert_eq!(result, "env FOO=bar claude --dangerously-skip-permissions");
}
#[test]
fn test_resolve_pane_command_none_when_no_command() {
let result =
resolve_pane_command(None, true, None, Path::new("/tmp"), None, "/bin/zsh", None);
assert!(result.is_none());
}
#[test]
fn test_resolve_pane_command_none_when_run_commands_false() {
let result = resolve_pane_command(
Some("echo hello"),
false,
None,
Path::new("/tmp"),
None,
"/bin/zsh",
None,
);
assert!(result.is_none());
}
#[test]
fn test_resolve_pane_command_returns_command_as_is() {
let result = resolve_pane_command(
Some("vim"),
true,
None,
Path::new("/tmp"),
None,
"/bin/zsh",
None,
);
let resolved = result.unwrap();
assert_eq!(resolved.command, "vim");
assert!(!resolved.prompt_injected);
}
#[test]
fn test_resolve_pane_command_agent_placeholder_with_agent() {
let result = resolve_pane_command(
Some("<agent>"),
true,
None,
Path::new("/tmp"),
Some("claude"),
"/bin/zsh",
None,
);
let resolved = result.unwrap();
assert_eq!(resolved.command, "claude");
assert!(!resolved.prompt_injected);
}
#[test]
fn test_resolve_pane_command_agent_placeholder_without_agent() {
let result = resolve_pane_command(
Some("<agent>"),
true,
None,
Path::new("/tmp"),
None,
"/bin/zsh",
None,
);
assert!(result.is_none());
}
#[test]
fn test_resolve_pane_command_with_prompt_injection() {
let prompt = PathBuf::from("/tmp/worktree/PROMPT.md");
let working_dir = PathBuf::from("/tmp/worktree");
let result = resolve_pane_command(
Some("claude"),
true,
Some(&prompt),
&working_dir,
Some("claude"),
"/bin/zsh",
None,
);
let resolved = result.unwrap();
assert!(resolved.prompt_injected);
assert!(resolved.command.contains("PROMPT.md"));
}
#[test]
fn test_resolve_pane_command_no_injection_for_mismatched_agent() {
let prompt = PathBuf::from("/tmp/worktree/PROMPT.md");
let working_dir = PathBuf::from("/tmp/worktree");
let result = resolve_pane_command(
Some("vim"),
true,
Some(&prompt),
&working_dir,
Some("claude"),
"/bin/zsh",
None,
);
let resolved = result.unwrap();
assert!(!resolved.prompt_injected);
assert_eq!(resolved.command, "vim");
}
#[test]
fn test_resolve_pane_command_bare_agent_effective_agent_field() {
let result = resolve_pane_command(
Some("<agent>"),
true,
None,
Path::new("/tmp"),
Some("claude"),
"/bin/zsh",
None,
);
let resolved = result.unwrap();
assert_eq!(resolved.command, "claude");
assert_eq!(resolved.effective_agent.as_deref(), Some("claude"));
}
#[test]
fn test_resolve_pane_command_regular_command_effective_agent_field() {
let result = resolve_pane_command(
Some("vim"),
true,
None,
Path::new("/tmp"),
Some("claude"),
"/bin/zsh",
None,
);
let resolved = result.unwrap();
assert_eq!(resolved.command, "vim");
assert_eq!(resolved.effective_agent.as_deref(), Some("claude"));
}
#[test]
fn test_resolve_pane_command_known_agent_auto_detected() {
let result = resolve_pane_command(
Some("codex --yolo"),
true,
None,
Path::new("/tmp"),
Some("claude"), "/bin/zsh",
None,
);
let resolved = result.unwrap();
assert_eq!(resolved.command, "codex --yolo");
assert_eq!(resolved.effective_agent.as_deref(), Some("codex --yolo"));
}
#[test]
fn test_resolve_pane_command_known_agent_prompt_injection() {
let prompt = PathBuf::from("/tmp/worktree/PROMPT.md");
let working_dir = PathBuf::from("/tmp/worktree");
let result = resolve_pane_command(
Some("codex"),
true,
Some(&prompt),
&working_dir,
Some("claude"), "/bin/zsh",
None,
);
let resolved = result.unwrap();
assert!(resolved.prompt_injected);
assert!(resolved.command.contains("PROMPT.md"));
assert_eq!(resolved.effective_agent.as_deref(), Some("codex"));
}
#[test]
fn test_resolve_pane_command_known_agent_no_window_agent() {
let prompt = PathBuf::from("/tmp/worktree/PROMPT.md");
let working_dir = PathBuf::from("/tmp/worktree");
let result = resolve_pane_command(
Some("gemini"),
true,
Some(&prompt),
&working_dir,
None, "/bin/zsh",
None,
);
let resolved = result.unwrap();
assert!(resolved.prompt_injected);
assert!(resolved.command.contains("-i"));
assert_eq!(resolved.effective_agent.as_deref(), Some("gemini"));
}
#[test]
fn test_resolve_pane_command_kiro_bare_inserts_chat() {
let result = resolve_pane_command(
Some("<agent>"),
true,
None,
Path::new("/tmp"),
Some("kiro-cli"),
"/bin/zsh",
None,
);
let resolved = result.unwrap();
assert_eq!(resolved.command, "kiro-cli chat");
}
#[test]
fn test_resolve_pane_command_kiro_with_chat_no_duplicate() {
let result = resolve_pane_command(
Some("<agent>"),
true,
None,
Path::new("/tmp"),
Some("kiro-cli chat"),
"/bin/zsh",
None,
);
let resolved = result.unwrap();
assert_eq!(resolved.command, "kiro-cli chat");
}
#[test]
fn test_resolve_pane_command_kiro_no_chat_on_vim() {
let result = resolve_pane_command(
Some("vim"),
true,
None,
Path::new("/tmp"),
Some("kiro-cli"),
"/bin/zsh",
None,
);
let resolved = result.unwrap();
assert_eq!(resolved.command, "vim");
}
#[test]
fn test_resolve_pane_command_kiro_with_prompt() {
let prompt = PathBuf::from("/tmp/worktree/PROMPT.md");
let working_dir = PathBuf::from("/tmp/worktree");
let result = resolve_pane_command(
Some("<agent>"),
true,
Some(&prompt),
&working_dir,
Some("kiro-cli"),
"/bin/zsh",
None,
);
let resolved = result.unwrap();
assert!(resolved.prompt_injected);
assert_eq!(resolved.command, " kiro-cli chat \"$(cat PROMPT.md)\"");
}
#[test]
fn test_resolve_pane_command_kiro_with_flags_inserts_chat() {
let result = resolve_pane_command(
Some("<agent>"),
true,
None,
Path::new("/tmp"),
Some("kiro-cli --verbose"),
"/bin/zsh",
None,
);
let resolved = result.unwrap();
assert_eq!(resolved.command, "kiro-cli chat --verbose");
}
#[test]
fn test_needs_default_subcommand_empty() {
assert!(needs_default_subcommand("", "chat"));
}
#[test]
fn test_needs_default_subcommand_already_present() {
assert!(!needs_default_subcommand("chat", "chat"));
assert!(!needs_default_subcommand("chat --model foo", "chat"));
}
#[test]
fn test_needs_default_subcommand_flag() {
assert!(needs_default_subcommand("--verbose", "chat"));
assert!(needs_default_subcommand("-v", "chat"));
}
#[test]
fn test_needs_default_subcommand_other_subcommand() {
assert!(!needs_default_subcommand("login", "chat"));
assert!(!needs_default_subcommand("agent list", "chat"));
}
}