mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
Fixes #4748. ## Problem Extensions that import `keyText`/`keyHint` from `@earendil-works/pi-coding-agent` can resolve their own copy of the package (their own `node_modules`, or the bundled `cli.js` vs `dist/index.js`). That copy's keybindings are never set by the host, so it falls back to a manager with only `tui.*` bindings, and `keyHint("app.tools.expand", "to expand")` renders as ` to expand`. ## Change The host passes its own `keyText` and `keyHint` to extension renderers, the same way it passes `theme`: - `ToolRenderContext` (`renderCall`/`renderResult` context): `context.keyHint(...)` - `MessageRenderOptions` and `EntryRenderOptions`: `options.keyHint(...)` All three extend a new exported `KeybindingHints` interface. The docs and the `debug-provider` example now use the injected functions and warn against importing them in renderers. Not done: - Moving the `pi-tui` keybindings singleton to `globalThis`. Per the discussion in #7011, extension-facing state should be host-owned and passed through the extension API instead of shared through process-wide module state. - `ctx.ui.keyText`/`keyHint`. No reported case needs them outside renderers, RPC and print modes never install app keybindings, and adding them to `ExtensionUIContext` would break SDK hosts that implement it. ## Tests - `test/extension-keybinding-hints.test.ts`: tool, message, and entry renderers format a user-overridden `app.tools.expand` binding. - `npm run check` passes.
82 lines
3.1 KiB
TypeScript
82 lines
3.1 KiB
TypeScript
// Issue #4748: extensions that import keyText/keyHint can resolve their own package copy, which does not see the
|
|
// host's keybindings. The host passes its own keyText/keyHint to renderers instead.
|
|
import { setKeybindings, Text, type TUI } from "@earendil-works/pi-tui";
|
|
import { Type } from "typebox";
|
|
import { beforeAll, describe, expect, test } from "vitest";
|
|
import type { KeybindingHints, ToolDefinition } from "../src/core/extensions/types.ts";
|
|
import { KeybindingsManager } from "../src/core/keybindings.ts";
|
|
import type { CustomMessage } from "../src/core/messages.ts";
|
|
import type { CustomEntry } from "../src/core/session-manager.ts";
|
|
import { CustomEntryComponent } from "../src/modes/interactive/components/custom-entry.ts";
|
|
import { CustomMessageComponent } from "../src/modes/interactive/components/custom-message.ts";
|
|
import { ToolExecutionComponent } from "../src/modes/interactive/components/tool-execution.ts";
|
|
import { initTheme } from "../src/modes/interactive/theme/theme.ts";
|
|
import { stripAnsi } from "../src/utils/ansi.ts";
|
|
|
|
function hintText(hints: KeybindingHints): string {
|
|
return `${hints.keyText("app.tools.expand")} | ${stripAnsi(hints.keyHint("app.tools.expand", "to expand"))}`;
|
|
}
|
|
|
|
const EXPECTED = "ctrl+e | ctrl+e to expand";
|
|
|
|
describe("host keybinding hints for extensions", () => {
|
|
beforeAll(() => {
|
|
initTheme("dark");
|
|
// A user override proves the hints read the host's configured keybindings, not the defaults.
|
|
setKeybindings(new KeybindingsManager({ "app.tools.expand": "ctrl+e" }));
|
|
});
|
|
|
|
test("tool renderers receive keyText and keyHint in the render context", () => {
|
|
const seen: string[] = [];
|
|
const toolDefinition: ToolDefinition = {
|
|
name: "custom_tool",
|
|
label: "custom_tool",
|
|
description: "custom tool",
|
|
parameters: Type.Any(),
|
|
execute: async () => ({ content: [{ type: "text", text: "ok" }], details: {} }),
|
|
renderCall: (_args, _theme, context) => {
|
|
seen.push(hintText(context));
|
|
return new Text("call", 0, 0);
|
|
},
|
|
};
|
|
const tui = { requestRender: () => {} } as unknown as TUI;
|
|
new ToolExecutionComponent("custom_tool", "tool-1", {}, {}, toolDefinition, tui, process.cwd()).render(80);
|
|
|
|
expect(seen.at(-1)).toBe(EXPECTED);
|
|
});
|
|
|
|
test("message renderers receive keyText and keyHint in the render options", () => {
|
|
const seen: string[] = [];
|
|
const message: CustomMessage = {
|
|
role: "custom",
|
|
customType: "test",
|
|
content: "custom",
|
|
display: true,
|
|
timestamp: Date.now(),
|
|
};
|
|
new CustomMessageComponent(message, (_message, options) => {
|
|
seen.push(hintText(options));
|
|
return new Text("custom", 0, 0);
|
|
});
|
|
|
|
expect(seen).toEqual([EXPECTED]);
|
|
});
|
|
|
|
test("entry renderers receive keyText and keyHint in the render options", () => {
|
|
const seen: string[] = [];
|
|
const entry: CustomEntry = {
|
|
type: "custom",
|
|
id: "entry-1",
|
|
parentId: null,
|
|
timestamp: new Date().toISOString(),
|
|
customType: "test",
|
|
data: undefined,
|
|
};
|
|
new CustomEntryComponent(entry, (_entry, options) => {
|
|
seen.push(hintText(options));
|
|
return new Text("custom", 0, 0);
|
|
});
|
|
|
|
expect(seen).toEqual([EXPECTED]);
|
|
});
|
|
});
|