fix: render TUI pickers as overlays

This commit is contained in:
Peter Steinberger
2026-01-15 01:59:05 +00:00
parent 1b79730db8
commit 7904a14af1
4 changed files with 83 additions and 20 deletions

View File

@@ -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);
});
});

19
src/tui/tui-overlays.ts Normal file
View File

@@ -0,0 +1,19 @@
import type { Component, TUI } from "@mariozechner/pi-tui";
type OverlayHost = Pick<TUI, "showOverlay" | "hideOverlay" | "hasOverlay" | "setFocus">;
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 };
}

View File

@@ -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;