From d563fed477e0a5647ba608b91122273e633a553f Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 5 Aug 2025 10:01:28 +0000 Subject: [PATCH] fix: convert escaped newlines to actual newlines in Claude Code output - Fix issue where Claude Code outputs containing \n were displayed literally - Add text.replace(/\\n/g, n) for both string chunks and text content - Add comprehensive tests to verify the fix works correctly - Resolves #6709 --- .../providers/__tests__/claude-code.spec.ts | 128 ++++++++++++++++++ src/api/providers/claude-code.ts | 4 +- 2 files changed, 130 insertions(+), 2 deletions(-) diff --git a/src/api/providers/__tests__/claude-code.spec.ts b/src/api/providers/__tests__/claude-code.spec.ts index af1bf809c4..f536ac6b93 100644 --- a/src/api/providers/__tests__/claude-code.spec.ts +++ b/src/api/providers/__tests__/claude-code.spec.ts @@ -563,4 +563,132 @@ describe("ClaudeCodeHandler", () => { consoleSpy.mockRestore() }) + + test("should convert escaped newlines to actual newlines in string chunks", async () => { + const systemPrompt = "You are a helpful assistant" + const messages = [{ role: "user" as const, content: "Hello" }] + + // Mock async generator that yields string chunks with escaped newlines + const mockGenerator = async function* (): AsyncGenerator { + yield "Line 1\\nLine 2\\nLine 3" + yield "Another chunk\\nwith newlines" + } + + mockRunClaudeCode.mockReturnValue(mockGenerator()) + + const stream = handler.createMessage(systemPrompt, messages) + const results = [] + + for await (const chunk of stream) { + results.push(chunk) + } + + expect(results).toHaveLength(2) + expect(results[0]).toEqual({ + type: "text", + text: "Line 1\nLine 2\nLine 3", + }) + expect(results[1]).toEqual({ + type: "text", + text: "Another chunk\nwith newlines", + }) + }) + + test("should convert escaped newlines in text content from assistant messages", async () => { + const systemPrompt = "You are a helpful assistant" + const messages = [{ role: "user" as const, content: "Hello" }] + + // Mock async generator that yields assistant message with escaped newlines + const mockGenerator = async function* (): AsyncGenerator { + yield { + type: "assistant" as const, + message: { + id: "msg_123", + type: "message", + role: "assistant", + model: "claude-3-5-sonnet-20241022", + content: [ + { + type: "text", + text: "# Claude Chat History\\n\\n## 2025-08-05\\n\\nHello there!", + }, + ], + stop_reason: null, + stop_sequence: null, + usage: { + input_tokens: 10, + output_tokens: 20, + }, + } as any, + session_id: "session_123", + } + } + + mockRunClaudeCode.mockReturnValue(mockGenerator()) + + const stream = handler.createMessage(systemPrompt, messages) + const results = [] + + for await (const chunk of stream) { + results.push(chunk) + } + + expect(results).toHaveLength(1) + expect(results[0]).toEqual({ + type: "text", + text: "# Claude Chat History\n\n## 2025-08-05\n\nHello there!", + }) + }) + + test("should handle mixed escaped sequences correctly", async () => { + const systemPrompt = "You are a helpful assistant" + const messages = [{ role: "user" as const, content: "Hello" }] + + // Mock async generator that yields text with various escape sequences + const mockGenerator = async function* (): AsyncGenerator { + yield "Text with\\nnewlines and\\ttabs" + yield { + type: "assistant" as const, + message: { + id: "msg_123", + type: "message", + role: "assistant", + model: "claude-3-5-sonnet-20241022", + content: [ + { + type: "text", + text: "More text\\nwith\\\\backslashes\\nand newlines", + }, + ], + stop_reason: null, + stop_sequence: null, + usage: { + input_tokens: 10, + output_tokens: 20, + }, + } as any, + session_id: "session_123", + } + } + + mockRunClaudeCode.mockReturnValue(mockGenerator()) + + const stream = handler.createMessage(systemPrompt, messages) + const results = [] + + for await (const chunk of stream) { + results.push(chunk) + } + + expect(results).toHaveLength(2) + // Only \n should be converted, not \t or \\ + expect(results[0]).toEqual({ + type: "text", + text: "Text with\nnewlines and\\ttabs", + }) + expect(results[1]).toEqual({ + type: "text", + text: "More text\nwith\\\\backslashes\nand newlines", + }) + }) }) diff --git a/src/api/providers/claude-code.ts b/src/api/providers/claude-code.ts index dfafb78aab..a31f656c95 100644 --- a/src/api/providers/claude-code.ts +++ b/src/api/providers/claude-code.ts @@ -56,7 +56,7 @@ export class ClaudeCodeHandler extends BaseProvider implements ApiHandler { if (typeof chunk === "string") { yield { type: "text", - text: chunk, + text: chunk.replace(/\\n/g, "\n"), } continue @@ -100,7 +100,7 @@ export class ClaudeCodeHandler extends BaseProvider implements ApiHandler { case "text": yield { type: "text", - text: content.text, + text: content.text.replace(/\\n/g, "\n"), } break case "thinking":