/** * ProxyPTYSession — `IPTYSession` implementation over an `IPTYTransport`. * * This is the canonical proxy session used by `PTYSessionManager`: every * remote session announced by a transport is wrapped in one of these. Local * operations (`write`, `resize`, `kill`) are forwarded to the transport using * the remote (un-namespaced) session ID; incoming transport events (`output`, * `resized`, `sessionEnded`, `focusChanged`) are filtered by that ID and * translated back into base-class state via `pushOutput` / `setSize` / * `setExited` / `setFocus`. * * Wire it up with `PTYSessionManager.setProxySessionFactory(createProxyPTYSession)`. */ import { BasePTYSession } from '#types'; import type { IPTYSession, IPTYTransport, SessionSource, IPCSessionMetadata, WSSessionMetadata, } from '#types'; import type { ProxySessionFactory } from './pty-session-manager.js'; // ─── Options ─────────────────────────────────────────────────────────────── /** * Factory options — mirrors the shape that `PTYSessionManager` passes to * a `ProxySessionFactory` when creating a proxy session. */ export interface ProxyPTYSessionOptions { /** Namespaced session ID (unique within this manager). */ id: string; /** Source type — 'ipc' or 'ws' for proxies. */ source: SessionSource; /** The un-namespaced session ID on the remote side. */ remoteSessionId: string; pid: number; command: string; cwd: string; cols: number; rows: number; metadata: IPCSessionMetadata | WSSessionMetadata; } // ─── Proxy PTY session ───────────────────────────────────────────────────── export class ProxyPTYSession extends BasePTYSession implements IPTYSession { private readonly transport: IPTYTransport; private readonly remoteSessionId: string; // Bound listeners kept so we can detach on dispose. private readonly onOutput: (sessionId: string, data: Buffer) => void; private readonly onResized: ( sessionId: string, cols: number, rows: number ) => void; private readonly onSessionEnded: ( sessionId: string, exitCode: number, signal?: string ) => void; private readonly onFocusChanged: ( sessionId: string, focused: boolean ) => void; constructor(transport: IPTYTransport, options: ProxyPTYSessionOptions) { super({ id: options.id, source: options.source, pid: options.pid, command: options.command, cwd: options.cwd, cols: options.cols, rows: options.rows, metadata: options.metadata, isRunning: true, }); this.transport = transport; this.remoteSessionId = options.remoteSessionId; this.onOutput = (sessionId, data): void => { if (sessionId !== this.remoteSessionId) return; this.pushOutput(data); }; this.onResized = (sessionId, cols, rows): void => { if (sessionId !== this.remoteSessionId) return; this.setSize(cols, rows); }; this.onSessionEnded = (sessionId, exitCode, signal): void => { if (sessionId !== this.remoteSessionId) return; this.setExited(exitCode, signal); }; this.onFocusChanged = (sessionId, focused): void => { if (sessionId !== this.remoteSessionId) return; this.setFocus(focused); }; transport.on('output', this.onOutput); transport.on('resized', this.onResized); transport.on('sessionEnded', this.onSessionEnded); transport.on('focusChanged', this.onFocusChanged); } // ─── IPTYSession operations (remote) ───────────────────────────────────── override write(data: string | Buffer): void { if (this._disposed || !this._isRunning) return; this.transport.sendInput(this.remoteSessionId, data); } override resize(cols: number, rows: number): void { if (this._disposed || !this._isRunning) return; if (this._cols === cols && this._rows === rows) return; this.transport.sendResize(this.remoteSessionId, cols, rows); // We don't update local cols/rows immediately — we wait for the remote // `resized` event, which will feed back in via onResized. } override kill(signal?: string): void { if (this._disposed || !this._isRunning) return; this.transport.sendKill(this.remoteSessionId, signal); } // ─── Disposal ──────────────────────────────────────────────────────────── override dispose(): void { if (this._disposed) return; // Detach listeners before calling super.dispose() (which clears all // listeners on the session itself, not the transport). this.transport.off('output', this.onOutput); this.transport.off('resized', this.onResized); this.transport.off('sessionEnded', this.onSessionEnded); this.transport.off('focusChanged', this.onFocusChanged); super.dispose(); } } // ─── Factory ─────────────────────────────────────────────────────────────── /** * Factory suitable for * `PTYSessionManager.setProxySessionFactory(createProxyPTYSession)`. * * Typed as `ProxySessionFactory` so TypeScript verifies the shape against the * manager's contract at definition time — no cast needed at the call site. */ export const createProxyPTYSession: ProxySessionFactory = (transport, options) => new ProxyPTYSession(transport, options);