mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 06:04:40 +00:00
feat: add terminal input hook for extensions
This commit is contained in:
parent
6da488a5aa
commit
30fd99bd82
7 changed files with 47 additions and 0 deletions
|
|
@ -123,6 +123,7 @@ export type {
|
|||
SetLabelHandler,
|
||||
SetModelHandler,
|
||||
SetThinkingLevelHandler,
|
||||
TerminalInputHandler,
|
||||
// Events - Tool
|
||||
ToolCallEvent,
|
||||
ToolCallEventResult,
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@ const noOpUIContext: ExtensionUIContext = {
|
|||
confirm: async () => false,
|
||||
input: async () => undefined,
|
||||
notify: () => {},
|
||||
onTerminalInput: () => () => {},
|
||||
setStatus: () => {},
|
||||
setWorkingMessage: () => {},
|
||||
setWidget: () => {},
|
||||
|
|
|
|||
|
|
@ -97,6 +97,9 @@ export interface ExtensionWidgetOptions {
|
|||
placement?: WidgetPlacement;
|
||||
}
|
||||
|
||||
/** Raw terminal input listener for extensions. */
|
||||
export type TerminalInputHandler = (data: string) => { consume?: boolean; data?: string } | undefined;
|
||||
|
||||
/**
|
||||
* UI context for extensions to request interactive UI.
|
||||
* Each mode (interactive, RPC, print) provides its own implementation.
|
||||
|
|
@ -114,6 +117,9 @@ export interface ExtensionUIContext {
|
|||
/** Show a notification to the user. */
|
||||
notify(message: string, type?: "info" | "warning" | "error"): void;
|
||||
|
||||
/** Listen to raw terminal input (interactive mode only). Returns an unsubscribe function. */
|
||||
onTerminalInput(handler: TerminalInputHandler): () => void;
|
||||
|
||||
/** Set status text in the footer/status bar. Pass undefined to clear. */
|
||||
setStatus(key: string, text: string | undefined): void;
|
||||
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ export type {
|
|||
SlashCommandInfo,
|
||||
SlashCommandLocation,
|
||||
SlashCommandSource,
|
||||
TerminalInputHandler,
|
||||
ToolCallEvent,
|
||||
ToolDefinition,
|
||||
ToolInfo,
|
||||
|
|
|
|||
|
|
@ -1368,6 +1368,7 @@ export class InteractiveMode {
|
|||
confirm: (title, message, opts) => this.showExtensionConfirm(title, message, opts),
|
||||
input: (title, placeholder, opts) => this.showExtensionInput(title, placeholder, opts),
|
||||
notify: (message, type) => this.showExtensionNotify(message, type),
|
||||
onTerminalInput: (handler) => this.ui.addInputListener(handler),
|
||||
setStatus: (key, text) => this.setExtensionStatus(key, text),
|
||||
setWorkingMessage: (message) => {
|
||||
if (this.loadingAnimation) {
|
||||
|
|
|
|||
|
|
@ -144,6 +144,11 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|||
} as RpcExtensionUIRequest);
|
||||
},
|
||||
|
||||
onTerminalInput(): () => void {
|
||||
// Raw terminal input not supported in RPC mode
|
||||
return () => {};
|
||||
},
|
||||
|
||||
setStatus(key: string, text: string | undefined): void {
|
||||
// Fire and forget - no response needed
|
||||
output({
|
||||
|
|
|
|||
|
|
@ -39,6 +39,9 @@ export interface Component {
|
|||
invalidate(): void;
|
||||
}
|
||||
|
||||
type InputListenerResult = { consume?: boolean; data?: string } | undefined;
|
||||
type InputListener = (data: string) => InputListenerResult;
|
||||
|
||||
/**
|
||||
* Interface for components that can receive focus and display a hardware cursor.
|
||||
* When focused, the component should emit CURSOR_MARKER at the cursor position
|
||||
|
|
@ -200,6 +203,7 @@ export class TUI extends Container {
|
|||
private previousLines: string[] = [];
|
||||
private previousWidth = 0;
|
||||
private focusedComponent: Component | null = null;
|
||||
private inputListeners = new Set<InputListener>();
|
||||
|
||||
/** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */
|
||||
public onDebug?: () => void;
|
||||
|
|
@ -377,6 +381,17 @@ export class TUI extends Container {
|
|||
this.requestRender();
|
||||
}
|
||||
|
||||
addInputListener(listener: InputListener): () => void {
|
||||
this.inputListeners.add(listener);
|
||||
return () => {
|
||||
this.inputListeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
removeInputListener(listener: InputListener): void {
|
||||
this.inputListeners.delete(listener);
|
||||
}
|
||||
|
||||
private queryCellSize(): void {
|
||||
// Only query if terminal supports images (cell size is only used for image rendering)
|
||||
if (!getCapabilities().images) {
|
||||
|
|
@ -424,6 +439,23 @@ export class TUI extends Container {
|
|||
}
|
||||
|
||||
private handleInput(data: string): void {
|
||||
if (this.inputListeners.size > 0) {
|
||||
let current = data;
|
||||
for (const listener of this.inputListeners) {
|
||||
const result = listener(current);
|
||||
if (result?.consume) {
|
||||
return;
|
||||
}
|
||||
if (result?.data !== undefined) {
|
||||
current = result.data;
|
||||
}
|
||||
}
|
||||
if (current.length === 0) {
|
||||
return;
|
||||
}
|
||||
data = current;
|
||||
}
|
||||
|
||||
// If we're waiting for cell size response, buffer input and parse
|
||||
if (this.cellSizeQueryPending) {
|
||||
this.inputBuffer += data;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue