// Reconstruction d'écran via @xterm/headless : terminal headless PERSISTANT par session, alimenté // incrémentalement par le flux PTY. Remplace le strip ANSI naïf (qui « mange les espaces » et casse // la détection des dialogues, verdict S1/S3). Aucune dépendance DOM (usage Node). import { createRequire } from 'node:module'; import type { Terminal as XtermTerminal } from '@xterm/headless'; // @xterm/headless est publié en CommonJS : sous Node ESM natif l'import nommé échoue // (cjs-module-lexer ne détecte pas l'export). On charge via require, typé par l'import type. const require = createRequire(import.meta.url); const { Terminal } = require('@xterm/headless') as { Terminal: typeof XtermTerminal }; export class ScreenReader { private readonly term: XtermTerminal; constructor(cols = 120, rows = 40) { this.term = new Terminal({ cols, rows, scrollback: 0, allowProposedApi: true }); } /** * Alimente le terminal avec un chunk PTY brut (la frontière UTF-8 et les séquences ANSI coupées * sont gérées par xterm). La promesse se résout quand le chunk a été parsé (snapshot fiable ensuite). */ feed(chunk: Uint8Array | string): Promise { return new Promise((resolve) => this.term.write(chunk as Uint8Array, resolve)); } resize(cols: number, rows: number): void { this.term.resize(cols, rows); } /** Lignes visibles du viewport courant, espaces préservés (trim à droite uniquement). */ snapshotLines(): string[] { const buf = this.term.buffer.active; const lines: string[] = []; for (let y = 0; y < this.term.rows; y++) { const line = buf.getLine(buf.baseY + y); lines.push(line ? line.translateToString(true) : ''); } return lines; } dispose(): void { this.term.dispose(); } }