Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/ephemlets/__serial__/renderer.test.ts

Raw
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<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();
		}
	});
});