refactor: address PR feedback - merge setWidget, use KeyId for shortcuts

1. Merge setWidget and setWidgetComponent into single overloaded method
   - Accepts either string[] or component factory function
   - Uses single Map<string, Component> internally
   - String arrays wrapped in Container with Text components

2. Use KeyId type for registerShortcut instead of plain string
   - Import Key from @mariozechner/pi-tui
   - Update plan-mode example to use Key.shift('p')
   - Type-safe shortcut registration

3. Fix tool API docs
   - Both built-in and custom tools can be enabled/disabled
   - Removed incorrect 'custom tools always active' statement

4. Use matchesKey instead of matchShortcut (already done in rebase)
This commit is contained in:
Helmut Januschka 2026-01-04 00:09:44 +01:00 committed by Mario Zechner
parent e3c2616713
commit 8ecb1d6c0b
11 changed files with 76 additions and 106 deletions

View file

@ -40,9 +40,8 @@
- Hook API: `pi.getActiveTools()` and `pi.setActiveTools(toolNames)` for dynamically enabling/disabling tools from hooks - Hook API: `pi.getActiveTools()` and `pi.setActiveTools(toolNames)` for dynamically enabling/disabling tools from hooks
- Hook API: `pi.getAllTools()` to enumerate all configured tools (built-in via --tools or default, plus custom tools) - Hook API: `pi.getAllTools()` to enumerate all configured tools (built-in via --tools or default, plus custom tools)
- Hook API: `pi.registerFlag(name, options)` and `pi.getFlag(name)` for hooks to register custom CLI flags (parsed automatically) - Hook API: `pi.registerFlag(name, options)` and `pi.getFlag(name)` for hooks to register custom CLI flags (parsed automatically)
- Hook API: `pi.registerShortcut(shortcut, options)` for hooks to register custom keyboard shortcuts (e.g., `shift+p`, `ctrl+shift+x`). Conflicts with built-in shortcuts are skipped, conflicts between hooks logged as warnings. - Hook API: `pi.registerShortcut(shortcut, options)` for hooks to register custom keyboard shortcuts using `KeyId` (e.g., `Key.shift("p")`). Conflicts with built-in shortcuts are skipped, conflicts between hooks logged as warnings.
- Hook API: `ctx.ui.setWidget(key, lines)` for multi-line status displays above the editor (todo lists, progress tracking) - Hook API: `ctx.ui.setWidget(key, content)` for status displays above the editor. Accepts either a string array or a component factory function.
- Hook API: `ctx.ui.setWidgetComponent(key, factory)` for custom TUI components as widgets (no focus, renders inline)
- Hook API: `theme.strikethrough(text)` for strikethrough text styling - Hook API: `theme.strikethrough(text)` for strikethrough text styling
- `/hotkeys` command now shows hook-registered shortcuts in a separate "Hooks" section - `/hotkeys` command now shows hook-registered shortcuts in a separate "Hooks" section
- New example hook: `plan-mode.ts` - Claude Code-style read-only exploration mode: - New example hook: `plan-mode.ts` - Claude Code-style read-only exploration mode:

View file

@ -446,22 +446,22 @@ const currentText = ctx.ui.getEditorText();
**Widget notes:** **Widget notes:**
- Widgets are multi-line displays shown above the editor (below "Working..." indicator) - Widgets are multi-line displays shown above the editor (below "Working..." indicator)
- Multiple hooks can set widgets using unique keys (all widgets are displayed, stacked vertically) - Multiple hooks can set widgets using unique keys (all widgets are displayed, stacked vertically)
- Use `setWidget()` for simple styled text, `setWidgetComponent()` for custom components - `setWidget()` accepts either a string array or a component factory function
- Supports ANSI styling via `ctx.ui.theme` (including `strikethrough`) - Supports ANSI styling via `ctx.ui.theme` (including `strikethrough`)
- **Caution:** Keep widgets small (a few lines). Large widgets from multiple hooks can cause viewport overflow and TUI flicker. Max 10 lines total across all string widgets. - **Caution:** Keep widgets small (a few lines). Large widgets from multiple hooks can cause viewport overflow and TUI flicker. Max 10 lines total across all string widgets.
**Custom widget components:** **Custom widget components:**
For more complex widgets, use `setWidgetComponent()` to render a custom TUI component: For more complex widgets, pass a factory function to `setWidget()`:
```typescript ```typescript
ctx.ui.setWidgetComponent("my-widget", (tui, theme) => { ctx.ui.setWidget("my-widget", (tui, theme) => {
// Return any Component that implements render(width): string[] // Return any Component that implements render(width): string[]
return new MyCustomComponent(tui, theme); return new MyCustomComponent(tui, theme);
}); });
// Clear the widget // Clear the widget
ctx.ui.setWidgetComponent("my-widget", undefined); ctx.ui.setWidget("my-widget", undefined);
``` ```
Unlike `ctx.ui.custom()`, widget components do NOT take keyboard focus - they render inline above the editor. Unlike `ctx.ui.custom()`, widget components do NOT take keyboard focus - they render inline above the editor.
@ -815,7 +815,7 @@ pi.setActiveTools(["read", "bash", "grep", "find", "ls"]);
pi.setActiveTools(["read", "bash", "edit", "write"]); pi.setActiveTools(["read", "bash", "edit", "write"]);
``` ```
Only built-in tools can be enabled/disabled. Custom tools are always active. Unknown tool names are ignored. Both built-in and custom tools can be enabled/disabled. Unknown tool names are ignored.
### pi.registerFlag(name, options) ### pi.registerFlag(name, options)

