Files
paseo/packages/client/src/terminal-stream-router.ts
T
Mohamed BoudraandGitHub 53c14d9855 Extract client SDK package (#1052)
* Extract client SDK package

* Polish SDK client identity defaults

* Build client before dependent CI jobs

* Restore daemon client server export

* Extract protocol and client SDK packages

* Fix provider override schema validation

* Fix app test daemon client imports

* Simplify workspace build targets

* Fix CLI test server build bootstrap

* Run SDK package tests in CI

* Fix rebase package split drift

* Restore lockfile registry metadata

* Update SDK config test for prompt default

* Move terminal stream router test to client package

* Fix rebase drift for protocol imports

* Fix SDK agent capability fixture

* Restore legacy server client exports

* Fix server export compatibility test

* Advertise custom mode icon client capability

* Remove server daemon-client exports

* Format rebased mode control import

* Fix rebase drift for protocol imports

Files added by upstream PRs (#893, #1147, #1154) referenced the pre-split
shared/ paths that this branch moves into @getpaseo/protocol. Redirect
those imports to the protocol package so typecheck stays green after the
rebase.
2026-05-28 01:58:18 +08:00

133 lines
3.4 KiB
TypeScript

import {
decodeTerminalSnapshotPayload,
encodeTerminalResizePayload,
encodeTerminalStreamFrame,
TerminalStreamOpcode,
type TerminalStreamFrame,
} from "@getpaseo/protocol/binary-frames/index";
import type { TerminalInput, TerminalState } from "@getpaseo/protocol/messages";
export type TerminalStreamEvent =
| { terminalId: string; type: "output"; data: Uint8Array }
| { terminalId: string; type: "snapshot"; state: TerminalState }
| { terminalId: string; type: "restore"; data: Uint8Array };
export class TerminalStreamRouter {
private readonly terminalSlots = new Map<string, number>();
private readonly slotTerminals = new Map<number, string>();
private readonly listeners = new Set<(event: TerminalStreamEvent) => void>();
onEvent(handler: (event: TerminalStreamEvent) => void): () => void {
this.listeners.add(handler);
return () => {
this.listeners.delete(handler);
};
}
setSlot(terminalId: string, slot: number): void {
const existingTerminalId = this.slotTerminals.get(slot);
if (existingTerminalId && existingTerminalId !== terminalId) {
this.terminalSlots.delete(existingTerminalId);
}
const existingSlot = this.terminalSlots.get(terminalId);
if (typeof existingSlot === "number" && existingSlot !== slot) {
this.slotTerminals.delete(existingSlot);
}
this.terminalSlots.set(terminalId, slot);
this.slotTerminals.set(slot, terminalId);
}
removeTerminal(terminalId: string): void {
const slot = this.terminalSlots.get(terminalId);
if (typeof slot !== "number") {
return;
}
this.terminalSlots.delete(terminalId);
if (this.slotTerminals.get(slot) === terminalId) {
this.slotTerminals.delete(slot);
}
}
clearSlots(): void {
this.terminalSlots.clear();
this.slotTerminals.clear();
}
encodeInput(terminalId: string, message: TerminalInput["message"]): Uint8Array | null {
const slot = this.terminalSlots.get(terminalId);
if (typeof slot !== "number") {
return null;
}
if (message.type === "input") {
return encodeTerminalStreamFrame({
opcode: TerminalStreamOpcode.Input,
slot,
payload: message.data,
});
}
if (message.type === "resize") {
return encodeTerminalStreamFrame({
opcode: TerminalStreamOpcode.Resize,
slot,
payload: encodeTerminalResizePayload({
rows: message.rows,
cols: message.cols,
}),
});
}
return null;
}
handleFrame(frame: TerminalStreamFrame): void {
const terminalId = this.slotTerminals.get(frame.slot);
if (!terminalId) {
return;
}
if (frame.opcode === TerminalStreamOpcode.Output) {
this.emit({
terminalId,
type: "output",
data: frame.payload,
});
return;
}
if (frame.opcode === TerminalStreamOpcode.Restore) {
this.emit({
terminalId,
type: "restore",
data: frame.payload,
});
return;
}
if (frame.opcode === TerminalStreamOpcode.Snapshot) {
const state = decodeTerminalSnapshotPayload(frame.payload);
if (!state) {
return;
}
this.emit({
terminalId,
type: "snapshot",
state,
});
}
}
private emit(event: TerminalStreamEvent): void {
for (const listener of this.listeners) {
try {
listener(event);
} catch {
// no-op
}
}
}
}