diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 66db0f79..6776cc4c 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -947,6 +947,21 @@ export class AgentSession { // Model Management // ========================================================================= + private async _emitModelSelect( + nextModel: Model, + previousModel: Model | undefined, + source: "set" | "cycle" | "restore", + ): Promise { + 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"); } } diff --git a/packages/coding-agent/src/core/extensions/index.ts b/packages/coding-agent/src/core/extensions/index.ts index 8a831f79..48c335e9 100644 --- a/packages/coding-agent/src/core/extensions/index.ts +++ b/packages/coding-agent/src/core/extensions/index.ts @@ -67,6 +67,8 @@ export type { // Message Rendering MessageRenderer, MessageRenderOptions, + ModelSelectEvent, + ModelSelectSource, ReadToolResultEvent, // Commands RegisteredCommand, diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index cb57b313..ae8e8356 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -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; + previousModel: Model | 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): void; on(event: "turn_start", handler: ExtensionHandler): void; on(event: "turn_end", handler: ExtensionHandler): void; + on(event: "model_select", handler: ExtensionHandler): void; on(event: "tool_call", handler: ExtensionHandler): void; on(event: "tool_result", handler: ExtensionHandler): void; on(event: "user_bash", handler: ExtensionHandler): void;