Compare commits

..

1 Commits

Author SHA1 Message Date
YoVinchen 9620a8c769 feat(terminal): add Tabby terminal support on macOS, Windows, and Linux
Add Tabby (https://github.com/Eugeny/tabby) as a preferred terminal option
across all three platforms:

- macOS: launch via `open -na Tabby --args run` with login shell
- Windows: search multiple executable candidates (PATH, LOCALAPPDATA,
  ProgramFiles) and launch via `tabby run cmd /K`
- Linux: add `tabby` and `tabby-terminal` to the auto-detection list

Also includes:
- Session manager integration with login-shell command wrapping
- Terminal selector UI entries for all platforms
- i18n labels (zh/en/ja)
- Unit tests for Tabby arg building (with/without cwd)

Closes https://github.com/farion1231/cc-switch/issues/1708
2026-03-30 00:43:55 +08:00
13 changed files with 198 additions and 250 deletions
+93 -203
View File
@@ -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",
]
);
}
}
+3 -3
View File
@@ -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>,
}
+4 -2
View File
@@ -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
View File
@@ -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
+6 -3
View File
@@ -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"
}
}
},
+6 -3
View File
@@ -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"
}
}
},
+6 -3
View File
@@ -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"
}
}
},
+2 -15
View File
@@ -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 });
},
/**
-4
View File
@@ -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
View File
@@ -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;
}