mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-30 02:14:43 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9620a8c769 |
+93
-203
@@ -6,7 +6,7 @@ use crate::services::ProviderService;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use tauri::AppHandle;
|
||||
use tauri::State;
|
||||
@@ -720,10 +720,8 @@ pub async fn open_provider_terminal(
|
||||
state: State<'_, crate::store::AppState>,
|
||||
app: String,
|
||||
#[allow(non_snake_case)] providerId: String,
|
||||
cwd: Option<String>,
|
||||
) -> Result<bool, String> {
|
||||
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
|
||||
let launch_cwd = resolve_launch_cwd(cwd)?;
|
||||
|
||||
// 获取提供商配置
|
||||
let providers = ProviderService::list(state.inner(), app_type.clone())
|
||||
@@ -738,8 +736,7 @@ pub async fn open_provider_terminal(
|
||||
let env_vars = extract_env_vars_from_config(config, &app_type);
|
||||
|
||||
// 根据平台启动终端,传入提供商ID用于生成唯一的配置文件名
|
||||
launch_terminal_with_env(env_vars, &providerId, launch_cwd.as_deref())
|
||||
.map_err(|e| format!("启动终端失败: {e}"))?;
|
||||
launch_terminal_with_env(env_vars, &providerId).map_err(|e| format!("启动终端失败: {e}"))?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
@@ -794,49 +791,11 @@ fn extract_env_vars_from_config(
|
||||
env_vars
|
||||
}
|
||||
|
||||
fn resolve_launch_cwd(cwd: Option<String>) -> Result<Option<PathBuf>, String> {
|
||||
let Some(raw_path) = cwd.filter(|value| !value.trim().is_empty()) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if raw_path.contains('\n') || raw_path.contains('\r') {
|
||||
return Err("目录路径包含非法换行符".to_string());
|
||||
}
|
||||
|
||||
let path = Path::new(&raw_path);
|
||||
if !path.exists() {
|
||||
return Err(format!("目录不存在: {raw_path}"));
|
||||
}
|
||||
|
||||
let resolved = std::fs::canonicalize(path).map_err(|e| format!("解析目录失败: {e}"))?;
|
||||
if !resolved.is_dir() {
|
||||
return Err(format!("选择的路径不是文件夹: {}", resolved.display()));
|
||||
}
|
||||
|
||||
// Strip Windows extended-length prefix that canonicalize produces,
|
||||
// as it can break batch scripts and other shell commands.
|
||||
// Special-case \\?\UNC\server\share -> \\server\share for network/WSL paths.
|
||||
#[cfg(target_os = "windows")]
|
||||
let resolved = {
|
||||
let s = resolved.to_string_lossy();
|
||||
if let Some(unc) = s.strip_prefix(r"\\?\UNC\") {
|
||||
PathBuf::from(format!(r"\\{unc}"))
|
||||
} else if let Some(stripped) = s.strip_prefix(r"\\?\") {
|
||||
PathBuf::from(stripped)
|
||||
} else {
|
||||
resolved
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Some(resolved))
|
||||
}
|
||||
|
||||
/// 创建临时配置文件并启动 claude 终端
|
||||
/// 使用 --settings 参数传入提供商特定的 API 配置
|
||||
fn launch_terminal_with_env(
|
||||
env_vars: Vec<(String, String)>,
|
||||
provider_id: &str,
|
||||
cwd: Option<&Path>,
|
||||
) -> Result<(), String> {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let config_file = temp_dir.join(format!(
|
||||
@@ -850,19 +809,19 @@ fn launch_terminal_with_env(
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
launch_macos_terminal(&config_file, cwd)?;
|
||||
launch_macos_terminal(&config_file)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
launch_linux_terminal(&config_file, cwd)?;
|
||||
launch_linux_terminal(&config_file)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
launch_windows_terminal(&temp_dir, &config_file, cwd)?;
|
||||
launch_windows_terminal(&temp_dir, &config_file)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -892,7 +851,7 @@ fn write_claude_config(
|
||||
|
||||
/// macOS: 根据用户首选终端启动
|
||||
#[cfg(target_os = "macos")]
|
||||
fn launch_macos_terminal(config_file: &std::path::Path, cwd: Option<&Path>) -> Result<(), String> {
|
||||
fn launch_macos_terminal(config_file: &std::path::Path) -> Result<(), String> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let preferred = crate::settings::get_preferred_terminal();
|
||||
@@ -901,21 +860,18 @@ fn launch_macos_terminal(config_file: &std::path::Path, cwd: Option<&Path>) -> R
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let script_file = temp_dir.join(format!("cc_switch_launcher_{}.sh", std::process::id()));
|
||||
let config_path = config_file.to_string_lossy();
|
||||
let cd_command = build_shell_cd_command(cwd);
|
||||
|
||||
// Write the shell script to a temp file
|
||||
let script_content = format!(
|
||||
r#"#!/bin/bash
|
||||
trap 'rm -f "{config_path}" "{script_file}"' EXIT
|
||||
{cd_command}
|
||||
echo "Using provider-specific claude config:"
|
||||
echo "{config_path}"
|
||||
claude --settings "{config_path}"
|
||||
exec bash --norc --noprofile
|
||||
"#,
|
||||
config_path = config_path,
|
||||
script_file = script_file.display(),
|
||||
cd_command = cd_command,
|
||||
script_file = script_file.display()
|
||||
);
|
||||
|
||||
std::fs::write(&script_file, &script_content).map_err(|e| format!("写入启动脚本失败: {e}"))?;
|
||||
@@ -932,6 +888,7 @@ exec bash --norc --noprofile
|
||||
"kitty" => launch_macos_open_app("kitty", &script_file, false),
|
||||
"ghostty" => launch_macos_open_app("Ghostty", &script_file, true),
|
||||
"wezterm" => launch_macos_open_app("WezTerm", &script_file, true),
|
||||
"tabby" => launch_macos_tabby(&script_file),
|
||||
_ => launch_macos_terminal_app(&script_file), // "terminal" or default
|
||||
};
|
||||
|
||||
@@ -1049,9 +1006,42 @@ fn launch_macos_open_app(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// macOS: Tabby
|
||||
#[cfg(target_os = "macos")]
|
||||
fn launch_macos_tabby(script_file: &std::path::Path) -> Result<(), String> {
|
||||
use std::process::Command;
|
||||
|
||||
let output = Command::new("open")
|
||||
.args(build_macos_tabby_args(script_file))
|
||||
.output()
|
||||
.map_err(|e| format!("启动 Tabby 失败: {e}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!(
|
||||
"Tabby 启动失败 (exit code: {:?}): {}",
|
||||
output.status.code(),
|
||||
stderr
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_macos_tabby_args(script_file: &std::path::Path) -> Vec<String> {
|
||||
vec![
|
||||
"-na".to_string(),
|
||||
"Tabby".to_string(),
|
||||
"--args".to_string(),
|
||||
"run".to_string(),
|
||||
"bash".to_string(),
|
||||
script_file.to_string_lossy().into_owned(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Linux: 根据用户首选终端启动
|
||||
#[cfg(target_os = "linux")]
|
||||
fn launch_linux_terminal(config_file: &std::path::Path, cwd: Option<&Path>) -> Result<(), String> {
|
||||
fn launch_linux_terminal(config_file: &std::path::Path) -> Result<(), String> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::process::Command;
|
||||
|
||||
@@ -1067,26 +1057,25 @@ fn launch_linux_terminal(config_file: &std::path::Path, cwd: Option<&Path>) -> R
|
||||
("alacritty", vec!["-e"]),
|
||||
("kitty", vec!["-e"]),
|
||||
("ghostty", vec!["-e"]),
|
||||
("tabby", vec!["run"]),
|
||||
("tabby-terminal", vec!["run"]),
|
||||
];
|
||||
|
||||
// Create temp script file
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let script_file = temp_dir.join(format!("cc_switch_launcher_{}.sh", std::process::id()));
|
||||
let config_path = config_file.to_string_lossy();
|
||||
let cd_command = build_shell_cd_command(cwd);
|
||||
|
||||
let script_content = format!(
|
||||
r#"#!/bin/bash
|
||||
trap 'rm -f "{config_path}" "{script_file}"' EXIT
|
||||
{cd_command}
|
||||
echo "Using provider-specific claude config:"
|
||||
echo "{config_path}"
|
||||
claude --settings "{config_path}"
|
||||
exec bash --norc --noprofile
|
||||
"#,
|
||||
config_path = config_path,
|
||||
script_file = script_file.display(),
|
||||
cd_command = cd_command,
|
||||
script_file = script_file.display()
|
||||
);
|
||||
|
||||
std::fs::write(&script_file, &script_content).map_err(|e| format!("写入启动脚本失败: {e}"))?;
|
||||
@@ -1165,35 +1154,28 @@ fn which_command(cmd: &str) -> bool {
|
||||
fn launch_windows_terminal(
|
||||
temp_dir: &std::path::Path,
|
||||
config_file: &std::path::Path,
|
||||
cwd: Option<&Path>,
|
||||
) -> Result<(), String> {
|
||||
let preferred = crate::settings::get_preferred_terminal();
|
||||
let terminal = preferred.as_deref().unwrap_or("cmd");
|
||||
|
||||
let bat_file = temp_dir.join(format!("cc_switch_claude_{}.bat", std::process::id()));
|
||||
let config_path_for_batch = escape_windows_batch_value(&config_file.to_string_lossy());
|
||||
let cwd_command = build_windows_cwd_command(cwd);
|
||||
let config_path_for_batch = config_file.to_string_lossy().replace('&', "^&");
|
||||
|
||||
let content = format!(
|
||||
"@echo off
|
||||
{cwd_command}
|
||||
echo Using provider-specific claude config:
|
||||
echo {}
|
||||
claude --settings \"{}\"
|
||||
del \"{}\" >nul 2>&1
|
||||
del \"%~f0\" >nul 2>&1
|
||||
",
|
||||
config_path_for_batch,
|
||||
config_path_for_batch,
|
||||
config_path_for_batch,
|
||||
cwd_command = cwd_command,
|
||||
config_path_for_batch, config_path_for_batch, config_path_for_batch
|
||||
);
|
||||
|
||||
std::fs::write(&bat_file, &content).map_err(|e| format!("写入批处理文件失败: {e}"))?;
|
||||
|
||||
let bat_path = bat_file.to_string_lossy();
|
||||
let bat_path_for_cmd = build_windows_cmd_command_str(&bat_path);
|
||||
let ps_cmd = format!("& '{}'", escape_powershell_single_quoted(&bat_path));
|
||||
let ps_cmd = format!("& '{}'", bat_path);
|
||||
|
||||
// Try the preferred terminal first
|
||||
let result = match terminal {
|
||||
@@ -1201,10 +1183,9 @@ del \"%~f0\" >nul 2>&1
|
||||
&["powershell", "-NoExit", "-Command", &ps_cmd],
|
||||
"PowerShell",
|
||||
),
|
||||
"wt" => {
|
||||
run_windows_start_command(&["wt", "cmd", "/K", &bat_path_for_cmd], "Windows Terminal")
|
||||
}
|
||||
_ => run_windows_start_command(&["cmd", "/K", &bat_path_for_cmd], "cmd"), // "cmd" or default
|
||||
"wt" => run_windows_start_command(&["wt", "cmd", "/K", &bat_path], "Windows Terminal"),
|
||||
"tabby" => run_windows_tabby_command(&bat_path),
|
||||
_ => run_windows_start_command(&["cmd", "/K", &bat_path], "cmd"), // "cmd" or default
|
||||
};
|
||||
|
||||
// If preferred terminal fails and it's not the default, try cmd as fallback
|
||||
@@ -1214,81 +1195,62 @@ del \"%~f0\" >nul 2>&1
|
||||
terminal,
|
||||
result.as_ref().err()
|
||||
);
|
||||
return run_windows_start_command(&["cmd", "/K", &bat_path_for_cmd], "cmd");
|
||||
return run_windows_start_command(&["cmd", "/K", &bat_path], "cmd");
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn build_shell_cd_command(cwd: Option<&Path>) -> String {
|
||||
cwd.map(|dir| {
|
||||
format!(
|
||||
"cd {} || exit 1\n",
|
||||
shell_single_quote(&dir.to_string_lossy())
|
||||
)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
fn run_windows_tabby_command(bat_path: &str) -> Result<(), String> {
|
||||
use std::process::Command;
|
||||
|
||||
fn shell_single_quote(value: &str) -> String {
|
||||
format!("'{}'", value.replace('\'', "'\"'\"'"))
|
||||
}
|
||||
let mut last_error = String::from("未找到可用的 Tabby 可执行文件");
|
||||
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
fn escape_powershell_single_quoted(value: &str) -> String {
|
||||
value.replace('\'', "''")
|
||||
}
|
||||
for candidate in windows_tabby_executable_candidates() {
|
||||
let result = Command::new(&candidate)
|
||||
.args(["run", "cmd", "/K", bat_path])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.spawn();
|
||||
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
fn build_windows_cmd_command_str(path: &str) -> String {
|
||||
// Avoid handing `cmd /K` a string that starts with a single quoted path:
|
||||
// per cmd.exe parsing rules, those outer quotes may be stripped when the
|
||||
// quoted text contains shell metacharacters. An explicit `call "..."` form
|
||||
// keeps the command from starting with a quote while still protecting
|
||||
// spaces and other special characters in the batch path.
|
||||
format!("call \"{}\"", escape_windows_cmd_quoted_path(path))
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
fn escape_windows_cmd_quoted_path(value: &str) -> String {
|
||||
value.replace('%', "%%")
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
fn is_windows_unc_path(path: &str) -> bool {
|
||||
path.starts_with(r"\\")
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
fn build_windows_cwd_command_str(path: &str) -> String {
|
||||
let escaped = escape_windows_batch_value(path);
|
||||
|
||||
if is_windows_unc_path(path) {
|
||||
// `cmd.exe` cannot make a UNC path current via `cd`; `pushd` maps it first.
|
||||
format!("pushd \"{escaped}\" || exit /b 1\r\n")
|
||||
} else {
|
||||
format!("cd /d \"{escaped}\" || exit /b 1\r\n")
|
||||
match result {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(e) => {
|
||||
last_error = format!("执行 {} 失败: {}", candidate.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(last_error)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn build_windows_cwd_command(cwd: Option<&Path>) -> String {
|
||||
cwd.map(|dir| build_windows_cwd_command_str(&dir.to_string_lossy()))
|
||||
.unwrap_or_default()
|
||||
fn windows_tabby_executable_candidates() -> Vec<std::path::PathBuf> {
|
||||
let mut candidates = Vec::new();
|
||||
|
||||
for candidate in ["tabby", "tabby.exe", "Tabby", "Tabby.exe"] {
|
||||
push_unique_path(&mut candidates, std::path::PathBuf::from(candidate));
|
||||
}
|
||||
|
||||
if let Some(local_app_data) = std::env::var_os("LOCALAPPDATA") {
|
||||
let tabby_dir = std::path::PathBuf::from(local_app_data)
|
||||
.join("Programs")
|
||||
.join("Tabby");
|
||||
push_unique_path(&mut candidates, tabby_dir.join("Tabby.exe"));
|
||||
push_unique_path(&mut candidates, tabby_dir.join("tabby.exe"));
|
||||
}
|
||||
|
||||
for env_key in ["ProgramFiles", "ProgramFiles(x86)"] {
|
||||
if let Some(base_dir) = std::env::var_os(env_key) {
|
||||
let tabby_dir = std::path::PathBuf::from(base_dir).join("Tabby");
|
||||
push_unique_path(&mut candidates, tabby_dir.join("Tabby.exe"));
|
||||
push_unique_path(&mut candidates, tabby_dir.join("tabby.exe"));
|
||||
}
|
||||
}
|
||||
|
||||
candidates
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
fn escape_windows_batch_value(value: &str) -> String {
|
||||
value
|
||||
.replace('^', "^^")
|
||||
.replace('%', "%%")
|
||||
.replace('&', "^&")
|
||||
.replace('|', "^|")
|
||||
.replace('<', "^<")
|
||||
.replace('>', "^>")
|
||||
.replace('(', "^(")
|
||||
.replace(')', "^)")
|
||||
}
|
||||
/// Windows: Run a start command with common error handling
|
||||
#[cfg(target_os = "windows")]
|
||||
fn run_windows_start_command(args: &[&str], terminal_name: &str) -> Result<(), String> {
|
||||
@@ -1463,76 +1425,4 @@ mod tests {
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_launch_cwd_accepts_existing_directory() {
|
||||
let resolved =
|
||||
resolve_launch_cwd(Some(std::env::temp_dir().to_string_lossy().into_owned()))
|
||||
.expect("temp dir should resolve")
|
||||
.expect("temp dir should be present");
|
||||
|
||||
assert!(resolved.is_dir());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_launch_cwd_rejects_missing_directory() {
|
||||
let unique = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.expect("clock should be after epoch")
|
||||
.as_nanos();
|
||||
let missing = std::env::temp_dir().join(format!("cc-switch-missing-{unique}"));
|
||||
|
||||
let error = resolve_launch_cwd(Some(missing.to_string_lossy().into_owned()))
|
||||
.expect_err("missing directory should fail");
|
||||
|
||||
assert!(error.contains("目录不存在"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_shell_cd_command_quotes_spaces_and_single_quotes() {
|
||||
let command = build_shell_cd_command(Some(Path::new("/tmp/project O'Brien")));
|
||||
|
||||
assert_eq!(command, "cd '/tmp/project O'\"'\"'Brien' || exit 1\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_powershell_single_quoted_doubles_embedded_quotes() {
|
||||
let escaped = escape_powershell_single_quoted(r"C:\Users\O'Brien\AppData\Local\Temp");
|
||||
|
||||
assert_eq!(escaped, r"C:\Users\O''Brien\AppData\Local\Temp");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_windows_cmd_command_str_quotes_and_escapes_metacharacters() {
|
||||
let command = build_windows_cmd_command_str(r"C:\Users\100%&(test)\cc switch.bat");
|
||||
|
||||
assert_eq!(command, "call \"C:\\Users\\100%%&(test)\\cc switch.bat\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_windows_cwd_command_str_uses_cd_for_drive_paths() {
|
||||
let command = build_windows_cwd_command_str(r"C:\work\repo");
|
||||
|
||||
assert_eq!(command, "cd /d \"C:\\work\\repo\" || exit /b 1\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_windows_cwd_command_str_uses_pushd_for_unc_paths() {
|
||||
let command = build_windows_cwd_command_str(r"\\wsl$\Ubuntu\home\coder\repo");
|
||||
|
||||
assert_eq!(
|
||||
command,
|
||||
"pushd \"\\\\wsl$\\Ubuntu\\home\\coder\\repo\" || exit /b 1\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_windows_cwd_command_str_escapes_batch_metacharacters() {
|
||||
let command = build_windows_cwd_command_str(r"\\server\share\100%&(test)");
|
||||
|
||||
assert_eq!(
|
||||
command,
|
||||
"pushd \"\\\\server\\share\\100%%^&^(test^)\" || exit /b 1\r\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -974,9 +974,7 @@ mod tests {
|
||||
"data: {\"type\":\"response.completed\",\"response\":{\"status\":\"completed\",\"usage\":{\"input_tokens\":5,\"output_tokens\":2}}}\n\n"
|
||||
);
|
||||
|
||||
let upstream = stream::iter(vec![Ok::<_, std::io::Error>(Bytes::from(
|
||||
input.as_bytes().to_vec(),
|
||||
))]);
|
||||
let upstream = stream::iter(vec![Ok(Bytes::from(input.as_bytes().to_vec()))]);
|
||||
let converted = create_anthropic_sse_stream_from_responses(upstream);
|
||||
let chunks: Vec<_> = converted.collect().await;
|
||||
let events: Vec<Value> = chunks
|
||||
|
||||
@@ -21,6 +21,7 @@ pub fn launch_terminal(
|
||||
"kitty" => launch_kitty(command, cwd),
|
||||
"wezterm" => launch_wezterm(command, cwd),
|
||||
"alacritty" => launch_alacritty(command, cwd),
|
||||
"tabby" => launch_tabby(command, cwd),
|
||||
"custom" => launch_custom(command, cwd, custom_config),
|
||||
_ => Err(format!("Unsupported terminal target: {target}")),
|
||||
}
|
||||
@@ -211,6 +212,35 @@ fn launch_alacritty(command: &str, cwd: Option<&str>) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn launch_tabby(command: &str, cwd: Option<&str>) -> Result<(), String> {
|
||||
let status = Command::new("open")
|
||||
.args(build_tabby_args(command, cwd))
|
||||
.status()
|
||||
.map_err(|e| format!("Failed to launch Tabby: {e}"))?;
|
||||
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Failed to launch Tabby.".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn build_tabby_args(command: &str, cwd: Option<&str>) -> Vec<String> {
|
||||
let full_command = build_shell_command(command, cwd);
|
||||
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string());
|
||||
|
||||
vec![
|
||||
"-na".to_string(),
|
||||
"Tabby".to_string(),
|
||||
"--args".to_string(),
|
||||
"run".to_string(),
|
||||
shell,
|
||||
"-l".to_string(),
|
||||
"-c".to_string(),
|
||||
full_command,
|
||||
]
|
||||
}
|
||||
|
||||
fn launch_custom(
|
||||
command: &str,
|
||||
cwd: Option<&str>,
|
||||
@@ -305,4 +335,44 @@ mod tests {
|
||||
"raw:echo foo\\\\\\\\bar\\npwd\\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tabby_uses_login_shell_for_resume_commands() {
|
||||
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string());
|
||||
let args = build_tabby_args("claude --resume abc-123", Some("/tmp/project dir"));
|
||||
|
||||
assert_eq!(
|
||||
args,
|
||||
vec![
|
||||
"-na",
|
||||
"Tabby",
|
||||
"--args",
|
||||
"run",
|
||||
&shell,
|
||||
"-l",
|
||||
"-c",
|
||||
"cd \"/tmp/project dir\" && claude --resume abc-123",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tabby_keeps_command_when_no_cwd_is_provided() {
|
||||
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string());
|
||||
let args = build_tabby_args("claude --resume abc-123", None);
|
||||
|
||||
assert_eq!(
|
||||
args,
|
||||
vec![
|
||||
"-na",
|
||||
"Tabby",
|
||||
"--args",
|
||||
"run",
|
||||
&shell,
|
||||
"-l",
|
||||
"-c",
|
||||
"claude --resume abc-123",
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,9 +264,9 @@ pub struct AppSettings {
|
||||
|
||||
// ===== 终端设置 =====
|
||||
/// 首选终端应用(可选,默认使用系统默认终端)
|
||||
/// - macOS: "terminal" | "iterm2" | "warp" | "alacritty" | "kitty" | "ghostty"
|
||||
/// - Windows: "cmd" | "powershell" | "wt" (Windows Terminal)
|
||||
/// - Linux: "gnome-terminal" | "konsole" | "xfce4-terminal" | "alacritty" | "kitty" | "ghostty"
|
||||
/// - macOS: "terminal" | "iterm2" | "alacritty" | "kitty" | "ghostty" | "wezterm" | "tabby"
|
||||
/// - Windows: "cmd" | "powershell" | "wt" (Windows Terminal) | "tabby"
|
||||
/// - Linux: "gnome-terminal" | "konsole" | "xfce4-terminal" | "alacritty" | "kitty" | "ghostty" | "tabby"
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub preferred_terminal: Option<String>,
|
||||
}
|
||||
|
||||
@@ -471,8 +471,10 @@ pub fn handle_tray_menu_event(app: &tauri::AppHandle, event_id: &str) {
|
||||
if let Err(e) = crate::lightweight::exit_lightweight_mode(app) {
|
||||
log::error!("退出轻量模式失败: {e}");
|
||||
}
|
||||
} else if let Err(e) = crate::lightweight::enter_lightweight_mode(app) {
|
||||
log::error!("进入轻量模式失败: {e}");
|
||||
} else {
|
||||
if let Err(e) = crate::lightweight::enter_lightweight_mode(app) {
|
||||
log::error!("进入轻量模式失败: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
"quit" => {
|
||||
|
||||
+1
-8
@@ -648,14 +648,7 @@ function App() {
|
||||
|
||||
const handleOpenTerminal = async (provider: Provider) => {
|
||||
try {
|
||||
const selectedDir = await settingsApi.pickDirectory();
|
||||
if (!selectedDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
await providersApi.openTerminal(provider.id, activeApp, {
|
||||
cwd: selectedDir,
|
||||
});
|
||||
await providersApi.openTerminal(provider.id, activeApp);
|
||||
toast.success(
|
||||
t("provider.terminalOpened", {
|
||||
defaultValue: "终端已打开",
|
||||
|
||||
@@ -16,6 +16,7 @@ const MACOS_TERMINALS = [
|
||||
{ value: "kitty", labelKey: "settings.terminal.options.macos.kitty" },
|
||||
{ value: "ghostty", labelKey: "settings.terminal.options.macos.ghostty" },
|
||||
{ value: "wezterm", labelKey: "settings.terminal.options.macos.wezterm" },
|
||||
{ value: "tabby", labelKey: "settings.terminal.options.macos.tabby" },
|
||||
] as const;
|
||||
|
||||
const WINDOWS_TERMINALS = [
|
||||
@@ -25,6 +26,7 @@ const WINDOWS_TERMINALS = [
|
||||
labelKey: "settings.terminal.options.windows.powershell",
|
||||
},
|
||||
{ value: "wt", labelKey: "settings.terminal.options.windows.wt" },
|
||||
{ value: "tabby", labelKey: "settings.terminal.options.windows.tabby" },
|
||||
] as const;
|
||||
|
||||
const LINUX_TERMINALS = [
|
||||
@@ -40,6 +42,7 @@ const LINUX_TERMINALS = [
|
||||
{ value: "alacritty", labelKey: "settings.terminal.options.linux.alacritty" },
|
||||
{ value: "kitty", labelKey: "settings.terminal.options.linux.kitty" },
|
||||
{ value: "ghostty", labelKey: "settings.terminal.options.linux.ghostty" },
|
||||
{ value: "tabby", labelKey: "settings.terminal.options.linux.tabby" },
|
||||
] as const;
|
||||
|
||||
// Get terminals for the current platform
|
||||
|
||||
@@ -496,12 +496,14 @@
|
||||
"alacritty": "Alacritty",
|
||||
"kitty": "Kitty",
|
||||
"ghostty": "Ghostty",
|
||||
"wezterm": "WezTerm"
|
||||
"wezterm": "WezTerm",
|
||||
"tabby": "Tabby"
|
||||
},
|
||||
"windows": {
|
||||
"cmd": "Command Prompt",
|
||||
"powershell": "PowerShell",
|
||||
"wt": "Windows Terminal"
|
||||
"wt": "Windows Terminal",
|
||||
"tabby": "Tabby"
|
||||
},
|
||||
"linux": {
|
||||
"gnomeTerminal": "GNOME Terminal",
|
||||
@@ -509,7 +511,8 @@
|
||||
"xfce4Terminal": "Xfce4 Terminal",
|
||||
"alacritty": "Alacritty",
|
||||
"kitty": "Kitty",
|
||||
"ghostty": "Ghostty"
|
||||
"ghostty": "Ghostty",
|
||||
"tabby": "Tabby"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -496,12 +496,14 @@
|
||||
"alacritty": "Alacritty",
|
||||
"kitty": "Kitty",
|
||||
"ghostty": "Ghostty",
|
||||
"wezterm": "WezTerm"
|
||||
"wezterm": "WezTerm",
|
||||
"tabby": "Tabby"
|
||||
},
|
||||
"windows": {
|
||||
"cmd": "コマンドプロンプト",
|
||||
"powershell": "PowerShell",
|
||||
"wt": "Windows Terminal"
|
||||
"wt": "Windows Terminal",
|
||||
"tabby": "Tabby"
|
||||
},
|
||||
"linux": {
|
||||
"gnomeTerminal": "GNOME Terminal",
|
||||
@@ -509,7 +511,8 @@
|
||||
"xfce4Terminal": "Xfce4 Terminal",
|
||||
"alacritty": "Alacritty",
|
||||
"kitty": "Kitty",
|
||||
"ghostty": "Ghostty"
|
||||
"ghostty": "Ghostty",
|
||||
"tabby": "Tabby"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -496,12 +496,14 @@
|
||||
"alacritty": "Alacritty",
|
||||
"kitty": "Kitty",
|
||||
"ghostty": "Ghostty",
|
||||
"wezterm": "WezTerm"
|
||||
"wezterm": "WezTerm",
|
||||
"tabby": "Tabby"
|
||||
},
|
||||
"windows": {
|
||||
"cmd": "命令提示符",
|
||||
"powershell": "PowerShell",
|
||||
"wt": "Windows Terminal"
|
||||
"wt": "Windows Terminal",
|
||||
"tabby": "Tabby"
|
||||
},
|
||||
"linux": {
|
||||
"gnomeTerminal": "GNOME Terminal",
|
||||
@@ -509,7 +511,8 @@
|
||||
"xfce4Terminal": "Xfce4 Terminal",
|
||||
"alacritty": "Alacritty",
|
||||
"kitty": "Kitty",
|
||||
"ghostty": "Ghostty"
|
||||
"ghostty": "Ghostty",
|
||||
"tabby": "Tabby"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -21,10 +21,6 @@ export interface SwitchResult {
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface OpenTerminalOptions {
|
||||
cwd?: string;
|
||||
}
|
||||
|
||||
export const providersApi = {
|
||||
async getAll(appId: AppId): Promise<Record<string, Provider>> {
|
||||
return await invoke("get_providers", { app: appId });
|
||||
@@ -87,17 +83,8 @@ export const providersApi = {
|
||||
* 任何提供商都可以打开终端,不受是否为当前激活提供商的限制
|
||||
* 终端会使用该提供商特定的 API 配置,不影响全局设置
|
||||
*/
|
||||
async openTerminal(
|
||||
providerId: string,
|
||||
appId: AppId,
|
||||
options?: OpenTerminalOptions,
|
||||
): Promise<boolean> {
|
||||
const { cwd } = options ?? {};
|
||||
return await invoke("open_provider_terminal", {
|
||||
providerId,
|
||||
app: appId,
|
||||
cwd,
|
||||
});
|
||||
async openTerminal(providerId: string, appId: AppId): Promise<boolean> {
|
||||
return await invoke("open_provider_terminal", { providerId, app: appId });
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -47,10 +47,6 @@ export const settingsApi = {
|
||||
await invoke("open_config_folder", { app: appId });
|
||||
},
|
||||
|
||||
async pickDirectory(defaultPath?: string): Promise<string | null> {
|
||||
return await invoke("pick_directory", { defaultPath });
|
||||
},
|
||||
|
||||
async selectConfigDirectory(defaultPath?: string): Promise<string | null> {
|
||||
return await invoke("pick_directory", { defaultPath });
|
||||
},
|
||||
|
||||
+3
-3
@@ -303,9 +303,9 @@ export interface Settings {
|
||||
|
||||
// ===== 终端设置 =====
|
||||
// 首选终端应用(可选,默认使用系统默认终端)
|
||||
// macOS: "terminal" | "iterm2" | "warp" | "alacritty" | "kitty" | "ghostty"
|
||||
// Windows: "cmd" | "powershell" | "wt"
|
||||
// Linux: "gnome-terminal" | "konsole" | "xfce4-terminal" | "alacritty" | "kitty" | "ghostty"
|
||||
// macOS: "terminal" | "iterm2" | "alacritty" | "kitty" | "ghostty" | "wezterm" | "tabby"
|
||||
// Windows: "cmd" | "powershell" | "wt" | "tabby"
|
||||
// Linux: "gnome-terminal" | "konsole" | "xfce4-terminal" | "alacritty" | "kitty" | "ghostty" | "tabby"
|
||||
preferredTerminal?: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user