Handle OSC and ascii

This commit is contained in:
Jared Miller 2026-01-28 16:38:25 -05:00
parent 9fff95e786
commit 45b232bcc7
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
3 changed files with 144 additions and 54 deletions

View file

@ -424,3 +424,61 @@ test("handles only ANSI codes with no text", () => {
// Opens and closes span even with no text
expect(result).toBe('<span style="color:#f85149"></span>');
});
// 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('<span style="color:#f85149">red</span>');
});
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;");
});

View file

@ -189,18 +189,41 @@ 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
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 = "";
while (j < text.length && /[0-9;]/.test(text[j] || "")) {
// 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++;
if (command === "m") {
// 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++) {
@ -241,11 +264,16 @@ export function ansiToHtml(text: string): string {
applyStyle();
}
// Skip other escape sequences (H, f, J, K, etc.) - they don't affect our line-based output
// Skip all CSI sequences (SGR handled above, others like cursor/clear ignored)
i = j;
continue;
}
// Unknown ESC sequence - skip the ESC character
i++;
continue;
}
// Track newlines in the content
if (text[i] === "\n" || text[i] === "\r") {
if (text[i] === "\n") {

View file

@ -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}`);
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}`,
);