From 50cd054edbce55e8b07a4c00ac1916d8b7d94d52 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 12 Jan 2026 00:53:52 +0100 Subject: [PATCH] Rework /scoped-models command UX - Renamed from /models to /scoped-models - Changes are now session-only by default (Ctrl+S to persist to settings) - Added search with fuzzy filtering - Hotkeys: Enter toggle, Ctrl+A enable all, Ctrl+X clear all, Ctrl+P toggle provider - Ctrl+C clears search (or exits if empty), Escape exits - Enabled models shown at top of list - No checkmarks when all models enabled (no scope) - First toggle when unscoped clears all and selects that model - Uses current session thinking level instead of settings default - Reads from session state first (preserves session-only changes across /scoped-models invocations) Builds on #626 --- packages/coding-agent/CHANGELOG.md | 2 +- packages/coding-agent/README.md | 2 +- .../src/modes/interactive/components/index.ts | 2 +- .../interactive/components/models-selector.ts | 146 -------- .../components/scoped-models-selector.ts | 312 ++++++++++++++++++ .../src/modes/interactive/interactive-mode.ts | 110 ++++-- 6 files changed, 392 insertions(+), 182 deletions(-) delete mode 100644 packages/coding-agent/src/modes/interactive/components/models-selector.ts create mode 100644 packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 8e6fc612..b6ca77eb 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -18,7 +18,7 @@ - `SessionManager.list()` and `SessionManager.listAll()` accept optional `onProgress` callback for progress updates - `SessionInfo.cwd` field containing the session's working directory (empty string for old sessions) - `SessionListProgress` type export for progress callbacks -- `/models` command to enable/disable models for Ctrl+P cycling. Changes persist to `enabledModels` in settings.json and take effect immediately. ([#626](https://github.com/badlogic/pi-mono/pull/626) by [@CarlosGtrz](https://github.com/CarlosGtrz)) +- `/scoped-models` command to enable/disable models for Ctrl+P cycling. Changes are session-only by default; press Ctrl+S to persist to settings.json. ([#626](https://github.com/badlogic/pi-mono/pull/626) by [@CarlosGtrz](https://github.com/CarlosGtrz)) - `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)) diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index f94ae4a9..9933a60a 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -236,7 +236,7 @@ The agent reads, writes, and edits files, and executes commands via bash. |---------|-------------| | `/settings` | Open settings menu (thinking, theme, message delivery modes, toggles) | | `/model` | Switch models mid-session. Use `/model ` or `provider/model` to prefilter/disambiguate. | -| `/models` | Enable/disable models for Ctrl+P cycling | +| `/scoped-models` | Enable/disable models for Ctrl+P cycling | | `/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/index.ts b/packages/coding-agent/src/modes/interactive/components/index.ts index 47502c73..b06e9434 100644 --- a/packages/coding-agent/src/modes/interactive/components/index.ts +++ b/packages/coding-agent/src/modes/interactive/components/index.ts @@ -15,8 +15,8 @@ export { ExtensionSelectorComponent } from "./extension-selector.js"; export { FooterComponent } from "./footer.js"; export { LoginDialogComponent } from "./login-dialog.js"; export { ModelSelectorComponent } from "./model-selector.js"; -export { type ModelsCallbacks, type ModelsConfig, ModelsSelectorComponent } from "./models-selector.js"; export { OAuthSelectorComponent } from "./oauth-selector.js"; +export { type ModelsCallbacks, type ModelsConfig, ScopedModelsSelectorComponent } from "./scoped-models-selector.js"; export { SessionSelectorComponent } from "./session-selector.js"; export { type SettingsCallbacks, type SettingsConfig, SettingsSelectorComponent } from "./settings-selector.js"; export { ShowImagesSelectorComponent } from "./show-images-selector.js"; diff --git a/packages/coding-agent/src/modes/interactive/components/models-selector.ts b/packages/coding-agent/src/modes/interactive/components/models-selector.ts deleted file mode 100644 index dcc8ed23..00000000 --- a/packages/coding-agent/src/modes/interactive/components/models-selector.ts +++ /dev/null @@ -1,146 +0,0 @@ -import type { Model } from "@mariozechner/pi-ai"; -import { Container, getEditorKeybindings, Spacer, Text } from "@mariozechner/pi-tui"; -import { theme } from "../theme/theme.js"; -import { DynamicBorder } from "./dynamic-border.js"; - -interface ModelItem { - fullId: string; - model: Model; - enabled: boolean; -} - -export interface ModelsConfig { - allModels: Model[]; - enabledModelIds: Set; - /** true if enabledModels setting is defined (empty = all enabled) */ - hasEnabledModelsFilter: boolean; -} - -export interface ModelsCallbacks { - onModelToggle: (modelId: string, enabled: boolean) => void; - onCancel: () => void; -} - -/** - * Component for enabling/disabling models for Ctrl+P cycling. - */ -export class ModelsSelectorComponent extends Container { - private items: ModelItem[] = []; - private selectedIndex = 0; - private listContainer: Container; - private callbacks: ModelsCallbacks; - private maxVisible = 15; - - constructor(config: ModelsConfig, callbacks: ModelsCallbacks) { - super(); - this.callbacks = callbacks; - - // Group models by provider for organized display - const modelsByProvider = new Map[]>(); - for (const model of config.allModels) { - const list = modelsByProvider.get(model.provider) ?? []; - list.push(model); - modelsByProvider.set(model.provider, list); - } - - // Build items - group by provider - for (const [provider, models] of modelsByProvider) { - for (const model of models) { - const fullId = `${provider}/${model.id}`; - // If no filter defined, all models are enabled by default - const isEnabled = !config.hasEnabledModelsFilter || config.enabledModelIds.has(fullId); - this.items.push({ - fullId, - model, - enabled: isEnabled, - }); - } - } - - // Header - this.addChild(new DynamicBorder()); - this.addChild(new Spacer(1)); - this.addChild(new Text(theme.fg("accent", theme.bold("Model Configuration")), 0, 0)); - this.addChild(new Text(theme.fg("muted", "Enable/disable models for Ctrl+P cycling"), 0, 0)); - this.addChild(new Spacer(1)); - - // List container - this.listContainer = new Container(); - this.addChild(this.listContainer); - - // Footer hint - this.addChild(new Spacer(1)); - this.addChild(new Text(theme.fg("dim", " Enter/Space to toggle · Esc to close"), 0, 0)); - - this.addChild(new DynamicBorder()); - - this.updateList(); - } - - private updateList(): void { - this.listContainer.clear(); - - const startIndex = Math.max( - 0, - Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.items.length - this.maxVisible), - ); - const endIndex = Math.min(startIndex + this.maxVisible, this.items.length); - - for (let i = startIndex; i < endIndex; i++) { - const item = this.items[i]; - if (!item) continue; - - const isSelected = i === this.selectedIndex; - - let line = ""; - if (isSelected) { - const prefix = theme.fg("accent", "→ "); - const modelText = theme.fg("accent", item.model.id); - const providerBadge = theme.fg("muted", ` [${item.model.provider}]`); - const status = item.enabled ? theme.fg("success", " ✓") : theme.fg("dim", " ✗"); - line = `${prefix}${modelText}${providerBadge}${status}`; - } else { - const prefix = " "; - const modelText = item.model.id; - const providerBadge = theme.fg("muted", ` [${item.model.provider}]`); - const status = item.enabled ? theme.fg("success", " ✓") : theme.fg("dim", " ✗"); - line = `${prefix}${modelText}${providerBadge}${status}`; - } - - this.listContainer.addChild(new Text(line, 0, 0)); - } - - // Add scroll indicator if needed - if (startIndex > 0 || endIndex < this.items.length) { - const scrollInfo = theme.fg("muted", ` (${this.selectedIndex + 1}/${this.items.length})`); - this.listContainer.addChild(new Text(scrollInfo, 0, 0)); - } - } - - handleInput(data: string): void { - const kb = getEditorKeybindings(); - - if (kb.matches(data, "selectUp")) { - this.selectedIndex = this.selectedIndex === 0 ? this.items.length - 1 : this.selectedIndex - 1; - this.updateList(); - } else if (kb.matches(data, "selectDown")) { - this.selectedIndex = this.selectedIndex === this.items.length - 1 ? 0 : this.selectedIndex + 1; - this.updateList(); - } else if (kb.matches(data, "selectConfirm") || data === " ") { - // Toggle on Enter or Space - const item = this.items[this.selectedIndex]; - if (item) { - item.enabled = !item.enabled; - this.callbacks.onModelToggle(item.fullId, item.enabled); - this.updateList(); - } - } else if (kb.matches(data, "selectCancel")) { - this.callbacks.onCancel(); - } - } - - getSettingsList(): this { - // Return self for focus management (compatible with showSelector interface) - return this; - } -} 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 new file mode 100644 index 00000000..17d82f7a --- /dev/null +++ b/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts @@ -0,0 +1,312 @@ +import type { Model } from "@mariozechner/pi-ai"; +import { + Container, + fuzzyFilter, + getEditorKeybindings, + Input, + Key, + matchesKey, + Spacer, + Text, +} from "@mariozechner/pi-tui"; +import { theme } from "../theme/theme.js"; +import { DynamicBorder } from "./dynamic-border.js"; + +interface ModelItem { + fullId: string; + model: Model; + enabled: boolean; +} + +export interface ModelsConfig { + allModels: Model[]; + enabledModelIds: Set; + /** true if enabledModels setting is defined (empty = all enabled) */ + hasEnabledModelsFilter: boolean; +} + +export interface ModelsCallbacks { + /** Called when a model is toggled (session-only, no persist) */ + onModelToggle: (modelId: string, enabled: boolean) => void; + /** Called when user wants to persist current selection to settings */ + onPersist: (enabledModelIds: string[]) => void; + /** Called when user enables all models. Returns list of all model IDs. */ + onEnableAll: (allModelIds: string[]) => void; + /** Called when user clears all models */ + onClearAll: () => void; + /** Called when user toggles all models for a provider. Returns affected model IDs. */ + onToggleProvider: (provider: string, modelIds: string[], enabled: boolean) => void; + onCancel: () => void; +} + +/** + * Component for enabling/disabling models for Ctrl+P cycling. + * Changes are session-only until explicitly persisted with Ctrl+S. + */ +export class ScopedModelsSelectorComponent extends Container { + private items: ModelItem[] = []; + private filteredItems: ModelItem[] = []; + private selectedIndex = 0; + private searchInput: Input; + private listContainer: Container; + private footerText: Text; + private callbacks: ModelsCallbacks; + private maxVisible = 15; + private isDirty = false; + + constructor(config: ModelsConfig, callbacks: ModelsCallbacks) { + super(); + this.callbacks = callbacks; + + // Group models by provider for organized display + const modelsByProvider = new Map[]>(); + for (const model of config.allModels) { + const list = modelsByProvider.get(model.provider) ?? []; + list.push(model); + modelsByProvider.set(model.provider, list); + } + + // Build items - group by provider + for (const [provider, models] of modelsByProvider) { + for (const model of models) { + const fullId = `${provider}/${model.id}`; + // If no filter defined, all models are enabled by default + const isEnabled = !config.hasEnabledModelsFilter || config.enabledModelIds.has(fullId); + this.items.push({ + fullId, + model, + enabled: isEnabled, + }); + } + } + this.filteredItems = this.getSortedItems(); + + // Header + this.addChild(new DynamicBorder()); + this.addChild(new Spacer(1)); + this.addChild(new Text(theme.fg("accent", theme.bold("Model Configuration")), 0, 0)); + this.addChild(new Text(theme.fg("muted", "Session-only. Ctrl+S to save to settings."), 0, 0)); + this.addChild(new Spacer(1)); + + // Search input + this.searchInput = new Input(); + this.addChild(this.searchInput); + this.addChild(new Spacer(1)); + + // List container + this.listContainer = new Container(); + this.addChild(this.listContainer); + + // Footer hint + this.addChild(new Spacer(1)); + this.footerText = new Text(this.getFooterText(), 0, 0); + this.addChild(this.footerText); + + this.addChild(new DynamicBorder()); + + this.updateList(); + } + + /** Get items sorted with enabled items first */ + private getSortedItems(): ModelItem[] { + const enabled = this.items.filter((i) => i.enabled); + const disabled = this.items.filter((i) => !i.enabled); + return [...enabled, ...disabled]; + } + + private getFooterText(): string { + const enabledCount = this.items.filter((i) => i.enabled).length; + const allEnabled = enabledCount === this.items.length; + const countText = allEnabled ? "all enabled" : `${enabledCount}/${this.items.length} enabled`; + const parts = ["Enter toggle", "^A all", "^X clear", "^P provider", "^S save", countText]; + if (this.isDirty) { + return theme.fg("dim", ` ${parts.join(" · ")} `) + theme.fg("warning", "(unsaved)"); + } + return theme.fg("dim", ` ${parts.join(" · ")}`); + } + + private updateFooter(): void { + this.footerText.setText(this.getFooterText()); + } + + private filterItems(query: string): void { + const sorted = this.getSortedItems(); + if (!query) { + this.filteredItems = sorted; + } else { + this.filteredItems = fuzzyFilter(sorted, query, (item) => `${item.model.id} ${item.model.provider}`); + } + this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredItems.length - 1)); + this.updateList(); + } + + private updateList(): void { + this.listContainer.clear(); + + if (this.filteredItems.length === 0) { + this.listContainer.addChild(new Text(theme.fg("muted", " No matching models"), 0, 0)); + return; + } + + const startIndex = Math.max( + 0, + Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredItems.length - this.maxVisible), + ); + const endIndex = Math.min(startIndex + this.maxVisible, this.filteredItems.length); + + // Only show status if there's a filter (not all models enabled) + const allEnabled = this.items.every((i) => i.enabled); + + for (let i = startIndex; i < endIndex; i++) { + const item = this.filteredItems[i]; + if (!item) continue; + + const isSelected = i === this.selectedIndex; + const prefix = isSelected ? theme.fg("accent", "→ ") : " "; + const modelText = isSelected ? theme.fg("accent", item.model.id) : item.model.id; + const providerBadge = theme.fg("muted", ` [${item.model.provider}]`); + // Only show checkmarks when there's actually a filter + const status = allEnabled ? "" : item.enabled ? theme.fg("success", " ✓") : theme.fg("dim", " ✗"); + + this.listContainer.addChild(new Text(`${prefix}${modelText}${providerBadge}${status}`, 0, 0)); + } + + // Add scroll indicator if needed + if (startIndex > 0 || endIndex < this.filteredItems.length) { + const scrollInfo = theme.fg("muted", ` (${this.selectedIndex + 1}/${this.filteredItems.length})`); + this.listContainer.addChild(new Text(scrollInfo, 0, 0)); + } + } + + private toggleItem(item: ModelItem): void { + // If all models are currently enabled (no scope yet), first toggle starts fresh: + // clear all and enable only the selected model + const allEnabled = this.items.every((i) => i.enabled); + if (allEnabled) { + for (const i of this.items) { + i.enabled = false; + } + item.enabled = true; + this.isDirty = true; + this.callbacks.onClearAll(); + this.callbacks.onModelToggle(item.fullId, true); + } else { + item.enabled = !item.enabled; + this.isDirty = true; + this.callbacks.onModelToggle(item.fullId, item.enabled); + } + // Re-sort and re-filter to move item to correct section + this.filterItems(this.searchInput.getValue()); + this.updateFooter(); + } + + handleInput(data: string): void { + const kb = getEditorKeybindings(); + + // Navigation + if (kb.matches(data, "selectUp")) { + if (this.filteredItems.length === 0) return; + this.selectedIndex = this.selectedIndex === 0 ? this.filteredItems.length - 1 : this.selectedIndex - 1; + this.updateList(); + return; + } + if (kb.matches(data, "selectDown")) { + if (this.filteredItems.length === 0) return; + this.selectedIndex = this.selectedIndex === this.filteredItems.length - 1 ? 0 : this.selectedIndex + 1; + this.updateList(); + return; + } + + // Toggle on Enter + if (matchesKey(data, Key.enter)) { + const item = this.filteredItems[this.selectedIndex]; + if (item) { + this.toggleItem(item); + } + return; + } + + // Ctrl+A - Enable all (filtered if search active, otherwise all) + if (matchesKey(data, Key.ctrl("a"))) { + const targets = this.searchInput.getValue() ? this.filteredItems : this.items; + for (const item of targets) { + item.enabled = true; + } + this.isDirty = true; + this.callbacks.onEnableAll(targets.map((i) => i.fullId)); + this.filterItems(this.searchInput.getValue()); + this.updateFooter(); + return; + } + + // Ctrl+X - Clear all (filtered if search active, otherwise all) + if (matchesKey(data, Key.ctrl("x"))) { + const targets = this.searchInput.getValue() ? this.filteredItems : this.items; + for (const item of targets) { + item.enabled = false; + } + this.isDirty = true; + this.callbacks.onClearAll(); + this.filterItems(this.searchInput.getValue()); + this.updateFooter(); + return; + } + + // Ctrl+P - Toggle provider of current item + if (matchesKey(data, Key.ctrl("p"))) { + const currentItem = this.filteredItems[this.selectedIndex]; + if (currentItem) { + const provider = currentItem.model.provider; + const providerItems = this.items.filter((i) => i.model.provider === provider); + const allEnabled = providerItems.every((i) => i.enabled); + const newState = !allEnabled; + for (const item of providerItems) { + item.enabled = newState; + } + this.isDirty = true; + this.callbacks.onToggleProvider( + provider, + providerItems.map((i) => i.fullId), + newState, + ); + this.filterItems(this.searchInput.getValue()); + this.updateFooter(); + } + return; + } + + // Ctrl+S - Save/persist to settings + if (matchesKey(data, Key.ctrl("s"))) { + const enabledIds = this.items.filter((i) => i.enabled).map((i) => i.fullId); + this.callbacks.onPersist(enabledIds); + this.isDirty = false; + this.updateFooter(); + return; + } + + // Ctrl+C - clear search or cancel if empty + if (matchesKey(data, Key.ctrl("c"))) { + if (this.searchInput.getValue()) { + this.searchInput.setValue(""); + this.filterItems(""); + } else { + this.callbacks.onCancel(); + } + return; + } + + // Escape - cancel + if (matchesKey(data, Key.escape)) { + this.callbacks.onCancel(); + return; + } + + // Pass everything else to search input + this.searchInput.handleInput(data); + this.filterItems(this.searchInput.getValue()); + } + + getSearchInput(): Input { + return this.searchInput; + } +} diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index f19d3b34..d40697fc 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -69,8 +69,8 @@ import { ExtensionSelectorComponent } from "./components/extension-selector.js"; import { FooterComponent } from "./components/footer.js"; import { LoginDialogComponent } from "./components/login-dialog.js"; import { ModelSelectorComponent } from "./components/model-selector.js"; -import { ModelsSelectorComponent } from "./components/models-selector.js"; import { OAuthSelectorComponent } from "./components/oauth-selector.js"; +import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.js"; import { SessionSelectorComponent } from "./components/session-selector.js"; import { SettingsSelectorComponent } from "./components/settings-selector.js"; import { ToolExecutionComponent } from "./components/tool-execution.js"; @@ -282,7 +282,7 @@ export class InteractiveMode { })); }, }, - { name: "models", description: "Enable/disable models for Ctrl+P cycling" }, + { name: "scoped-models", description: "Enable/disable models for Ctrl+P cycling" }, { name: "export", description: "Export session to HTML file" }, { name: "share", description: "Share session as a secret GitHub gist" }, { name: "copy", description: "Copy last agent message to clipboard" }, @@ -1415,7 +1415,7 @@ export class InteractiveMode { this.editor.setText(""); return; } - if (text === "/models") { + if (text === "/scoped-models") { this.editor.setText(""); await this.showModelsSelector(); return; @@ -2674,25 +2674,56 @@ export class InteractiveMode { return; } - // Get current enabledModels patterns - const patterns = this.settingsManager.getEnabledModels(); - const hasFilter = patterns !== undefined && patterns.length > 0; + // Check if session has scoped models (from previous session-only changes or CLI --models) + const sessionScopedModels = this.session.scopedModels; + const hasSessionScope = sessionScopedModels.length > 0; - // Resolve patterns to get currently enabled model IDs + // Build enabled model IDs from session state or settings const enabledModelIds = new Set(); - if (hasFilter) { - const scopedModels = await resolveModelScope(patterns, this.session.modelRegistry); - for (const sm of scopedModels) { + let hasFilter = false; + + if (hasSessionScope) { + // Use current session's scoped models + for (const sm of sessionScopedModels) { enabledModelIds.add(`${sm.model.provider}/${sm.model.id}`); } + hasFilter = true; + } else { + // Fall back to settings + const patterns = this.settingsManager.getEnabledModels(); + if (patterns !== undefined && patterns.length > 0) { + hasFilter = true; + const scopedModels = await resolveModelScope(patterns, this.session.modelRegistry); + for (const sm of scopedModels) { + enabledModelIds.add(`${sm.model.provider}/${sm.model.id}`); + } + } } - // Track current enabled state + // Track current enabled state (session-only until persisted) const currentEnabledIds = new Set(enabledModelIds); let currentHasFilter = hasFilter; + // Helper to update session's scoped models (session-only, no persist) + const updateSessionModels = async (enabledIds: Set) => { + if (enabledIds.size > 0 && enabledIds.size < allModels.length) { + // Use current session thinking level, not settings default + const currentThinkingLevel = this.session.thinkingLevel; + const newScopedModels = await resolveModelScope(Array.from(enabledIds), this.session.modelRegistry); + this.session.setScopedModels( + newScopedModels.map((sm) => ({ + model: sm.model, + thinkingLevel: sm.thinkingLevel ?? currentThinkingLevel, + })), + ); + } else { + // All enabled or none enabled = no filter + this.session.setScopedModels([]); + } + }; + this.showSelector((done) => { - const selector = new ModelsSelectorComponent( + const selector = new ScopedModelsSelectorComponent( { allModels, enabledModelIds: currentEnabledIds, @@ -2706,27 +2737,40 @@ export class InteractiveMode { currentEnabledIds.delete(modelId); } currentHasFilter = true; - - // Save to settings - const newPatterns = - currentEnabledIds.size === allModels.length - ? undefined // All enabled = clear filter - : Array.from(currentEnabledIds); - this.settingsManager.setEnabledModels(newPatterns); - - // Update session's scoped models - if (newPatterns && newPatterns.length > 0) { - const defaultThinkingLevel = this.settingsManager.getDefaultThinkingLevel() ?? "off"; - const newScopedModels = await resolveModelScope(newPatterns, this.session.modelRegistry); - this.session.setScopedModels( - newScopedModels.map((sm) => ({ - model: sm.model, - thinkingLevel: sm.thinkingLevel ?? defaultThinkingLevel, - })), - ); - } else { - this.session.setScopedModels([]); + await updateSessionModels(currentEnabledIds); + }, + onEnableAll: async (allModelIds) => { + currentEnabledIds.clear(); + for (const id of allModelIds) { + currentEnabledIds.add(id); } + currentHasFilter = false; + await updateSessionModels(currentEnabledIds); + }, + onClearAll: async () => { + currentEnabledIds.clear(); + currentHasFilter = true; + await updateSessionModels(currentEnabledIds); + }, + onToggleProvider: async (_provider, modelIds, enabled) => { + for (const id of modelIds) { + if (enabled) { + currentEnabledIds.add(id); + } else { + currentEnabledIds.delete(id); + } + } + currentHasFilter = true; + await updateSessionModels(currentEnabledIds); + }, + onPersist: (enabledIds) => { + // Persist to settings + const newPatterns = + enabledIds.length === allModels.length + ? undefined // All enabled = clear filter + : enabledIds; + this.settingsManager.setEnabledModels(newPatterns); + this.showStatus("Model selection saved to settings"); }, onCancel: () => { done(); @@ -2734,7 +2778,7 @@ export class InteractiveMode { }, }, ); - return { component: selector, focus: selector.getSettingsList() }; + return { component: selector, focus: selector }; }); }