// Adaptateur vscode.Pseudoterminal : expose une session Arboretum comme un terminal NATIF de VS Code // (rendu, scrollback, copier/coller, liens gratuits). C'est la pièce maîtresse de l'intégration : // pas de xterm-in-webview. Le pont WS/décodage vit dans SessionBridge (pur) ; cet adaptateur ne fait // que relier les events Pseudoterminal ↔ bridge. import * as vscode from 'vscode'; import type { SessionSummary } from '@arboretum/shared'; import type { ArbWsClient } from '../api/ws-client.js'; import { SessionBridge } from './session-bridge.js'; const ANSI_RESET = '\x1bc'; // RIS : réinitialisation complète du terminal (avant le replay d'un RESYNC) export class SessionTerminalManager { private readonly terminals = new Map(); private readonly bridges = new Map(); constructor(private readonly ws: ArbWsClient) {} /** Bridge d'une session si un terminal est ouvert (pour répondre à un dialogue sans ré-attacher). */ getBridge(sessionId: string): SessionBridge | undefined { return this.bridges.get(sessionId); } /** Ouvre (ou ré-affiche) un terminal natif attaché à la session. */ open(session: SessionSummary, mode: 'interactive' | 'observer'): void { const existing = this.terminals.get(session.id); if (existing) { existing.show(); return; } const writeEmitter = new vscode.EventEmitter(); const closeEmitter = new vscode.EventEmitter(); let bridge: SessionBridge | null = null; const pty: vscode.Pseudoterminal = { onDidWrite: writeEmitter.event, onDidClose: closeEmitter.event, open: (dims) => { bridge = new SessionBridge(this.ws, session.id, mode, { onText: (text) => writeEmitter.fire(text), onReset: () => writeEmitter.fire(ANSI_RESET), onDetached: () => { writeEmitter.fire('\r\n\x1b[2m[arboretum: session ended]\x1b[0m\r\n'); closeEmitter.fire(); }, onControlChanged: (controlling) => { if (!controlling && mode === 'interactive') { writeEmitter.fire('\r\n\x1b[33m[arboretum: control lost: now observing]\x1b[0m\r\n'); } }, }); this.bridges.set(session.id, bridge); bridge.open(dims?.columns ?? 80, dims?.rows ?? 24).catch((err: Error) => { writeEmitter.fire(`\r\n\x1b[31m[arboretum: cannot attach: ${err.message}]\x1b[0m\r\n`); closeEmitter.fire(); }); }, close: () => { bridge?.close(); this.bridges.delete(session.id); this.terminals.delete(session.id); }, handleInput: (data) => { if (mode === 'interactive') bridge?.sendStdin(data); }, setDimensions: (dims) => bridge?.resize(dims.columns, dims.rows), }; const name = this.terminalName(session, mode); const terminal = vscode.window.createTerminal({ name, pty, iconPath: new vscode.ThemeIcon('comment-discussion') }); this.terminals.set(session.id, terminal); terminal.show(); } private terminalName(session: SessionSummary, mode: 'interactive' | 'observer'): string { const base = session.title?.trim() || session.cwd.split('/').pop() || session.command; const prefix = mode === 'observer' ? '👁 ' : ''; return `${prefix}${base}`; } dispose(): void { for (const t of this.terminals.values()) t.dispose(); this.terminals.clear(); this.bridges.clear(); } }