Merge model-select-hook: add model_select extension hook (#628)

This commit is contained in:
Mario Zechner 2026-01-11 18:12:54 +01:00
commit 741262d89d
6 changed files with 102 additions and 0 deletions

View file

@ -4,6 +4,7 @@
### Added
- `model_select` extension hook fires when model changes via `/model`, model cycling, or session restore with `source` field and `previousModel` ([#628](https://github.com/badlogic/pi-mono/pull/628) by [@marckrenn](https://github.com/marckrenn))
- `ctx.ui.setWorkingMessage()` extension API to customize the "Working..." message during streaming ([#625](https://github.com/badlogic/pi-mono/pull/625) by [@nicobailon](https://github.com/nicobailon))
- Skill slash commands: loaded skills are registered as `/skill:name` commands for quick access. Toggle via `/settings` or `skills.enableSkillCommands` in settings.json. ([#630](https://github.com/badlogic/pi-mono/pull/630) by [@Dwsy](https://github.com/Dwsy))
- Slash command autocomplete now uses fuzzy matching (type `/skbra` to match `/skill:brave-search`)

View file

@ -290,6 +290,9 @@ user sends another prompt ◄─────────────────
├─► session_before_tree (can cancel or customize)
└─► session_tree
/model or Ctrl+P (model selection/cycling)
└─► model_select
exit (Ctrl+C, Ctrl+D)
└─► session_shutdown
```
@ -481,6 +484,31 @@ pi.on("context", async (event, ctx) => {
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts)
### Model Events
#### model_select
Fired when the model changes via `/model` command, model cycling (`Ctrl+P`), or session restore.
```typescript
pi.on("model_select", async (event, ctx) => {
// event.model - newly selected model
// event.previousModel - previous model (undefined if first selection)
// event.source - "set" | "cycle" | "restore"
const prev = event.previousModel
? `${event.previousModel.provider}/${event.previousModel.id}`
: "none";
const next = `${event.model.provider}/${event.model.id}`;
ctx.ui.notify(`Model changed (${event.source}): ${prev} -> ${next}`, "info");
});
```
Use this to update UI elements (status bars, footers) or perform model-specific initialization when the active model changes.
**Examples:** [model-status.ts](../examples/extensions/model-status.ts)
### Tool Events
#### tool_call

View file

@ -0,0 +1,31 @@
/**
* Model status extension - shows model changes in the status bar.
*
* Demonstrates the `model_select` hook which fires when the model changes
* via /model command, Ctrl+P cycling, or session restore.
*
* Usage: pi -e ./model-status.ts
*/
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
export default function (pi: ExtensionAPI) {
pi.on("model_select", async (event, ctx) => {
const { model, previousModel, source } = event;
// Format model identifiers
const next = `${model.provider}/${model.id}`;
const prev = previousModel ? `${previousModel.provider}/${previousModel.id}` : "none";
// Show notification on change
if (source !== "restore") {
ctx.ui.notify(`Model: ${next}`, "info");
}
// Update status bar with current model
ctx.ui.setStatus("model", `🤖 ${model.id}`);
// Log change details (visible in debug output)
console.log(`[model_select] ${prev}${next} (${source})`);
});
}

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;