From e8eb4c254acfe6883d600fde52ffa06542057ee7 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 9 Jan 2026 22:39:15 +0100 Subject: [PATCH] feat: add search parameter and auto-select to /model command - /model pre-filters selector or auto-selects on exact match - Support provider/model syntax for disambiguation (e.g., /model openai/gpt-4) - Auto-select logic moved to InteractiveMode to avoid selector UI flicker Closes #587 --- .pi/extensions/prompt-url-widget.ts | 1 - packages/coding-agent/CHANGELOG.md | 4 ++ packages/coding-agent/README.md | 2 +- .../interactive/components/model-selector.ts | 10 ++- .../src/modes/interactive/interactive-mode.ts | 71 ++++++++++++++++++- 5 files changed, 82 insertions(+), 6 deletions(-) diff --git a/.pi/extensions/prompt-url-widget.ts b/.pi/extensions/prompt-url-widget.ts index f5346ef6..809eb27d 100644 --- a/.pi/extensions/prompt-url-widget.ts +++ b/.pi/extensions/prompt-url-widget.ts @@ -80,7 +80,6 @@ export default function promptUrlWidgetExtension(pi: ExtensionAPI) { if (!ctx.hasUI) return; const match = extractPromptMatch(event.prompt); if (!match) { - ctx.ui.setWidget("prompt-url", undefined); return; } diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 920f8175..8586977f 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- `/model ` now pre-filters the model selector or auto-selects on exact match. Use `provider/model` syntax to disambiguate (e.g., `/model openai/gpt-4`). ([#587](https://github.com/badlogic/pi-mono/pull/587) by [@zedrdave](https://github.com/zedrdave)) + ### Fixed - Fixed LM Studio compatibility for OpenAI Responses tool strict mapping in the ai provider ([#598](https://github.com/badlogic/pi-mono/pull/598) by [@gnattu](https://github.com/gnattu)) diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 9e15f499..d08f0a64 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -234,7 +234,7 @@ The agent reads, writes, and edits files, and executes commands via bash. | Command | Description | |---------|-------------| | `/settings` | Open settings menu (thinking, theme, message delivery modes, toggles) | -| `/model` | Switch models mid-session (fuzzy search, arrow keys, Enter to select) | +| `/model` | Switch models mid-session. Use `/model ` or `provider/model` to prefilter/disambiguate. | | `/export [file]` | Export session to self-contained HTML | | `/share` | Upload session as secret GitHub gist, get shareable URL (requires `gh` CLI) | | `/session` | Show session info: path, message counts, token usage, cost | diff --git a/packages/coding-agent/src/modes/interactive/components/model-selector.ts b/packages/coding-agent/src/modes/interactive/components/model-selector.ts index 4b1e5778..b71cfd6a 100644 --- a/packages/coding-agent/src/modes/interactive/components/model-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/model-selector.ts @@ -43,6 +43,7 @@ export class ModelSelectorComponent extends Container { scopedModels: ReadonlyArray, onSelect: (model: Model) => void, onCancel: () => void, + initialSearchInput?: string, ) { super(); @@ -68,6 +69,9 @@ export class ModelSelectorComponent extends Container { // Create search input this.searchInput = new Input(); + if (initialSearchInput) { + this.searchInput.setValue(initialSearchInput); + } this.searchInput.onSubmit = () => { // Enter on search input selects the first filtered item if (this.filteredModels[this.selectedIndex]) { @@ -89,7 +93,11 @@ export class ModelSelectorComponent extends Container { // Load models and do initial render this.loadModels().then(() => { - this.updateList(); + if (initialSearchInput) { + this.filterModels(initialSearchInput); + } else { + this.updateList(); + } // Request re-render after models are loaded this.tui.requestRender(); }); diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 96bfcaf0..80ab2ed5 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -13,6 +13,7 @@ import { getOAuthProviders, type ImageContent, type Message, + type Model, type OAuthProvider, } from "@mariozechner/pi-ai"; import type { EditorComponent, EditorTheme, KeyId, SlashCommand } from "@mariozechner/pi-tui"; @@ -1366,9 +1367,10 @@ export class InteractiveMode { this.editor.setText(""); return; } - if (text === "/model") { - this.showModelSelector(); + if (text === "/model" || text.startsWith("/model ")) { + const searchTerm = text.startsWith("/model ") ? text.slice(7).trim() : undefined; this.editor.setText(""); + await this.handleModelCommand(searchTerm); return; } if (text.startsWith("/export")) { @@ -2497,7 +2499,69 @@ export class InteractiveMode { }); } - private showModelSelector(): void { + private async handleModelCommand(searchTerm?: string): Promise { + if (!searchTerm) { + this.showModelSelector(); + return; + } + + const model = await this.findExactModelMatch(searchTerm); + if (model) { + try { + await this.session.setModel(model); + this.footer.invalidate(); + this.updateEditorBorderColor(); + this.showStatus(`Model: ${model.id}`); + } catch (error) { + this.showError(error instanceof Error ? error.message : String(error)); + } + return; + } + + this.showModelSelector(searchTerm); + } + + private async findExactModelMatch(searchTerm: string): Promise | undefined> { + const term = searchTerm.trim(); + if (!term) return undefined; + + let targetProvider: string | undefined; + let targetModelId = ""; + + if (term.includes("/")) { + const parts = term.split("/", 2); + targetProvider = parts[0]?.trim().toLowerCase(); + targetModelId = parts[1]?.trim().toLowerCase() ?? ""; + } else { + targetModelId = term.toLowerCase(); + } + + if (!targetModelId) return undefined; + + const models = await this.getModelCandidates(); + const exactMatches = models.filter((item) => { + const idMatch = item.id.toLowerCase() === targetModelId; + const providerMatch = !targetProvider || item.provider.toLowerCase() === targetProvider; + return idMatch && providerMatch; + }); + + return exactMatches.length === 1 ? exactMatches[0] : undefined; + } + + private async getModelCandidates(): Promise[]> { + if (this.session.scopedModels.length > 0) { + return this.session.scopedModels.map((scoped) => scoped.model); + } + + this.session.modelRegistry.refresh(); + try { + return await this.session.modelRegistry.getAvailable(); + } catch { + return []; + } + } + + private showModelSelector(initialSearchInput?: string): void { this.showSelector((done) => { const selector = new ModelSelectorComponent( this.ui, @@ -2521,6 +2585,7 @@ export class InteractiveMode { done(); this.ui.requestRender(); }, + initialSearchInput, ); return { component: selector, focus: selector }; });