UNPKG

@earendil-works/pi-coding-agent

Version:

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

324 lines • 11.8 kB
import { TUI_KEYBINDINGS, KeybindingsManager as TuiKeybindingsManager, } from "@earendil-works/pi-tui"; import { existsSync, readFileSync } from "fs"; import { join } from "path"; import { Compile } from "typebox/compile"; import { getAgentDir } from "../config.js"; import { stripBom } from "../utils/text.js"; import { KeybindingValueSchema } from "./key-id-schema.js"; export function useWindowsKeybindings(platform = process.platform, env = process.env) { return platform === "win32" || (platform === "linux" && Boolean(env.WSL_DISTRO_NAME || env.WSL_INTEROP)); } const windowsKeybindings = useWindowsKeybindings(); const APP_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.thinking.save": { defaultKeys: "ctrl+s", description: "Save thinking level", }, "app.model.cycleForward": { defaultKeys: "ctrl+p", description: "Cycle to next model", }, "app.model.cycleBackward": { defaultKeys: windowsKeybindings ? "alt+p" : "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 selection or last assistant message", }, "app.message.followUp": { defaultKeys: windowsKeybindings ? "ctrl+q" : "alt+enter", description: "Queue follow-up message", }, "app.message.dequeue": { defaultKeys: windowsKeybindings ? "alt+q" : "alt+up", description: "Restore queued messages", }, "app.clipboard.pasteImage": { defaultKeys: windowsKeybindings ? ["alt+v", "ctrl+v"] : "ctrl+v", description: "Paste files on macOS, images, or text from clipboard", }, "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", }, }; export const KEYBINDINGS = { ...TUI_KEYBINDINGS, "tui.editor.undo": { ...TUI_KEYBINDINGS["tui.editor.undo"], defaultKeys: process.platform === "win32" ? "ctrl+z" : windowsKeybindings ? "alt+z" : "ctrl+-", }, "tui.altScreen.previousPrompt": { ...TUI_KEYBINDINGS["tui.altScreen.previousPrompt"], defaultKeys: windowsKeybindings ? "ctrl+up" : ["ctrl+shift+up", "ctrl+up"], }, "tui.altScreen.nextPrompt": { ...TUI_KEYBINDINGS["tui.altScreen.nextPrompt"], defaultKeys: windowsKeybindings ? "ctrl+down" : ["ctrl+shift+down", "ctrl+down"], }, "tui.altScreen.search": { ...TUI_KEYBINDINGS["tui.altScreen.search"], defaultKeys: windowsKeybindings ? "ctrl+f" : "ctrl+shift+f", }, ...APP_KEYBINDINGS, }; 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; } const validateKeybindingValue = Compile(KeybindingValueSchema); function toKeybindingsConfig(value) { const config = {}; for (const [key, binding] of Object.entries(value)) { if (key === "$schema") continue; if (validateKeybindingValue.Check(binding)) 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(stripBom(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