diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 73e538fcc..3bbc9cbd0 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed `/model` autocomplete and model selection searches to match provider/model queries regardless of whether the provider or model token is typed first. - Fixed the tree navigator to horizontally pan deep entries so the selected item remains readable ([#5830](https://github.com/earendil-works/pi/issues/5830)). ## [0.79.6] - 2026-06-16 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 b9f5ec776..a3cdf7d52 100644 --- a/packages/coding-agent/src/modes/interactive/components/model-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/model-selector.ts @@ -11,6 +11,7 @@ import { } from "@earendil-works/pi-tui"; import type { ModelRegistry } from "../../../core/model-registry.ts"; import type { SettingsManager } from "../../../core/settings-manager.ts"; +import { getModelSearchText } from "../model-search.ts"; import { theme } from "../theme/theme.ts"; import { DynamicBorder } from "./dynamic-border.ts"; import { keyHint } from "./keybinding-hints.ts"; @@ -217,10 +218,8 @@ export class ModelSelectorComponent extends Container implements Focusable { private filterModels(query: string): void { this.filteredModels = query - ? fuzzyFilter( - this.activeModels, - query, - ({ id, provider }) => `${id} ${provider} ${provider}/${id} ${provider} ${id}`, + ? fuzzyFilter(this.activeModels, query, ({ id, provider, model }) => + getModelSearchText({ id, provider, name: model.name }), ) : this.activeModels; this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1)); diff --git a/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts b/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts index 06ce91691..772e3af06 100644 --- a/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts @@ -10,6 +10,7 @@ import { Spacer, Text, } from "@earendil-works/pi-tui"; +import { getModelSearchText } from "../model-search.ts"; import { theme } from "../theme/theme.ts"; import { DynamicBorder } from "./dynamic-border.ts"; import { keyText } from "./keybinding-hints.ts"; @@ -182,7 +183,11 @@ export class ScopedModelsSelectorComponent extends Container implements Focusabl private refresh(): void { const query = this.searchInput.getValue(); const items = this.buildItems(); - this.filteredItems = query ? fuzzyFilter(items, query, (i) => `${i.model.id} ${i.model.provider}`) : items; + this.filteredItems = query + ? fuzzyFilter(items, query, (i) => + getModelSearchText({ id: i.model.id, provider: i.model.provider, name: i.model.name }), + ) + : items; this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredItems.length - 1)); this.updateList(); this.footerText.setText(this.getFooterText()); diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 56ef90983..c2dc78755 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -125,6 +125,7 @@ import { TreeSelectorComponent } from "./components/tree-selector.ts"; import { TrustSelectorComponent } from "./components/trust-selector.ts"; import { UserMessageComponent } from "./components/user-message.ts"; import { UserMessageSelectorComponent } from "./components/user-message-selector.ts"; +import { getModelSearchText } from "./model-search.ts"; import { detectTerminalBackgroundTheme, getAvailableThemes, @@ -518,11 +519,12 @@ export class InteractiveMode { const items = models.map((m) => ({ id: m.id, provider: m.provider, + name: m.name, label: `${m.provider}/${m.id}`, })); - // Fuzzy filter by model ID + provider (allows "opus anthropic" to match) - const filtered = fuzzyFilter(items, prefix, (item) => `${item.id} ${item.provider}`); + // Fuzzy filter by model ID + provider in either order. + const filtered = fuzzyFilter(items, prefix, getModelSearchText); if (filtered.length === 0) return null; diff --git a/packages/coding-agent/src/modes/interactive/model-search.ts b/packages/coding-agent/src/modes/interactive/model-search.ts new file mode 100644 index 000000000..f1dbc73cf --- /dev/null +++ b/packages/coding-agent/src/modes/interactive/model-search.ts @@ -0,0 +1,11 @@ +export interface ModelSearchItem { + id: string; + provider: string; + name?: string; +} + +export function getModelSearchText(item: ModelSearchItem): string { + const { id, provider } = item; + const name = item.name ? ` ${item.name}` : ""; + return `${id} ${provider} ${provider}/${id} ${provider} ${id}${name}`; +} diff --git a/packages/coding-agent/test/interactive-mode-status.test.ts b/packages/coding-agent/test/interactive-mode-status.test.ts index 7ec54c7b9..8f1018b23 100644 --- a/packages/coding-agent/test/interactive-mode-status.test.ts +++ b/packages/coding-agent/test/interactive-mode-status.test.ts @@ -356,6 +356,59 @@ describe("InteractiveMode.setupAutocompleteProvider", () => { }); }); +describe("InteractiveMode.createBaseAutocompleteProvider", () => { + test("matches model command arguments across provider/model order", async () => { + type TestModel = { id: string; provider: string; name: string }; + type FakeInteractiveMode = { + session: { + scopedModels: Array<{ model: TestModel }>; + modelRegistry: { getAvailable: () => TestModel[] }; + promptTemplates: []; + extensionRunner: { getRegisteredCommands: () => [] }; + resourceLoader: { getSkills: () => { skills: [] } }; + }; + settingsManager: { getEnableSkillCommands: () => boolean }; + skillCommands: Map; + sessionManager: { getCwd: () => string }; + fdPath: null; + }; + + const createBaseAutocompleteProvider = ( + InteractiveMode as unknown as { + prototype: { createBaseAutocompleteProvider(this: FakeInteractiveMode): AutocompleteProvider }; + } + ).prototype.createBaseAutocompleteProvider; + const models = [ + { id: "gpt-5.2-codex", provider: "github-copilot", name: "GPT-5.2 Codex" }, + { id: "gpt-5.5", provider: "openai-codex", name: "GPT-5.5" }, + ]; + const fakeThis: FakeInteractiveMode = { + session: { + scopedModels: [], + modelRegistry: { getAvailable: () => models }, + promptTemplates: [], + extensionRunner: { getRegisteredCommands: () => [] }, + resourceLoader: { getSkills: () => ({ skills: [] }) }, + }, + settingsManager: { getEnableSkillCommands: () => false }, + skillCommands: new Map(), + sessionManager: { getCwd: () => "/tmp" }, + fdPath: null, + }; + + const provider = createBaseAutocompleteProvider.call(fakeThis); + const line = "/model codexgpt"; + const suggestions = await provider.getSuggestions([line], 0, line.length, { + signal: new AbortController().signal, + }); + + expect(suggestions?.items.map((item) => item.value)).toEqual([ + "openai-codex/gpt-5.5", + "github-copilot/gpt-5.2-codex", + ]); + }); +}); + describe("InteractiveMode.showLoadedResources", () => { beforeAll(() => { initTheme("dark");