repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/ephemlets/__serial__/renderer.test.ts
Rawimport {
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<void> {}
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();
}
});
});