Replace ansiToHtml with terminal serialization (Phase 2)

The output handler now:
- Writes raw data to terminal emulator
- Stores raw data in DB (unchanged)
- Serializes terminal state as HTML
- Broadcasts serialized HTML via SSE

This replaces the ansiToHtml() + splitAnsiCarryover() approach with
proper terminal emulation. The terminal emulator handles all ANSI
sequences internally, including incomplete sequences across chunks.
This commit is contained in:
Jared Miller 2026-01-31 09:49:53 -05:00
parent 6c7de2332b
commit 88afb7249d
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C

View file

@ -1,8 +1,6 @@
// Core server: HTTP + WebSocket + SSE
import type { ServerWebSocket } from "bun";
import { ansiToHtml } from "./ansi";
import { splitAnsiCarryover } from "./ansi-carryover";
import {
appendOutput,
createDevice,
@ -19,7 +17,12 @@ import {
updateLastSeen,
updateSessionStats,
} from "./db";
import { createTerminal, disposeTerminal, type TerminalSession } from "./terminal";
import {
createTerminal,
disposeTerminal,
serializeAsHTML,
type TerminalSession,
} from "./terminal";
import type {
AnswerResponse,
ClientMessage,
@ -494,35 +497,29 @@ const server = Bun.serve<SessionData>({
// Handle output message
if (msg.type === "output") {
const sessionId = ws.data.sessionId;
// Join with any saved carryover from previous chunk
const prevCarry = ansiCarryovers.get(sessionId) || "";
const combined = prevCarry ? prevCarry + msg.data : msg.data;
// 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)`,
);
ansiCarryovers.set(sessionId, carry);
} else if (prevCarry) {
console.debug(`Session ${sessionId}: ANSI carryover resolved`);
// Clear carry if previously set and now resolved
ansiCarryovers.delete(sessionId);
}
appendOutput(sessionId, body); // Store raw ANSI without trailing incomplete fragment
// Write to terminal emulator
const termSession = sessionTerminals.get(sessionId);
if (termSession) {
termSession.terminal.write(msg.data);
if (!termSession) {
console.error(`No terminal for session ${sessionId}`);
return;
}
// Write to terminal emulator (handles all ANSI sequences)
termSession.terminal.write(msg.data);
// Store raw ANSI in database
appendOutput(sessionId, msg.data);
// Serialize current terminal state as HTML
const html = serializeAsHTML(termSession);
// Broadcast to dashboards
broadcastSSE({
type: "output",
session_id: sessionId,
data: ansiToHtml(body), // Parse for display
data: html,
});
return;
}