UNPKG

@earendil-works/pi-coding-agent

Version:

Coding agent CLI with read, bash, edit, write tools and session management

296 lines 10.4 kB
import { 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 const KEYBINDINGS = { ...TUI_KEYBINDINGS, "app.interrupt": { defaultKeys: "escape", description: "Cancel or abort" }, "app.clear": { defaultKeys: "ctrl+c", description: "Clear editor" }, "app.exit": { defaultKeys: "ctrl+d", description: "Exit when editor is empty" }, "app.suspend": { defaultKeys: process.platform === "win32" ? [] : "ctrl+z", description: "Suspend to background", }, "app.thinking.cycle": { defaultKeys: "shift+tab", description: "Cycle thinking level", }, "app.model.cycleForward": { defaultKeys: "ctrl+p", description: "Cycle to next model", }, "app.model.cycleBackward": { defaultKeys: "shift+ctrl+p", description: "Cycle to previous model", }, "app.model.select": { defaultKeys: "ctrl+l", description: "Open model selector" }, "app.tools.expand": { defaultKeys: "ctrl+o", description: "Toggle tool output" }, "app.thinking.toggle": { defaultKeys: "ctrl+t", description: "Toggle thinking blocks", }, "app.session.toggleNamedFilter": { defaultKeys: "ctrl+n", description: "Toggle named session filter", }, "app.editor.external": { defaultKeys: "ctrl+g", description: "Open external editor", }, "app.message.copy": { defaultKeys: "ctrl+x", description: "Copy message to clipboard", }, "app.message.followUp": { defaultKeys: "alt+enter", description: "Queue follow-up message", }, "app.message.dequeue": { defaultKeys: "alt+up", description: "Restore queued messages", }, "app.clipboard.pasteImage": { defaultKeys: process.platform === "win32" ? "alt+v" : "ctrl+v", description: "Paste image from clipboard (text fallback)", }, "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.tree.foldOrUp": { defaultKeys: process.platform === "darwin" ? ["alt+left", "ctrl+left"] : ["ctrl+left", "alt+left"], description: "Fold tree branch or move up", }, "app.tree.unfoldOrDown": { defaultKeys: process.platform === "darwin" ? ["alt+right", "ctrl+right"] : ["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.session.togglePath": { defaultKeys: "ctrl+p", description: "Toggle session path display", }, "app.session.toggleSort": { defaultKeys: "ctrl+s", description: "Toggle session sort mode", }, "app.session.rename": { defaultKeys: "ctrl+r", description: "Rename session", }, "app.session.delete": { defaultKeys: "ctrl+d", description: "Delete session", }, "app.session.deleteNoninvasive": { defaultKeys: "ctrl+backspace", description: "Delete session when query is empty", }, "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", }, }; const KEYBINDING_NAME_MIGRATIONS = { 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", exit: "app.exit", suspend: "app.suspend", cycleThinkingLevel: "app.thinking.cycle", cycleModelForward: "app.model.cycleForward", cycleModelBackward: "app.model.cycleBackward", selectModel: "app.model.select", expandTools: "app.tools.expand", toggleThinking: "app.thinking.toggle", toggleSessionNamedFilter: "app.session.toggleNamedFilter", externalEditor: "app.editor.external", followUp: "app.message.followUp", dequeue: "app.message.dequeue", pasteImage: "app.clipboard.pasteImage", newSession: "app.session.new", tree: "app.session.tree", fork: "app.session.fork", resume: "app.session.resume", treeFoldOrUp: "app.tree.foldOrUp", treeUnfoldOrDown: "app.tree.unfoldOrDown", treeEditLabel: "app.tree.editLabel", treeToggleLabelTimestamp: "app.tree.toggleLabelTimestamp", toggleSessionPath: "app.session.togglePath", toggleSessionSort: "app.session.toggleSort", renameSession: "app.session.rename", deleteSession: "app.session.delete", deleteSessionNoninvasive: "app.session.deleteNoninvasive", }; function isLegacyKeybindingName(key) { return key in KEYBINDING_NAME_MIGRATIONS; } function toKeybindingsConfig(value) { const config = {}; for (const [key, binding] of Object.entries(value)) { if (typeof binding === "string") { config[key] = binding; continue; } if (Array.isArray(binding) && binding.every((entry) => typeof entry === "string")) { config[key] = binding; } } return config; } export function migrateKeybindingsConfig(rawConfig) { const config = {}; 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) { const ordered = {}; 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) { if (!existsSync(path)) return undefined; try { const parsed = JSON.parse(readFileSync(path, "utf-8")); if (typeof parsed !== "object" || parsed === null) return undefined; return parsed; } catch { return undefined; } } export class KeybindingsManager extends TuiKeybindingsManager { configPath; constructor(userBindings = {}, configPath) { super(KEYBINDINGS, userBindings); this.configPath = configPath; } static create(agentDir = getAgentDir()) { const configPath = join(agentDir, "keybindings.json"); const userBindings = KeybindingsManager.loadFromFile(configPath); return new KeybindingsManager(userBindings, configPath); } reload() { if (!this.configPath) return; this.setUserBindings(KeybindingsManager.loadFromFile(this.configPath)); } getEffectiveConfig() { return this.getResolvedBindings(); } static loadFromFile(path) { const rawConfig = loadRawConfig(path); if (!rawConfig) return {}; return toKeybindingsConfig(migrateKeybindingsConfig(rawConfig).config); } } //# sourceMappingURL=keybindings.js.map