fix(coding-agent): match provider-first model searches
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
### Fixed
|
### 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)).
|
- 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
|
## [0.79.6] - 2026-06-16
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from "@earendil-works/pi-tui";
|
} from "@earendil-works/pi-tui";
|
||||||
import type { ModelRegistry } from "../../../core/model-registry.ts";
|
import type { ModelRegistry } from "../../../core/model-registry.ts";
|
||||||
import type { SettingsManager } from "../../../core/settings-manager.ts";
|
import type { SettingsManager } from "../../../core/settings-manager.ts";
|
||||||
|
import { getModelSearchText } from "../model-search.ts";
|
||||||
import { theme } from "../theme/theme.ts";
|
import { theme } from "../theme/theme.ts";
|
||||||
import { DynamicBorder } from "./dynamic-border.ts";
|
import { DynamicBorder } from "./dynamic-border.ts";
|
||||||
import { keyHint } from "./keybinding-hints.ts";
|
import { keyHint } from "./keybinding-hints.ts";
|
||||||
@@ -217,10 +218,8 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
|||||||
|
|
||||||
private filterModels(query: string): void {
|
private filterModels(query: string): void {
|
||||||
this.filteredModels = query
|
this.filteredModels = query
|
||||||
? fuzzyFilter(
|
? fuzzyFilter(this.activeModels, query, ({ id, provider, model }) =>
|
||||||
this.activeModels,
|
getModelSearchText({ id, provider, name: model.name }),
|
||||||
query,
|
|
||||||
({ id, provider }) => `${id} ${provider} ${provider}/${id} ${provider} ${id}`,
|
|
||||||
)
|
)
|
||||||
: this.activeModels;
|
: this.activeModels;
|
||||||
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));
|
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
Spacer,
|
Spacer,
|
||||||
Text,
|
Text,
|
||||||
} from "@earendil-works/pi-tui";
|
} from "@earendil-works/pi-tui";
|
||||||
|
import { getModelSearchText } from "../model-search.ts";
|
||||||
import { theme } from "../theme/theme.ts";
|
import { theme } from "../theme/theme.ts";
|
||||||
import { DynamicBorder } from "./dynamic-border.ts";
|
import { DynamicBorder } from "./dynamic-border.ts";
|
||||||
import { keyText } from "./keybinding-hints.ts";
|
import { keyText } from "./keybinding-hints.ts";
|
||||||
@@ -182,7 +183,11 @@ export class ScopedModelsSelectorComponent extends Container implements Focusabl
|
|||||||
private refresh(): void {
|
private refresh(): void {
|
||||||
const query = this.searchInput.getValue();
|
const query = this.searchInput.getValue();
|
||||||
const items = this.buildItems();
|
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.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredItems.length - 1));
|
||||||
this.updateList();
|
this.updateList();
|
||||||
this.footerText.setText(this.getFooterText());
|
this.footerText.setText(this.getFooterText());
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ import { TreeSelectorComponent } from "./components/tree-selector.ts";
|
|||||||
import { TrustSelectorComponent } from "./components/trust-selector.ts";
|
import { TrustSelectorComponent } from "./components/trust-selector.ts";
|
||||||
import { UserMessageComponent } from "./components/user-message.ts";
|
import { UserMessageComponent } from "./components/user-message.ts";
|
||||||
import { UserMessageSelectorComponent } from "./components/user-message-selector.ts";
|
import { UserMessageSelectorComponent } from "./components/user-message-selector.ts";
|
||||||
|
import { getModelSearchText } from "./model-search.ts";
|
||||||
import {
|
import {
|
||||||
detectTerminalBackgroundTheme,
|
detectTerminalBackgroundTheme,
|
||||||
getAvailableThemes,
|
getAvailableThemes,
|
||||||
@@ -518,11 +519,12 @@ export class InteractiveMode {
|
|||||||
const items = models.map((m) => ({
|
const items = models.map((m) => ({
|
||||||
id: m.id,
|
id: m.id,
|
||||||
provider: m.provider,
|
provider: m.provider,
|
||||||
|
name: m.name,
|
||||||
label: `${m.provider}/${m.id}`,
|
label: `${m.provider}/${m.id}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Fuzzy filter by model ID + provider (allows "opus anthropic" to match)
|
// Fuzzy filter by model ID + provider in either order.
|
||||||
const filtered = fuzzyFilter(items, prefix, (item) => `${item.id} ${item.provider}`);
|
const filtered = fuzzyFilter(items, prefix, getModelSearchText);
|
||||||
|
|
||||||
if (filtered.length === 0) return null;
|
if (filtered.length === 0) return null;
|
||||||
|
|
||||||
|
|||||||
11
packages/coding-agent/src/modes/interactive/model-search.ts
Normal file
11
packages/coding-agent/src/modes/interactive/model-search.ts
Normal file
@@ -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}`;
|
||||||
|
}
|
||||||
@@ -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<string, string>;
|
||||||
|
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", () => {
|
describe("InteractiveMode.showLoadedResources", () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
initTheme("dark");
|
initTheme("dark");
|
||||||
|
|||||||
Reference in New Issue
Block a user