diff --git a/src/integrations/terminal/ExecaTerminalProcess.ts b/src/integrations/terminal/ExecaTerminalProcess.ts index 370bf0d377b..6a1442dcdf9 100644 --- a/src/integrations/terminal/ExecaTerminalProcess.ts +++ b/src/integrations/terminal/ExecaTerminalProcess.ts @@ -49,6 +49,15 @@ export class ExecaTerminalProcess extends BaseTerminalProcess { // Ensure UTF-8 encoding for Ruby, CocoaPods, etc. LANG: "en_US.UTF-8", LC_ALL: "en_US.UTF-8", + // Windows-specific UTF-8 environment variables to prevent character corruption + // when the system uses non-UTF-8 encodings like GBK (code page 936) + // See: https://github.com/RooCodeInc/Roo-Code/issues/10709 + // Python: Force UTF-8 encoding for stdin/stdout/stderr + PYTHONIOENCODING: "utf-8", + // Python 3.7+: Enable UTF-8 mode + PYTHONUTF8: "1", + // Ruby: Force UTF-8 encoding + RUBYOPT: "-EUTF-8", }, })`${command}` diff --git a/src/integrations/terminal/Terminal.ts b/src/integrations/terminal/Terminal.ts index 8bf2072f3d4..0e16fc9afa1 100644 --- a/src/integrations/terminal/Terminal.ts +++ b/src/integrations/terminal/Terminal.ts @@ -159,6 +159,18 @@ export class Terminal extends BaseTerminal { VTE_VERSION: "0", } + // Add Windows-specific UTF-8 environment variables to prevent character corruption + // when the system uses non-UTF-8 encodings like GBK (code page 936) + // See: https://github.com/RooCodeInc/Roo-Code/issues/10709 + if (process.platform === "win32") { + // Python: Force UTF-8 encoding for stdin/stdout/stderr + env.PYTHONIOENCODING = "utf-8" + // Python 3.7+: Enable UTF-8 mode + env.PYTHONUTF8 = "1" + // Ruby: Force UTF-8 encoding + env.RUBYOPT = "-EUTF-8" + } + // Set Oh My Zsh shell integration if enabled if (Terminal.getTerminalZshOhMy()) { env.ITERM_SHELL_INTEGRATION_INSTALLED = "Yes" diff --git a/src/integrations/terminal/__tests__/ExecaTerminalProcess.spec.ts b/src/integrations/terminal/__tests__/ExecaTerminalProcess.spec.ts index c87ee5ad05d..d81995d5054 100644 --- a/src/integrations/terminal/__tests__/ExecaTerminalProcess.spec.ts +++ b/src/integrations/terminal/__tests__/ExecaTerminalProcess.spec.ts @@ -72,6 +72,22 @@ describe("ExecaTerminalProcess", () => { ) }) + it("should set Windows-specific UTF-8 environment variables", async () => { + await terminalProcess.run("echo test") + const execaMock = vitest.mocked(execa) + expect(execaMock).toHaveBeenCalledWith( + expect.objectContaining({ + env: expect.objectContaining({ + // Python UTF-8 encoding + PYTHONIOENCODING: "utf-8", + PYTHONUTF8: "1", + // Ruby UTF-8 encoding + RUBYOPT: "-EUTF-8", + }), + }), + ) + }) + it("should preserve existing environment variables", async () => { process.env.EXISTING_VAR = "existing" terminalProcess = new ExecaTerminalProcess(mockTerminal) @@ -91,6 +107,19 @@ describe("ExecaTerminalProcess", () => { expect(calledOptions.env.LANG).toBe("en_US.UTF-8") expect(calledOptions.env.LC_ALL).toBe("en_US.UTF-8") }) + + it("should override existing Python and Ruby encoding environment variables", async () => { + process.env.PYTHONIOENCODING = "latin-1" + process.env.PYTHONUTF8 = "0" + process.env.RUBYOPT = "-ELATIN-1" + terminalProcess = new ExecaTerminalProcess(mockTerminal) + await terminalProcess.run("echo test") + const execaMock = vitest.mocked(execa) + const calledOptions = execaMock.mock.calls[0][0] as any + expect(calledOptions.env.PYTHONIOENCODING).toBe("utf-8") + expect(calledOptions.env.PYTHONUTF8).toBe("1") + expect(calledOptions.env.RUBYOPT).toBe("-EUTF-8") + }) }) describe("basic functionality", () => { diff --git a/src/integrations/terminal/__tests__/Terminal.getEnv.spec.ts b/src/integrations/terminal/__tests__/Terminal.getEnv.spec.ts new file mode 100644 index 00000000000..72512c9334f --- /dev/null +++ b/src/integrations/terminal/__tests__/Terminal.getEnv.spec.ts @@ -0,0 +1,65 @@ +// npx vitest run integrations/terminal/__tests__/Terminal.getEnv.spec.ts + +import { Terminal } from "../Terminal" + +describe("Terminal.getEnv", () => { + let originalPlatform: PropertyDescriptor | undefined + + beforeAll(() => { + originalPlatform = Object.getOwnPropertyDescriptor(process, "platform") + }) + + afterAll(() => { + if (originalPlatform) { + Object.defineProperty(process, "platform", originalPlatform) + } + }) + + describe("common environment variables", () => { + it("should set VTE_VERSION to 0", () => { + const env = Terminal.getEnv() + expect(env.VTE_VERSION).toBe("0") + }) + + it("should set PAGER to empty string on Windows", () => { + Object.defineProperty(process, "platform", { value: "win32" }) + const env = Terminal.getEnv() + expect(env.PAGER).toBe("") + }) + + it("should set PAGER to cat on non-Windows", () => { + Object.defineProperty(process, "platform", { value: "linux" }) + const env = Terminal.getEnv() + expect(env.PAGER).toBe("cat") + }) + }) + + describe("Windows UTF-8 encoding fix", () => { + beforeEach(() => { + Object.defineProperty(process, "platform", { value: "win32" }) + }) + + it("should set PYTHONIOENCODING to utf-8 on Windows", () => { + const env = Terminal.getEnv() + expect(env.PYTHONIOENCODING).toBe("utf-8") + }) + + it("should set PYTHONUTF8 to 1 on Windows", () => { + const env = Terminal.getEnv() + expect(env.PYTHONUTF8).toBe("1") + }) + + it("should set RUBYOPT to -EUTF-8 on Windows", () => { + const env = Terminal.getEnv() + expect(env.RUBYOPT).toBe("-EUTF-8") + }) + + it("should not set Python/Ruby UTF-8 vars on non-Windows", () => { + Object.defineProperty(process, "platform", { value: "linux" }) + const env = Terminal.getEnv() + expect(env.PYTHONIOENCODING).toBeUndefined() + expect(env.PYTHONUTF8).toBeUndefined() + expect(env.RUBYOPT).toBeUndefined() + }) + }) +})