0419a9e01f
- Added abort signal support to LSP file operations (ensureFileOpen, refreshFile) enabling cancellation of long-running file operations. - Added abort signal propagation through LSP request handlers (definition, references, hover, symbols, rename) with $/cancelRequest notification support. - Added shouldBypassAutocompleteOnEscape callback to custom editor allowing parent components to override escape key handling behavior. - Changed escape key handling in custom editor to respect bypass callback, enabling global escape handling during active operations. - Fixed LSP operations to properly handle abort signals and throw ToolAbortError when operations are cancelled, with input controller bypassing autocomplete during streaming/loading states.
182 lines
4.6 KiB
TypeScript
182 lines
4.6 KiB
TypeScript
import { Editor, type KeyId, matchesKey, parseKittySequence } from "@oh-my-pi/pi-tui";
|
|
|
|
/**
|
|
* Custom editor that handles Escape and Ctrl+C keys for coding-agent
|
|
*/
|
|
export class CustomEditor extends Editor {
|
|
onEscape?: () => void;
|
|
shouldBypassAutocompleteOnEscape?: () => boolean;
|
|
onCtrlC?: () => void;
|
|
onCtrlD?: () => void;
|
|
onShiftTab?: () => void;
|
|
onCtrlP?: () => void;
|
|
onShiftCtrlP?: () => void;
|
|
onCtrlL?: () => void;
|
|
onCtrlR?: () => void;
|
|
onCtrlO?: () => void;
|
|
onCtrlT?: () => void;
|
|
onCtrlG?: () => void;
|
|
onCtrlZ?: () => void;
|
|
onQuestionMark?: () => void;
|
|
onCapsLock?: () => void;
|
|
onAltP?: () => void;
|
|
/** Called when Alt+Shift+C is pressed to copy prompt to clipboard. */
|
|
onCopyPrompt?: () => void;
|
|
/** Called when Ctrl+V is pressed. Returns true if handled (image found), false to fall through to text paste. */
|
|
onCtrlV?: () => Promise<boolean>;
|
|
/** Called when Alt+Up is pressed (dequeue keybinding). */
|
|
onAltUp?: () => void;
|
|
|
|
/** Custom key handlers from extensions */
|
|
#customKeyHandlers = new Map<KeyId, () => void>();
|
|
|
|
/**
|
|
* Register a custom key handler. Extensions use this for shortcuts.
|
|
*/
|
|
setCustomKeyHandler(key: KeyId, handler: () => void): void {
|
|
this.#customKeyHandlers.set(key, handler);
|
|
}
|
|
|
|
/**
|
|
* Remove a custom key handler.
|
|
*/
|
|
removeCustomKeyHandler(key: KeyId): void {
|
|
this.#customKeyHandlers.delete(key);
|
|
}
|
|
|
|
/**
|
|
* Clear all custom key handlers.
|
|
*/
|
|
clearCustomKeyHandlers(): void {
|
|
this.#customKeyHandlers.clear();
|
|
}
|
|
|
|
handleInput(data: string): void {
|
|
const parsed = parseKittySequence(data);
|
|
if (parsed && (parsed.modifier & 64) !== 0 && this.onCapsLock) {
|
|
// Caps Lock is modifier bit 64
|
|
this.onCapsLock();
|
|
return;
|
|
}
|
|
|
|
// Intercept Ctrl+V for image paste (async - fires and handles result)
|
|
if (matchesKey(data, "ctrl+v") && this.onCtrlV) {
|
|
void this.onCtrlV();
|
|
return;
|
|
}
|
|
|
|
// Intercept Ctrl+G for external editor
|
|
if (matchesKey(data, "ctrl+g") && this.onCtrlG) {
|
|
this.onCtrlG();
|
|
return;
|
|
}
|
|
|
|
// Intercept Alt+P for quick model switching
|
|
if (matchesKey(data, "alt+p") && this.onAltP) {
|
|
this.onAltP();
|
|
return;
|
|
}
|
|
|
|
// Intercept Ctrl+Z for suspend
|
|
if (matchesKey(data, "ctrl+z") && this.onCtrlZ) {
|
|
this.onCtrlZ();
|
|
return;
|
|
}
|
|
|
|
// Intercept Ctrl+T for thinking block visibility toggle
|
|
if (matchesKey(data, "ctrl+t") && this.onCtrlT) {
|
|
this.onCtrlT();
|
|
return;
|
|
}
|
|
|
|
// Intercept Ctrl+L for model selector
|
|
if (matchesKey(data, "ctrl+l") && this.onCtrlL) {
|
|
this.onCtrlL();
|
|
return;
|
|
}
|
|
|
|
// Intercept Ctrl+R for history search
|
|
if (matchesKey(data, "ctrl+r") && this.onCtrlR) {
|
|
this.onCtrlR();
|
|
return;
|
|
}
|
|
|
|
// Intercept Ctrl+O for tool output expansion
|
|
if (matchesKey(data, "ctrl+o") && this.onCtrlO) {
|
|
this.onCtrlO();
|
|
return;
|
|
}
|
|
|
|
// Intercept Shift+Ctrl+P for backward model cycling (check before Ctrl+P)
|
|
if ((matchesKey(data, "shift+ctrl+p") || matchesKey(data, "ctrl+shift+p")) && this.onShiftCtrlP) {
|
|
this.onShiftCtrlP();
|
|
return;
|
|
}
|
|
|
|
// Intercept Ctrl+P for model cycling
|
|
if (matchesKey(data, "ctrl+p") && this.onCtrlP) {
|
|
this.onCtrlP();
|
|
return;
|
|
}
|
|
|
|
// Intercept Shift+Tab for thinking level cycling
|
|
if (matchesKey(data, "shift+tab") && this.onShiftTab) {
|
|
this.onShiftTab();
|
|
return;
|
|
}
|
|
|
|
// Intercept Escape key.
|
|
// Default behavior keeps autocomplete dismissal, but parent can prioritize global escape handling.
|
|
if ((matchesKey(data, "escape") || matchesKey(data, "esc")) && this.onEscape) {
|
|
if (!this.isShowingAutocomplete() || this.shouldBypassAutocompleteOnEscape?.()) {
|
|
this.onEscape();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Intercept Ctrl+C
|
|
if (matchesKey(data, "ctrl+c") && this.onCtrlC) {
|
|
this.onCtrlC();
|
|
return;
|
|
}
|
|
|
|
// Intercept Ctrl+D (only when editor is empty)
|
|
if (matchesKey(data, "ctrl+d")) {
|
|
if (this.getText().length === 0 && this.onCtrlD) {
|
|
this.onCtrlD();
|
|
}
|
|
// Always consume Ctrl+D (don't pass to parent)
|
|
return;
|
|
}
|
|
|
|
// Intercept Alt+Up for dequeue (restore queued message to editor)
|
|
if (matchesKey(data, "alt+up") && this.onAltUp) {
|
|
this.onAltUp();
|
|
return;
|
|
}
|
|
|
|
// Intercept Alt+Shift+C to copy prompt to clipboard
|
|
if (matchesKey(data, "alt+shift+c") && this.onCopyPrompt) {
|
|
this.onCopyPrompt();
|
|
return;
|
|
}
|
|
|
|
// Intercept ? when editor is empty to show hotkeys
|
|
if (data === "?" && this.getText().length === 0 && this.onQuestionMark) {
|
|
this.onQuestionMark();
|
|
return;
|
|
}
|
|
|
|
// Check custom key handlers (extensions)
|
|
for (const [keyId, handler] of this.#customKeyHandlers) {
|
|
if (matchesKey(data, keyId)) {
|
|
handler();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Pass to parent for normal handling
|
|
super.handleInput(data);
|
|
}
|
|
}
|