mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 21:03:19 +00:00
Add ui.custom() for custom hook components with keyboard focus
- Add custom() to HookUIContext: returns { close, requestRender }
- Component receives keyboard input via handleInput()
- CustomMessageComponent default rendering now limits to 5 lines when collapsed
- Add snake.ts example hook with /snake command
This commit is contained in:
parent
a8866d7a83
commit
14ad8d6228
9 changed files with 315 additions and 2 deletions
|
|
@ -90,6 +90,7 @@ function createNoOpUIContext(): HookUIContext {
|
|||
confirm: async () => false,
|
||||
input: async () => null,
|
||||
notify: () => {},
|
||||
custom: () => ({ close: () => {}, requestRender: () => {} }),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@ export type {
|
|||
AgentEndEvent,
|
||||
AgentStartEvent,
|
||||
BashToolResultEvent,
|
||||
HookMessageRenderer,
|
||||
HookMessageRenderOptions,
|
||||
CustomToolResultEvent,
|
||||
EditToolResultEvent,
|
||||
ExecOptions,
|
||||
|
|
@ -28,6 +26,8 @@ export type {
|
|||
HookEventContext,
|
||||
HookFactory,
|
||||
HookMessage,
|
||||
HookMessageRenderer,
|
||||
HookMessageRenderOptions,
|
||||
HookUIContext,
|
||||
LsToolResultEvent,
|
||||
ReadToolResultEvent,
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ const noOpUIContext: HookUIContext = {
|
|||
confirm: async () => false,
|
||||
input: async () => null,
|
||||
notify: () => {},
|
||||
custom: () => ({ close: () => {}, requestRender: () => {} }),
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -54,6 +54,15 @@ export interface HookUIContext {
|
|||
* Show a notification to the user.
|
||||
*/
|
||||
notify(message: string, type?: "info" | "warning" | "error"): void;
|
||||
|
||||
/**
|
||||
* Show a custom component with keyboard focus.
|
||||
* The component receives keyboard input via handleInput() if implemented.
|
||||
*
|
||||
* @param component - Component to display (implement handleInput for keyboard, dispose for cleanup)
|
||||
* @returns Object with close() to restore normal UI and requestRender() to trigger redraw
|
||||
*/
|
||||
custom(component: Component & { dispose?(): void }): { close: () => void; requestRender: () => void };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -75,6 +75,14 @@ export class CustomMessageComponent extends Container {
|
|||
.join("\n");
|
||||
}
|
||||
|
||||
// Limit lines when collapsed
|
||||
if (!this._expanded) {
|
||||
const lines = text.split("\n");
|
||||
if (lines.length > 5) {
|
||||
text = `${lines.slice(0, 5).join("\n")}\n...`;
|
||||
}
|
||||
}
|
||||
|
||||
this.box.addChild(
|
||||
new Markdown(text, 0, 0, getMarkdownTheme(), {
|
||||
color: (text: string) => theme.fg("customMessageText", text),
|
||||
|
|
|
|||
|
|
@ -443,6 +443,7 @@ 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),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -539,6 +540,42 @@ 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
|
||||
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 control object
|
||||
return {
|
||||
close: () => {
|
||||
// Call dispose if available
|
||||
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: () => {
|
||||
this.ui.requestRender();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a hook error in the UI.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -118,6 +118,11 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|||
notifyType: type,
|
||||
} as RpcHookUIRequest);
|
||||
},
|
||||
|
||||
custom() {
|
||||
// Custom UI not supported in RPC mode
|
||||
return { close: () => {}, requestRender: () => {} };
|
||||
},
|
||||
});
|
||||
|
||||
// Load entries once for session start events
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue