diff --git a/src/entrypoints/format-turns.ts b/src/entrypoints/format-turns.ts index 3241745..59db37d 100755 --- a/src/entrypoints/format-turns.ts +++ b/src/entrypoints/format-turns.ts @@ -2,6 +2,7 @@ import { readFileSync, existsSync } from "fs"; import { exit } from "process"; +import { stripAnsiCodes } from "../github/utils/sanitizer"; export type ToolUse = { type: string; @@ -172,6 +173,9 @@ export function formatResultContent(content: any): string { contentStr = String(content).trim(); } + // Strip ANSI escape codes from terminal output + contentStr = stripAnsiCodes(contentStr); + // Truncate very long results if (contentStr.length > 3000) { contentStr = contentStr.substring(0, 2997) + "..."; diff --git a/src/github/utils/sanitizer.ts b/src/github/utils/sanitizer.ts index 83ee096..ec63d48 100644 --- a/src/github/utils/sanitizer.ts +++ b/src/github/utils/sanitizer.ts @@ -1,3 +1,11 @@ +export function stripAnsiCodes(content: string): string { + // Matches ANSI escape sequences: + // - \x1B[ (CSI) followed by parameters and a final byte + // - \x1B followed by single-character sequences + // Common sequences: \x1B[1;33m (colors), \x1B[0m (reset), \x1B[K (clear line) + return content.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, ""); +} + export function stripInvisibleCharacters(content: string): string { content = content.replace(/[\u200B\u200C\u200D\uFEFF]/g, ""); content = content.replace( diff --git a/test/format-turns.test.ts b/test/format-turns.test.ts index bb26f2e..f8b9e66 100644 --- a/test/format-turns.test.ts +++ b/test/format-turns.test.ts @@ -111,6 +111,27 @@ describe("formatResultContent", () => { const result = formatResultContent(JSON.stringify(structuredContent)); expect(result).toBe("**→** Hello world\n\n"); }); + + test("strips ANSI color codes from terminal output", () => { + // Test bold yellow warning (the issue reported: [1;33m) + const coloredOutput = "\x1B[1;33mWarning: something happened\x1B[0m"; + const result = formatResultContent(coloredOutput); + expect(result).toBe("**→** Warning: something happened\n\n"); + expect(result).not.toContain("\x1B"); + expect(result).not.toContain("[1;33m"); + }); + + test("strips ANSI codes from longer output in code blocks", () => { + const longColoredOutput = + "\x1B[32m✓\x1B[0m Test 1 passed\n" + + "\x1B[32m✓\x1B[0m Test 2 passed\n" + + "\x1B[31m✗\x1B[0m Test 3 failed\n" + + "Some additional output to make it longer"; + const result = formatResultContent(longColoredOutput); + expect(result).toContain("✓ Test 1 passed"); + expect(result).toContain("✗ Test 3 failed"); + expect(result).not.toContain("\x1B"); + }); }); describe("formatToolWithResult", () => { diff --git a/test/sanitizer.test.ts b/test/sanitizer.test.ts index a89353b..ed241da 100644 --- a/test/sanitizer.test.ts +++ b/test/sanitizer.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "bun:test"; import { + stripAnsiCodes, stripInvisibleCharacters, stripMarkdownImageAltText, stripMarkdownLinkTitles, @@ -10,6 +11,51 @@ import { redactGitHubTokens, } from "../src/github/utils/sanitizer"; +describe("stripAnsiCodes", () => { + it("should remove color codes", () => { + // Bold yellow text: \x1B[1;33m + expect(stripAnsiCodes("\x1B[1;33mWarning\x1B[0m")).toBe("Warning"); + // Red text: \x1B[31m + expect(stripAnsiCodes("\x1B[31mError\x1B[0m")).toBe("Error"); + // Green text: \x1B[32m + expect(stripAnsiCodes("\x1B[32mSuccess\x1B[0m")).toBe("Success"); + }); + + it("should remove bold and other style codes", () => { + // Bold: \x1B[1m + expect(stripAnsiCodes("\x1B[1mBold text\x1B[0m")).toBe("Bold text"); + // Underline: \x1B[4m + expect(stripAnsiCodes("\x1B[4mUnderlined\x1B[0m")).toBe("Underlined"); + }); + + it("should remove cursor movement codes", () => { + // Clear line: \x1B[K + expect(stripAnsiCodes("Text\x1B[K")).toBe("Text"); + // Cursor up: \x1B[A + expect(stripAnsiCodes("Line1\x1B[ALine2")).toBe("Line1Line2"); + }); + + it("should handle multiple ANSI codes in one string", () => { + const input = "\x1B[1;31mError:\x1B[0m \x1B[33mWarning\x1B[0m text"; + expect(stripAnsiCodes(input)).toBe("Error: Warning text"); + }); + + it("should preserve text without ANSI codes", () => { + expect(stripAnsiCodes("Normal text")).toBe("Normal text"); + expect(stripAnsiCodes("Text with [brackets]")).toBe("Text with [brackets]"); + }); + + it("should handle empty string", () => { + expect(stripAnsiCodes("")).toBe(""); + }); + + it("should handle complex terminal output", () => { + // Simulates npm/yarn output with colors + const input = "\x1B[2K\x1B[1G\x1B[32m✓\x1B[0m Tests passed"; + expect(stripAnsiCodes(input)).toBe("✓ Tests passed"); + }); +}); + describe("stripInvisibleCharacters", () => { it("should remove zero-width characters", () => { expect(stripInvisibleCharacters("Hello\u200BWorld")).toBe("HelloWorld");