mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-17 08:00:59 +00:00
feat: add search parameter and auto-select to /model command
- /model <search> 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
This commit is contained in:
parent
92eb6665fe
commit
e8eb4c254a
5 changed files with 82 additions and 6 deletions
|
|
@ -80,7 +80,6 @@ export default function promptUrlWidgetExtension(pi: ExtensionAPI) {
|
||||||
if (!ctx.hasUI) return;
|
if (!ctx.hasUI) return;
|
||||||
const match = extractPromptMatch(event.prompt);
|
const match = extractPromptMatch(event.prompt);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
ctx.ui.setWidget("prompt-url", undefined);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `/model <search>` 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
|
||||||
|
|
||||||
- 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))
|
- 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))
|
||||||
|
|
|
||||||
|
|
@ -234,7 +234,7 @@ The agent reads, writes, and edits files, and executes commands via bash.
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `/settings` | Open settings menu (thinking, theme, message delivery modes, toggles) |
|
| `/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 <search>` or `provider/model` to prefilter/disambiguate. |
|
||||||
| `/export [file]` | Export session to self-contained HTML |
|
| `/export [file]` | Export session to self-contained HTML |
|
||||||
| `/share` | Upload session as secret GitHub gist, get shareable URL (requires `gh` CLI) |
|
| `/share` | Upload session as secret GitHub gist, get shareable URL (requires `gh` CLI) |
|
||||||
| `/session` | Show session info: path, message counts, token usage, cost |
|
| `/session` | Show session info: path, message counts, token usage, cost |
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ export class ModelSelectorComponent extends Container {
|
||||||
scopedModels: ReadonlyArray<ScopedModelItem>,
|
scopedModels: ReadonlyArray<ScopedModelItem>,
|
||||||
onSelect: (model: Model<any>) => void,
|
onSelect: (model: Model<any>) => void,
|
||||||
onCancel: () => void,
|
onCancel: () => void,
|
||||||
|
initialSearchInput?: string,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
|
@ -68,6 +69,9 @@ export class ModelSelectorComponent extends Container {
|
||||||
|
|
||||||
// Create search input
|
// Create search input
|
||||||
this.searchInput = new Input();
|
this.searchInput = new Input();
|
||||||
|
if (initialSearchInput) {
|
||||||
|
this.searchInput.setValue(initialSearchInput);
|
||||||
|
}
|
||||||
this.searchInput.onSubmit = () => {
|
this.searchInput.onSubmit = () => {
|
||||||
// Enter on search input selects the first filtered item
|
// Enter on search input selects the first filtered item
|
||||||
if (this.filteredModels[this.selectedIndex]) {
|
if (this.filteredModels[this.selectedIndex]) {
|
||||||
|
|
@ -89,7 +93,11 @@ export class ModelSelectorComponent extends Container {
|
||||||
|
|
||||||
// Load models and do initial render
|
// Load models and do initial render
|
||||||
this.loadModels().then(() => {
|
this.loadModels().then(() => {
|
||||||
this.updateList();
|
if (initialSearchInput) {
|
||||||
|
this.filterModels(initialSearchInput);
|
||||||
|
} else {
|
||||||
|
this.updateList();
|
||||||
|
}
|
||||||
// Request re-render after models are loaded
|
// Request re-render after models are loaded
|
||||||
this.tui.requestRender();
|
this.tui.requestRender();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
getOAuthProviders,
|
getOAuthProviders,
|
||||||
type ImageContent,
|
type ImageContent,
|
||||||
type Message,
|
type Message,
|
||||||
|
type Model,
|
||||||
type OAuthProvider,
|
type OAuthProvider,
|
||||||
} from "@mariozechner/pi-ai";
|
} from "@mariozechner/pi-ai";
|
||||||
import type { EditorComponent, EditorTheme, KeyId, SlashCommand } from "@mariozechner/pi-tui";
|
import type { EditorComponent, EditorTheme, KeyId, SlashCommand } from "@mariozechner/pi-tui";
|
||||||
|
|
@ -1366,9 +1367,10 @@ export class InteractiveMode {
|
||||||
this.editor.setText("");
|
this.editor.setText("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (text === "/model") {
|
if (text === "/model" || text.startsWith("/model ")) {
|
||||||
this.showModelSelector();
|
const searchTerm = text.startsWith("/model ") ? text.slice(7).trim() : undefined;
|
||||||
this.editor.setText("");
|
this.editor.setText("");
|
||||||
|
await this.handleModelCommand(searchTerm);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (text.startsWith("/export")) {
|
if (text.startsWith("/export")) {
|
||||||
|
|
@ -2497,7 +2499,69 @@ export class InteractiveMode {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private showModelSelector(): void {
|
private async handleModelCommand(searchTerm?: string): Promise<void> {
|
||||||
|
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<Model<any> | 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<Model<any>[]> {
|
||||||
|
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) => {
|
this.showSelector((done) => {
|
||||||
const selector = new ModelSelectorComponent(
|
const selector = new ModelSelectorComponent(
|
||||||
this.ui,
|
this.ui,
|
||||||
|
|
@ -2521,6 +2585,7 @@ export class InteractiveMode {
|
||||||
done();
|
done();
|
||||||
this.ui.requestRender();
|
this.ui.requestRender();
|
||||||
},
|
},
|
||||||
|
initialSearchInput,
|
||||||
);
|
);
|
||||||
return { component: selector, focus: selector };
|
return { component: selector, focus: selector };
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue