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.
90 lines
2.7 KiB
TypeScript
90 lines
2.7 KiB
TypeScript
/**
|
|
* Raw provider event viewer.
|
|
*
|
|
* Usage: /debug-provider [on|off]
|
|
* With no argument, the command toggles capture. Captured events are persisted
|
|
* as expandable custom entries.
|
|
*/
|
|
|
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
import { Box, Text } from "@earendil-works/pi-tui";
|
|
|
|
const ENTRY_TYPE = "debug-provider-events";
|
|
const STATUS_KEY = "debug-provider";
|
|
|
|
interface ProviderDebugEntry {
|
|
provider: string;
|
|
api: string;
|
|
model: string;
|
|
events: unknown[];
|
|
}
|
|
|
|
export default function (pi: ExtensionAPI) {
|
|
let enabled = false;
|
|
let activeEvents: unknown[] | undefined;
|
|
let completedEntry: ProviderDebugEntry | undefined;
|
|
|
|
pi.registerEntryRenderer<ProviderDebugEntry>(ENTRY_TYPE, (entry, { expanded, keyHint }, theme) => {
|
|
const data = entry.data;
|
|
if (!data) return new Text(theme.fg("warning", "[provider debug] Missing event data"), 0, 0);
|
|
|
|
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
const count = `${data.events.length} event${data.events.length === 1 ? "" : "s"}`;
|
|
const expandHint = expanded ? "" : ` (${keyHint("app.tools.expand", "to view events")})`;
|
|
box.addChild(
|
|
new Text(
|
|
`${theme.fg("accent", "[provider debug]")} ${data.provider}/${data.model} (${data.api}) · ${count}${expandHint}`,
|
|
0,
|
|
0,
|
|
),
|
|
);
|
|
if (expanded) {
|
|
box.addChild(new Text(JSON.stringify(data.events, null, 2), 0, 0));
|
|
}
|
|
return box;
|
|
});
|
|
|
|
pi.registerCommand("debug-provider", {
|
|
description: "Toggle capture of raw provider stream events",
|
|
handler: async (args, ctx) => {
|
|
const requestedState = args.trim().toLowerCase();
|
|
if (requestedState !== "" && requestedState !== "on" && requestedState !== "off") {
|
|
ctx.ui.notify("Usage: /debug-provider [on|off]", "warning");
|
|
return;
|
|
}
|
|
|
|
enabled = requestedState === "" ? !enabled : requestedState === "on";
|
|
if (!enabled) {
|
|
activeEvents = undefined;
|
|
completedEntry = undefined;
|
|
}
|
|
ctx.ui.setStatus(STATUS_KEY, enabled ? "provider debug" : undefined);
|
|
ctx.ui.notify(`Provider event capture ${enabled ? "enabled" : "disabled"}`, "info");
|
|
},
|
|
});
|
|
|
|
pi.on("turn_start", () => {
|
|
activeEvents = enabled ? [] : undefined;
|
|
});
|
|
|
|
pi.on("provider_stream_event", (event) => {
|
|
activeEvents?.push(structuredClone(event.data));
|
|
});
|
|
|
|
pi.on("message_end", (event) => {
|
|
if (event.message.role !== "assistant" || !activeEvents) return;
|
|
completedEntry = {
|
|
provider: event.message.provider,
|
|
api: event.message.api,
|
|
model: event.message.model,
|
|
events: activeEvents,
|
|
};
|
|
activeEvents = undefined;
|
|
});
|
|
|
|
pi.on("turn_end", () => {
|
|
if (!completedEntry) return;
|
|
pi.appendEntry<ProviderDebugEntry>(ENTRY_TYPE, completedEntry);
|
|
completedEntry = undefined;
|
|
});
|
|
}
|