Download packages/coding-agent/src/modes/interactive/components/custom-editor.ts from SaylorTwift/pi: direct link, hf CLI and curl.
- Browser
- Download file 5.15 kB
-
https://huggingface.co/SaylorTwift/pi/resolve/main/packages/coding-agent/src/modes/interactive/components/custom-editor.ts
- Command line
-
hf download hf://SaylorTwift/pi/packages/coding-agent/src/modes/interactive/components/custom-editor.ts
-
curl -L -o custom-editor.ts https://huggingface.co/SaylorTwift/pi/resolve/main/packages/coding-agent/src/modes/interactive/components/custom-editor.ts
5.15 kB
| import { Editor, type EditorOptions, type EditorTheme, type TUI, visibleWidth } from "@earendil-works/pi-tui"; | |
| import type { AppKeybinding, KeybindingsManager } from "../../../core/keybindings.ts"; | |
| import type { StatusIndicator } from "./status-indicator.ts"; | |
| export type CustomEditorOptions = EditorOptions & { | |
| /** Render working, compaction, summarization, and retry status in the editor's top border. */ | |
| embedWorkingStatus?: boolean; | |
| }; | |
| /** | |
| * Custom editor that handles app-level keybindings for coding-agent. | |
| */ | |
| export class CustomEditor extends Editor { | |
| private keybindings: KeybindingsManager; | |
| private workingStatusIndicator: StatusIndicator | undefined; | |
| public readonly embedWorkingStatus: boolean; | |
| public actionHandlers: Map<AppKeybinding, () => 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(tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager, options?: CustomEditorOptions) { | |
| super(tui, theme, options); | |
| this.keybindings = keybindings; | |
| this.embedWorkingStatus = options?.embedWorkingStatus ?? false; | |
| } | |
| setWorkingStatusIndicator(indicator: StatusIndicator | undefined): void { | |
| this.workingStatusIndicator = indicator; | |
| } | |
| protected override renderTopBorder(width: number, hiddenLineCount: number): string { | |
| if (!this.embedWorkingStatus || !this.workingStatusIndicator || width <= 0) { | |
| return super.renderTopBorder(width, hiddenLineCount); | |
| } | |
| let status = this.workingStatusIndicator.renderInBorder(Math.max(1, width - 5)); | |
| let statusWidth = visibleWidth(status); | |
| if (statusWidth === 0) return super.renderTopBorder(width, hiddenLineCount); | |
| const overflowLabel = hiddenLineCount > 0 ? ` β ${hiddenLineCount} more ` : undefined; | |
| const overflowLabelWidth = overflowLabel ? visibleWidth(overflowLabel) : 0; | |
| const overflowStart = Math.floor((width - overflowLabelWidth) / 2); | |
| const canFitOverflow = () => | |
| overflowLabel !== undefined && overflowLabelWidth + 2 <= width && overflowStart - (3 + statusWidth + 1) >= 1; | |
| if (overflowLabel && !canFitOverflow()) { | |
| status = this.workingStatusIndicator.renderSpinnerInBorder(width); | |
| statusWidth = visibleWidth(status); | |
| } | |
| if (canFitOverflow()) { | |
| const leftBlockWidth = 3 + statusWidth + 1; | |
| return ( | |
| this.borderColor("ββ ") + | |
| status + | |
| this.borderColor( | |
| ` ${"β".repeat(overflowStart - leftBlockWidth)}${overflowLabel}${"β".repeat(width - overflowStart - overflowLabelWidth)}`, | |
| ) | |
| ); | |
| } | |
| if (width >= statusWidth + 5) { | |
| return this.borderColor("ββ ") + status + this.borderColor(` ${"β".repeat(width - statusWidth - 4)}`); | |
| } | |
| status = this.workingStatusIndicator.renderSpinnerInBorder(width); | |
| statusWidth = visibleWidth(status); | |
| const prefixWidth = Math.min(3, Math.max(0, width - statusWidth)); | |
| return ( | |
| this.borderColor("β".repeat(prefixWidth)) + | |
| status + | |
| this.borderColor("β".repeat(Math.max(0, width - prefixWidth - statusWidth))) | |
| ); | |
| } | |
| /** | |
| * Register a handler for an app action. | |
| */ | |
| onAction(action: AppKeybinding, handler: () => void): void { | |
| this.actionHandlers.set(action, handler); | |
| } | |
| handleInput(data: string): void { | |
| // Check extension-registered shortcuts first | |
| if (this.onExtensionShortcut?.(data)) { | |
| return; | |
| } | |
| // Check for clipboard paste keybinding | |
| if (this.keybindings.matches(data, "app.clipboard.pasteImage")) { | |
| this.onPasteImage?.(); | |
| return; | |
| } | |
| // Check app keybindings first | |
| // Escape/interrupt - only if autocomplete is NOT active | |
| if (this.keybindings.matches(data, "app.interrupt")) { | |
| if (!this.isShowingAutocomplete()) { | |
| // Use dynamic onEscape if set, otherwise registered handler | |
| const handler = this.onEscape ?? this.actionHandlers.get("app.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, "app.exit")) { | |
| if (this.getText().length === 0) { | |
| const handler = this.onCtrlD ?? this.actionHandlers.get("app.exit"); | |
| if (handler) handler(); | |
| return; | |
| } | |
| // Fall through to editor handling for delete-char-forward when not empty | |
| } | |
| // Explicit history bindings take precedence over app actions while the editor is focused. | |
| // This lets users bind Ctrl+P even though it cycles models by default. | |
| if ( | |
| this.keybindings.matches(data, "tui.editor.historyPrevious") || | |
| this.keybindings.matches(data, "tui.editor.historyNext") | |
| ) { | |
| super.handleInput(data); | |
| return; | |
| } | |
| // Check all other app actions | |
| for (const [action, handler] of this.actionHandlers) { | |
| if (action !== "app.interrupt" && action !== "app.exit" && this.keybindings.matches(data, action)) { | |
| handler(); | |
| return; | |
| } | |
| } | |
| // Pass to parent for editor handling | |
| super.handleInput(data); | |
| } | |
| } | |