fix(coding-agent): preserve session thinking for scoped model cycling

closes #1789
This commit is contained in:
Mario Zechner 2026-03-04 19:51:55 +01:00
parent 4cb1a56b53
commit e64cd15c25
6 changed files with 22 additions and 17 deletions

View file

@ -135,7 +135,7 @@ export interface AgentSessionConfig {
settingsManager: SettingsManager;
cwd: string;
/** Models to cycle through with Ctrl+P (from --models flag) */
scopedModels?: Array<{ model: Model<any>; thinkingLevel: ThinkingLevel }>;
scopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;
/** Resource loader for skills, prompts, themes, context files, system prompt */
resourceLoader: ResourceLoader;
/** SDK custom tools registered outside extensions */
@ -215,7 +215,7 @@ export class AgentSession {
readonly sessionManager: SessionManager;
readonly settingsManager: SettingsManager;
private _scopedModels: Array<{ model: Model<any>; thinkingLevel: ThinkingLevel }>;
private _scopedModels: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;
// Event subscription state
private _unsubscribeAgent?: () => void;
@ -717,12 +717,12 @@ export class AgentSession {
}
/** Scoped models for cycling (from --models flag) */
get scopedModels(): ReadonlyArray<{ model: Model<any>; thinkingLevel: ThinkingLevel }> {
get scopedModels(): ReadonlyArray<{ model: Model<any>; thinkingLevel?: ThinkingLevel }> {
return this._scopedModels;
}
/** Update scoped models for cycling */
setScopedModels(scopedModels: Array<{ model: Model<any>; thinkingLevel: ThinkingLevel }>): void {
setScopedModels(scopedModels: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>): void {
this._scopedModels = scopedModels;
}
@ -1338,9 +1338,9 @@ export class AgentSession {
return this._cycleAvailableModel(direction);
}
private async _getScopedModelsWithApiKey(): Promise<Array<{ model: Model<any>; thinkingLevel: ThinkingLevel }>> {
private async _getScopedModelsWithApiKey(): Promise<Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>> {
const apiKeysByProvider = new Map<string, string | undefined>();
const result: Array<{ model: Model<any>; thinkingLevel: ThinkingLevel }> = [];
const result: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }> = [];
for (const scoped of this._scopedModels) {
const provider = scoped.model.provider;
@ -1377,8 +1377,11 @@ export class AgentSession {
this.sessionManager.appendModelChange(next.model.provider, next.model.id);
this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
// Apply thinking level (setThinkingLevel clamps to model capabilities)
this.setThinkingLevel(next.thinkingLevel);
// Apply thinking level.
// - Explicit scoped model thinking level overrides current session level
// - Undefined scoped model thinking level inherits current session level
// setThinkingLevel clamps to model capabilities.
this.setThinkingLevel(next.thinkingLevel ?? this.thinkingLevel);
await this._emitModelSelect(next.model, currentModel, "cycle");

View file

@ -54,7 +54,7 @@ export interface CreateAgentSessionOptions {
/** Thinking level. Default: from settings, else 'medium' (clamped to model capabilities) */
thinkingLevel?: ThinkingLevel;
/** Models available for cycling (Ctrl+P in interactive mode) */
scopedModels?: Array<{ model: Model<any>; thinkingLevel: ThinkingLevel }>;
scopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;
/** Built-in tools to use. Default: codingTools [read, bash, edit, write] */
tools?: Tool[];

View file

@ -15,7 +15,6 @@ import { listModels } from "./cli/list-models.js";
import { selectSession } from "./cli/session-picker.js";
import { APP_NAME, getAgentDir, getModelsPath, VERSION } from "./config.js";
import { AuthStorage } from "./core/auth-storage.js";
import { DEFAULT_THINKING_LEVEL } from "./core/defaults.js";
import { exportFromFile } from "./core/export-html/index.js";
import type { LoadExtensionsResult } from "./core/extensions/index.js";
import { KeybindingsManager } from "./core/keybindings.js";
@ -488,12 +487,13 @@ function buildSessionOptions(
options.thinkingLevel = parsed.thinking;
}
// Scoped models for Ctrl+P cycling - fill in default thinking level for models without explicit level
// Scoped models for Ctrl+P cycling
// Keep thinking level undefined when not explicitly set in the model pattern.
// Undefined means "inherit current session thinking level" during cycling.
if (scopedModels.length > 0) {
const defaultThinkingLevel = settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;
options.scopedModels = scopedModels.map((sm) => ({
model: sm.model,
thinkingLevel: sm.thinkingLevel ?? defaultThinkingLevel,
thinkingLevel: sm.thinkingLevel,
}));
}

View file

@ -23,7 +23,7 @@ interface ModelItem {
interface ScopedModelItem {
model: Model<any>;
thinkingLevel: string;
thinkingLevel?: string;
}
type ModelScope = "all" | "scoped";

View file

@ -3307,13 +3307,11 @@ export class InteractiveMode {
// 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,
thinkingLevel: sm.thinkingLevel,
})),
);
} else {