/** * WebSocket frame contract for the web (client/server) transport — ticket #13, * lot F1. Mirrors the first-draft frames in * `docs/ticket13-b0-backend-transport-inventory.md` (§ "WebSocket first draft"). * * F1 delivers the *types + skeleton* only. The full PTY wiring runs on top of * these in F3/B5 once a server exists (B3/B4). Keeping the frame shapes here — in * the adapters layer — lets the terminal/agent/chat gateways be structurally * correct against the contract today without a live backend. * * Envelope (B0): every frame carries an `id` (client→server) or optional * `replyTo` (server→client, present for acknowledgements, absent for unsolicited * output/events). PTY bytes travel base64-encoded (`bytesBase64`) so binary * survives JSON. */ // --------------------------------------------------------------------------- // Base64 helpers (binary PTY bytes over JSON frames) // --------------------------------------------------------------------------- /** Encodes raw bytes to a base64 string for a `bytesBase64` frame field. */ export function bytesToBase64(bytes: Uint8Array): string { let binary = ""; for (let i = 0; i < bytes.length; i += 1) { binary += String.fromCharCode(bytes[i]); } // `btoa` exists in browsers and jsdom. return btoa(binary); } /** Decodes a base64 `bytesBase64` frame field back to raw bytes. */ export function base64ToBytes(base64: string): Uint8Array { const binary = atob(base64); const out = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i += 1) { out[i] = binary.charCodeAt(i); } return out; } // --------------------------------------------------------------------------- // Client → server frames // --------------------------------------------------------------------------- /** Client→server frame kinds (B0 § PTY + structured chat + ping). */ export type ClientFrameKind = | "terminal.attach" | "terminal.open" | "agent.launch" | "terminal.input" | "terminal.resize" | "terminal.detach" | "terminal.close" | "chat.attach" | "chat.send" | "chat.detach" | "ping"; /** A client→server frame. `id` correlates the eventual `replyTo`. */ export interface ClientFrame { id: string; kind: ClientFrameKind; payload: Record; } // --------------------------------------------------------------------------- // Server → client frames // --------------------------------------------------------------------------- /** Server→client frame kinds. */ export type ServerFrameKind = | "terminal.attached" | "terminal.output" | "terminal.status" | "chat.attached" | "chat.output" | "event.domain" | "error" | "pong"; /** Suggested lifecycle statuses (B0). */ export type TerminalStatus = | "starting" | "running" | "exited" | "closed" | "detached" | "reattached"; /** A server→client frame. `replyTo` is present only for acknowledgements. */ export interface ServerFrame { kind: ServerFrameKind; replyTo?: string; payload: Record; } /** Payload of a `terminal.attached` acknowledgement (B5, `server.rs`). */ export interface AttachedPayload { session: { sessionId: string; projectId?: string; nodeId?: string | null; rows: number; cols: number; }; nextSeq: number; /** * Bounded scrollback replayed at (re)attach. B5 sends a single entry (`seq:0`) * carrying all retained bytes, or an empty array when there is nothing to * replay. */ scrollback: { seq: number; bytesBase64: string }[]; gap: boolean; /** Lifecycle status carried on the ack (B5 sends `"running"`). */ status?: string; /** Conversation id minted by an `agent.launch` (mirrors `assignedConversationId`). */ assignedConversationId?: string; } /** Payload of a `terminal.output` frame. */ export interface OutputPayload { sessionId: string; seq: number; bytesBase64: string; } /** * Payload of a `terminal.status` frame (B5). `status` is a lifecycle transition * (`"exited"` on close); `exitCode` is present for `"exited"` (may be `null`). */ export interface StatusPayload { sessionId: string; status: string; exitCode?: number | null; } /** Payload of a `chat.output` frame (structured assistant stream). */ export interface ChatOutputPayload { sessionId: string; chunk: unknown; // ReplyChunk-shaped (kind-tagged); normalized by the caller. }