import { type ExtensionAPI, type ExtensionContext, initTheme, type Theme, } from "@earendil-works/pi-coding-agent"; import { type Component, type OverlayHandle, type Terminal, Text, TuiAltScreen, } from "@earendil-works/pi-tui"; import { describe, expect, it, vi } from "vitest"; import { PanelHost } from "../src/panel.ts"; import { EphemletStore } from "../src/store.ts"; class MemoryTerminal implements Terminal { columns = 120; rows = 32; kittyProtocolActive = false; input: (data: string) => void = () => {}; resize: () => void = () => {}; start(input: (data: string) => void, resize: () => void): void { this.input = input; this.resize = resize; } stop(): void {} async drainInput(): Promise {} write(): void {} moveBy(): void {} hideCursor(): void {} showCursor(): void {} clearLine(): void {} clearFromCursor(): void {} clearScreen(): void {} setTitle(): void {} setProgress(): void {} setProgramStatus(): void {} click(x: number, y: number): void { this.input(`\x1b[<0;${x + 1};${y + 1}M`); this.input(`\x1b[<0;${x + 1};${y + 1}m`); } } describe("real Pi fullscreen renderer integration", () => { it("renders, routes SGR mouse/keys, resizes, and removes only its own overlay", async () => { initTheme("dark", false); const terminal = new MemoryTerminal(); const tui = new TuiAltScreen(terminal, false, undefined, { mouse: true }); const store = new EphemletStore(() => {}); const mouseEvents: string[] = []; const record = store.put({ title: "Native", purpose: "Interaction", kind: "interactive", state: 0, source: `export default c => ({ render: () => ["Count: " + c.state], invalidate() {}, handleInput(data) { if (data === "+") c.setState(c.state + 1); }, handleMouse(event) { c.pi.events.emit("mouse", event.type); if (event.type === "click") c.setState(c.state + 1); return { handled: true, capture: event.type === "press" }; } })`, }); store.view(record.id, true); const theme = { fg: (_color: string, text: string) => text } as Theme; let widget: (Component & { dispose?(): void }) | undefined; let handle: OverlayHandle | undefined; const show = tui.showOverlay.bind(tui); tui.showOverlay = (component, options) => { handle = show(component, options); return handle; }; const ctx = { mode: "tui", ui: { setWidget( _key: string, factory: | ((tui: TuiAltScreen, theme: Theme) => typeof widget) | undefined, ) { widget?.dispose?.(); widget = factory?.(tui, theme); }, notify() {}, }, } as unknown as ExtensionContext; const saved: string[] = []; const pi = { events: { emit(_name: string, type: string) { mouseEvents.push(type); }, }, } as unknown as ExtensionAPI; const host = new PanelHost(pi, store, () => ctx, { changed: () => host.refresh(true), close: (id) => { store.delete(id); host.refresh(); }, save: (id) => saved.push(id), hide: () => { store.view(store.selected, false); host.refresh(); }, }); tui.addChild(new Text("Conversation", 0, 0)); tui.start(); try { host.refresh(true); await vi.waitFor(() => { tui.renderNow(); expect(tui.getScreenLines().join("\n")).toContain("Count: 0"); }); terminal.input("+"); tui.renderNow(); expect(store.get(record.id).state).toBe(1); const bounds = handle?.getBounds(); if (!bounds) throw new Error("Overlay bounds unavailable"); terminal.click(bounds.col + 2, bounds.row + 3); tui.renderNow(); expect(store.get(record.id).state).toBe(2); expect(mouseEvents).toEqual(["press", "release", "click"]); terminal.input(`\x1b[<0;${bounds.col + 3};${bounds.row + 4}M`); terminal.input("\x1b[<32;1;1M"); terminal.input("\x1b[<0;1;1m"); expect(mouseEvents.slice(-3)).toEqual(["press", "drag", "release"]); terminal.input(`\x1b[<0;${bounds.col + 3};${bounds.row + 4}M`); const before = mouseEvents.length; store.put({ id: record.id, expectedRevision: 1, title: record.title, purpose: record.purpose, kind: "interactive", source: record.source, state: 2, }); host.refresh(); terminal.input(`\x1b[<0;${bounds.col + 3};${bounds.row + 4}m`); expect(mouseEvents).toHaveLength(before); await vi.waitFor(() => { tui.renderNow(); expect(tui.getScreenLines().join("\n")).toContain("Count: 2"); }); terminal.click(bounds.col + 10, bounds.row + 1); expect(saved).toEqual([record.id]); terminal.columns = 40; terminal.rows = 50; terminal.resize(); tui.renderNow(); expect(handle?.getBounds()).toMatchObject({ col: 0, row: 0, width: 40 }); terminal.input("\x1b"); tui.renderNow(); expect(store.visible).toBe(false); expect(store.records.size).toBe(1); store.view(record.id, true); host.refresh(); tui.renderNow(); show(new Text("Another overlay", 0, 0), { width: 20, anchor: "bottom-left", }); host.dispose(); tui.renderNow(); expect(tui.getScreenLines().join("\n")).toContain("Another overlay"); } finally { host.dispose(); tui.stop(); } }); });