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:
YoVinchen
2026-03-30 23:05:28 +08:00
parent f5320dbdd2
commit 9ab54c93ef
5 changed files with 62 additions and 24 deletions
+51 -12
View File
@@ -1172,11 +1172,11 @@ fn launch_windows_terminal(
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 cd_command = build_windows_cd_command(cwd);
let cwd_command = build_windows_cwd_command(cwd);
let content = format!(
"@echo off
{cd_command}
{cwd_command}
echo Using provider-specific claude config:
echo {}
claude --settings \"{}\"
@@ -1186,7 +1186,7 @@ del \"%~f0\" >nul 2>&1
config_path_for_batch,
config_path_for_batch,
config_path_for_batch,
cd_command = cd_command,
cwd_command = cwd_command,
);
std::fs::write(&bat_file, &content).map_err(|e| format!("写入批处理文件失败: {e}"))?;
@@ -1231,18 +1231,30 @@ fn shell_single_quote(value: &str) -> String {
format!("'{}'", value.replace('\'', "'\"'\"'"))
}
#[cfg(target_os = "windows")]
fn build_windows_cd_command(cwd: Option<&Path>) -> String {
cwd.map(|dir| {
format!(
"cd /d \"{}\" || exit /b 1\r\n",
escape_windows_batch_value(&dir.to_string_lossy())
)
})
.unwrap_or_default()
#[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('^', "^^")
@@ -1459,4 +1471,31 @@ mod tests {
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"
);
}
}
+2 -2
View File
@@ -22,10 +22,10 @@ pub mod skill;
mod stream_check;
mod sync_support;
mod lightweight;
mod usage;
mod webdav_sync;
mod workspace;
mod lightweight;
pub use auth::*;
pub use config::*;
@@ -48,7 +48,7 @@ pub use settings::*;
pub use skill::*;
pub use stream_check::*;
pub use lightweight::*;
pub use usage::*;
pub use webdav_sync::*;
pub use workspace::*;
pub use lightweight::*;
+1 -1
View File
@@ -87,4 +87,4 @@ pub fn exit_lightweight_mode(app: &tauri::AppHandle) -> Result<(), String> {
pub fn is_lightweight_mode() -> bool {
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"
);
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 chunks: Vec<_> = converted.collect().await;
let events: Vec<Value> = chunks
+5 -8
View File
@@ -393,11 +393,10 @@ pub fn create_tray_menu(
true,
crate::lightweight::is_lightweight_mode(),
None::<&str>,
).map_err(|e| AppError::Message(format!("创建轻量模式菜单失败: {e}")))?;
)
.map_err(|e| AppError::Message(format!("创建轻量模式菜单失败: {e}")))?;
menu_builder = menu_builder
.item(&lightweight_item)
.separator();
menu_builder = menu_builder.item(&lightweight_item).separator();
// 退出菜单(分隔符已在上面的 section 循环中添加)
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) {
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" => {