Files
my-pi/pi-subagents/test/conversation-viewer-keybindings.test.ts
T

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