diff --git a/src/ansi.test.ts b/src/ansi.test.ts
index 55eb5ba..0fa1611 100644
--- a/src/ansi.test.ts
+++ b/src/ansi.test.ts
@@ -424,3 +424,61 @@ test("handles only ANSI codes with no text", () => {
// 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
index 6ee2b3d..d787844 100644
--- a/src/ansi.ts
+++ b/src/ansi.ts
@@ -189,60 +189,88 @@ export function ansiToHtml(text: string): string {
while (i < text.length) {
// Check for ESC character (char code 27)
- if (text.charCodeAt(i) === 27 && text[i + 1] === "[") {
- // Parse CSI sequence: ESC [ params command
- let j = i + 2;
- let params = "";
- while (j < text.length && /[0-9;]/.test(text[j] || "")) {
- params += text[j];
- j++;
- }
- const command = text[j] || "";
- j++;
-
- if (command === "m") {
- // 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];
+ 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++;
}
-
- applyStyle();
+ i = j;
+ continue;
}
- // Skip other escape sequences (H, f, J, K, etc.) - they don't affect our line-based output
- i = j;
+
+ // 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;
}
diff --git a/src/seed.ts b/src/seed.ts
index 6d3bd88..d9f31f0 100644
--- a/src/seed.ts
+++ b/src/seed.ts
@@ -12,9 +12,13 @@ initDb();
const existing = getDeviceBySecret(secret);
if (existing) {
- console.log(`device already exists: id=${existing.id} secret=${existing.secret} name=${existing.name}`);
- process.exit(0);
+ console.log(
+ `device already exists: id=${existing.id} secret=${existing.secret} name=${existing.name}`,
+ );
+ process.exit(0);
}
const device = createDevice(secret, name);
-console.log(`created device: id=${device.id} secret=${device.secret} name=${device.name}`);
+console.log(
+ `created device: id=${device.id} secret=${device.secret} name=${device.name}`,
+);