From dc7a9c6dca555f165bb09c193d2802467fae92ed Mon Sep 17 00:00:00 2001 From: Sannidhya Sah Date: Sat, 12 Jul 2025 22:39:32 +0530 Subject: [PATCH] fix: implement platform-specific stdin handling for Claude Code on Windows - Add OS detection to use stdin for both system prompt and messages on Windows - Keep existing behavior on non-Windows platforms (system prompt as arg, messages via stdin) - Consolidate verbose comments to be more concise - Add comprehensive test coverage for both platform scenarios - Resolves Windows command-line length limitations (8191 chars) --- .../claude-code/__tests__/run.spec.ts | 74 ++++++++++--------- src/integrations/claude-code/run.ts | 37 +++++++--- 2 files changed, 63 insertions(+), 48 deletions(-) diff --git a/src/integrations/claude-code/__tests__/run.spec.ts b/src/integrations/claude-code/__tests__/run.spec.ts index d2fda08fc0f..27af274447a 100644 --- a/src/integrations/claude-code/__tests__/run.spec.ts +++ b/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 () => { diff --git a/src/integrations/claude-code/run.ts b/src/integrations/claude-code/run.ts index 59a5bf701a2..65e32bd96f7 100644 --- a/src/integrations/claude-code/run.ts +++ b/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()