Files
oh-my-pi/packages/coding-agent/src/modes/components/custom-editor.ts
T
can1357 0419a9e01f feat(coding-agent/lsp): fixed LSP cancellation with abort signal support for file operations and requests
- 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.
2026-02-16 18:33:04 +01:00

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);
}
}