feat(coding-agent): add model_select extension hook

Fires when model changes via setModel(), cycleModel(), or session restore.
Includes source field ("set" | "cycle" | "restore") and previous model.
This commit is contained in:
Marc Krenn 2026-01-11 13:18:14 +01:00 committed by Mario Zechner
parent 5db7cc693b
commit c41714662a
3 changed files with 42 additions and 0 deletions

View file

@ -947,6 +947,21 @@ export class AgentSession {
// Model Management
// =========================================================================
private async _emitModelSelect(
nextModel: Model<any>,
previousModel: Model<any> | undefined,
source: "set" | "cycle" | "restore",
): Promise<void> {
if (!this._extensionRunner) return;
if (modelsAreEqual(previousModel, nextModel)) return;
await this._extensionRunner.emit({
type: "model_select",
model: nextModel,
previousModel,
source,
});
}
/**
* Set model directly.
* Validates API key, saves to session and settings.
@ -958,12 +973,15 @@ export class AgentSession {
throw new Error(`No API key for ${model.provider}/${model.id}`);
}
const previousModel = this.model;
this.agent.setModel(model);
this.sessionManager.appendModelChange(model.provider, model.id);
this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
// Re-clamp thinking level for new model's capabilities
this.setThinkingLevel(this.thinkingLevel);
await this._emitModelSelect(model, previousModel, "set");
}
/**
@ -1004,6 +1022,8 @@ export class AgentSession {
// Apply thinking level (setThinkingLevel clamps to model capabilities)
this.setThinkingLevel(next.thinkingLevel);
await this._emitModelSelect(next.model, currentModel, "cycle");
return { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true };
}
@ -1031,6 +1051,8 @@ export class AgentSession {
// Re-clamp thinking level for new model's capabilities
this.setThinkingLevel(this.thinkingLevel);
await this._emitModelSelect(nextModel, currentModel, "cycle");
return { model: nextModel, thinkingLevel: this.thinkingLevel, isScoped: false };
}
@ -1783,12 +1805,14 @@ export class AgentSession {
// Restore model if saved
if (sessionContext.model) {
const previousModel = this.model;
const availableModels = await this._modelRegistry.getAvailable();
const match = availableModels.find(
(m) => m.provider === sessionContext.model!.provider && m.id === sessionContext.model!.modelId,
);
if (match) {
this.agent.setModel(match);
await this._emitModelSelect(match, previousModel, "restore");
}
}

View file

@ -67,6 +67,8 @@ export type {
// Message Rendering
MessageRenderer,
MessageRenderOptions,
ModelSelectEvent,
ModelSelectSource,
ReadToolResultEvent,
// Commands
RegisteredCommand,

View file

@ -403,6 +403,20 @@ export interface TurnEndEvent {
toolResults: ToolResultMessage[];
}
// ============================================================================
// Model Events
// ============================================================================
export type ModelSelectSource = "set" | "cycle" | "restore";
/** Fired when a new model is selected */
export interface ModelSelectEvent {
type: "model_select";
model: Model<any>;
previousModel: Model<any> | undefined;
source: ModelSelectSource;
}
// ============================================================================
// User Bash Events
// ============================================================================
@ -521,6 +535,7 @@ export type ExtensionEvent =
| AgentEndEvent
| TurnStartEvent
| TurnEndEvent
| ModelSelectEvent
| UserBashEvent
| ToolCallEvent
| ToolResultEvent;
@ -645,6 +660,7 @@ export interface ExtensionAPI {
on(event: "agent_end", handler: ExtensionHandler<AgentEndEvent>): void;
on(event: "turn_start", handler: ExtensionHandler<TurnStartEvent>): void;
on(event: "turn_end", handler: ExtensionHandler<TurnEndEvent>): void;
on(event: "model_select", handler: ExtensionHandler<ModelSelectEvent>): void;
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;