diff --git a/src-tauri/src/session_manager/terminal/mod.rs b/src-tauri/src/session_manager/terminal/mod.rs index 3e35df30e..085a86f0d 100644 --- a/src-tauri/src/session_manager/terminal/mod.rs +++ b/src-tauri/src/session_manager/terminal/mod.rs @@ -78,22 +78,7 @@ end tell"# } fn launch_ghostty(command: &str, cwd: Option<&str>) -> Result<(), String> { - let args = build_ghostty_args(command, cwd); - - let status = Command::new("open") - .args(args.iter().map(String::as_str)) - .status() - .map_err(|e| format!("Failed to launch Ghostty: {e}"))?; - - if status.success() { - Ok(()) - } else { - Err("Failed to launch Ghostty. Make sure it is installed.".to_string()) - } -} - -fn build_ghostty_args(command: &str, cwd: Option<&str>) -> Vec { - let input = ghostty_raw_input(command); + let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string()); let mut args = vec![ "-na".to_string(), @@ -108,22 +93,22 @@ fn build_ghostty_args(command: &str, cwd: Option<&str>) -> Vec { } } - args.push(format!("--input={input}")); - args -} + args.push("-e".to_string()); + args.push(shell); + args.push("-l".to_string()); + args.push("-c".to_string()); + args.push(command.to_string()); -fn ghostty_raw_input(command: &str) -> String { - let mut escaped = String::from("raw:"); - for ch in command.chars() { - match ch { - '\\' => escaped.push_str("\\\\"), - '\n' => escaped.push_str("\\n"), - '\r' => escaped.push_str("\\r"), - _ => escaped.push(ch), - } + let status = Command::new("open") + .args(&args) + .status() + .map_err(|e| format!("Failed to launch Ghostty: {e}"))?; + + if status.success() { + Ok(()) + } else { + Err("Failed to launch Ghostty. Make sure it is installed.".to_string()) } - escaped.push_str("\\n"); - escaped } fn launch_kitty(command: &str, cwd: Option<&str>) -> Result<(), String> { @@ -300,43 +285,10 @@ mod tests { use super::*; #[test] - fn ghostty_uses_shell_mode_for_resume_commands() { - let args = build_ghostty_args("claude --resume abc-123", Some("/tmp/project dir")); - + fn build_shell_command_keeps_command_without_cwd_prefix_when_not_provided() { assert_eq!( - args, - vec![ - "-na", - "Ghostty", - "--args", - "--quit-after-last-window-closed=true", - "--working-directory=/tmp/project dir", - "--input=raw:claude --resume abc-123\\n", - ] - ); - } - - #[test] - fn ghostty_keeps_command_without_cwd_prefix_when_not_provided() { - let args = build_ghostty_args("claude --resume abc-123", None); - - assert_eq!( - args, - vec![ - "-na", - "Ghostty", - "--args", - "--quit-after-last-window-closed=true", - "--input=raw:claude --resume abc-123\\n", - ] - ); - } - - #[test] - fn ghostty_escapes_newlines_and_backslashes_in_input() { - assert_eq!( - ghostty_raw_input("echo foo\\\\bar\npwd"), - "raw:echo foo\\\\\\\\bar\\npwd\\n" + build_shell_command("claude --resume abc-123", None), + "claude --resume abc-123" ); } @@ -365,4 +317,22 @@ mod tests { ] ); } + + #[test] + fn ghostty_uses_working_directory_arg_for_cwd() { + // cwd should be passed as --working-directory, not embedded in the shell command string + // This avoids shell expansion of special characters in directory paths + let cwd = "/tmp/project dir"; + let command = "claude --resume abc-123"; + + // Verify build_shell_command does NOT include cwd when used in ghostty context + // (ghostty passes cwd via --working-directory flag instead) + assert_eq!( + build_shell_command(command, None), + "claude --resume abc-123" + ); + + // Verify shell_escape works correctly for paths with spaces + assert_eq!(shell_escape(cwd), "\"/tmp/project dir\""); + } }