From 87eff23b8197ce45901146d3e04da881945bb82d Mon Sep 17 00:00:00 2001 From: Shubham Naik Date: Wed, 18 Mar 2026 12:03:43 -0700 Subject: [PATCH] fix: wire terminal events through V2 listener [LET-7999] (#1430) Co-authored-by: Letta Code --- src/types/protocol_v2.ts | 31 ++++++++++- src/websocket/listener/client.ts | 29 ++++++++++- src/websocket/listener/protocol-inbound.ts | 60 +++++++++++++++++++++- 3 files changed, 117 insertions(+), 3 deletions(-) diff --git a/src/types/protocol_v2.ts b/src/types/protocol_v2.ts index 6a71f63..442855a 100644 --- a/src/types/protocol_v2.ts +++ b/src/types/protocol_v2.ts @@ -357,11 +357,40 @@ export interface SyncCommand { runtime: RuntimeScope; } +export interface TerminalSpawnCommand { + type: "terminal_spawn"; + terminal_id: string; + cols: number; + rows: number; +} + +export interface TerminalInputCommand { + type: "terminal_input"; + terminal_id: string; + data: string; +} + +export interface TerminalResizeCommand { + type: "terminal_resize"; + terminal_id: string; + cols: number; + rows: number; +} + +export interface TerminalKillCommand { + type: "terminal_kill"; + terminal_id: string; +} + export type WsProtocolCommand = | InputCommand | ChangeDeviceStateCommand | AbortMessageCommand - | SyncCommand; + | SyncCommand + | TerminalSpawnCommand + | TerminalInputCommand + | TerminalResizeCommand + | TerminalKillCommand; export type WsProtocolMessage = | DeviceStatusUpdateMessage diff --git a/src/websocket/listener/client.ts b/src/websocket/listener/client.ts index 8538963..0906873 100644 --- a/src/websocket/listener/client.ts +++ b/src/websocket/listener/client.ts @@ -17,7 +17,13 @@ import { settingsManager } from "../../settings-manager"; import { loadTools } from "../../tools/manager"; import type { ApprovalResponseBody } from "../../types/protocol_v2"; import { isDebugEnabled } from "../../utils/debug"; -import { killAllTerminals } from "../terminalHandler"; +import { + handleTerminalInput, + handleTerminalKill, + handleTerminalResize, + handleTerminalSpawn, + killAllTerminals, +} from "../terminalHandler"; import { clearPendingApprovalBatchIds, rejectPendingApprovalResolvers, @@ -917,6 +923,27 @@ async function connectWithRetry( scheduleQueuePump(scopedRuntime, socket, opts, processQueuedTurn); return; } + + // ── Terminal commands (no runtime scope required) ────────────────── + if (parsed.type === "terminal_spawn") { + handleTerminalSpawn(parsed, socket, runtime.bootWorkingDirectory); + return; + } + + if (parsed.type === "terminal_input") { + handleTerminalInput(parsed); + return; + } + + if (parsed.type === "terminal_resize") { + handleTerminalResize(parsed); + return; + } + + if (parsed.type === "terminal_kill") { + handleTerminalKill(parsed); + return; + } }); socket.on("close", (code: number, reason: Buffer) => { diff --git a/src/websocket/listener/protocol-inbound.ts b/src/websocket/listener/protocol-inbound.ts index f075689..454fe30 100644 --- a/src/websocket/listener/protocol-inbound.ts +++ b/src/websocket/listener/protocol-inbound.ts @@ -5,6 +5,10 @@ import type { InputCommand, RuntimeScope, SyncCommand, + TerminalInputCommand, + TerminalKillCommand, + TerminalResizeCommand, + TerminalSpawnCommand, WsProtocolCommand, } from "../../types/protocol_v2"; import { isValidApprovalResponseBody } from "./approval"; @@ -186,6 +190,56 @@ function isSyncCommand(value: unknown): value is SyncCommand { return candidate.type === "sync" && isRuntimeScope(candidate.runtime); } +function isTerminalSpawnCommand(value: unknown): value is TerminalSpawnCommand { + if (!value || typeof value !== "object") return false; + const c = value as { + type?: unknown; + terminal_id?: unknown; + cols?: unknown; + rows?: unknown; + }; + return ( + c.type === "terminal_spawn" && + typeof c.terminal_id === "string" && + typeof c.cols === "number" && + typeof c.rows === "number" + ); +} + +function isTerminalInputCommand(value: unknown): value is TerminalInputCommand { + if (!value || typeof value !== "object") return false; + const c = value as { type?: unknown; terminal_id?: unknown; data?: unknown }; + return ( + c.type === "terminal_input" && + typeof c.terminal_id === "string" && + typeof c.data === "string" + ); +} + +function isTerminalResizeCommand( + value: unknown, +): value is TerminalResizeCommand { + if (!value || typeof value !== "object") return false; + const c = value as { + type?: unknown; + terminal_id?: unknown; + cols?: unknown; + rows?: unknown; + }; + return ( + c.type === "terminal_resize" && + typeof c.terminal_id === "string" && + typeof c.cols === "number" && + typeof c.rows === "number" + ); +} + +function isTerminalKillCommand(value: unknown): value is TerminalKillCommand { + if (!value || typeof value !== "object") return false; + const c = value as { type?: unknown; terminal_id?: unknown }; + return c.type === "terminal_kill" && typeof c.terminal_id === "string"; +} + export function parseServerMessage( data: WebSocket.RawData, ): ParsedServerMessage | null { @@ -196,7 +250,11 @@ export function parseServerMessage( isInputCommand(parsed) || isChangeDeviceStateCommand(parsed) || isAbortMessageCommand(parsed) || - isSyncCommand(parsed) + isSyncCommand(parsed) || + isTerminalSpawnCommand(parsed) || + isTerminalInputCommand(parsed) || + isTerminalResizeCommand(parsed) || + isTerminalKillCommand(parsed) ) { return parsed as WsProtocolCommand; }