feat(coding-agent): add input event for extension input interception (#761)

* feat(coding-agent): add input event for extension input interception

Extensions can now intercept, transform, or handle user input before the
agent processes it. Three result types: continue (pass through), transform
(modify text/images), handled (respond without LLM). Handlers chain
transforms and short-circuit on handled. Source field identifies origin.

* fix: make source public, use if/else over ternary

* fix: remove response field, extension handles own UI
This commit is contained in:
Nico Bailon 2026-01-15 17:41:56 -08:00 committed by GitHub
parent 012319e15a
commit 3e5d91f287
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 282 additions and 5 deletions

View file

@ -61,6 +61,10 @@ export type {
GetAllToolsHandler,
GetThinkingLevelHandler,
GrepToolResultEvent,
// Events - Input
InputEvent,
InputEventResult,
InputSource,
KeybindingsManager,
LoadExtensionsResult,
LsToolResultEvent,

View file

@ -25,6 +25,9 @@ import type {
ExtensionRuntime,
ExtensionShortcut,
ExtensionUIContext,
InputEvent,
InputEventResult,
InputSource,
MessageRenderer,
RegisteredCommand,
RegisteredTool,
@ -538,4 +541,35 @@ export class ExtensionRunner {
return undefined;
}
/** Emit input event. Transforms chain, "handled" short-circuits. */
async emitInput(text: string, images: ImageContent[] | undefined, source: InputSource): Promise<InputEventResult> {
const ctx = this.createContext();
let currentText = text;
let currentImages = images;
for (const ext of this.extensions) {
for (const handler of ext.handlers.get("input") ?? []) {
try {
const event: InputEvent = { type: "input", text: currentText, images: currentImages, source };
const result = (await handler(event, ctx)) as InputEventResult | undefined;
if (result?.action === "handled") return result;
if (result?.action === "transform") {
currentText = result.text;
currentImages = result.images ?? currentImages;
}
} catch (err) {
this.emitError({
extensionPath: ext.path,
event: "input",
error: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined,
});
}
}
}
return currentText !== text || currentImages !== images
? { action: "transform", text: currentText, images: currentImages }
: { action: "continue" };
}
}

View file

@ -446,6 +446,30 @@ export interface UserBashEvent {
cwd: string;
}
// ============================================================================
// Input Events
// ============================================================================
/** Source of user input */
export type InputSource = "interactive" | "rpc" | "extension";
/** Fired when user input is received, before agent processing */
export interface InputEvent {
type: "input";
/** The input text */
text: string;
/** Attached images, if any */
images?: ImageContent[];
/** Where the input came from */
source: InputSource;
}
/** Result from input event handler */
export type InputEventResult =
| { action: "continue" }
| { action: "transform"; text: string; images?: ImageContent[] }
| { action: "handled" };
// ============================================================================
// Tool Events
// ============================================================================
@ -551,6 +575,7 @@ export type ExtensionEvent =
| TurnEndEvent
| ModelSelectEvent
| UserBashEvent
| InputEvent
| ToolCallEvent
| ToolResultEvent;
@ -675,6 +700,7 @@ export interface ExtensionAPI {
on(event: "tool_call", handler: ExtensionHandler<ToolCallEvent, ToolCallEventResult>): void;
on(event: "tool_result", handler: ExtensionHandler<ToolResultEvent, ToolResultEventResult>): void;
on(event: "user_bash", handler: ExtensionHandler<UserBashEvent, UserBashEventResult>): void;
on(event: "input", handler: ExtensionHandler<InputEvent, InputEventResult>): void;
// =========================================================================
// Tool Registration