Skip to content

Commit 824c494

Browse files
authored
feat: enable Claude Code provider to run natively on Windows (#5615)
1 parent e019632 commit 824c494

File tree

2 files changed

+63
-48
lines changed

2 files changed

+63
-48
lines changed

src/integrations/claude-code/__tests__/run.spec.ts

Lines changed: 38 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { describe, test, expect, vi, beforeEach } from "vitest"
1+
import { describe, test, expect, vi, beforeEach, afterEach } from "vitest"
2+
3+
// Mock os module
4+
vi.mock("os", () => ({
5+
platform: vi.fn(() => "darwin"), // Default to non-Windows
6+
}))
27

38
// Mock vscode workspace
49
vi.mock("vscode", () => ({
@@ -118,56 +123,53 @@ describe("runClaudeCode", () => {
118123
expect(typeof result[Symbol.asyncIterator]).toBe("function")
119124
})
120125

121-
test("should use stdin instead of command line arguments for messages", async () => {
126+
test("should handle platform-specific stdin behavior", async () => {
122127
const { runClaudeCode } = await import("../run")
123128
const messages = [{ role: "user" as const, content: "Hello world!" }]
129+
const systemPrompt = "You are a helpful assistant"
124130
const options = {
125-
systemPrompt: "You are a helpful assistant",
131+
systemPrompt,
126132
messages,
127133
}
128134

129-
const generator = runClaudeCode(options)
135+
// Test on Windows
136+
const os = await import("os")
137+
vi.mocked(os.platform).mockReturnValue("win32")
130138

131-
// Consume the generator to completion
139+
const generator = runClaudeCode(options)
132140
const results = []
133141
for await (const chunk of generator) {
134142
results.push(chunk)
135143
}
136144

137-
// Verify execa was called with correct arguments (no JSON.stringify(messages) in args)
138-
expect(mockExeca).toHaveBeenCalledWith(
139-
"claude",
140-
expect.arrayContaining([
141-
"-p",
142-
"--system-prompt",
143-
"You are a helpful assistant",
144-
"--verbose",
145-
"--output-format",
146-
"stream-json",
147-
"--disallowedTools",
148-
expect.any(String),
149-
"--max-turns",
150-
"1",
151-
]),
152-
expect.objectContaining({
153-
stdin: "pipe",
154-
stdout: "pipe",
155-
stderr: "pipe",
156-
}),
157-
)
158-
159-
// Verify the arguments do NOT contain the stringified messages
145+
// On Windows, should NOT have --system-prompt in args
160146
const [, args] = mockExeca.mock.calls[0]
161-
expect(args).not.toContain(JSON.stringify(messages))
147+
expect(args).not.toContain("--system-prompt")
162148

163-
// Verify messages were written to stdin with callback
164-
expect(mockStdin.write).toHaveBeenCalledWith(JSON.stringify(messages), "utf8", expect.any(Function))
165-
expect(mockStdin.end).toHaveBeenCalled()
149+
// Should pass both system prompt and messages via stdin
150+
const expectedStdinData = JSON.stringify({ systemPrompt, messages })
151+
expect(mockStdin.write).toHaveBeenCalledWith(expectedStdinData, "utf8", expect.any(Function))
166152

167-
// Verify we got the expected mock output
168-
expect(results).toHaveLength(2)
169-
expect(results[0]).toEqual({ type: "text", text: "Hello" })
170-
expect(results[1]).toEqual({ type: "text", text: " world" })
153+
// Reset mocks for non-Windows test
154+
vi.clearAllMocks()
155+
mockExeca.mockReturnValue(createMockProcess())
156+
157+
// Test on non-Windows
158+
vi.mocked(os.platform).mockReturnValue("darwin")
159+
160+
const generator2 = runClaudeCode(options)
161+
const results2 = []
162+
for await (const chunk of generator2) {
163+
results2.push(chunk)
164+
}
165+
166+
// On non-Windows, should have --system-prompt in args
167+
const [, args2] = mockExeca.mock.calls[0]
168+
expect(args2).toContain("--system-prompt")
169+
expect(args2).toContain(systemPrompt)
170+
171+
// Should only pass messages via stdin
172+
expect(mockStdin.write).toHaveBeenCalledWith(JSON.stringify(messages), "utf8", expect.any(Function))
171173
})
172174

173175
test("should include model parameter when provided", async () => {

src/integrations/claude-code/run.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { execa } from "execa"
44
import { ClaudeCodeMessage } from "./types"
55
import readline from "readline"
66
import { CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS } from "@roo-code/types"
7+
import * as os from "os"
78

89
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
910

@@ -118,11 +119,17 @@ function runProcess({
118119
maxOutputTokens,
119120
}: ClaudeCodeOptions & { maxOutputTokens?: number }) {
120121
const claudePath = path || "claude"
122+
const isWindows = os.platform() === "win32"
121123

122-
const args = [
123-
"-p",
124-
"--system-prompt",
125-
systemPrompt,
124+
// Build args based on platform
125+
const args = ["-p"]
126+
127+
// Pass system prompt as flag on non-Windows, via stdin on Windows (avoids cmd length limits)
128+
if (!isWindows) {
129+
args.push("--system-prompt", systemPrompt)
130+
}
131+
132+
args.push(
126133
"--verbose",
127134
"--output-format",
128135
"stream-json",
@@ -131,7 +138,7 @@ function runProcess({
131138
// Roo Code will handle recursive calls
132139
"--max-turns",
133140
"1",
134-
]
141+
)
135142

136143
if (modelId) {
137144
args.push("--model", modelId)
@@ -154,16 +161,22 @@ function runProcess({
154161
timeout: CLAUDE_CODE_TIMEOUT,
155162
})
156163

157-
// Write messages to stdin after process is spawned
158-
// This avoids the E2BIG error on Linux when passing large messages as command line arguments
159-
// Linux has a per-argument limit of ~128KiB for execve() system calls
160-
const messagesJson = JSON.stringify(messages)
164+
// Prepare stdin data: Windows gets both system prompt & messages (avoids 8191 char limit),
165+
// other platforms get messages only (avoids Linux E2BIG error from ~128KiB execve limit)
166+
let stdinData: string
167+
if (isWindows) {
168+
stdinData = JSON.stringify({
169+
systemPrompt,
170+
messages,
171+
})
172+
} else {
173+
stdinData = JSON.stringify(messages)
174+
}
161175

162-
// Use setImmediate to ensure the process has been spawned before writing to stdin
163-
// This prevents potential race conditions where stdin might not be ready
176+
// Use setImmediate to ensure process is spawned before writing (prevents stdin race conditions)
164177
setImmediate(() => {
165178
try {
166-
child.stdin.write(messagesJson, "utf8", (error) => {
179+
child.stdin.write(stdinData, "utf8", (error: Error | null | undefined) => {
167180
if (error) {
168181
console.error("Error writing to Claude Code stdin:", error)
169182
child.kill()

0 commit comments

Comments
 (0)