View file

@ -20,6 +20,7 @@
*/ */
import type { HookAPI, HookContext } from "@mariozechner/pi-coding-agent/hooks"; import type { HookAPI, HookContext } from "@mariozechner/pi-coding-agent/hooks";
import { Key } from "@mariozechner/pi-tui";
// Read-only tools for plan mode // Read-only tools for plan mode
const PLAN_MODE_TOOLS = ["read", "bash", "grep", "find", "ls"]; const PLAN_MODE_TOOLS = ["read", "bash", "grep", "find", "ls"];
@ -292,7 +293,7 @@ export default function planModeHook(pi: HookAPI) {
}); });
// Register Shift+P shortcut // Register Shift+P shortcut
pi.registerShortcut("shift+p", { pi.registerShortcut(Key.shift("p"), {
description: "Toggle plan mode", description: "Toggle plan mode",
handler: async (ctx) => { handler: async (ctx) => {
togglePlanMode(ctx); togglePlanMode(ctx);

View file

@ -93,7 +93,6 @@ function createNoOpUIContext(): HookUIContext {
notify: () => {}, notify: () => {},
setStatus: () => {}, setStatus: () => {},
setWidget: () => {}, setWidget: () => {},
setWidgetComponent: () => {},
custom: async () => undefined as never, custom: async () => undefined as never,
setEditorText: () => {}, setEditorText: () => {},
getEditorText: () => "", getEditorText: () => "",

View file

@ -7,6 +7,7 @@ import { createRequire } from "node:module";
import * as os from "node:os"; import * as os from "node:os";
import * as path from "node:path"; import * as path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import type { KeyId } from "@mariozechner/pi-tui";
import { createJiti } from "jiti"; import { createJiti } from "jiti";
import { getAgentDir } from "../../config.js"; import { getAgentDir } from "../../config.js";
import type { HookMessage } from "../messages.js"; import type { HookMessage } from "../messages.js";
@ -103,8 +104,8 @@ export interface HookFlag {
* Keyboard shortcut registered by a hook. * Keyboard shortcut registered by a hook.
*/ */
export interface HookShortcut { export interface HookShortcut {
/** Shortcut string (e.g., "shift+p", "ctrl+shift+x") */ /** Key identifier (e.g., Key.shift("p"), "ctrl+x") */
shortcut: string; shortcut: KeyId;
/** Description for help */ /** Description for help */
description?: string; description?: string;
/** Handler function */ /** Handler function */
@ -153,7 +154,7 @@ export interface LoadedHook {
/** Flag values (set after CLI parsing) */ /** Flag values (set after CLI parsing) */
flagValues: Map<string, boolean | string>; flagValues: Map<string, boolean | string>;
/** Keyboard shortcuts registered by this hook */ /** Keyboard shortcuts registered by this hook */
shortcuts: Map<string, HookShortcut>; shortcuts: Map<KeyId, HookShortcut>;
/** Set the send message handler for this hook's pi.sendMessage() */ /** Set the send message handler for this hook's pi.sendMessage() */
setSendMessageHandler: (handler: SendMessageHandler) => void; setSendMessageHandler: (handler: SendMessageHandler) => void;
/** Set the append entry handler for this hook's pi.appendEntry() */ /** Set the append entry handler for this hook's pi.appendEntry() */
@ -226,7 +227,7 @@ function createHookAPI(
commands: Map<string, RegisteredCommand>; commands: Map<string, RegisteredCommand>;
flags: Map<string, HookFlag>; flags: Map<string, HookFlag>;
flagValues: Map<string, boolean | string>; flagValues: Map<string, boolean | string>;
shortcuts: Map<string, HookShortcut>; shortcuts: Map<KeyId, HookShortcut>;
setSendMessageHandler: (handler: SendMessageHandler) => void; setSendMessageHandler: (handler: SendMessageHandler) => void;
setAppendEntryHandler: (handler: AppendEntryHandler) => void; setAppendEntryHandler: (handler: AppendEntryHandler) => void;
setGetActiveToolsHandler: (handler: GetActiveToolsHandler) => void; setGetActiveToolsHandler: (handler: GetActiveToolsHandler) => void;
@ -249,7 +250,7 @@ function createHookAPI(
const commands = new Map<string, RegisteredCommand>(); const commands = new Map<string, RegisteredCommand>();
const flags = new Map<string, HookFlag>(); const flags = new Map<string, HookFlag>();
const flagValues = new Map<string, boolean | string>(); const flagValues = new Map<string, boolean | string>();
const shortcuts = new Map<string, HookShortcut>(); const shortcuts = new Map<KeyId, HookShortcut>();
// Cast to HookAPI - the implementation is more general (string event names) // Cast to HookAPI - the implementation is more general (string event names)
// but the interface has specific overloads for type safety in hooks // but the interface has specific overloads for type safety in hooks
@ -300,7 +301,7 @@ function createHookAPI(
return flagValues.get(name); return flagValues.get(name);
}, },
registerShortcut( registerShortcut(
shortcut: string, shortcut: KeyId,
options: { options: {
description?: string; description?: string;
handler: (ctx: HookContext) => Promise<void> | void; handler: (ctx: HookContext) => Promise<void> | void;

View file

@ -4,6 +4,7 @@
import type { AgentMessage } from "@mariozechner/pi-agent-core"; import type { AgentMessage } from "@mariozechner/pi-agent-core";
import type { ImageContent, Model } from "@mariozechner/pi-ai"; import type { ImageContent, Model } from "@mariozechner/pi-ai";
import type { KeyId } from "@mariozechner/pi-tui";
import { theme } from "../../modes/interactive/theme/theme.js"; import { theme } from "../../modes/interactive/theme/theme.js";
import type { ModelRegistry } from "../model-registry.js"; import type { ModelRegistry } from "../model-registry.js";
import type { SessionManager } from "../session-manager.js"; import type { SessionManager } from "../session-manager.js";
@ -52,7 +53,6 @@ const noOpUIContext: HookUIContext = {
notify: () => {}, notify: () => {},
setStatus: () => {}, setStatus: () => {},
setWidget: () => {}, setWidget: () => {},
setWidgetComponent: () => {},
custom: async () => undefined as never, custom: async () => undefined as never,
setEditorText: () => {}, setEditorText: () => {},
getEditorText: () => "", getEditorText: () => "",
@ -223,11 +223,12 @@ export class HookRunner {
* Conflicts with built-in shortcuts are skipped with a warning. * Conflicts with built-in shortcuts are skipped with a warning.
* Conflicts between hooks are logged as warnings. * Conflicts between hooks are logged as warnings.
*/ */
getShortcuts(): Map<string, HookShortcut> { getShortcuts(): Map<KeyId, HookShortcut> {
const allShortcuts = new Map<string, HookShortcut>(); const allShortcuts = new Map<KeyId, HookShortcut>();
for (const hook of this.hooks) { for (const hook of this.hooks) {
for (const [key, shortcut] of hook.shortcuts) { for (const [key, shortcut] of hook.shortcuts) {
const normalizedKey = key.toLowerCase(); // Normalize to lowercase for comparison (KeyId is string at runtime)
const normalizedKey = key.toLowerCase() as KeyId;
// Check for built-in shortcut conflicts // Check for built-in shortcut conflicts
if (HookRunner.RESERVED_SHORTCUTS.has(normalizedKey)) { if (HookRunner.RESERVED_SHORTCUTS.has(normalizedKey)) {

View file

@ -7,7 +7,7 @@
import type { AgentMessage } from "@mariozechner/pi-agent-core"; import type { AgentMessage } from "@mariozechner/pi-agent-core";
import type { ImageContent, Model, TextContent, ToolResultMessage } from "@mariozechner/pi-ai"; import type { ImageContent, Model, TextContent, ToolResultMessage } from "@mariozechner/pi-ai";
import type { Component, TUI } from "@mariozechner/pi-tui"; import type { Component, KeyId, TUI } from "@mariozechner/pi-tui";
import type { Theme } from "../../modes/interactive/theme/theme.js"; import type { Theme } from "../../modes/interactive/theme/theme.js";
import type { CompactionPreparation, CompactionResult } from "../compaction/index.js"; import type { CompactionPreparation, CompactionResult } from "../compaction/index.js";
import type { ExecOptions, ExecResult } from "../exec.js"; import type { ExecOptions, ExecResult } from "../exec.js";
@ -79,13 +79,13 @@ export interface HookUIContext {
* Supports multi-line content. Pass undefined to clear. * Supports multi-line content. Pass undefined to clear.
* Text can include ANSI escape codes for styling. * Text can include ANSI escape codes for styling.
* *
* For simple text displays, use this method. For custom components, use setWidgetComponent(). * Accepts either an array of styled strings, or a factory function that creates a Component.
* *
* @param key - Unique key to identify this widget (e.g., hook name) * @param key - Unique key to identify this widget (e.g., hook name)
* @param lines - Array of lines to display, or undefined to clear * @param content - Array of lines to display, or undefined to clear
* *
* @example * @example
* // Show a todo list * // Show a todo list with styled strings
* ctx.ui.setWidget("plan-todos", [ * ctx.ui.setWidget("plan-todos", [
* theme.fg("accent", "Plan Progress:"), * theme.fg("accent", "Plan Progress:"),
* "☑ " + theme.fg("muted", theme.strikethrough("Step 1: Read files")), * "☑ " + theme.fg("muted", theme.strikethrough("Step 1: Read files")),
@ -96,7 +96,7 @@ export interface HookUIContext {
* // Clear the widget * // Clear the widget
* ctx.ui.setWidget("plan-todos", undefined); * ctx.ui.setWidget("plan-todos", undefined);
*/ */
setWidget(key: string, lines: string[] | undefined): void; setWidget(key: string, content: string[] | undefined): void;
/** /**
* Set a custom component as a widget (above the editor, below "Working..." indicator). * Set a custom component as a widget (above the editor, below "Working..." indicator).
@ -107,21 +107,18 @@ export interface HookUIContext {
* Components are rendered inline without taking focus - they cannot handle keyboard input. * Components are rendered inline without taking focus - they cannot handle keyboard input.
* *
* @param key - Unique key to identify this widget (e.g., hook name) * @param key - Unique key to identify this widget (e.g., hook name)
* @param factory - Function that creates the component, or undefined to clear * @param content - Factory function that creates the component, or undefined to clear
* *
* @example * @example
* // Show a custom progress component * // Show a custom progress component
* ctx.ui.setWidgetComponent("my-progress", (tui, theme) => { * ctx.ui.setWidget("my-progress", (tui, theme) => {
* return new MyProgressComponent(tui, theme); * return new MyProgressComponent(tui, theme);
* }); * });
* *
* // Clear the widget * // Clear the widget
* ctx.ui.setWidgetComponent("my-progress", undefined); * ctx.ui.setWidget("my-progress", undefined);
*/ */
setWidgetComponent( setWidget(key: string, content: ((tui: TUI, theme: Theme) => Component & { dispose?(): void }) | undefined): void;
key: string,
factory: ((tui: TUI, theme: Theme) => Component & { dispose?(): void }) | undefined,
): void;
/** /**
* Show a custom component with keyboard focus. * Show a custom component with keyboard focus.
@ -813,7 +810,7 @@ export interface HookAPI {
/** /**
* Set the active tools by name. * Set the active tools by name.
* Only built-in tools can be enabled/disabled. Custom tools are always active. * Both built-in and custom tools can be enabled/disabled.
* Changes take effect on the next agent turn. * Changes take effect on the next agent turn.
* Note: This will invalidate prompt caching for the next request. * Note: This will invalidate prompt caching for the next request.
* *
@ -871,11 +868,13 @@ export interface HookAPI {
* Register a keyboard shortcut for this hook. * Register a keyboard shortcut for this hook.
* The handler is called when the shortcut is pressed in interactive mode. * The handler is called when the shortcut is pressed in interactive mode.
* *
* @param shortcut - Shortcut definition (e.g., "shift+p", "ctrl+shift+x") * @param shortcut - Key identifier (e.g., Key.shift("p"), "ctrl+x")
* @param options - Shortcut configuration * @param options - Shortcut configuration
* *
* @example * @example
* pi.registerShortcut("shift+p", { * import { Key } from "@mariozechner/pi-tui";
*
* pi.registerShortcut(Key.shift("p"), {
* description: "Toggle plan mode", * description: "Toggle plan mode",
* handler: async (ctx) => { * handler: async (ctx) => {
* // toggle plan mode * // toggle plan mode
@ -883,7 +882,7 @@ export interface HookAPI {
* }); * });
*/ */
registerShortcut( registerShortcut(
shortcut: string, shortcut: KeyId,
options: { options: {
/** Description shown in help */ /** Description shown in help */
description?: string; description?: string;

View file

@ -31,6 +31,7 @@
import { Agent, type AgentTool, type ThinkingLevel } from "@mariozechner/pi-agent-core"; import { Agent, type AgentTool, type ThinkingLevel } from "@mariozechner/pi-agent-core";
import type { Model } from "@mariozechner/pi-ai"; import type { Model } from "@mariozechner/pi-ai";
import type { KeyId } from "@mariozechner/pi-tui";
import { join } from "path"; import { join } from "path";
import { getAgentDir } from "../config.js"; import { getAgentDir } from "../config.js";
import { AgentSession } from "./agent-session.js"; import { AgentSession } from "./agent-session.js";
@ -349,7 +350,7 @@ function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; fa
const commands = new Map<string, any>(); const commands = new Map<string, any>();
const flags = new Map<string, any>(); const flags = new Map<string, any>();
const flagValues = new Map<string, boolean | string>(); const flagValues = new Map<string, boolean | string>();
const shortcuts = new Map<string, any>(); const shortcuts = new Map<KeyId, any>();
let sendMessageHandler: ( let sendMessageHandler: (
message: any, message: any,
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" }, options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" },
@ -389,7 +390,7 @@ function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; fa
} }
}, },
getFlag: (name: string) => flagValues.get(name), getFlag: (name: string) => flagValues.get(name),
registerShortcut: (shortcut: string, options: any) => { registerShortcut: (shortcut: KeyId, options: any) => {
shortcuts.set(shortcut, { shortcut, hookPath, ...options }); shortcuts.set(shortcut, { shortcut, hookPath, ...options });
}, },
newSession: (options?: any) => newSessionHandler(options), newSession: (options?: any) => newSessionHandler(options),

View file

@ -141,9 +141,8 @@ export class InteractiveMode {
private hookInput: HookInputComponent | undefined = undefined; private hookInput: HookInputComponent | undefined = undefined;
private hookEditor: HookEditorComponent | undefined = undefined; private hookEditor: HookEditorComponent | undefined = undefined;
// Hook widgets (multi-line status displays or custom components) // Hook widgets (components rendered above the editor)
private hookWidgets = new Map<string, string[]>(); private hookWidgets = new Map<string, Component & { dispose?(): void }>();
private hookWidgetComponents = new Map<string, Component & { dispose?(): void }>();
private widgetContainer!: Container; private widgetContainer!: Container;
// Custom tools for custom rendering // Custom tools for custom rendering
@ -623,41 +622,32 @@ export class InteractiveMode {
} }
/** /**
* Set a hook widget (multi-line status display). * Set a hook widget (string array or custom component).
*/ */
private setHookWidget(key: string, lines: string[] | undefined): void { private setHookWidget(
if (lines === undefined) {
this.hookWidgets.delete(key);
} else {
// Clear any component widget with same key
const existing = this.hookWidgetComponents.get(key);
if (existing?.dispose) existing.dispose();
this.hookWidgetComponents.delete(key);
this.hookWidgets.set(key, lines);
}
this.renderWidgets();
}
/**
* Set a hook widget component (custom component without focus).
*/
private setHookWidgetComponent(
key: string, key: string,
factory: ((tui: TUI, thm: Theme) => Component & { dispose?(): void }) | undefined, content: string[] | ((tui: TUI, thm: Theme) => Component & { dispose?(): void }) | undefined,
): void { ): void {
// Dispose existing component // Dispose and remove existing widget
const existing = this.hookWidgetComponents.get(key); const existing = this.hookWidgets.get(key);
if (existing?.dispose) existing.dispose(); if (existing?.dispose) existing.dispose();
if (factory === undefined) { if (content === undefined) {
this.hookWidgetComponents.delete(key);
} else {
// Clear any string widget with same key
this.hookWidgets.delete(key); this.hookWidgets.delete(key);
} else if (Array.isArray(content)) {
const component = factory(this.ui, theme); // Wrap string array in a Container with Text components
this.hookWidgetComponents.set(key, component); const container = new Container();
for (const line of content.slice(0, InteractiveMode.MAX_WIDGET_LINES)) {
container.addChild(new Text(line, 1, 0));
}
if (content.length > InteractiveMode.MAX_WIDGET_LINES) {
container.addChild(new Text(theme.fg("muted", "... (widget truncated)"), 1, 0));
}
this.hookWidgets.set(key, container);
} else {
// Factory function - create component
const component = content(this.ui, theme);
this.hookWidgets.set(key, component);
} }
this.renderWidgets(); this.renderWidgets();
} }
@ -672,30 +662,12 @@ export class InteractiveMode {
if (!this.widgetContainer) return; if (!this.widgetContainer) return;
this.widgetContainer.clear(); this.widgetContainer.clear();
const hasStringWidgets = this.hookWidgets.size > 0; if (this.hookWidgets.size === 0) {
const hasComponentWidgets = this.hookWidgetComponents.size > 0;
if (!hasStringWidgets && !hasComponentWidgets) {
this.ui.requestRender(); this.ui.requestRender();
return; return;
} }
// Render string widgets first, respecting max lines for (const [_key, component] of this.hookWidgets) {
let totalLines = 0;
for (const [_key, lines] of this.hookWidgets) {
for (const line of lines) {
if (totalLines >= InteractiveMode.MAX_WIDGET_LINES) {
this.widgetContainer.addChild(new Text(theme.fg("muted", "... (widget truncated)"), 1, 0));
this.ui.requestRender();
return;
}
this.widgetContainer.addChild(new Text(line, 1, 0));
totalLines++;
}
}
// Render component widgets
for (const [_key, component] of this.hookWidgetComponents) {
this.widgetContainer.addChild(component); this.widgetContainer.addChild(component);
} }
@ -712,8 +684,7 @@ export class InteractiveMode {
input: (title, placeholder) => this.showHookInput(title, placeholder), input: (title, placeholder) => this.showHookInput(title, placeholder),
notify: (message, type) => this.showHookNotify(message, type), notify: (message, type) => this.showHookNotify(message, type),
setStatus: (key, text) => this.setHookStatus(key, text), setStatus: (key, text) => this.setHookStatus(key, text),
setWidget: (key, lines) => this.setHookWidget(key, lines), setWidget: (key, content) => this.setHookWidget(key, content),
setWidgetComponent: (key, factory) => this.setHookWidgetComponent(key, factory),
custom: (factory) => this.showHookCustom(factory), custom: (factory) => this.showHookCustom(factory),
setEditorText: (text) => this.editor.setText(text), setEditorText: (text) => this.editor.setText(text),
getEditorText: () => this.editor.getText(), getEditorText: () => this.editor.getText(),

View file

@ -131,19 +131,18 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
} as RpcHookUIRequest); } as RpcHookUIRequest);
}, },
setWidget(key: string, lines: string[] | undefined): void { setWidget(key: string, content: unknown): void {
// Fire and forget - host can implement widget display // Only support string arrays in RPC mode - factory functions are ignored
output({ if (content === undefined || Array.isArray(content)) {
type: "hook_ui_request", output({
id: crypto.randomUUID(), type: "hook_ui_request",
method: "setWidget", id: crypto.randomUUID(),
widgetKey: key, method: "setWidget",
widgetLines: lines, widgetKey: key,
} as RpcHookUIRequest); widgetLines: content as string[] | undefined,
}, } as RpcHookUIRequest);
}
setWidgetComponent(): void { // Component factories are not supported in RPC mode - would need TUI access
// Custom components not supported in RPC mode - host would need to implement
}, },
async custom() { async custom() {

View file

@ -121,7 +121,6 @@ describe.skipIf(!API_KEY)("Compaction hooks", () => {
notify: () => {}, notify: () => {},
setStatus: () => {}, setStatus: () => {},
setWidget: () => {}, setWidget: () => {},
setWidgetComponent: () => {},
custom: async () => undefined as never, custom: async () => undefined as never,
setEditorText: () => {}, setEditorText: () => {},
getEditorText: () => "", getEditorText: () => "",