mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
141 lines
4.7 KiB
TypeScript
141 lines
4.7 KiB
TypeScript
import { KeybindingsManager, TUI_KEYBINDINGS } from "@earendil-works/pi-tui";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import type { AgentRecord } from "../src/types.js";
|
|
import { ConversationViewer } from "../src/ui/conversation-viewer.js";
|
|
import type { ViewerKeybindings } from "../src/ui/viewer-keys.js";
|
|
import { createViewerKeys } from "../src/ui/viewer-keys.js";
|
|
|
|
const CTRL_P = "\x10";
|
|
const CTRL_N = "\x0e";
|
|
const UP = "\x1b[A";
|
|
const DOWN = "\x1b[B";
|
|
const SHIFT_UP = "\x1b[1;2A";
|
|
const SHIFT_DOWN = "\x1b[1;2B";
|
|
const PAGE_UP = "\x1b[5~";
|
|
const PAGE_DOWN = "\x1b[6~";
|
|
|
|
function createEmacsKeybindings(): KeybindingsManager {
|
|
return new KeybindingsManager(TUI_KEYBINDINGS, {
|
|
"tui.select.up": ["up", "ctrl+p"],
|
|
"tui.select.down": ["down", "ctrl+n"],
|
|
});
|
|
}
|
|
|
|
function createViewer(keybindings?: ViewerKeybindings) {
|
|
const tui = {
|
|
terminal: { rows: 20, columns: 80 },
|
|
requestRender: vi.fn(),
|
|
} as any;
|
|
const messages = Array.from({ length: 60 }, (_, i) => ({
|
|
role: "user",
|
|
content: `message ${i}`,
|
|
}));
|
|
const session = {
|
|
messages,
|
|
subscribe: vi.fn(() => vi.fn()),
|
|
} as any;
|
|
const record = {
|
|
id: "test-1",
|
|
type: "general-purpose",
|
|
description: "test agent",
|
|
status: "completed",
|
|
toolUses: 0,
|
|
startedAt: Date.now(),
|
|
} as AgentRecord;
|
|
const theme = {
|
|
fg: (_color: string, text: string) => text,
|
|
bold: (text: string) => text,
|
|
} as any;
|
|
const viewer = new ConversationViewer(tui, session, record, undefined, theme, vi.fn(), undefined, keybindings);
|
|
viewer.render(80); // sets lastInnerW and scrolls to bottom (autoScroll)
|
|
return viewer;
|
|
}
|
|
|
|
function scrollOffset(viewer: ConversationViewer): number {
|
|
return (viewer as any).scrollOffset;
|
|
}
|
|
|
|
describe("viewer-keys", () => {
|
|
it("honors user keybindings when a manager is provided", () => {
|
|
const keys = createViewerKeys(createEmacsKeybindings());
|
|
expect(keys.scrollUp(CTRL_P)).toBe(true);
|
|
expect(keys.scrollUp(UP)).toBe(true);
|
|
expect(keys.scrollDown(CTRL_N)).toBe(true);
|
|
expect(keys.scrollDown(DOWN)).toBe(true);
|
|
});
|
|
|
|
it("falls back to hardcoded defaults without a manager", () => {
|
|
const keys = createViewerKeys();
|
|
expect(keys.scrollUp(UP)).toBe(true);
|
|
expect(keys.scrollUp(CTRL_P)).toBe(false);
|
|
expect(keys.scrollDown(DOWN)).toBe(true);
|
|
expect(keys.scrollDown(CTRL_N)).toBe(false);
|
|
expect(keys.pageUp(PAGE_UP)).toBe(true);
|
|
expect(keys.pageDown(PAGE_DOWN)).toBe(true);
|
|
});
|
|
|
|
it("keeps the k/j and shift+arrow aliases with and without a manager", () => {
|
|
for (const keys of [createViewerKeys(), createViewerKeys(createEmacsKeybindings())]) {
|
|
expect(keys.scrollUp("k")).toBe(true);
|
|
expect(keys.scrollDown("j")).toBe(true);
|
|
expect(keys.pageUp(SHIFT_UP)).toBe(true);
|
|
expect(keys.pageDown(SHIFT_DOWN)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("manager with no user overrides behaves like the hardcoded defaults", () => {
|
|
const keys = createViewerKeys(new KeybindingsManager(TUI_KEYBINDINGS, {}));
|
|
expect(keys.scrollUp(UP)).toBe(true);
|
|
expect(keys.scrollDown(DOWN)).toBe(true);
|
|
expect(keys.pageUp(PAGE_UP)).toBe(true);
|
|
expect(keys.pageDown(PAGE_DOWN)).toBe(true);
|
|
expect(keys.scrollUp(CTRL_P)).toBe(false);
|
|
expect(keys.scrollDown(CTRL_N)).toBe(false);
|
|
});
|
|
|
|
it("respects rebinding that removes a default key", () => {
|
|
const manager = new KeybindingsManager(TUI_KEYBINDINGS, {
|
|
"tui.select.up": "ctrl+p",
|
|
});
|
|
const keys = createViewerKeys(manager);
|
|
expect(keys.scrollUp(CTRL_P)).toBe(true);
|
|
expect(keys.scrollUp(UP)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("ConversationViewer custom keybindings", () => {
|
|
it("scrolls with ctrl+p/ctrl+n when bound to tui.select.up/down", () => {
|
|
const viewer = createViewer(createEmacsKeybindings());
|
|
const bottom = scrollOffset(viewer);
|
|
expect(bottom).toBeGreaterThan(0);
|
|
|
|
viewer.handleInput(CTRL_P);
|
|
expect(scrollOffset(viewer)).toBe(bottom - 1);
|
|
viewer.handleInput(CTRL_N);
|
|
expect(scrollOffset(viewer)).toBe(bottom);
|
|
});
|
|
|
|
it("keeps arrows and k/j working alongside custom bindings", () => {
|
|
const viewer = createViewer(createEmacsKeybindings());
|
|
const bottom = scrollOffset(viewer);
|
|
|
|
viewer.handleInput(UP);
|
|
viewer.handleInput("k");
|
|
expect(scrollOffset(viewer)).toBe(bottom - 2);
|
|
viewer.handleInput(DOWN);
|
|
viewer.handleInput("j");
|
|
expect(scrollOffset(viewer)).toBe(bottom);
|
|
});
|
|
|
|
it("treats ctrl+p/ctrl+n as unbound without a keybindings manager", () => {
|
|
const viewer = createViewer();
|
|
const bottom = scrollOffset(viewer);
|
|
|
|
viewer.handleInput(CTRL_P);
|
|
viewer.handleInput(CTRL_N);
|
|
expect(scrollOffset(viewer)).toBe(bottom);
|
|
viewer.handleInput(UP);
|
|
expect(scrollOffset(viewer)).toBe(bottom - 1);
|
|
});
|
|
});
|