mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
feat(terminal): add directory picker before launching Claude terminal (#1752)
* Add directory picker before launching Claude terminal * fix(terminal): preserve cwd path and strip Windows verbatim prefix - Stop trimming non-empty paths so directories with leading/trailing spaces on Unix are handled correctly - Strip \\?\ extended-length prefix from canonicalized paths on Windows to prevent batch script cd failures * fix(terminal): restore UNC paths when stripping Windows verbatim prefix Handle \\?\UNC\server\share form separately from regular \\?\ prefix, converting it back to \\server\share so network/WSL directory paths remain valid in batch cd commands. * fix(terminal): use pushd for UNC paths in Windows batch launcher `cmd.exe` cannot set a UNC path (e.g. `\\wsl$\...`) as the current directory via `cd /d`; it errors with "CMD does not support UNC paths as current directories". Switch to `pushd` which temporarily maps the UNC share to a drive letter. Rename `build_windows_cd_command` → `build_windows_cwd_command` to reflect the broader semantics. Extract `build_windows_cwd_command_str` and `is_windows_unc_path` helpers for testability, and add unit tests covering drive paths, UNC paths, and batch metacharacter escaping. Also fix minor style issues: sort mod declarations alphabetically, add missing EOF newline in lightweight.rs, add explicit type annotation in streaming_responses test, and reformat tray menu builder chain.
This commit is contained in:
+171
-11
@@ -6,7 +6,7 @@ use crate::services::ProviderService;
|
|||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
@@ -720,8 +720,10 @@ pub async fn open_provider_terminal(
|
|||||||
state: State<'_, crate::store::AppState>,
|
state: State<'_, crate::store::AppState>,
|
||||||
app: String,
|
app: String,
|
||||||
#[allow(non_snake_case)] providerId: String,
|
#[allow(non_snake_case)] providerId: String,
|
||||||
|
cwd: Option<String>,
|
||||||
) -> Result<bool, String> {
|
) -> Result<bool, String> {
|
||||||
let app_type = AppType::from_str(&app).map_err(|e| e.to_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())
|
let providers = ProviderService::list(state.inner(), app_type.clone())
|
||||||
@@ -736,7 +738,8 @@ pub async fn open_provider_terminal(
|
|||||||
let env_vars = extract_env_vars_from_config(config, &app_type);
|
let env_vars = extract_env_vars_from_config(config, &app_type);
|
||||||
|
|
||||||
// 根据平台启动终端,传入提供商ID用于生成唯一的配置文件名
|
// 根据平台启动终端,传入提供商ID用于生成唯一的配置文件名
|
||||||
launch_terminal_with_env(env_vars, &providerId).map_err(|e| format!("启动终端失败: {e}"))?;
|
launch_terminal_with_env(env_vars, &providerId, launch_cwd.as_deref())
|
||||||
|
.map_err(|e| format!("启动终端失败: {e}"))?;
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
@@ -791,11 +794,49 @@ fn extract_env_vars_from_config(
|
|||||||
env_vars
|
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 终端
|
/// 创建临时配置文件并启动 claude 终端
|
||||||
/// 使用 --settings 参数传入提供商特定的 API 配置
|
/// 使用 --settings 参数传入提供商特定的 API 配置
|
||||||
fn launch_terminal_with_env(
|
fn launch_terminal_with_env(
|
||||||
env_vars: Vec<(String, String)>,
|
env_vars: Vec<(String, String)>,
|
||||||
provider_id: &str,
|
provider_id: &str,
|
||||||
|
cwd: Option<&Path>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let temp_dir = std::env::temp_dir();
|
let temp_dir = std::env::temp_dir();
|
||||||
let config_file = temp_dir.join(format!(
|
let config_file = temp_dir.join(format!(
|
||||||
@@ -809,19 +850,19 @@ fn launch_terminal_with_env(
|
|||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
launch_macos_terminal(&config_file)?;
|
launch_macos_terminal(&config_file, cwd)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
launch_linux_terminal(&config_file)?;
|
launch_linux_terminal(&config_file, cwd)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
launch_windows_terminal(&temp_dir, &config_file)?;
|
launch_windows_terminal(&temp_dir, &config_file, cwd)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -851,7 +892,7 @@ fn write_claude_config(
|
|||||||
|
|
||||||
/// macOS: 根据用户首选终端启动
|
/// macOS: 根据用户首选终端启动
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
fn launch_macos_terminal(config_file: &std::path::Path) -> Result<(), String> {
|
fn launch_macos_terminal(config_file: &std::path::Path, cwd: Option<&Path>) -> Result<(), String> {
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
let preferred = crate::settings::get_preferred_terminal();
|
let preferred = crate::settings::get_preferred_terminal();
|
||||||
@@ -860,18 +901,21 @@ fn launch_macos_terminal(config_file: &std::path::Path) -> Result<(), String> {
|
|||||||
let temp_dir = std::env::temp_dir();
|
let temp_dir = std::env::temp_dir();
|
||||||
let script_file = temp_dir.join(format!("cc_switch_launcher_{}.sh", std::process::id()));
|
let script_file = temp_dir.join(format!("cc_switch_launcher_{}.sh", std::process::id()));
|
||||||
let config_path = config_file.to_string_lossy();
|
let config_path = config_file.to_string_lossy();
|
||||||
|
let cd_command = build_shell_cd_command(cwd);
|
||||||
|
|
||||||
// Write the shell script to a temp file
|
// Write the shell script to a temp file
|
||||||
let script_content = format!(
|
let script_content = format!(
|
||||||
r#"#!/bin/bash
|
r#"#!/bin/bash
|
||||||
trap 'rm -f "{config_path}" "{script_file}"' EXIT
|
trap 'rm -f "{config_path}" "{script_file}"' EXIT
|
||||||
|
{cd_command}
|
||||||
echo "Using provider-specific claude config:"
|
echo "Using provider-specific claude config:"
|
||||||
echo "{config_path}"
|
echo "{config_path}"
|
||||||
claude --settings "{config_path}"
|
claude --settings "{config_path}"
|
||||||
exec bash --norc --noprofile
|
exec bash --norc --noprofile
|
||||||
"#,
|
"#,
|
||||||
config_path = config_path,
|
config_path = config_path,
|
||||||
script_file = script_file.display()
|
script_file = script_file.display(),
|
||||||
|
cd_command = cd_command,
|
||||||
);
|
);
|
||||||
|
|
||||||
std::fs::write(&script_file, &script_content).map_err(|e| format!("写入启动脚本失败: {e}"))?;
|
std::fs::write(&script_file, &script_content).map_err(|e| format!("写入启动脚本失败: {e}"))?;
|
||||||
@@ -1007,7 +1051,7 @@ fn launch_macos_open_app(
|
|||||||
|
|
||||||
/// Linux: 根据用户首选终端启动
|
/// Linux: 根据用户首选终端启动
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
fn launch_linux_terminal(config_file: &std::path::Path) -> Result<(), String> {
|
fn launch_linux_terminal(config_file: &std::path::Path, cwd: Option<&Path>) -> Result<(), String> {
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
@@ -1029,17 +1073,20 @@ fn launch_linux_terminal(config_file: &std::path::Path) -> Result<(), String> {
|
|||||||
let temp_dir = std::env::temp_dir();
|
let temp_dir = std::env::temp_dir();
|
||||||
let script_file = temp_dir.join(format!("cc_switch_launcher_{}.sh", std::process::id()));
|
let script_file = temp_dir.join(format!("cc_switch_launcher_{}.sh", std::process::id()));
|
||||||
let config_path = config_file.to_string_lossy();
|
let config_path = config_file.to_string_lossy();
|
||||||
|
let cd_command = build_shell_cd_command(cwd);
|
||||||
|
|
||||||
let script_content = format!(
|
let script_content = format!(
|
||||||
r#"#!/bin/bash
|
r#"#!/bin/bash
|
||||||
trap 'rm -f "{config_path}" "{script_file}"' EXIT
|
trap 'rm -f "{config_path}" "{script_file}"' EXIT
|
||||||
|
{cd_command}
|
||||||
echo "Using provider-specific claude config:"
|
echo "Using provider-specific claude config:"
|
||||||
echo "{config_path}"
|
echo "{config_path}"
|
||||||
claude --settings "{config_path}"
|
claude --settings "{config_path}"
|
||||||
exec bash --norc --noprofile
|
exec bash --norc --noprofile
|
||||||
"#,
|
"#,
|
||||||
config_path = config_path,
|
config_path = config_path,
|
||||||
script_file = script_file.display()
|
script_file = script_file.display(),
|
||||||
|
cd_command = cd_command,
|
||||||
);
|
);
|
||||||
|
|
||||||
std::fs::write(&script_file, &script_content).map_err(|e| format!("写入启动脚本失败: {e}"))?;
|
std::fs::write(&script_file, &script_content).map_err(|e| format!("写入启动脚本失败: {e}"))?;
|
||||||
@@ -1118,22 +1165,28 @@ fn which_command(cmd: &str) -> bool {
|
|||||||
fn launch_windows_terminal(
|
fn launch_windows_terminal(
|
||||||
temp_dir: &std::path::Path,
|
temp_dir: &std::path::Path,
|
||||||
config_file: &std::path::Path,
|
config_file: &std::path::Path,
|
||||||
|
cwd: Option<&Path>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let preferred = crate::settings::get_preferred_terminal();
|
let preferred = crate::settings::get_preferred_terminal();
|
||||||
let terminal = preferred.as_deref().unwrap_or("cmd");
|
let terminal = preferred.as_deref().unwrap_or("cmd");
|
||||||
|
|
||||||
let bat_file = temp_dir.join(format!("cc_switch_claude_{}.bat", std::process::id()));
|
let bat_file = temp_dir.join(format!("cc_switch_claude_{}.bat", std::process::id()));
|
||||||
let config_path_for_batch = config_file.to_string_lossy().replace('&', "^&");
|
let config_path_for_batch = escape_windows_batch_value(&config_file.to_string_lossy());
|
||||||
|
let cwd_command = build_windows_cwd_command(cwd);
|
||||||
|
|
||||||
let content = format!(
|
let content = format!(
|
||||||
"@echo off
|
"@echo off
|
||||||
|
{cwd_command}
|
||||||
echo Using provider-specific claude config:
|
echo Using provider-specific claude config:
|
||||||
echo {}
|
echo {}
|
||||||
claude --settings \"{}\"
|
claude --settings \"{}\"
|
||||||
del \"{}\" >nul 2>&1
|
del \"{}\" >nul 2>&1
|
||||||
del \"%~f0\" >nul 2>&1
|
del \"%~f0\" >nul 2>&1
|
||||||
",
|
",
|
||||||
config_path_for_batch, config_path_for_batch, config_path_for_batch
|
config_path_for_batch,
|
||||||
|
config_path_for_batch,
|
||||||
|
config_path_for_batch,
|
||||||
|
cwd_command = cwd_command,
|
||||||
);
|
);
|
||||||
|
|
||||||
std::fs::write(&bat_file, &content).map_err(|e| format!("写入批处理文件失败: {e}"))?;
|
std::fs::write(&bat_file, &content).map_err(|e| format!("写入批处理文件失败: {e}"))?;
|
||||||
@@ -1164,6 +1217,55 @@ del \"%~f0\" >nul 2>&1
|
|||||||
result
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shell_single_quote(value: &str) -> String {
|
||||||
|
format!("'{}'", 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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
|
/// Windows: Run a start command with common error handling
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
fn run_windows_start_command(args: &[&str], terminal_name: &str) -> Result<(), String> {
|
fn run_windows_start_command(args: &[&str], terminal_name: &str) -> Result<(), String> {
|
||||||
@@ -1338,4 +1440,62 @@ 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 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"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ pub mod skill;
|
|||||||
mod stream_check;
|
mod stream_check;
|
||||||
mod sync_support;
|
mod sync_support;
|
||||||
|
|
||||||
|
mod lightweight;
|
||||||
mod usage;
|
mod usage;
|
||||||
mod webdav_sync;
|
mod webdav_sync;
|
||||||
mod workspace;
|
mod workspace;
|
||||||
mod lightweight;
|
|
||||||
|
|
||||||
pub use auth::*;
|
pub use auth::*;
|
||||||
pub use config::*;
|
pub use config::*;
|
||||||
@@ -48,7 +48,7 @@ pub use settings::*;
|
|||||||
pub use skill::*;
|
pub use skill::*;
|
||||||
pub use stream_check::*;
|
pub use stream_check::*;
|
||||||
|
|
||||||
|
pub use lightweight::*;
|
||||||
pub use usage::*;
|
pub use usage::*;
|
||||||
pub use webdav_sync::*;
|
pub use webdav_sync::*;
|
||||||
pub use workspace::*;
|
pub use workspace::*;
|
||||||
pub use lightweight::*;
|
|
||||||
|
|||||||
@@ -87,4 +87,4 @@ pub fn exit_lightweight_mode(app: &tauri::AppHandle) -> Result<(), String> {
|
|||||||
|
|
||||||
pub fn is_lightweight_mode() -> bool {
|
pub fn is_lightweight_mode() -> bool {
|
||||||
LIGHTWEIGHT_MODE.load(Ordering::Acquire)
|
LIGHTWEIGHT_MODE.load(Ordering::Acquire)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -974,7 +974,9 @@ mod tests {
|
|||||||
"data: {\"type\":\"response.completed\",\"response\":{\"status\":\"completed\",\"usage\":{\"input_tokens\":5,\"output_tokens\":2}}}\n\n"
|
"data: {\"type\":\"response.completed\",\"response\":{\"status\":\"completed\",\"usage\":{\"input_tokens\":5,\"output_tokens\":2}}}\n\n"
|
||||||
);
|
);
|
||||||
|
|
||||||
let upstream = stream::iter(vec![Ok(Bytes::from(input.as_bytes().to_vec()))]);
|
let upstream = stream::iter(vec![Ok::<_, std::io::Error>(Bytes::from(
|
||||||
|
input.as_bytes().to_vec(),
|
||||||
|
))]);
|
||||||
let converted = create_anthropic_sse_stream_from_responses(upstream);
|
let converted = create_anthropic_sse_stream_from_responses(upstream);
|
||||||
let chunks: Vec<_> = converted.collect().await;
|
let chunks: Vec<_> = converted.collect().await;
|
||||||
let events: Vec<Value> = chunks
|
let events: Vec<Value> = chunks
|
||||||
|
|||||||
@@ -393,11 +393,10 @@ pub fn create_tray_menu(
|
|||||||
true,
|
true,
|
||||||
crate::lightweight::is_lightweight_mode(),
|
crate::lightweight::is_lightweight_mode(),
|
||||||
None::<&str>,
|
None::<&str>,
|
||||||
).map_err(|e| AppError::Message(format!("创建轻量模式菜单失败: {e}")))?;
|
)
|
||||||
|
.map_err(|e| AppError::Message(format!("创建轻量模式菜单失败: {e}")))?;
|
||||||
|
|
||||||
menu_builder = menu_builder
|
menu_builder = menu_builder.item(&lightweight_item).separator();
|
||||||
.item(&lightweight_item)
|
|
||||||
.separator();
|
|
||||||
|
|
||||||
// 退出菜单(分隔符已在上面的 section 循环中添加)
|
// 退出菜单(分隔符已在上面的 section 循环中添加)
|
||||||
let quit_item = MenuItem::with_id(app, "quit", tray_texts.quit, true, None::<&str>)
|
let quit_item = MenuItem::with_id(app, "quit", tray_texts.quit, true, None::<&str>)
|
||||||
@@ -472,10 +471,8 @@ pub fn handle_tray_menu_event(app: &tauri::AppHandle, event_id: &str) {
|
|||||||
if let Err(e) = crate::lightweight::exit_lightweight_mode(app) {
|
if let Err(e) = crate::lightweight::exit_lightweight_mode(app) {
|
||||||
log::error!("退出轻量模式失败: {e}");
|
log::error!("退出轻量模式失败: {e}");
|
||||||
}
|
}
|
||||||
} else {
|
} else if let Err(e) = crate::lightweight::enter_lightweight_mode(app) {
|
||||||
if let Err(e) = crate::lightweight::enter_lightweight_mode(app) {
|
log::error!("进入轻量模式失败: {e}");
|
||||||
log::error!("进入轻量模式失败: {e}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"quit" => {
|
"quit" => {
|
||||||
|
|||||||
+8
-1
@@ -648,7 +648,14 @@ function App() {
|
|||||||
|
|
||||||
const handleOpenTerminal = async (provider: Provider) => {
|
const handleOpenTerminal = async (provider: Provider) => {
|
||||||
try {
|
try {
|
||||||
await providersApi.openTerminal(provider.id, activeApp);
|
const selectedDir = await settingsApi.pickDirectory();
|
||||||
|
if (!selectedDir) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await providersApi.openTerminal(provider.id, activeApp, {
|
||||||
|
cwd: selectedDir,
|
||||||
|
});
|
||||||
toast.success(
|
toast.success(
|
||||||
t("provider.terminalOpened", {
|
t("provider.terminalOpened", {
|
||||||
defaultValue: "终端已打开",
|
defaultValue: "终端已打开",
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ export interface SwitchResult {
|
|||||||
warnings: string[];
|
warnings: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OpenTerminalOptions {
|
||||||
|
cwd?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const providersApi = {
|
export const providersApi = {
|
||||||
async getAll(appId: AppId): Promise<Record<string, Provider>> {
|
async getAll(appId: AppId): Promise<Record<string, Provider>> {
|
||||||
return await invoke("get_providers", { app: appId });
|
return await invoke("get_providers", { app: appId });
|
||||||
@@ -83,8 +87,17 @@ export const providersApi = {
|
|||||||
* 任何提供商都可以打开终端,不受是否为当前激活提供商的限制
|
* 任何提供商都可以打开终端,不受是否为当前激活提供商的限制
|
||||||
* 终端会使用该提供商特定的 API 配置,不影响全局设置
|
* 终端会使用该提供商特定的 API 配置,不影响全局设置
|
||||||
*/
|
*/
|
||||||
async openTerminal(providerId: string, appId: AppId): Promise<boolean> {
|
async openTerminal(
|
||||||
return await invoke("open_provider_terminal", { providerId, app: appId });
|
providerId: string,
|
||||||
|
appId: AppId,
|
||||||
|
options?: OpenTerminalOptions,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const { cwd } = options ?? {};
|
||||||
|
return await invoke("open_provider_terminal", {
|
||||||
|
providerId,
|
||||||
|
app: appId,
|
||||||
|
cwd,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ export const settingsApi = {
|
|||||||
await invoke("open_config_folder", { app: appId });
|
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> {
|
async selectConfigDirectory(defaultPath?: string): Promise<string | null> {
|
||||||
return await invoke("pick_directory", { defaultPath });
|
return await invoke("pick_directory", { defaultPath });
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user