diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index ce58d520d..d54c3e4fe 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed Windows bash-tool child processes defaulting interpreter pipe I/O to the ANSI codepage by adding UTF-8 encoding defaults when the inherited environment is unset ([#2701](https://github.com/can1357/oh-my-pi/issues/2701)). + ## [16.0.1] - 2026-06-15 ### Breaking Changes diff --git a/packages/coding-agent/src/exec/bash-executor.ts b/packages/coding-agent/src/exec/bash-executor.ts index f781289a6..81a5afc82 100644 --- a/packages/coding-agent/src/exec/bash-executor.ts +++ b/packages/coding-agent/src/exec/bash-executor.ts @@ -11,7 +11,7 @@ import { Settings, type ShellMinimizerSettings } from "../config/settings"; import { OutputSink } from "../session/streaming-output"; import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../tools/output-meta"; import { getOrCreateSnapshot } from "../utils/shell-snapshot"; -import { NON_INTERACTIVE_ENV } from "./non-interactive-env"; +import { buildNonInteractiveEnv } from "./non-interactive-env"; export interface BashExecutorOptions { cwd?: string; @@ -184,7 +184,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions const minimizer = buildMinimizerOptions(settings.getGroup("shellMinimizer")); const commandCwd = await resolveShellCwd(options?.cwd); - const commandEnv = options?.env ? { ...NON_INTERACTIVE_ENV, ...options.env } : NON_INTERACTIVE_ENV; + const commandEnv = buildNonInteractiveEnv(options?.env); // Apply command prefix if configured const prefixedCommand = prefix ? `${prefix} ${command}` : command; diff --git a/packages/coding-agent/src/exec/non-interactive-env.ts b/packages/coding-agent/src/exec/non-interactive-env.ts index 8befc7260..94bb61343 100644 --- a/packages/coding-agent/src/exec/non-interactive-env.ts +++ b/packages/coding-agent/src/exec/non-interactive-env.ts @@ -46,3 +46,45 @@ export const NON_INTERACTIVE_ENV: Readonly> = { COMPOSER_NO_INTERACTION: "1", CLOUDSDK_CORE_DISABLE_PROMPTS: "1", }; + +const WINDOWS_UTF8_ENV_DEFAULTS: Readonly> = { + PYTHONIOENCODING: "utf-8", + PYTHONUTF8: "1", + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8", +}; + +function hasEnvValue( + env: Record | undefined, + key: string, + platform: NodeJS.Platform, +): boolean { + if (!env) return false; + if (platform !== "win32") return env[key] !== undefined; + + for (const [existingKey, value] of Object.entries(env)) { + if (value !== undefined && existingKey.toLowerCase() === key.toLowerCase()) { + return true; + } + } + return false; +} + +/** Builds the per-command environment for non-interactive child processes. */ +export function buildNonInteractiveEnv( + overrides?: Record, + baseEnv: Record = Bun.env, + platform: NodeJS.Platform = process.platform, +): Record { + if (platform !== "win32") { + return overrides ? { ...NON_INTERACTIVE_ENV, ...overrides } : NON_INTERACTIVE_ENV; + } + + const env: Record = { ...NON_INTERACTIVE_ENV }; + for (const [key, value] of Object.entries(WINDOWS_UTF8_ENV_DEFAULTS)) { + if (!hasEnvValue(baseEnv, key, platform) && !hasEnvValue(overrides, key, platform)) { + env[key] = value; + } + } + return overrides ? { ...env, ...overrides } : env; +} diff --git a/packages/coding-agent/test/non-interactive-env.test.ts b/packages/coding-agent/test/non-interactive-env.test.ts new file mode 100644 index 000000000..29e1a4366 --- /dev/null +++ b/packages/coding-agent/test/non-interactive-env.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "bun:test"; +import { buildNonInteractiveEnv } from "@oh-my-pi/pi-coding-agent/exec/non-interactive-env"; + +describe("buildNonInteractiveEnv", () => { + it("defaults Windows child-process encoding to UTF-8 when inherited env is unset", () => { + const env = buildNonInteractiveEnv(undefined, {}, "win32"); + + expect(env.PYTHONIOENCODING).toBe("utf-8"); + expect(env.PYTHONUTF8).toBe("1"); + expect(env.LANG).toBe("C.UTF-8"); + expect(env.LC_ALL).toBe("C.UTF-8"); + }); + + it("preserves inherited and per-command Windows encoding overrides", () => { + const env = buildNonInteractiveEnv( + { PYTHONUTF8: "0", LC_ALL: "en_US.UTF-8" }, + { pythonioencoding: "cp1252", LANG: "de_DE.UTF-8" }, + "win32", + ); + + expect(env.PYTHONIOENCODING).toBeUndefined(); + expect(env.PYTHONUTF8).toBe("0"); + expect(env.LANG).toBeUndefined(); + expect(env.LC_ALL).toBe("en_US.UTF-8"); + }); + + it("does not force UTF-8 encoding defaults on non-Windows platforms", () => { + const env = buildNonInteractiveEnv(undefined, {}, "linux"); + + expect(env.PYTHONIOENCODING).toBeUndefined(); + expect(env.PYTHONUTF8).toBeUndefined(); + expect(env.LANG).toBeUndefined(); + expect(env.LC_ALL).toBeUndefined(); + }); +});