feat(hooks): add setWidgetComponent for custom TUI components

- New ctx.ui.setWidgetComponent(key, factory) method
- Allows custom Component to render as widget without taking focus
- Unlike custom(), widget components render inline above editor
- Components are disposed when cleared or replaced
- Falls back to no-op in RPC/print modes
This commit is contained in:
Helmut Januschka 2026-01-03 21:47:54 +01:00 committed by Mario Zechner
parent 9b53b89bd5
commit ce88ebcd68
8 changed files with 94 additions and 6 deletions

View file

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

View file

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

View file

@ -79,6 +79,8 @@ export interface HookUIContext {
* Supports multi-line content. Pass undefined to clear.
* Text can include ANSI escape codes for styling.
*
* For simple text displays, use this method. For custom components, use setWidgetComponent().
*
* @param key - Unique key to identify this widget (e.g., hook name)
* @param lines - Array of lines to display, or undefined to clear
*
@ -96,6 +98,31 @@ export interface HookUIContext {
*/
setWidget(key: string, lines: string[] | undefined): void;
/**
* Set a custom component as a widget (above the editor, below "Working..." indicator).
* Unlike custom(), this does NOT take keyboard focus - the editor remains focused.
* Pass undefined to clear the widget.
*
* The component should implement render(width) and optionally dispose().
* 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 factory - Function that creates the component, or undefined to clear
*
* @example
* // Show a custom progress component
* ctx.ui.setWidgetComponent("my-progress", (tui, theme) => {
* return new MyProgressComponent(tui, theme);
* });
*
* // Clear the widget
* ctx.ui.setWidgetComponent("my-progress", undefined);
*/
setWidgetComponent(
key: string,
factory: ((tui: TUI, theme: Theme) => Component & { dispose?(): void }) | undefined,
): void;
/**
* Show a custom component with keyboard focus.
* The factory receives TUI, theme, and a done() callback to close the component.