Fix Ghostty session restore launch path (#1976)

* fix: launch Ghostty via shell command

Use Ghostty's shell execution path instead of injecting raw terminal input so Claude resume commands run reliably when opening a session terminal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(ghostty): pass cwd via --working-directory instead of shell string

Use Ghostty's native --working-directory flag to set the working
directory, matching the pattern used by Alacritty. This avoids shell
expansion of special characters (e.g. $VAR, spaces) in project paths.

The command is now passed directly to -c without a cd prefix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Suda202
2026-04-20 12:42:00 +08:00
committed by GitHub
parent 34349a2743
commit 2c9252dec5
+36 -66
View File
@@ -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<String> {
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<String> {
}
}
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\"");
}
}