Add setEditorText/getEditorText to hook UI context, improve custom() API

- Add setEditorText() and getEditorText() to HookUIContext for prompt generator pattern
- custom() now accepts async factories for fire-and-forget work
- Add CancellableLoader component to tui package
- Add BorderedLoader component for hooks with cancel UI
- Export HookAPI, HookContext, HookFactory from main package
- Update all examples to import from packages instead of relative paths
- Update hooks.md and custom-tools.md documentation

fixes #350
This commit is contained in:
Mario Zechner 2026-01-01 00:04:56 +01:00
parent 02d0d6e192
commit 6f7c10e323
39 changed files with 477 additions and 163 deletions

View file

@ -54,7 +54,15 @@ import { ToolExecutionComponent } from "./components/tool-execution.js";
import { TreeSelectorComponent } from "./components/tree-selector.js";
import { UserMessageComponent } from "./components/user-message.js";
import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
import { getAvailableThemes, getEditorTheme, getMarkdownTheme, onThemeChange, setTheme, theme } from "./theme/theme.js";
import {
getAvailableThemes,
getEditorTheme,
getMarkdownTheme,
onThemeChange,
setTheme,
type Theme,
theme,
} from "./theme/theme.js";
/** Interface for components that can be expanded/collapsed */
interface Expandable {
@ -356,7 +364,9 @@ export class InteractiveMode {
confirm: (title, message) => this.showHookConfirm(title, message),
input: (title, placeholder) => this.showHookInput(title, placeholder),
notify: (message, type) => this.showHookNotify(message, type),
custom: (component) => this.showHookCustom(component),
custom: (factory) => this.showHookCustom(factory),
setEditorText: (text) => this.editor.setText(text),
getEditorText: () => this.editor.getText(),
};
this.setToolUIContext(uiContext, true);
@ -537,38 +547,37 @@ export class InteractiveMode {
/**
* Show a custom component with keyboard focus.
* Returns a function to call when done.
*/
private showHookCustom(component: Component & { dispose?(): void }): {
close: () => void;
requestRender: () => void;
} {
// Store current editor content
private async showHookCustom<T>(
factory: (
tui: TUI,
theme: Theme,
done: (result: T) => void,
) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,
): Promise<T> {
const savedText = this.editor.getText();
// Replace editor with custom component
this.editorContainer.clear();
this.editorContainer.addChild(component);
this.ui.setFocus(component);
this.ui.requestRender();
return new Promise((resolve) => {
let component: Component & { dispose?(): void };
// Return control object
return {
close: () => {
// Call dispose if available
const close = (result: T) => {
component.dispose?.();
// Restore editor
this.editorContainer.clear();
this.editorContainer.addChild(this.editor);
this.editor.setText(savedText);
this.ui.setFocus(this.editor);
this.ui.requestRender();
},
requestRender: () => {
resolve(result);
};
Promise.resolve(factory(this.ui, theme, close)).then((c) => {
component = c;
this.editorContainer.clear();
this.editorContainer.addChild(component);
this.ui.setFocus(component);
this.ui.requestRender();
},
};
});
});
}
/**