import { CustomEditor, type ExtensionAPI } from "@earendil-works/pi-coding-agent"; import type { Component, OverlayHandle, TUI } from "@earendil-works/pi-tui"; /** Menu desenhado pelo TUI como camada flutuante, fora do fluxo do Editor. */ class AutocompleteOverlay implements Component { lines: string[] = []; render(width: number): string[] { return this.lines.map((line) => line.slice(0, width)); } invalidate(): void {} } /** * Mantém o autocomplete nativo (filtro, seleção e atalhos), mas extrai suas * linhas do Editor e as apresenta em um overlay acima da caixa de entrada. */ class AutocompleteOverlayEditor extends CustomEditor { private readonly hostTui: TUI; private overlay?: AutocompleteOverlay; private overlayHandle?: OverlayHandle; private overlayBottomMargin?: number; constructor(...args: ConstructorParameters) { super(...args); this.hostTui = args[0]; } render(width: number): string[] { const lines = super.render(width); const internals = this as unknown as { autocompleteState: unknown | null; autocompleteList?: { render(width: number): string[] }; paddingX: number; }; if (!internals.autocompleteState || !internals.autocompleteList) { this.hideAutocompleteOverlay(); return lines; } const maxPadding = Math.max(0, Math.floor((width - 1) / 2)); const paddingX = Math.min(internals.paddingX, maxPadding); const contentWidth = Math.max(1, width - paddingX * 2); const menuHeight = internals.autocompleteList.render(contentWidth).length; if (menuHeight <= 0 || menuHeight >= lines.length) { this.hideAutocompleteOverlay(); return lines; } const editorHeight = lines.length - menuHeight; const editorLines = lines.slice(0, editorHeight); const menuLines = lines.slice(editorHeight); // O anchor bottom-left é relativo ao terminal inteiro. Além da altura do // Editor, reservamos a linha do footer para o menu terminar exatamente // na borda superior da caixa de entrada, sem cobri-la. this.showAutocompleteOverlay(menuLines, editorHeight + 1); return editorLines; } dispose(): void { this.overlayHandle?.hide(); this.overlayHandle = undefined; this.overlay = undefined; } private showAutocompleteOverlay(lines: string[], bottomMargin: number): void { // O tamanho do Editor pode variar se a mensagem tiver várias linhas. // Nesse caso, recriamos o overlay na nova posição. if (this.overlayHandle && this.overlayBottomMargin !== bottomMargin) { this.overlayHandle.hide(); this.overlayHandle = undefined; this.overlay = undefined; } if (!this.overlay || !this.overlayHandle) { this.overlay = new AutocompleteOverlay(); this.overlayBottomMargin = bottomMargin; this.overlayHandle = this.hostTui.showOverlay(this.overlay, { anchor: "bottom-left", width: "100%", margin: { bottom: bottomMargin }, nonCapturing: true, }); } this.overlay.lines = lines; this.overlayHandle.setHidden(false); } private hideAutocompleteOverlay(): void { if (this.overlayHandle && !this.overlayHandle.isHidden()) { this.overlayHandle.setHidden(true); } } } export default function autocompleteOverlay(pi: ExtensionAPI) { let editor: AutocompleteOverlayEditor | undefined; pi.on("session_start", (_event, ctx) => { if (ctx.mode !== "tui") return; ctx.ui.setEditorComponent((tui, theme, keybindings) => { editor = new AutocompleteOverlayEditor(tui, theme, keybindings); return editor; }); }); pi.on("session_shutdown", () => { editor?.dispose(); editor = undefined; }); }