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.
This commit is contained in:
can1357
2026-06-28 08:25:32 +02:00
parent 2fd5fd55f2
commit 670d60fa60
4 changed files with 57 additions and 12 deletions
@@ -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;
@@ -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", () => {
+14
View File
@@ -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;
+11 -10
View File
@@ -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<void> {
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,