Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
14 kB · 413 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414import { type Keybinding, type KeybindingDefinitions, type KeybindingsConfig, type KeyId, TUI_KEYBINDINGS, KeybindingsManager as TuiKeybindingsManager,} from "@earendil-works/pi-tui";import { existsSync, readFileSync } from "fs";import { join } from "path";import { getAgentDir } from "../config.js";
export interface AppKeybindings { "app.interrupt": true; "app.clear": true; "app.input.clear": true; "app.shortcuts": true; "app.exit": true; "app.suspend": true; "app.model.select": true; "app.model.toggleScope": true; "app.model.cycleForward": true; "app.model.cycleBackward": true; "app.tools.expand": true; "app.messages.expand": true; "app.edits.expand": true; "app.subagents.focus": true; "app.heartbeats.open": true; "app.heartbeats.openSelected": true; "app.editor.external": true; "app.editor.pasteFile": true; "app.editor.pasteSecret": true; "app.prompt.stash": true; "app.message.followUp": true; "app.message.navigateOlder": true; "app.message.navigateNewer": true; "app.message.moveEarlier": true; "app.message.moveLater": true; "app.clipboard.pasteImage": true; "app.clipboard.copyLoginUrl": true; "app.session.new": true; "app.session.tree": true; "app.session.fork": true; "app.session.resume": true; "app.agents.back": true; "app.agents.open": true; "app.modal.back": true; "app.agents.reply": true; "app.agents.new": true; "app.agents.delete": true; "app.agents.program": true; "app.agents.rename": true; "app.agents.pin": true; "app.agents.inactiveCollapse": true; "app.agents.expand": true; "app.tree.foldOrUp": true; "app.tree.unfoldOrDown": true; "app.tree.editLabel": true; "app.tree.toggleLabelTimestamp": true; "app.models.save": true; "app.models.enableAll": true; "app.models.clearAll": true; "app.models.toggleProvider": true; "app.models.reorderUp": true; "app.models.reorderDown": true; "app.tree.filter.default": true; "app.tree.filter.noTools": true; "app.tree.filter.userOnly": true; "app.tree.filter.labeledOnly": true; "app.tree.filter.all": true; "app.tree.filter.cycleForward": true; "app.tree.filter.cycleBackward": true;}
export type AppKeybinding = keyof AppKeybindings;
declare module "@earendil-works/pi-tui" { interface Keybindings extends AppKeybindings {}}
export const KEYBINDINGS = { ...TUI_KEYBINDINGS, "app.interrupt": { defaultKeys: [], description: "Interrupt current operation" }, "app.clear": { defaultKeys: "ctrl+c", description: "Interrupt current operation, then exit" }, "app.input.clear": { defaultKeys: "escape", description: "Interrupt response or clear prompt" }, "app.shortcuts": { defaultKeys: "?", description: "Show keyboard shortcuts" }, "app.exit": { defaultKeys: "ctrl+d", description: "Exit when editor is empty" }, "app.suspend": { defaultKeys: process.platform === "win32" ? [] : "ctrl+z", description: "Suspend to background", }, "app.model.select": { defaultKeys: "ctrl+l", description: "Open model selector" }, "app.model.toggleScope": { defaultKeys: "alt+s", description: "Toggle model selector scope" }, "app.model.cycleForward": { defaultKeys: "alt+m", description: "Cycle to the next scoped model" }, "app.model.cycleBackward": { defaultKeys: "shift+alt+m", description: "Cycle to the previous scoped model" }, "app.tools.expand": { defaultKeys: "ctrl+o", description: "Cycle conversation detail", defaultKeyScope: "editor" }, "app.messages.expand": { defaultKeys: "ctrl+p", description: "Toggle agent message bodies", defaultKeyScope: "editor", }, "app.edits.expand": { defaultKeys: "ctrl+j", description: "Toggle edit diffs", defaultKeyScope: "editor" }, "app.subagents.focus": { defaultKeys: "alt+a", description: "Open child agents", }, "app.heartbeats.open": { defaultKeys: "ctrl+r", description: "Manage heartbeats", }, "app.heartbeats.openSelected": { defaultKeys: "right", description: "Open selected heartbeat", }, "app.editor.external": { defaultKeys: "ctrl+g", description: "Open external editor", }, "app.editor.pasteFile": { defaultKeys: "alt+k", description: "Move the most recent paste into a file, or toggle a live pasted file between plain and secret", }, "app.editor.pasteSecret": { defaultKeys: "shift+alt+k", description: "Move the most recent paste into a secret file", }, "app.prompt.stash": { defaultKeys: "ctrl+s", description: "Stash or restore draft prompt", }, "app.message.followUp": { defaultKeys: "alt+enter", description: "Queue follow-up message", }, "app.message.navigateOlder": { defaultKeys: "alt+up", description: "Select older pending message", }, "app.message.navigateNewer": { defaultKeys: "alt+down", description: "Select newer pending message or draft", }, "app.message.moveEarlier": { defaultKeys: "ctrl+alt+up", description: "Move selected pending message earlier", }, "app.message.moveLater": { defaultKeys: "ctrl+alt+down", description: "Move selected pending message later", }, "app.clipboard.pasteImage": { defaultKeys: process.platform === "win32" ? "alt+v" : "ctrl+v", description: "Paste image from clipboard", }, "app.clipboard.copyLoginUrl": { defaultKeys: ["c", "alt+c"], description: "Copy login URL", }, "app.session.new": { defaultKeys: [], description: "Start a new session" }, "app.session.tree": { defaultKeys: [], description: "Open session tree" }, "app.session.fork": { defaultKeys: [], description: "Fork current session" }, "app.session.resume": { defaultKeys: [], description: "Resume a session" }, "app.agents.back": { defaultKeys: "left", description: "Return to parent agent scope" }, "app.agents.open": { defaultKeys: "right", description: "Drill into selected agent" }, "app.modal.back": { defaultKeys: "left", description: "Go back / close the current dialog" }, "app.agents.reply": { defaultKeys: "space", description: "Reply to selected agent" }, "app.agents.new": { defaultKeys: "ctrl+n", description: "Start a new session from the agents view" }, "app.agents.delete": { defaultKeys: "ctrl+x", description: "Stop or delete selected agent" }, "app.agents.program": { defaultKeys: "ctrl+o", description: "Show the program that spawned subagents" }, "app.agents.rename": { defaultKeys: "ctrl+r", description: "Rename selected agent session" }, "app.agents.pin": { defaultKeys: "ctrl+u", description: "Pin or unpin selected agent session" }, "app.agents.inactiveCollapse": { defaultKeys: "alt+i", description: "Show or hide inactive sessions" }, "app.agents.expand": { defaultKeys: "alt+right", description: "Expand or collapse selected agent subagents" }, "app.tree.foldOrUp": { defaultKeys: ["ctrl+left", "alt+left"], description: "Fold tree branch or move up", }, "app.tree.unfoldOrDown": { defaultKeys: ["ctrl+right", "alt+right"], description: "Unfold tree branch or move down", }, "app.tree.editLabel": { defaultKeys: "shift+l", description: "Edit tree label", }, "app.tree.toggleLabelTimestamp": { defaultKeys: "shift+t", description: "Toggle tree label timestamps", }, "app.models.save": { defaultKeys: "ctrl+s", description: "Save model selection", }, "app.models.enableAll": { defaultKeys: "ctrl+a", description: "Enable all models", }, "app.models.clearAll": { defaultKeys: "ctrl+x", description: "Clear all models", }, "app.models.toggleProvider": { defaultKeys: "ctrl+p", description: "Toggle all models for provider", }, "app.models.reorderUp": { defaultKeys: "alt+up", description: "Move model up in order", }, "app.models.reorderDown": { defaultKeys: "alt+down", description: "Move model down in order", }, "app.tree.filter.default": { defaultKeys: "ctrl+d", description: "Tree filter: default view", }, "app.tree.filter.noTools": { defaultKeys: "ctrl+t", description: "Tree filter: hide tool results", }, "app.tree.filter.userOnly": { defaultKeys: "ctrl+u", description: "Tree filter: user messages only", }, "app.tree.filter.labeledOnly": { defaultKeys: "ctrl+l", description: "Tree filter: labeled entries only", }, "app.tree.filter.all": { defaultKeys: "ctrl+a", description: "Tree filter: show all entries", }, "app.tree.filter.cycleForward": { defaultKeys: "ctrl+o", description: "Tree filter: cycle forward", }, "app.tree.filter.cycleBackward": { defaultKeys: "shift+ctrl+o", description: "Tree filter: cycle backward", },} as const satisfies KeybindingDefinitions;
export const DEFAULT_APP_KEYBINDINGS = KEYBINDINGS;
const KEYBINDING_NAME_MIGRATIONS = { "app.message.dequeue": "app.message.navigateOlder", cursorUp: "tui.editor.cursorUp", cursorDown: "tui.editor.cursorDown", cursorLeft: "tui.editor.cursorLeft", cursorRight: "tui.editor.cursorRight", cursorWordLeft: "tui.editor.cursorWordLeft", cursorWordRight: "tui.editor.cursorWordRight", cursorLineStart: "tui.editor.cursorLineStart", cursorLineEnd: "tui.editor.cursorLineEnd", jumpForward: "tui.editor.jumpForward", jumpBackward: "tui.editor.jumpBackward", pageUp: "tui.editor.pageUp", pageDown: "tui.editor.pageDown", deleteCharBackward: "tui.editor.deleteCharBackward", deleteCharForward: "tui.editor.deleteCharForward", deleteWordBackward: "tui.editor.deleteWordBackward", deleteWordForward: "tui.editor.deleteWordForward", deleteToLineStart: "tui.editor.deleteToLineStart", deleteToLineEnd: "tui.editor.deleteToLineEnd", yank: "tui.editor.yank", yankPop: "tui.editor.yankPop", undo: "tui.editor.undo", newLine: "tui.input.newLine", submit: "tui.input.submit", tab: "tui.input.tab", copy: "tui.input.copy", selectUp: "tui.select.up", selectDown: "tui.select.down", selectPageUp: "tui.select.pageUp", selectPageDown: "tui.select.pageDown", selectConfirm: "tui.select.confirm", selectCancel: "tui.select.cancel", interrupt: "app.interrupt", clear: "app.clear", clearInput: "app.input.clear", exit: "app.exit", suspend: "app.suspend", selectModel: "app.model.select", expandTools: "app.tools.expand", expandAgentMessages: "app.messages.expand", expandEditDiffs: "app.edits.expand", focusSubagents: "app.subagents.focus", externalEditor: "app.editor.external", followUp: "app.message.followUp", dequeue: "app.message.navigateOlder", pasteImage: "app.clipboard.pasteImage", newSession: "app.session.new", tree: "app.session.tree", fork: "app.session.fork", resume: "app.session.resume", agentsBack: "app.agents.back", agentsReply: "app.agents.reply", agentsNew: "app.agents.new", agentsDelete: "app.agents.delete", agentsProgram: "app.agents.program", agentsRename: "app.agents.rename", treeFoldOrUp: "app.tree.foldOrUp", treeUnfoldOrDown: "app.tree.unfoldOrDown", treeEditLabel: "app.tree.editLabel", treeToggleLabelTimestamp: "app.tree.toggleLabelTimestamp",} as const satisfies Record<string, Keybinding>;
function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value);}
function isLegacyKeybindingName(key: string): key is keyof typeof KEYBINDING_NAME_MIGRATIONS { return key in KEYBINDING_NAME_MIGRATIONS;}
function toKeybindingsConfig(value: unknown): KeybindingsConfig { if (!isRecord(value)) return {};
const config: KeybindingsConfig = {}; for (const [key, binding] of Object.entries(value)) { if (typeof binding === "string") { config[key] = binding as KeyId; continue; } if (Array.isArray(binding) && binding.every((entry) => typeof entry === "string")) { config[key] = binding as KeyId[]; } } return config;}
export function migrateKeybindingsConfig(rawConfig: Record<string, unknown>): { config: Record<string, unknown>; migrated: boolean;} { const config: Record<string, unknown> = {}; let migrated = false;
for (const [key, value] of Object.entries(rawConfig)) { const nextKey = isLegacyKeybindingName(key) ? KEYBINDING_NAME_MIGRATIONS[key] : key; if (nextKey !== key) { migrated = true; } if (key !== nextKey && Object.hasOwn(rawConfig, nextKey)) { migrated = true; continue; } config[nextKey] = value; }
return { config: orderKeybindingsConfig(config), migrated };}
function orderKeybindingsConfig(config: Record<string, unknown>): Record<string, unknown> { const ordered: Record<string, unknown> = {}; for (const keybinding of Object.keys(KEYBINDINGS)) { if (Object.hasOwn(config, keybinding)) { ordered[keybinding] = config[keybinding]; } }
const extras = Object.keys(config) .filter((key) => !Object.hasOwn(ordered, key)) .sort(); for (const key of extras) { ordered[key] = config[key]; }
return ordered;}
function loadRawConfig(path: string): Record<string, unknown> | undefined { if (!existsSync(path)) return undefined; try { const parsed = JSON.parse(readFileSync(path, "utf-8")) as unknown; return isRecord(parsed) ? parsed : undefined; } catch { return undefined; }}
export class KeybindingsManager extends TuiKeybindingsManager { private configPath: string | undefined;
constructor(userBindings: KeybindingsConfig = {}, configPath?: string) { super(KEYBINDINGS, userBindings); this.configPath = configPath; }
static create(agentDir: string = getAgentDir()): KeybindingsManager { const configPath = join(agentDir, "keybindings.json"); const userBindings = KeybindingsManager.loadFromFile(configPath); return new KeybindingsManager(userBindings, configPath); }
reload(): void { if (!this.configPath) return; this.setUserBindings(KeybindingsManager.loadFromFile(this.configPath)); }
getEffectiveConfig(): KeybindingsConfig { return this.getResolvedBindings(); }
private static loadFromFile(path: string): KeybindingsConfig { const rawConfig = loadRawConfig(path); if (!rawConfig) return {}; return toKeybindingsConfig(migrateKeybindingsConfig(rawConfig).config); }}
export type { Keybinding, KeybindingsConfig, KeyId };