fix(tui): close selection overlays and report rejected handlers (#124876)

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.
This commit is contained in:
Peter Steinberger
2026-08-16 16:14:28 -07:00
committed by GitHub
parent a4efa7c22b
commit 00990506c0
2 changed files with 29 additions and 1 deletions
+22
View File
@@ -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();
+7 -1
View File
@@ -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);
})();
};