Browse Source

feat: enable Claude Code provider to run natively on Windows (#5615)

SannidhyaSah 7 months ago
parent
commit
824c49487b
2 changed files with 63 additions and 48 deletions
  1. 38 36
      src/integrations/claude-code/__tests__/run.spec.ts
  2. 25 12
      src/integrations/claude-code/run.ts

+ 38 - 36
src/integrations/claude-code/__tests__/run.spec.ts

@@ -1,4 +1,9 @@
-import { describe, test, expect, vi, beforeEach } from "vitest"
+import { describe, test, expect, vi, beforeEach, afterEach } from "vitest"
+
+// Mock os module
+vi.mock("os", () => ({
+	platform: vi.fn(() => "darwin"), // Default to non-Windows
+}))
 
 // Mock vscode workspace
 vi.mock("vscode", () => ({
@@ -118,56 +123,53 @@ describe("runClaudeCode", () => {
 		expect(typeof result[Symbol.asyncIterator]).toBe("function")
 	})
 
-	test("should use stdin instead of command line arguments for messages", async () => {
+	test("should handle platform-specific stdin behavior", async () => {
 		const { runClaudeCode } = await import("../run")
 		const messages = [{ role: "user" as const, content: "Hello world!" }]
+		const systemPrompt = "You are a helpful assistant"
 		const options = {
-			systemPrompt: "You are a helpful assistant",
+			systemPrompt,
 			messages,
 		}
 
-		const generator = runClaudeCode(options)
+		// Test on Windows
+		const os = await import("os")
+		vi.mocked(os.platform).mockReturnValue("win32")
 
-		// Consume the generator to completion
+		const generator = runClaudeCode(options)
 		const results = []
 		for await (const chunk of generator) {
 			results.push(chunk)
 		}
 
-		// Verify execa was called with correct arguments (no JSON.stringify(messages) in args)
-		expect(mockExeca).toHaveBeenCalledWith(
-			"claude",
-			expect.arrayContaining([
-				"-p",
-				"--system-prompt",
-				"You are a helpful assistant",
-				"--verbose",
-				"--output-format",
-				"stream-json",
-				"--disallowedTools",
-				expect.any(String),
-				"--max-turns",
-				"1",
-			]),
-			expect.objectContaining({
-				stdin: "pipe",
-				stdout: "pipe",
-				stderr: "pipe",
-			}),
-		)
-
-		// Verify the arguments do NOT contain the stringified messages
+		// On Windows, should NOT have --system-prompt in args
 		const [, args] = mockExeca.mock.calls[0]
-		expect(args).not.toContain(JSON.stringify(messages))
+		expect(args).not.toContain("--system-prompt")
 
-		// Verify messages were written to stdin with callback
-		expect(mockStdin.write).toHaveBeenCalledWith(JSON.stringify(messages), "utf8", expect.any(Function))
-		expect(mockStdin.end).toHaveBeenCalled()
+		// Should pass both system prompt and messages via stdin
+		const expectedStdinData = JSON.stringify({ systemPrompt, messages })
+		expect(mockStdin.write).toHaveBeenCalledWith(expectedStdinData, "utf8", expect.any(Function))
 
-		// Verify we got the expected mock output
-		expect(results).toHaveLength(2)
-		expect(results[0]).toEqual({ type: "text", text: "Hello" })
-		expect(results[1]).toEqual({ type: "text", text: " world" })
+		// Reset mocks for non-Windows test
+		vi.clearAllMocks()
+		mockExeca.mockReturnValue(createMockProcess())
+
+		// Test on non-Windows
+		vi.mocked(os.platform).mockReturnValue("darwin")
+
+		const generator2 = runClaudeCode(options)
+		const results2 = []
+		for await (const chunk of generator2) {
+			results2.push(chunk)
+		}
+
+		// On non-Windows, should have --system-prompt in args
+		const [, args2] = mockExeca.mock.calls[0]
+		expect(args2).toContain("--system-prompt")
+		expect(args2).toContain(systemPrompt)
+
+		// Should only pass messages via stdin
+		expect(mockStdin.write).toHaveBeenCalledWith(JSON.stringify(messages), "utf8", expect.any(Function))
 	})
 
 	test("should include model parameter when provided", async () => {

+ 25 - 12
src/integrations/claude-code/run.ts

@@ -4,6 +4,7 @@ import { execa } from "execa"
 import { ClaudeCodeMessage } from "./types"
 import readline from "readline"
 import { CLAUDE_CODE_DEFAULT_MAX_OUTPUT_TOKENS } from "@roo-code/types"
+import * as os from "os"
 
 const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
 
@@ -118,11 +119,17 @@ function runProcess({
 	maxOutputTokens,
 }: ClaudeCodeOptions & { maxOutputTokens?: number }) {
 	const claudePath = path || "claude"
+	const isWindows = os.platform() === "win32"
 
-	const args = [
-		"-p",
-		"--system-prompt",
-		systemPrompt,
+	// Build args based on platform
+	const args = ["-p"]
+
+	// Pass system prompt as flag on non-Windows, via stdin on Windows (avoids cmd length limits)
+	if (!isWindows) {
+		args.push("--system-prompt", systemPrompt)
+	}
+
+	args.push(
 		"--verbose",
 		"--output-format",
 		"stream-json",
@@ -131,7 +138,7 @@ function runProcess({
 		// Roo Code will handle recursive calls
 		"--max-turns",
 		"1",
-	]
+	)
 
 	if (modelId) {
 		args.push("--model", modelId)
@@ -154,16 +161,22 @@ function runProcess({
 		timeout: CLAUDE_CODE_TIMEOUT,
 	})
 
-	// Write messages to stdin after process is spawned
-	// This avoids the E2BIG error on Linux when passing large messages as command line arguments
-	// Linux has a per-argument limit of ~128KiB for execve() system calls
-	const messagesJson = JSON.stringify(messages)
+	// Prepare stdin data: Windows gets both system prompt & messages (avoids 8191 char limit),
+	// other platforms get messages only (avoids Linux E2BIG error from ~128KiB execve limit)
+	let stdinData: string
+	if (isWindows) {
+		stdinData = JSON.stringify({
+			systemPrompt,
+			messages,
+		})
+	} else {
+		stdinData = JSON.stringify(messages)
+	}
 
-	// Use setImmediate to ensure the process has been spawned before writing to stdin
-	// This prevents potential race conditions where stdin might not be ready
+	// Use setImmediate to ensure process is spawned before writing (prevents stdin race conditions)
 	setImmediate(() => {
 		try {
-			child.stdin.write(messagesJson, "utf8", (error) => {
+			child.stdin.write(stdinData, "utf8", (error: Error | null | undefined) => {
 				if (error) {
 					console.error("Error writing to Claude Code stdin:", error)
 					child.kill()