From 00990506c0ec04b101040b60b68cd9df984104dc Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 16 Aug 2026 16:14:28 -0700 Subject: [PATCH] fix(tui): close selection overlays and report rejected handlers (#124876) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit openSelector's onSelect fired the async handler with void and only closed the overlay after a successful await. A rejecting handler (e.g. setAgent -> setSession against a failing gateway) left the selector stranded open with an unhandled rejection: the TUI froze on the picker with no visible cause — a silent dead-end. Root cause: failure path missing from the overlay lifecycle. The handler now catches, surfaces the cause via chatLog, and always closes the overlay. Regression: /agent selection with a rejecting setSession asserts the overlay closes and the cause reaches the chat log — fails pre-fix. --- src/tui/tui-command-handlers.test.ts | 22 ++++++++++++++++++++++ src/tui/tui-command-handlers.ts | 8 +++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/tui/tui-command-handlers.test.ts b/src/tui/tui-command-handlers.test.ts index 3529d645bec1..653212cfbe40 100644 --- a/src/tui/tui-command-handlers.test.ts +++ b/src/tui/tui-command-handlers.test.ts @@ -741,6 +741,28 @@ describe("tui command handlers", () => { expect(closeOverlay).toHaveBeenCalledWith(overlayHandle); }); + it("closes the overlay and reports the cause when a selection handler rejects", async () => { + const setSession = vi + .fn() + .mockRejectedValue(new Error("gateway unavailable")) as SetSessionMock; + const { handleCommand, openOverlay, closeOverlay, overlayHandle, addSystem } = createHarness({ + setSession, + agents: [{ id: "work" }], + }); + + await handleCommand("/agent"); + const selector = firstMockArg(openOverlay, "openOverlay") as SelectableOverlay; + selector?.onSelect?.({ value: "work", label: "work" }); + await flushAsyncSelect(); + + // The selector must not stay stranded open on a rejected selection, and + // the failure must reach the chat log instead of an unhandled rejection. + expect(closeOverlay).toHaveBeenCalledWith(overlayHandle); + expect( + addSystem.mock.calls.some(([line]) => String(line).includes("gateway unavailable")), + ).toBe(true); + }); + it("forwards /context list directly", async () => { const { handleCommand, sendChat, openOverlay } = createHarness(); diff --git a/src/tui/tui-command-handlers.ts b/src/tui/tui-command-handlers.ts index 71385c3db310..548eb5341367 100644 --- a/src/tui/tui-command-handlers.ts +++ b/src/tui/tui-command-handlers.ts @@ -304,7 +304,13 @@ export function createCommandHandlers(context: CommandHandlerContext) { ) => { selector.onSelect = (item) => { void (async () => { - await onSelect(item.value); + try { + await onSelect(item.value); + } catch (err) { + // A rejected selection must not strand the overlay open with an + // unhandled rejection; close it and surface the cause in chat. + chatLog.addSystem(`selection failed: ${formatTuiErrorMessage(err)}`); + } closeOverlayAndRender(overlayHandle); })(); };