From 493dc0d6e834832a5541c6a893f23542042f2a76 Mon Sep 17 00:00:00 2001 From: John Doe <44110547+chnjames@users.noreply.github.com> Date: Fri, 8 May 2026 01:43:48 +0800 Subject: [PATCH] fix(shell): force UTF-8 output on Windows via chcp 65001 (#1018) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prefixes Windows shell commands with `chcp 65001 >/dev/null & ` so subprocess output is UTF-8 instead of the system ANSI code page (e.g. GBK on Chinese-locale machines). The `display_command` helper strips the prefix so transcripts and approval prompts show the original command. Closes #982. Thanks to @chnjames for the fix and the test update — the prefix-strip in `display_command` is exactly the right symmetric move. --- crates/tui/src/sandbox/mod.rs | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/crates/tui/src/sandbox/mod.rs b/crates/tui/src/sandbox/mod.rs index 3d9a9314..7fd278f8 100644 --- a/crates/tui/src/sandbox/mod.rs +++ b/crates/tui/src/sandbox/mod.rs @@ -78,10 +78,14 @@ impl CommandSpec { /// Create a `CommandSpec` for running a shell command via the platform shell. pub fn shell(command: &str, cwd: PathBuf, timeout: Duration) -> Self { #[cfg(windows)] - let (program, args) = ( - "cmd".to_string(), - vec!["/C".to_string(), command.to_string()], - ); + let (program, args) = { + // Force UTF-8 output on Windows by running `chcp 65001` before the + // actual command. Without this, subprocesses output in the system's + // ANSI code page (e.g. GBK for Chinese locales), causing garbled + // text in the shell output panel. See issue #982. + let cmd = format!("chcp 65001 >NUL & {command}"); + ("cmd".to_string(), vec!["/C".to_string(), cmd]) + }; #[cfg(not(windows))] let (program, args) = ( "sh".to_string(), @@ -145,7 +149,12 @@ impl CommandSpec { && self.args.len() == 2 && self.args[0].eq_ignore_ascii_case("/C") { - self.args[1].clone() + // Strip the `chcp 65001 >NUL & ` prefix we add on Windows for + // UTF-8 output (issue #982). + let raw = &self.args[1]; + raw.strip_prefix("chcp 65001 >NUL & ") + .unwrap_or(raw) + .to_string() } else { // For other commands, join program and args let mut parts = vec![self.program.clone()]; @@ -530,7 +539,11 @@ mod tests { fn expected_shell_command(command: &str) -> Vec { #[cfg(windows)] { - vec!["cmd".to_string(), "/C".to_string(), command.to_string()] + vec![ + "cmd".to_string(), + "/C".to_string(), + format!("chcp 65001 >NUL & {command}"), + ] } #[cfg(not(windows))] { @@ -545,7 +558,7 @@ mod tests { #[cfg(windows)] { assert_eq!(spec.program, "cmd"); - assert_eq!(spec.args, vec!["/C", "echo hello"]); + assert_eq!(spec.args, vec!["/C", "chcp 65001 >NUL & echo hello"]); } #[cfg(not(windows))] {