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");
-});