mirror of
https://github.com/earendil-works/pi.git
synced 2026-06-18 15:54:04 +08:00
Breaking changes: - Settings: 'hooks' and 'customTools' arrays replaced with 'extensions' - CLI: '--hook' and '--tool' flags replaced with '--extension' / '-e' - API: HookMessage renamed to CustomMessage, role 'hookMessage' to 'custom' - API: FileSlashCommand renamed to PromptTemplate - API: discoverSlashCommands() renamed to discoverPromptTemplates() - Directories: commands/ renamed to prompts/ for prompt templates Migration: - Session version bumped to 3 (auto-migrates v2 sessions) - Old 'hookMessage' role entries converted to 'custom' Structural changes: - src/core/hooks/ and src/core/custom-tools/ merged into src/core/extensions/ - src/core/slash-commands.ts renamed to src/core/prompt-templates.ts - examples/hooks/ and examples/custom-tools/ merged into examples/extensions/ - docs/hooks.md and docs/custom-tools.md merged into docs/extensions.md New test coverage: - test/extensions-runner.test.ts (10 tests) - test/extensions-discovery.test.ts (26 tests) - test/prompt-templates.test.ts
80 lines
2.2 KiB
TypeScript
80 lines
2.2 KiB
TypeScript
import { Editor, type EditorTheme, matchesKey } from "@mariozechner/pi-tui";
|
|
import type { AppAction, KeybindingsManager } from "../../../core/keybindings.js";
|
|
|
|
/**
|
|
* Custom editor that handles app-level keybindings for coding-agent.
|
|
*/
|
|
export class CustomEditor extends Editor {
|
|
private keybindings: KeybindingsManager;
|
|
private actionHandlers: Map<AppAction, () => void> = new Map();
|
|
|
|
// Special handlers that can be dynamically replaced
|
|
public onEscape?: () => void;
|
|
public onCtrlD?: () => void;
|
|
public onPasteImage?: () => void;
|
|
/** Handler for extension-registered shortcuts. Returns true if handled. */
|
|
public onExtensionShortcut?: (data: string) => boolean;
|
|
|
|
constructor(theme: EditorTheme, keybindings: KeybindingsManager) {
|
|
super(theme);
|
|
this.keybindings = keybindings;
|
|
}
|
|
|
|
/**
|
|
* Register a handler for an app action.
|
|
*/
|
|
onAction(action: AppAction, handler: () => void): void {
|
|
this.actionHandlers.set(action, handler);
|
|
}
|
|
|
|
handleInput(data: string): void {
|
|
// Check extension-registered shortcuts first
|
|
if (this.onExtensionShortcut?.(data)) {
|
|
return;
|
|
}
|
|
|
|
// Check for Ctrl+V to handle clipboard image paste
|
|
if (matchesKey(data, "ctrl+v")) {
|
|
this.onPasteImage?.();
|
|
return;
|
|
}
|
|
|
|
// Check app keybindings first
|
|
|
|
// Escape/interrupt - only if autocomplete is NOT active
|
|
if (this.keybindings.matches(data, "interrupt")) {
|
|
if (!this.isShowingAutocomplete()) {
|
|
// Use dynamic onEscape if set, otherwise registered handler
|
|
const handler = this.onEscape ?? this.actionHandlers.get("interrupt");
|
|
if (handler) {
|
|
handler();
|
|
return;
|
|
}
|
|
}
|
|
// Let parent handle escape for autocomplete cancellation
|
|
super.handleInput(data);
|
|
return;
|
|
}
|
|
|
|
// Exit (Ctrl+D) - only when editor is empty
|
|
if (this.keybindings.matches(data, "exit")) {
|
|
if (this.getText().length === 0) {
|
|
const handler = this.onCtrlD ?? this.actionHandlers.get("exit");
|
|
if (handler) handler();
|
|
}
|
|
return; // Always consume
|
|
}
|
|
|
|
// Check all other app actions
|
|
for (const [action, handler] of this.actionHandlers) {
|
|
if (action !== "interrupt" && action !== "exit" && this.keybindings.matches(data, action)) {
|
|
handler();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Pass to parent for editor handling
|
|
super.handleInput(data);
|
|
}
|
|
}
|