From 670d60fa60e62095e506af587e7f3fe4da9a1316 Mon Sep 17 00:00:00 2001 From: can1357 Date: Sun, 28 Jun 2026 08:25:32 +0200 Subject: [PATCH] feat(tui): added file-path completion support to autocomplete provider - Added `getForceFileSuggestions` and `shouldTriggerFileCompletion` to `AutocompleteProvider` interface. - Updated `Editor` to invoke file completion logic dynamically when providers support it. - Corrected log viewer scroll offset calculation to account for viewport height. - Added comprehensive unit test for log row selection and expansion. --- packages/coding-agent/src/debug/log-viewer.ts | 2 +- .../test/debug/log-viewer.test.ts | 32 ++++++++++++++++++- packages/tui/src/autocomplete.ts | 14 ++++++++ packages/tui/src/components/editor.ts | 21 ++++++------ 4 files changed, 57 insertions(+), 12 deletions(-) diff --git a/packages/coding-agent/src/debug/log-viewer.ts b/packages/coding-agent/src/debug/log-viewer.ts index 3e53a7b95..11008836f 100644 --- a/packages/coding-agent/src/debug/log-viewer.ts +++ b/packages/coding-agent/src/debug/log-viewer.ts @@ -641,7 +641,7 @@ export class DebugLogViewerComponent implements Component { const overBody = event.row >= this.#bodyRowStart && event.row < this.#bodyRowStart + this.#bodyRowCount; if (event.wheel !== null && overBody) { this.#statusMessage = undefined; - const maxOffset = Math.max(0, this.#model.rows.length - 1); + const maxOffset = Math.max(0, this.#model.rows.length - this.#bodyHeight()); this.#scrollRowOffset = Math.max(0, Math.min(maxOffset, this.#scrollRowOffset + event.wheel * 3)); this.#onUpdate?.(); return true; diff --git a/packages/coding-agent/test/debug/log-viewer.test.ts b/packages/coding-agent/test/debug/log-viewer.test.ts index d32b5de96..48152f9ee 100644 --- a/packages/coding-agent/test/debug/log-viewer.test.ts +++ b/packages/coding-agent/test/debug/log-viewer.test.ts @@ -1,10 +1,18 @@ -import { describe, expect, it } from "bun:test"; +import { beforeAll, describe, expect, it } from "bun:test"; import { buildLogCopyPayload, + DebugLogViewerComponent, DebugLogViewerModel, LOAD_OLDER_LABEL, SESSION_BOUNDARY_WARNING, } from "@oh-my-pi/pi-coding-agent/debug/log-viewer"; +import { getThemeByName, setThemeInstance } from "@oh-my-pi/pi-coding-agent/modes/theme/theme"; + +beforeAll(async () => { + const theme = await getThemeByName("dark"); + if (!theme) throw new Error("Expected dark theme"); + setThemeInstance(theme); +}); describe("DebugLogViewerModel", () => { const describeRow = (row: { kind: string; logIndex?: number }): string => { @@ -190,6 +198,28 @@ describe("DebugLogViewerModel", () => { expect(model.isExpanded(0)).toBe(false); expect(model.isExpanded(1)).toBe(false); }); + + it("selects and expands a clicked visible log row", () => { + let updates = 0; + const viewer = new DebugLogViewerComponent({ + logs: ["alpha", "beta", "gamma"].join("\n"), + terminalRows: 12, + onExit: () => {}, + onUpdate: () => { + updates++; + }, + }); + + viewer.render(80); + viewer.handleInput("\x1b[<0;2;6M"); + + const rendered = viewer + .render(80) + .map(line => Bun.stripANSI(line)) + .join("\n"); + expect(rendered).toContain("▾ beta"); + expect(updates).toBe(1); + }); }); describe("buildLogCopyPayload", () => { diff --git a/packages/tui/src/autocomplete.ts b/packages/tui/src/autocomplete.ts index b8d41fbf4..d1087630f 100644 --- a/packages/tui/src/autocomplete.ts +++ b/packages/tui/src/autocomplete.ts @@ -230,6 +230,20 @@ export interface AutocompleteProvider { * buffer untouched. */ trySyncInlineReplace?(textBeforeCursor: string): { replaceLen: number; insert: string } | null; + + /** + * Force file-path completion (called on Tab). Returns matched items plus the + * full prefix, or null when no path token sits before the cursor. Present on + * file-aware providers; absent on slash-only ones. + */ + getForceFileSuggestions?( + lines: string[], + cursorLine: number, + cursorCol: number, + ): Promise<{ items: AutocompleteItem[]; prefix: string } | null>; + + /** Whether a Tab press should attempt file completion at the cursor. */ + shouldTriggerFileCompletion?(lines: string[], cursorLine: number, cursorCol: number): boolean; } type CommandEntry = SlashCommand | AutocompleteItem; diff --git a/packages/tui/src/components/editor.ts b/packages/tui/src/components/editor.ts index 56d272418..cda6387da 100644 --- a/packages/tui/src/components/editor.ts +++ b/packages/tui/src/components/editor.ts @@ -1,7 +1,6 @@ import { getProjectDir, logger } from "@oh-my-pi/pi-utils"; import { type AutocompleteProvider, - type CombinedAutocompleteProvider, findLeadingSlashCommandStart, findTrailingSlashCommandStart, } from "../autocomplete"; @@ -2859,10 +2858,13 @@ export class Editor implements Component, Focusable { if (!this.#autocompleteProvider) return; // Check if we should trigger file completion on Tab if (explicitTab) { - const provider = this.#autocompleteProvider as CombinedAutocompleteProvider; const shouldTrigger = - !provider.shouldTriggerFileCompletion || - provider.shouldTriggerFileCompletion(this.#state.lines, this.#state.cursorLine, this.#state.cursorCol); + !this.#autocompleteProvider.shouldTriggerFileCompletion || + this.#autocompleteProvider.shouldTriggerFileCompletion( + this.#state.lines, + this.#state.cursorLine, + this.#state.cursorCol, + ); if (!shouldTrigger) { return; } @@ -2924,17 +2926,16 @@ https://github.com/EsotericSoftware/spine-runtimes/actions/runs/19536643416/job/ async #forceFileAutocomplete(explicitTab: boolean = false): Promise { if (!this.#autocompleteProvider) return; - // Check if provider supports force file suggestions via runtime check - const provider = this.#autocompleteProvider as { - getForceFileSuggestions?: CombinedAutocompleteProvider["getForceFileSuggestions"]; - }; - if (typeof provider.getForceFileSuggestions !== "function") { + // File-aware providers expose getForceFileSuggestions; slash-only ones fall back to regular completion. + const getForceFileSuggestions = this.#autocompleteProvider.getForceFileSuggestions; + if (typeof getForceFileSuggestions !== "function") { await this.#tryTriggerAutocomplete(true); return; } const requestId = ++this.#autocompleteRequestId; - const suggestions = await provider.getForceFileSuggestions( + const suggestions = await getForceFileSuggestions.call( + this.#autocompleteProvider, this.#state.lines, this.#state.cursorLine, this.#state.cursorCol,