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
This commit is contained in:
Mario Zechner 2026-01-12 00:53:52 +01:00
parent 4755da4bfb
commit 50cd054edb
6 changed files with 392 additions and 182 deletions

View file

@ -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<string>();
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<string>) => {
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 };
});
}