Files
oh-my-pi/packages/coding-agent/src/modes/controllers/extension-ui-controller.test.ts
T
can1357 c944870566 fix(coding-agent): implemented pi's ui.addAutocompleteProvider API
Extensions calling ctx.ui.addAutocompleteProvider (e.g. @ff-labs/pi-fff)
crashed at load with 'TypeError: ... is not a function' because omp's
ExtensionAPI.ui omitted pi's autocomplete-provider API; the throw also
aborted the rest of a try/catch-guarded session_start init.

ExtensionUIContext now declares addAutocompleteProvider(factory).
Interactive mode stacks each factory on the built-in editor provider in
registration order, re-applies the stack on every slash-command refresh,
and skips throwing/malformed factories; RPC, ACP, and headless contexts
accept the factory as a no-op, matching upstream pi's RPC behavior.

Fixes #4919
2026-07-09 18:27:23 +02:00

75 lines
2.2 KiB
TypeScript

import { describe, expect, it, vi } from "bun:test";
import type { ExtensionUIContext } from "../../extensibility/extensions";
import { CustomEditor } from "../components/custom-editor";
import { getEditorTheme } from "../theme/theme";
import type { InteractiveModeContext } from "../types";
import { ExtensionUiController } from "./extension-ui-controller";
function makeHarness() {
const editor = new CustomEditor(getEditorTheme());
const requestRender = vi.fn();
const addAutocompleteProvider = vi.fn();
let uiContext: ExtensionUIContext | undefined;
const ctx = {
editor,
ui: {
requestRender,
},
session: {
extensionRunner: undefined,
},
setToolUIContext(context: ExtensionUIContext, hasUI: boolean): void {
expect(hasUI).toBe(true);
uiContext = context;
},
addAutocompleteProvider,
} as unknown as InteractiveModeContext;
return {
editor,
requestRender,
addAutocompleteProvider,
async init(): Promise<ExtensionUIContext> {
await new ExtensionUiController(ctx).initHooksAndCustomTools();
expect(uiContext).toBeDefined();
return uiContext!;
},
};
}
describe("ExtensionUiController editor UI", () => {
it("requests a render after extension pasteToEditor mutates the prompt", async () => {
const harness = makeHarness();
const ui = await harness.init();
ui.pasteToEditor("hello");
ui.pasteToEditor(" world");
expect(harness.editor.getText()).toBe("hello world");
expect(harness.requestRender).toHaveBeenCalledTimes(2);
});
it("requests a render after extension setEditorText replaces the prompt", async () => {
const harness = makeHarness();
const ui = await harness.init();
ui.setEditorText("hello");
expect(harness.editor.getText()).toBe("hello");
expect(harness.requestRender).toHaveBeenCalledTimes(1);
});
it("bridges addAutocompleteProvider factories to the interactive mode context (#4919)", async () => {
const harness = makeHarness();
const ui = await harness.init();
expect(typeof ui.addAutocompleteProvider).toBe("function");
const factory = (current: unknown) => current as never;
ui.addAutocompleteProvider(factory);
expect(harness.addAutocompleteProvider).toHaveBeenCalledTimes(1);
expect(harness.addAutocompleteProvider).toHaveBeenCalledWith(factory);
});
});