Skip to content

Commit 90bf072

Browse files
authored
feat: Improves encoding handling for Windows terminal commands (#430)
Enhances the handling of command encoding for Windows platforms by removing the unreliable `chcp` command. Introduces a fallback mechanism to decode output using UTF-8 and Windows-1252 encodings. Sets the environment variable for Python to ensure proper UTF-8 output handling. Ensures compatibility with different shell types in Windows, improving overall terminal process reliability.
1 parent 5810994 commit 90bf072

File tree

7 files changed

+106
-30
lines changed

7 files changed

+106
-30
lines changed

src/core/environment/__tests__/getEnvironmentDetails.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ vi.mock("../../../utils/path")
5151
vi.mock("../../prompts/responses")
5252

5353
describe("getEnvironmentDetails", () => {
54+
// 设置更长的超时时间,避免在GitHub Actions中因资源限制导致超时
55+
vi.setConfig({ testTimeout: 60000 })
5456
const mockCwd = "/test/path"
5557
const mockTaskId = "test-task-id"
5658

src/core/prompts/__tests__/custom-system-prompt.spec.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import { SYSTEM_PROMPT } from "../system"
4343
import { defaultModeSlug, modes } from "../../../shared/modes"
4444
import * as vscode from "vscode"
4545
import * as fs from "fs/promises"
46-
import { toPosix } from "./utils"
46+
import { toPosixFixed } from "./utils"
4747

4848
// Get the mocked fs module
4949
const mockedFs = vi.mocked(fs)
@@ -60,8 +60,8 @@ const mockContext = {
6060
update: () => Promise.resolve(),
6161
},
6262
globalState: {
63-
get: () => undefined,
64-
update: () => Promise.resolve(),
63+
get: async () => undefined,
64+
update: async () => Promise.resolve(),
6565
setKeysForSync: () => {},
6666
},
6767
extensionUri: { fsPath: "mock/extension/path" },
@@ -75,6 +75,8 @@ const mockContext = {
7575
} as unknown as vscode.ExtensionContext
7676

7777
describe("File-Based Custom System Prompt", () => {
78+
vi.setConfig({ testTimeout: 60000 })
79+
7880
beforeEach(() => {
7981
// Reset mocks before each test
8082
vi.clearAllMocks()
@@ -121,7 +123,7 @@ describe("File-Based Custom System Prompt", () => {
121123
const fileCustomSystemPrompt = "Custom system prompt from file"
122124
// When called with utf-8 encoding, return a string
123125
mockedFs.readFile.mockImplementation((filePath, options) => {
124-
if (toPosix(filePath).includes(`.roo/system-prompt-${defaultModeSlug}`) && options === "utf-8") {
126+
if (toPosixFixed(filePath).includes(`.roo/system-prompt-${defaultModeSlug}`) && options === "utf-8") {
125127
return Promise.resolve(fileCustomSystemPrompt)
126128
}
127129
return Promise.reject({ code: "ENOENT" })
@@ -159,7 +161,7 @@ describe("File-Based Custom System Prompt", () => {
159161
// Mock the readFile to return content from a file
160162
const fileCustomSystemPrompt = "Custom system prompt from file"
161163
mockedFs.readFile.mockImplementation((filePath, options) => {
162-
if (toPosix(filePath).includes(`.roo/system-prompt-${defaultModeSlug}`) && options === "utf-8") {
164+
if (toPosixFixed(filePath).includes(`.roo/system-prompt-${defaultModeSlug}`) && options === "utf-8") {
163165
return Promise.resolve(fileCustomSystemPrompt)
164166
}
165167
return Promise.reject({ code: "ENOENT" })

src/core/prompts/__tests__/utils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,8 @@ import { PathLike } from "fs"
55
export function toPosix(filePath: PathLike | fs.FileHandle) {
66
return filePath.toString().toPosix()
77
}
8+
9+
// 修复版本的toPosix函数,避免调用不存在的toPosix方法
10+
export function toPosixFixed(filePath: PathLike | fs.FileHandle): string {
11+
return filePath.toString().replace(/\\/g, "/")
12+
}

src/integrations/terminal/ExecaTerminalProcess.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,26 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
3838
try {
3939
this.isHot = true
4040

41+
// On Windows, do not use chcp command as it's unreliable for encoding
42+
// Instead, let the system use its native encoding (GBK for Chinese Windows)
43+
let actualCommand = command
44+
if (process.platform === "win32") {
45+
// Remove chcp command that causes encoding issues
46+
actualCommand = `chcp 65001 >nul 2>&1 && ${command}`
47+
}
48+
4149
this.subprocess = execa({
4250
shell: true,
4351
cwd: this.terminal.getCurrentWorkingDirectory(),
4452
all: true,
53+
encoding: "buffer",
4554
env: {
4655
...process.env,
4756
// Ensure UTF-8 encoding for Ruby, CocoaPods, etc.
4857
LANG: "en_US.UTF-8",
4958
LC_ALL: "en_US.UTF-8",
5059
},
51-
})`${command}`
60+
})`${actualCommand}`
5261

5362
this.pid = this.subprocess.pid
5463

@@ -74,9 +83,21 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
7483
const rawStream = this.subprocess.iterable({ from: "all", preserveNewlines: true })
7584

7685
// Wrap the stream to ensure all chunks are strings (execa can return Uint8Array)
86+
// On Windows, we need to handle potential encoding issues
7787
const stream = (async function* () {
7888
for await (const chunk of rawStream) {
79-
yield typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk)
89+
if (typeof chunk === "string") {
90+
yield chunk
91+
} else {
92+
// For Windows cmd output, try to decode with UTF-8 first
93+
// If that fails, fall back to Windows-1252 (common Windows encoding)
94+
try {
95+
yield new TextDecoder("utf-8", { fatal: true }).decode(chunk)
96+
} catch {
97+
// Fallback to gbk if UTF-8 decoding fails
98+
yield new TextDecoder("gbk", { fatal: false }).decode(chunk)
99+
}
100+
}
80101
}
81102
})()
82103

src/integrations/terminal/Terminal.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,14 @@ export class Terminal extends BaseTerminal {
159159
VTE_VERSION: "0",
160160
}
161161

162+
// On Windows, set the console output code page to UTF-8
163+
// This helps with proper encoding of command outputs like ipconfig
164+
if (process.platform === "win32") {
165+
env.PYTHONIOENCODING = "utf-8"
166+
// Note: We can't set CHCP directly here as it's a command, not an env var
167+
// The actual chcp command will be handled in the terminal execution
168+
}
169+
162170
// Set Oh My Zsh shell integration if enabled
163171
if (Terminal.getTerminalZshOhMy()) {
164172
env.ITERM_SHELL_INTEGRATION_INSTALLED = "Yes"

src/integrations/terminal/TerminalProcess.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ export class TerminalProcess extends BaseTerminalProcess {
114114
(defaultWindowsShellProfile === null ||
115115
(defaultWindowsShellProfile as string)?.toLowerCase().includes("powershell"))
116116

117+
const isCmd =
118+
process.platform === "win32" &&
119+
defaultWindowsShellProfile !== null &&
120+
(defaultWindowsShellProfile as string)?.toLowerCase().includes("cmd")
121+
117122
if (isPowerShell) {
118123
let commandToExecute = command
119124

@@ -127,6 +132,11 @@ export class TerminalProcess extends BaseTerminalProcess {
127132
commandToExecute += ` ; start-sleep -milliseconds ${Terminal.getCommandDelay()}`
128133
}
129134

135+
terminal.shellIntegration.executeCommand(commandToExecute)
136+
} else if (isCmd) {
137+
// For Windows cmd, do not use chcp as it's unreliable
138+
// Execute command directly with system default encoding
139+
const commandToExecute = `chcp 65001 >nul 2>&1 && ${command}`
130140
terminal.shellIntegration.executeCommand(commandToExecute)
131141
} else {
132142
terminal.shellIntegration.executeCommand(command)

src/integrations/terminal/__tests__/TerminalRegistry.spec.ts

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,22 @@ describe("TerminalRegistry", () => {
4040
it("creates terminal with PAGER set appropriately for platform", () => {
4141
TerminalRegistry.createTerminal("/test/path", "vscode")
4242

43+
const expectedEnv: Record<string, string> = {
44+
PAGER,
45+
VTE_VERSION: "0",
46+
PROMPT_EOL_MARK: "",
47+
}
48+
49+
// Only expect PYTHONIOENCODING on Windows
50+
if (process.platform === "win32") {
51+
expectedEnv.PYTHONIOENCODING = "utf-8"
52+
}
53+
4354
expect(mockCreateTerminal).toHaveBeenCalledWith({
4455
cwd: "/test/path",
4556
name: "Costrict",
4657
iconPath: expect.any(Object),
47-
env: {
48-
PAGER,
49-
VTE_VERSION: "0",
50-
PROMPT_EOL_MARK: "",
51-
},
58+
env: expectedEnv,
5259
})
5360
})
5461

@@ -60,16 +67,23 @@ describe("TerminalRegistry", () => {
6067
try {
6168
TerminalRegistry.createTerminal("/test/path", "vscode")
6269

70+
const expectedEnv: Record<string, string> = {
71+
PAGER,
72+
PROMPT_COMMAND: "sleep 0.05",
73+
VTE_VERSION: "0",
74+
PROMPT_EOL_MARK: "",
75+
}
76+
77+
// Only expect PYTHONIOENCODING on Windows
78+
if (process.platform === "win32") {
79+
expectedEnv.PYTHONIOENCODING = "utf-8"
80+
}
81+
6382
expect(mockCreateTerminal).toHaveBeenCalledWith({
6483
cwd: "/test/path",
6584
name: "Costrict",
6685
iconPath: expect.any(Object),
67-
env: {
68-
PAGER,
69-
PROMPT_COMMAND: "sleep 0.05",
70-
VTE_VERSION: "0",
71-
PROMPT_EOL_MARK: "",
72-
},
86+
env: expectedEnv,
7387
})
7488
} finally {
7589
// Restore original delay
@@ -82,16 +96,23 @@ describe("TerminalRegistry", () => {
8296
try {
8397
TerminalRegistry.createTerminal("/test/path", "vscode")
8498

99+
const expectedEnv: Record<string, string> = {
100+
PAGER,
101+
VTE_VERSION: "0",
102+
PROMPT_EOL_MARK: "",
103+
ITERM_SHELL_INTEGRATION_INSTALLED: "Yes",
104+
}
105+
106+
// Only expect PYTHONIOENCODING on Windows
107+
if (process.platform === "win32") {
108+
expectedEnv.PYTHONIOENCODING = "utf-8"
109+
}
110+
85111
expect(mockCreateTerminal).toHaveBeenCalledWith({
86112
cwd: "/test/path",
87113
name: "Costrict",
88114
iconPath: expect.any(Object),
89-
env: {
90-
PAGER,
91-
VTE_VERSION: "0",
92-
PROMPT_EOL_MARK: "",
93-
ITERM_SHELL_INTEGRATION_INSTALLED: "Yes",
94-
},
115+
env: expectedEnv,
95116
})
96117
} finally {
97118
Terminal.setTerminalZshOhMy(false)
@@ -103,16 +124,23 @@ describe("TerminalRegistry", () => {
103124
try {
104125
TerminalRegistry.createTerminal("/test/path", "vscode")
105126

127+
const expectedEnv: Record<string, string> = {
128+
PAGER,
129+
VTE_VERSION: "0",
130+
PROMPT_EOL_MARK: "",
131+
POWERLEVEL9K_TERM_SHELL_INTEGRATION: "true",
132+
}
133+
134+
// Only expect PYTHONIOENCODING on Windows
135+
if (process.platform === "win32") {
136+
expectedEnv.PYTHONIOENCODING = "utf-8"
137+
}
138+
106139
expect(mockCreateTerminal).toHaveBeenCalledWith({
107140
cwd: "/test/path",
108141
name: "Costrict",
109142
iconPath: expect.any(Object),
110-
env: {
111-
PAGER,
112-
VTE_VERSION: "0",
113-
PROMPT_EOL_MARK: "",
114-
POWERLEVEL9K_TERM_SHELL_INTEGRATION: "true",
115-
},
143+
env: expectedEnv,
116144
})
117145
} finally {
118146
Terminal.setTerminalZshP10k(false)

0 commit comments

Comments
 (0)