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