From 956828fa55d6c5f1cacf88a6cb4f2734b8106f9c Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 23 Mar 2026 15:34:07 +0700 Subject: [PATCH] Fix terminal stream stalls after resize --- .../runtime/terminal-stream-controller.ts | 33 ---------------- .../server/daemon-e2e/terminal.e2e.test.ts | 39 +++++++++++++++++++ packages/server/src/server/session.ts | 2 +- 3 files changed, 40 insertions(+), 34 deletions(-) diff --git a/packages/app/src/terminal/runtime/terminal-stream-controller.ts b/packages/app/src/terminal/runtime/terminal-stream-controller.ts index f7deb1523..b5978b64c 100644 --- a/packages/app/src/terminal/runtime/terminal-stream-controller.ts +++ b/packages/app/src/terminal/runtime/terminal-stream-controller.ts @@ -40,10 +40,6 @@ export type TerminalStreamControllerOptions = { const TERMINAL_EXITED_ERROR = "Terminal exited"; -function logTerminalStreamController(event: string, details: Record): void { - console.log("[terminal-stream-controller]", event, details); -} - export class TerminalStreamController { private readonly decoder = new TextDecoder(); private readonly unsubscribeStreamEvents: () => void; @@ -69,19 +65,10 @@ export class TerminalStreamController { setTerminal(input: { terminalId: string | null }): void { if (this.disposed || input.terminalId === this.terminalId) { - logTerminalStreamController("set_terminal_noop", { - disposed: this.disposed, - currentTerminalId: this.terminalId, - requestedTerminalId: input.terminalId, - }); return; } const nextTerminalId = input.terminalId; const previousTerminalId = this.terminalId; - logTerminalStreamController("set_terminal", { - previousTerminalId, - nextTerminalId, - }); this.terminalId = nextTerminalId; this.decoder.decode(); if (previousTerminalId) { @@ -95,11 +82,6 @@ export class TerminalStreamController { void this.options.client .subscribeTerminal(nextTerminalId) .then((payload) => { - logTerminalStreamController("subscribe_resolved", { - nextTerminalId, - payloadTerminalId: payload.terminalId, - error: payload.error ?? null, - }); if (this.disposed || this.terminalId !== nextTerminalId) { return; } @@ -113,10 +95,6 @@ export class TerminalStreamController { return; } const preferredSize = this.options.getPreferredSize(); - logTerminalStreamController("preferred_size_after_subscribe", { - nextTerminalId, - preferredSize, - }); if (preferredSize) { this.options.client.sendTerminalInput(nextTerminalId, { type: "resize", @@ -131,10 +109,6 @@ export class TerminalStreamController { }); }) .catch((error: unknown) => { - logTerminalStreamController("subscribe_rejected", { - nextTerminalId, - error: error instanceof Error ? error.message : "Unable to subscribe to terminal", - }); if (this.disposed || this.terminalId !== nextTerminalId) { return; } @@ -148,10 +122,6 @@ export class TerminalStreamController { } handleTerminalExit(input: { terminalId: string }): void { - logTerminalStreamController("terminal_exit", { - currentTerminalId: this.terminalId, - exitedTerminalId: input.terminalId, - }); if (this.disposed || input.terminalId !== this.terminalId) { return; } @@ -168,9 +138,6 @@ export class TerminalStreamController { if (this.disposed) { return; } - logTerminalStreamController("dispose", { - terminalId: this.terminalId, - }); this.disposed = true; this.decoder.decode(); const terminalId = this.terminalId; diff --git a/packages/server/src/server/daemon-e2e/terminal.e2e.test.ts b/packages/server/src/server/daemon-e2e/terminal.e2e.test.ts index 59fbe2989..aa02d0a9a 100644 --- a/packages/server/src/server/daemon-e2e/terminal.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/terminal.e2e.test.ts @@ -626,6 +626,45 @@ describe("daemon E2E terminal", () => { rmSync(cwd, { recursive: true, force: true }); }, 30000); + test("resize does not stall streamed output for an attached client", async () => { + const cwd = tmpCwd(); + const created = await ctx.client.createTerminal(cwd); + const terminalId = created.terminal!.id; + const secondClient = await connectClient( + ctx.daemon.port, + `terminal-resize-stream-${Date.now()}-${Math.random().toString(36).slice(2)}`, + ); + + try { + await ctx.client.subscribeTerminal(terminalId); + await secondClient.subscribeTerminal(terminalId); + + secondClient.sendTerminalInput(terminalId, { + type: "resize", + rows: 12, + cols: 40, + }); + + const firstOutput = waitForTerminalOutput(ctx.client, terminalId, (text) => + text.includes("after-resize-stream"), + ); + const secondOutput = waitForTerminalOutput(secondClient, terminalId, (text) => + text.includes("after-resize-stream"), + ); + + secondClient.sendTerminalInput(terminalId, { + type: "input", + data: "printf 'after-resize-stream\\n'\r", + }); + + expect(await firstOutput).toContain("after-resize-stream"); + expect(await secondOutput).toContain("after-resize-stream"); + } finally { + await secondClient.close(); + rmSync(cwd, { recursive: true, force: true }); + } + }, 30000); + test("terminal exits notify the client", async () => { const cwd = tmpCwd(); const created = await ctx.client.createTerminal(cwd); diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 85f2a1ddd..8b78f07a2 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -1817,7 +1817,6 @@ export class Session { if (!resize) { return; } - activeStream.needsSnapshot = true; terminal.send({ type: "resize", rows: resize.rows, cols: resize.cols }); return; } @@ -7859,6 +7858,7 @@ export class Session { this.markAllActiveTerminalStreamsForSnapshot(); } }); + this.trySendTerminalSnapshot(activeStream); return slot; }