From 7904a14af1dfcda492125dab6d2708623934ea7b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 15 Jan 2026 01:59:05 +0000 Subject: [PATCH] fix: render TUI pickers as overlays --- CHANGELOG.md | 1 + src/tui/tui-overlays.test.ts | 60 ++++++++++++++++++++++++++++++++++++ src/tui/tui-overlays.ts | 19 ++++++++++++ src/tui/tui.ts | 23 ++------------ 4 files changed, 83 insertions(+), 20 deletions(-) create mode 100644 src/tui/tui-overlays.test.ts create mode 100644 src/tui/tui-overlays.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 1584a2fc8..b104362e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ ### Fixes - Mac: pass auth token/password to dashboard URL for authenticated access. (#918) — thanks @rahthakor. - UI: use application-defined WebSocket close code (browser compatibility). (#918) — thanks @rahthakor. +- TUI: render picker overlays via the overlay stack so /models and /settings display. (#921) — thanks @grizzdank. - Gateway/Dev: ensure `pnpm gateway:dev` always uses the dev profile config + state (`~/.clawdbot-dev`). - macOS: fix cron preview/testing payload to use `channel` key. (#867) — thanks @wes-davis. - Telegram: honor `channels.telegram.timeoutSeconds` for grammY API requests. (#863) — thanks @Snaver. diff --git a/src/tui/tui-overlays.test.ts b/src/tui/tui-overlays.test.ts new file mode 100644 index 000000000..a612c8c76 --- /dev/null +++ b/src/tui/tui-overlays.test.ts @@ -0,0 +1,60 @@ +import type { Component } from "@mariozechner/pi-tui"; +import { describe, expect, it, vi } from "vitest"; + +import { createOverlayHandlers } from "./tui-overlays.js"; + +class DummyComponent implements Component { + render() { + return ["dummy"]; + } + + invalidate() {} +} + +describe("createOverlayHandlers", () => { + it("routes overlays through the TUI overlay stack", () => { + const showOverlay = vi.fn(); + const hideOverlay = vi.fn(); + const setFocus = vi.fn(); + let open = false; + + const host = { + showOverlay: (component: Component) => { + open = true; + showOverlay(component); + }, + hideOverlay: () => { + open = false; + hideOverlay(); + }, + hasOverlay: () => open, + setFocus, + }; + + const { openOverlay, closeOverlay } = createOverlayHandlers(host, new DummyComponent()); + const overlay = new DummyComponent(); + + openOverlay(overlay); + expect(showOverlay).toHaveBeenCalledWith(overlay); + + closeOverlay(); + expect(hideOverlay).toHaveBeenCalledTimes(1); + expect(setFocus).not.toHaveBeenCalled(); + }); + + it("restores focus when closing without an overlay", () => { + const setFocus = vi.fn(); + const host = { + showOverlay: vi.fn(), + hideOverlay: vi.fn(), + hasOverlay: () => false, + setFocus, + }; + const fallback = new DummyComponent(); + + const { closeOverlay } = createOverlayHandlers(host, fallback); + closeOverlay(); + + expect(setFocus).toHaveBeenCalledWith(fallback); + }); +}); diff --git a/src/tui/tui-overlays.ts b/src/tui/tui-overlays.ts new file mode 100644 index 000000000..51ba45a98 --- /dev/null +++ b/src/tui/tui-overlays.ts @@ -0,0 +1,19 @@ +import type { Component, TUI } from "@mariozechner/pi-tui"; + +type OverlayHost = Pick; + +export function createOverlayHandlers(host: OverlayHost, fallbackFocus: Component) { + const openOverlay = (component: Component) => { + host.showOverlay(component); + }; + + const closeOverlay = () => { + if (host.hasOverlay()) { + host.hideOverlay(); + return; + } + host.setFocus(fallbackFocus); + }; + + return { openOverlay, closeOverlay }; +} diff --git a/src/tui/tui.ts b/src/tui/tui.ts index 309839353..d43fcaeb0 100644 --- a/src/tui/tui.ts +++ b/src/tui/tui.ts @@ -1,11 +1,4 @@ -import { - CombinedAutocompleteProvider, - type Component, - Container, - ProcessTerminal, - Text, - TUI, -} from "@mariozechner/pi-tui"; +import { CombinedAutocompleteProvider, Container, ProcessTerminal, Text, TUI } from "@mariozechner/pi-tui"; import { resolveDefaultAgentId } from "../agents/agent-scope.js"; import { loadConfig } from "../config/config.js"; import { @@ -22,6 +15,7 @@ import { editorTheme, theme } from "./theme/theme.js"; import { createCommandHandlers } from "./tui-command-handlers.js"; import { createEventHandlers } from "./tui-event-handlers.js"; import { formatTokens } from "./tui-formatters.js"; +import { createOverlayHandlers } from "./tui-overlays.js"; import { createSessionActions } from "./tui-session-actions.js"; import type { AgentSummary, @@ -188,10 +182,8 @@ export async function runTui(opts: TuiOptions) { const footer = new Text("", 1, 0); const chatLog = new ChatLog(); const editor = new CustomEditor(editorTheme); - const overlay = new Container(); const root = new Container(); root.addChild(header); - root.addChild(overlay); root.addChild(chatLog); root.addChild(status); root.addChild(footer); @@ -300,16 +292,7 @@ export async function runTui(opts: TuiOptions) { ); }; - const closeOverlay = () => { - overlay.clear(); - tui.setFocus(editor); - }; - - const openOverlay = (component: Component) => { - overlay.clear(); - overlay.addChild(component); - tui.setFocus(component); - }; + const { openOverlay, closeOverlay } = createOverlayHandlers(tui, editor); const initialSessionAgentId = (() => { if (!initialSessionInput) return null;