From baa6ef9d707c9b48e5b395e26f0051a13a2c5aac Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Sat, 31 Jan 2026 12:01:24 -0500 Subject: [PATCH] Delete deprecated ANSI processing files Remove ansi.ts, ansi-carryover.ts, and their test files. Terminal emulation is now handled by @xterm/headless via terminal.ts. --- TODO.txt | 7 + src/ansi-carryover.ts | 68 ------ src/ansi.test.ts | 505 ------------------------------------------ src/ansi.ts | 344 ---------------------------- src/server.test.ts | 62 ------ 5 files changed, 7 insertions(+), 979 deletions(-) delete mode 100644 src/ansi-carryover.ts delete mode 100644 src/ansi.test.ts delete mode 100644 src/ansi.ts delete mode 100644 src/server.test.ts diff --git a/TODO.txt b/TODO.txt index c17d3c7..7479ce2 100644 --- a/TODO.txt +++ b/TODO.txt @@ -24,3 +24,10 @@ IDEAS - can it still @ file refs? +---- +i sent a prompt still queued, wtf is "pending prompts" i nvr see anything appear there + +http://cesspit.dungeon.red/i/YaACEY.png +---- +i want to make the padding black too, the inside of the card, so the terminal bg doesnt stand out +https://cesspit.dungeon.red/i/uLfyxL.png diff --git a/src/ansi-carryover.ts b/src/ansi-carryover.ts deleted file mode 100644 index b2651cf..0000000 --- a/src/ansi-carryover.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * @deprecated This module is deprecated. Terminal output is now handled by - * the headless terminal emulator in terminal.ts. Kept for backward compatibility. - * See docs/terminal-emulation.md for details. - */ - -/** - * Detect and split any incomplete ANSI control sequence at the end of a chunk. - * - * ANSI sequences can span multiple network packets. This function identifies - * incomplete sequences at the end of a chunk to prevent them from being - * rendered as partial/broken control codes. - * - * @param chunk - Raw output string potentially containing ANSI sequences - * @returns Tuple of [body, carry] where: - * - body: Complete portion safe to render - * - carry: Incomplete sequence to prepend to next chunk - * - * @example - * const [body, carry] = splitAnsiCarryover("hello\x1b[31"); - * // body = "hello", carry = "\x1b[31" - */ -export function splitAnsiCarryover(chunk: string): [string, string] { - if (!chunk) return ["", ""]; - - const ESC = 0x1b; - const len = chunk.length; - - // If last char is ESC, entire ESC starts a sequence we can't complete - if (chunk.charCodeAt(len - 1) === ESC) { - return [chunk.slice(0, -1), "\x1b"]; - } - - // Search from the last ESC backwards for a potentially incomplete sequence - for (let i = len - 2; i >= 0; i--) { - if (chunk.charCodeAt(i) !== ESC) continue; - const next = chunk[i + 1]; - // OSC: ESC ] ... (terminated by BEL 0x07 or ST = ESC \ - if (next === "]") { - const tail = chunk.slice(i + 2); - const hasBEL = tail.indexOf("\x07") !== -1; - const hasST = tail.indexOf("\x1b\\") !== -1; - if (!hasBEL && !hasST) { - return [chunk.slice(0, i), chunk.slice(i)]; - } - // else complete; continue scanning - continue; - } - // CSI: ESC [ params final — ensure we have a final byte 0x40-0x7E - if (next === "[") { - let j = i + 2; - while (j < len && /[0-9;?]/.test(chunk[j] || "")) j++; - // If we reached end without a final byte, it's incomplete - if (j >= len) { - return [chunk.slice(0, i), chunk.slice(i)]; - } - // Final byte exists at chunk[j]; sequence complete - continue; - } - // DCS/PM/APC and other ESC-prefixed two-char intros — if ESC is near end and likely incomplete, carry - // If ESC is the penultimate and we're at end, treat as incomplete unknown sequence - if (i >= len - 2) { - return [chunk.slice(0, i), chunk.slice(i)]; - } - // Otherwise, unknown but complete; continue - } - return [chunk, ""]; -} diff --git a/src/ansi.test.ts b/src/ansi.test.ts deleted file mode 100644 index c6fc227..0000000 --- a/src/ansi.test.ts +++ /dev/null @@ -1,505 +0,0 @@ -import { expect, test } from "bun:test"; -import { ansiToHtml, trimLineEndPreserveAnsi } from "./ansi"; - -// 1. XSS/HTML escaping tests - -test("escapes < to <", () => { - const result = ansiToHtml("
"); - expect(result).toBe("<div>"); -}); - -test("escapes > to >", () => { - const result = ansiToHtml("a > b"); - expect(result).toBe("a > b"); -}); - -test("escapes & to &", () => { - const result = ansiToHtml("foo & bar"); - expect(result).toBe("foo & bar"); -}); - -test("escapes "); - expect(result).toBe("<script>alert('xss')</script>"); -}); - -test("escapes HTML in styled text", () => { - const result = ansiToHtml("\x1b[31m\x1b[0m"); - expect(result).toContain("<script>"); - expect(result).toContain("</script>"); - expect(result).not.toContain("\x1b[0m"); - expect(result).toContain("<script>"); - expect(result).toContain("&alert"); - expect(result).toContain("color:#f85149"); -}); - -test("preserves internal spaces", () => { - const result = ansiToHtml("hello world"); - expect(result).toBe("hello world"); -}); - -test("handles only ANSI codes with no text", () => { - const result = ansiToHtml("\x1b[31m\x1b[0m"); - // Opens and closes span even with no text - expect(result).toBe(''); -}); - -// OSC sequences (Operating System Command) -test("strips OSC terminal title with BEL", () => { - // ESC ] 0 ; title BEL - const result = ansiToHtml("\x1b]0;My Terminal Title\x07text after"); - expect(result).toBe("text after"); -}); - -test("strips OSC terminal title with ST", () => { - // ESC ] 0 ; title ESC \ - const result = ansiToHtml("\x1b]0;My Terminal Title\x1b\\text after"); - expect(result).toBe("text after"); -}); - -test("strips OSC with emoji in title", () => { - const result = ansiToHtml("\x1b]0;★ Claude Code\x07hello"); - expect(result).toBe("hello"); -}); - -test("strips multiple OSC sequences", () => { - const result = ansiToHtml("\x1b]0;title1\x07text\x1b]0;title2\x07more"); - expect(result).toBe("textmore"); -}); - -// Private mode sequences (DEC) -test("strips bracketed paste mode enable", () => { - // ESC [ ? 2004 h - const result = ansiToHtml("\x1b[?2004htext"); - expect(result).toBe("text"); -}); - -test("strips bracketed paste mode disable", () => { - // ESC [ ? 2004 l - const result = ansiToHtml("\x1b[?2004ltext"); - expect(result).toBe("text"); -}); - -test("strips mixed private mode and SGR", () => { - const result = ansiToHtml("\x1b[?2004h\x1b[31mred\x1b[0m\x1b[?2004l"); - expect(result).toBe('red'); -}); - -test("strips cursor visibility sequences", () => { - // ESC [ ? 25 h (show cursor) and ESC [ ? 25 l (hide cursor) - const result = ansiToHtml("\x1b[?25lhidden cursor\x1b[?25h"); - expect(result).toBe("hidden cursor"); -}); - -test("handles Claude Code typical output", () => { - // Simulated Claude Code startup with title, bracketed paste, and colors - const input = - "\x1b]0;★ Claude Code\x07\x1b[?2004h\x1b[1;33mClaude Code\x1b[0m v2.1.15\x1b[?2004l"; - const result = ansiToHtml(input); - expect(result).toContain("Claude Code"); - expect(result).toContain("v2.1.15"); - expect(result).not.toContain("2004"); - expect(result).not.toContain("]0;"); -}); diff --git a/src/ansi.ts b/src/ansi.ts deleted file mode 100644 index a33c673..0000000 --- a/src/ansi.ts +++ /dev/null @@ -1,344 +0,0 @@ -/** - * @deprecated This module is deprecated. Terminal output is now handled by - * the headless terminal emulator in terminal.ts. Kept for backward compatibility. - * See docs/terminal-emulation.md for details. - */ - -// ANSI to HTML converter - processes escape sequences and renders as inline styled spans - -const colors: Record = { - 30: "#0d1117", - 31: "#f85149", - 32: "#3fb950", - 33: "#d29922", - 34: "#58a6ff", - 35: "#bc8cff", - 36: "#39c5cf", - 37: "#c9d1d9", - 90: "#6e7681", - 91: "#ff7b72", - 92: "#7ee787", - 93: "#e3b341", - 94: "#79c0ff", - 95: "#d2a8ff", - 96: "#56d4dd", - 97: "#ffffff", -}; - -const bgColors: Record = { - 40: "#0d1117", - 41: "#f85149", - 42: "#3fb950", - 43: "#d29922", - 44: "#58a6ff", - 45: "#bc8cff", - 46: "#39c5cf", - 47: "#c9d1d9", - 100: "#6e7681", - 101: "#ff7b72", - 102: "#7ee787", - 103: "#e3b341", - 104: "#79c0ff", - 105: "#d2a8ff", - 106: "#56d4dd", - 107: "#ffffff", -}; - -interface AnsiState { - fg: string | null; - bg: string | null; - bold: boolean; - dim: boolean; - italic: boolean; - underline: boolean; - inverse: boolean; -} - -interface ExtendedColorResult { - color: string; - skip: number; -} - -// Trim trailing whitespace from each line to prevent background color bleeding. -// Terminal buffers often pad lines with spaces. Preserve trailing ANSI resets. -export function trimLineEndPreserveAnsi(line: string): string { - let end = line.length; - let ansiSuffix = ""; - while (end > 0) { - // biome-ignore lint/suspicious/noControlCharactersInRegex: ESC character is intentional for ANSI sequences - const match = line.slice(0, end).match(/\u001b\[[0-9;?]*[A-Za-z]$/); - if (!match) break; - ansiSuffix = match[0] + ansiSuffix; - end -= match[0].length; - } - return line.slice(0, end).trimEnd() + ansiSuffix; -} - -// Parse extended color: 38;2;R;G;B (24-bit) or 38;5;N (256-color) -// Returns { color, skip } where skip is additional codes to skip -function parseExtendedColor( - codes: number[], - idx: number, -): ExtendedColorResult | null { - const mode = codes[idx + 1]; - - // 24-bit RGB: 38;2;R;G;B - if (mode === 2 && codes[idx + 4] !== undefined) { - const r = codes[idx + 2]; - const g = codes[idx + 3]; - const b = codes[idx + 4]; - return { color: `rgb(${r},${g},${b})`, skip: 4 }; - } - - // 256-color palette: 38;5;N - if (mode === 5) { - const colorNum = codes[idx + 2]; - if (colorNum === undefined) return null; - - let color: string; - if (colorNum < 16) { - const basic = [ - "#0d1117", - "#cd3131", - "#0dbc79", - "#e5e510", - "#2472c8", - "#bc3fbc", - "#11a8cd", - "#e5e5e5", - "#666666", - "#f14c4c", - "#23d18b", - "#f5f543", - "#3b8eea", - "#d670d6", - "#29b8db", - "#ffffff", - ]; - color = basic[colorNum] || "#ffffff"; - } else if (colorNum < 232) { - const n = colorNum - 16; - const ri = Math.floor(n / 36); - const gi = Math.floor((n % 36) / 6); - const bi = n % 6; - const r = ri === 0 ? 0 : ri * 40 + 55; - const g = gi === 0 ? 0 : gi * 40 + 55; - const b = bi === 0 ? 0 : bi * 40 + 55; - color = `rgb(${r},${g},${b})`; - } else { - const gray = (colorNum - 232) * 10 + 8; - color = `rgb(${gray},${gray},${gray})`; - } - return { color, skip: 2 }; - } - return null; -} - -function resetState(): AnsiState { - return { - fg: null, - bg: null, - bold: false, - dim: false, - italic: false, - underline: false, - inverse: false, - }; -} - -function buildStyle(state: AnsiState): string { - let fg = state.fg; - let bg = state.bg; - - // For inverse, swap fg and bg - if (state.inverse) { - const tmp = fg; - fg = bg || "#ffffff"; - bg = tmp || "#0d1117"; - } - - const styles: string[] = []; - if (fg) styles.push(`color:${fg}`); - if (bg) styles.push(`background-color:${bg}`); - if (state.bold) styles.push("font-weight:bold"); - if (state.dim) styles.push("opacity:0.5"); - if (state.italic) styles.push("font-style:italic"); - if (state.underline) styles.push("text-decoration:underline"); - return styles.join(";"); -} - -export function ansiToHtml(text: string): string { - if (!text) return ""; - - // Trim trailing whitespace from each line to prevent background color bleeding - text = text.split("\n").map(trimLineEndPreserveAnsi).join("\n"); - - let result = ""; - let inSpan = false; - let i = 0; - let currentStyle = ""; - let state = resetState(); - - function applyStyle(): void { - const nextStyle = buildStyle(state); - if (nextStyle === currentStyle) return; - if (inSpan) { - result += ""; - inSpan = false; - } - if (nextStyle) { - result += ``; - inSpan = true; - } - currentStyle = nextStyle; - } - - while (i < text.length) { - // Check for ESC character (char code 27) - if (text.charCodeAt(i) === 27) { - // OSC sequence: ESC ] ... BEL or ESC ] ... ESC \ - // Used for terminal title, hyperlinks, etc. - if (text[i + 1] === "]") { - let j = i + 2; - // Skip until BEL (0x07) or ST (ESC \) - while (j < text.length) { - if (text.charCodeAt(j) === 0x07) { - j++; - break; - } - if (text.charCodeAt(j) === 27 && text[j + 1] === "\\") { - j += 2; - break; - } - j++; - } - i = j; - continue; - } - - // CSI sequence: ESC [ params command - if (text[i + 1] === "[") { - let j = i + 2; - let params = ""; - // Include ? for private mode sequences like ESC[?2004h (bracketed paste) - while (j < text.length && /[0-9;?]/.test(text[j] || "")) { - params += text[j]; - j++; - } - const command = text[j] || ""; - j++; - - // Only process SGR (m command) for styling, skip others (cursor, clear, etc.) - if (command === "m" && !params.includes("?")) { - // SGR - Select Graphic Rendition - const codes = params ? params.split(";").map(Number) : [0]; - for (let k = 0; k < codes.length; k++) { - const code = codes[k]; - if (code === 0) { - state = resetState(); - } else if (code === 1) state.bold = true; - else if (code === 2) state.dim = true; - else if (code === 3) state.italic = true; - else if (code === 4) state.underline = true; - else if (code === 7) state.inverse = true; - else if (code === 22) { - state.bold = false; - state.dim = false; - } else if (code === 23) state.italic = false; - else if (code === 24) state.underline = false; - else if (code === 27) state.inverse = false; - else if (code === 39) state.fg = null; - else if (code === 49) state.bg = null; - else if (code === 38) { - const extColor = parseExtendedColor(codes, k); - if (extColor) { - state.fg = extColor.color; - k += extColor.skip; - } - } else if (code === 48) { - const extColor = parseExtendedColor(codes, k); - if (extColor) { - state.bg = extColor.color; - k += extColor.skip; - } - } else if (code !== undefined && colors[code]) { - state.fg = colors[code]; - } else if (code !== undefined && bgColors[code]) { - state.bg = bgColors[code]; - } - } - - applyStyle(); - } - // Skip all CSI sequences (SGR handled above, others like cursor/clear ignored) - i = j; - continue; - } - - // Unknown ESC sequence - skip the ESC character - i++; - continue; - } - - // Backspace: drop it to avoid stray control characters in HTML output - if (text[i] === "\b") { - i++; - continue; - } - - // Track newlines and carriage returns - if (text[i] === "\n") { - // Close span before newline to prevent background color bleeding - if (inSpan) { - result += ""; - } - result += "\n"; - // Reopen span after newline if we had styling - if (inSpan) { - result += ``; - } - i++; - continue; - } - - // Carriage return: overwrite from start of current line - if (text[i] === "\r") { - // Check if this is \r\n - if so, just skip the \r - if (text[i + 1] === "\n") { - i++; - continue; - } - - // Standalone \r: truncate back to last newline (or start) - // Need to handle open spans carefully - const lastNewline = result.lastIndexOf("\n"); - if (lastNewline >= 0) { - // Truncate after the last newline - result = result.substring(0, lastNewline + 1); - } else { - // No newline found, truncate everything - result = ""; - } - - // If we had a span open, we need to reopen it after truncation - if (inSpan) { - result += ``; - } - - i++; - continue; - } - - // Regular character - escape HTML - const ch = text[i]; - if (ch === "<") result += "<"; - else if (ch === ">") result += ">"; - else if (ch === "&") result += "&"; - else result += ch; - i++; - } - - if (inSpan) result += ""; - - return result; -} diff --git a/src/server.test.ts b/src/server.test.ts deleted file mode 100644 index a9801c5..0000000 --- a/src/server.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { expect, test } from "bun:test"; -import { splitAnsiCarryover } from "./ansi-carryover"; - -test("splitAnsiCarryover: complete chunk with no incomplete sequences", () => { - const [body, carry] = splitAnsiCarryover("hello world"); - expect(body).toBe("hello world"); - expect(carry).toBe(""); -}); - -test("splitAnsiCarryover: ESC at end of chunk", () => { - const [body, carry] = splitAnsiCarryover("hello\x1b"); - expect(body).toBe("hello"); - expect(carry).toBe("\x1b"); -}); - -test("splitAnsiCarryover: incomplete CSI sequence without final byte", () => { - const [body, carry] = splitAnsiCarryover("hello\x1b[31"); - expect(body).toBe("hello"); - expect(carry).toBe("\x1b[31"); -}); - -test("splitAnsiCarryover: incomplete OSC without BEL terminator", () => { - const [body, carry] = splitAnsiCarryover("hello\x1b]0;title"); - expect(body).toBe("hello"); - expect(carry).toBe("\x1b]0;title"); -}); - -test("splitAnsiCarryover: complete OSC with BEL terminator", () => { - const [body, carry] = splitAnsiCarryover("hello\x1b]0;title\x07world"); - expect(body).toBe("hello\x1b]0;title\x07world"); - expect(carry).toBe(""); -}); - -test("splitAnsiCarryover: complete CSI sequence", () => { - const [body, carry] = splitAnsiCarryover("hello\x1b[31mworld"); - expect(body).toBe("hello\x1b[31mworld"); - expect(carry).toBe(""); -}); - -test("splitAnsiCarryover: empty string", () => { - const [body, carry] = splitAnsiCarryover(""); - expect(body).toBe(""); - expect(carry).toBe(""); -}); - -test("splitAnsiCarryover: multiple complete sequences", () => { - const [body, carry] = splitAnsiCarryover("\x1b[1m\x1b[31mhello\x1b[0m"); - expect(body).toBe("\x1b[1m\x1b[31mhello\x1b[0m"); - expect(carry).toBe(""); -}); - -test("splitAnsiCarryover: incomplete CSI at end with complete CSI before", () => { - const [body, carry] = splitAnsiCarryover("\x1b[31mhello\x1b["); - expect(body).toBe("\x1b[31mhello"); - expect(carry).toBe("\x1b["); -}); - -test("splitAnsiCarryover: ESC with unknown sequence near end", () => { - const [body, carry] = splitAnsiCarryover("hello\x1bP"); - expect(body).toBe("hello"); - expect(carry).toBe("\x1bP"); -});