Add terminal.ts module for headless terminal emulation

Creates new module providing terminal session management using @xterm/headless:
- createTerminal(): spawn headless terminal emulator instances
- serializeAsHTML(): export terminal state as HTML
- disposeTerminal(): clean up resources

This replaces stateless ANSI processing with proper VT emulation that tracks
cursor position, screen buffer, and terminal attributes across output chunks.
This commit is contained in:
Jared Miller 2026-01-31 09:43:46 -05:00
parent 5016cd9960
commit c9908c87c3
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
4 changed files with 56 additions and 1 deletions

View file

@ -5,6 +5,8 @@
"": {
"name": "clarc",
"dependencies": {
"@xterm/addon-serialize": "^0.14.0",
"@xterm/headless": "^6.0.0",
"bun-pty": "^0.4.8",
},
"devDependencies": {
@ -39,6 +41,10 @@
"@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="],
"@xterm/addon-serialize": ["@xterm/addon-serialize@0.14.0", "", {}, "sha512-uteyTU1EkrQa2Ux6P/uFl2fzmXI46jy5uoQMKEOM0fKTyiW7cSn0WrFenHm5vO5uEXX/GpwW/FgILvv3r0WbkA=="],
"@xterm/headless": ["@xterm/headless@6.0.0", "", {}, "sha512-5Yj1QINYCyzrZtf8OFIHi47iQtI+0qYFPHmouEfG8dHNxbZ9Tb9YGSuLcsEwj9Z+OL75GJqPyJbyoFer80a2Hw=="],
"bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="],
"bun-types": ["bun-types@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-qyschsA03Qz+gou+apt6HNl6HnI+sJJLL4wLDke4iugsE6584CMupOtTY1n+2YC9nGVrEKUlTs99jjRLKgWnjQ=="],

View file

@ -19,6 +19,8 @@
"typescript": "^5"
},
"dependencies": {
"@xterm/addon-serialize": "^0.14.0",
"@xterm/headless": "^6.0.0",
"bun-pty": "^0.4.8"
}
}

View file

@ -494,7 +494,9 @@ const server = Bun.serve<SessionData>({
// Determine if new tail is an incomplete control sequence and split
const [body, carry] = splitAnsiCarryover(combined);
if (carry) {
console.debug(`Session ${sessionId}: ANSI carryover detected (${carry.length} bytes)`);
console.debug(
`Session ${sessionId}: ANSI carryover detected (${carry.length} bytes)`,
);
ansiCarryovers.set(sessionId, carry);
} else if (prevCarry) {
console.debug(`Session ${sessionId}: ANSI carryover resolved`);

45
src/terminal.ts Normal file
View file

@ -0,0 +1,45 @@
import { SerializeAddon } from "@xterm/addon-serialize";
import { Terminal } from "@xterm/headless";
export interface TerminalSession {
terminal: Terminal;
serialize: SerializeAddon;
}
/**
* Create a new headless terminal emulator instance
* @param cols - Terminal width in columns
* @param rows - Terminal height in rows
* @returns Terminal session with emulator and serialization addon
*/
export function createTerminal(cols: number, rows: number): TerminalSession {
const terminal = new Terminal({
cols,
rows,
scrollback: 1000, // Keep 1000 lines of scrollback
allowProposedApi: true,
});
const serialize = new SerializeAddon();
terminal.loadAddon(serialize);
return { terminal, serialize };
}
/**
* Serialize terminal screen buffer as HTML
*/
export function serializeAsHTML(session: TerminalSession): string {
return session.serialize.serializeAsHTML({
onlySelection: false,
includeGlobalBackground: true,
});
}
/**
* Clean up terminal resources
*/
export function disposeTerminal(session: TerminalSession): void {
session.serialize.dispose();
session.terminal.dispose();
}