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

@ -2,6 +2,10 @@
## [Unreleased] ## [Unreleased]
### Breaking Changes
- Changed scoped model thinking semantics. Scoped entries without an explicit `:<thinking>` suffix now inherit the current session thinking level when selected, instead of applying a startup-captured default.
### Fixed ### Fixed
- Fixed IME hardware cursor positioning in the custom extension editor (`ctx.ui.editor()` / extension editor dialog) by propagating focus to the internal `Editor`, preventing the terminal cursor from getting stuck at the bottom-right during composition. - Fixed IME hardware cursor positioning in the custom extension editor (`ctx.ui.editor()` / extension editor dialog) by propagating focus to the internal `Editor`, preventing the terminal cursor from getting stuck at the bottom-right during composition.

View file

@ -135,7 +135,7 @@ export interface AgentSessionConfig {
settingsManager: SettingsManager; settingsManager: SettingsManager;
cwd: string; cwd: string;
/** Models to cycle through with Ctrl+P (from --models flag) */ /** 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 */ /** Resource loader for skills, prompts, themes, context files, system prompt */
resourceLoader: ResourceLoader; resourceLoader: ResourceLoader;
/** SDK custom tools registered outside extensions */ /** SDK custom tools registered outside extensions */
@ -215,7 +215,7 @@ export class AgentSession {
readonly sessionManager: SessionManager; readonly sessionManager: SessionManager;
readonly settingsManager: SettingsManager; readonly settingsManager: SettingsManager;
private _scopedModels: Array<{ model: Model<any>; thinkingLevel: ThinkingLevel }>; private _scopedModels: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;
// Event subscription state // Event subscription state
private _unsubscribeAgent?: () => void; private _unsubscribeAgent?: () => void;
@ -717,12 +717,12 @@ export class AgentSession {
} }
/** Scoped models for cycling (from --models flag) */ /** 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; return this._scopedModels;
} }
/** Update scoped models for cycling */ /** 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; this._scopedModels = scopedModels;
} }
@ -1338,9 +1338,9 @@ export class AgentSession {
return this._cycleAvailableModel(direction); 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 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) { for (const scoped of this._scopedModels) {
const provider = scoped.model.provider; const provider = scoped.model.provider;
@ -1377,8 +1377,11 @@ export class AgentSession {
this.sessionManager.appendModelChange(next.model.provider, next.model.id); this.sessionManager.appendModelChange(next.model.provider, next.model.id);
this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id); this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
// Apply thinking level (setThinkingLevel clamps to model capabilities) // Apply thinking level.
this.setThinkingLevel(next.thinkingLevel); // - 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"); 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) */ /** Thinking level. Default: from settings, else 'medium' (clamped to model capabilities) */
thinkingLevel?: ThinkingLevel; thinkingLevel?: ThinkingLevel;
/** Models available for cycling (Ctrl+P in interactive mode) */ /** 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] */ /** Built-in tools to use. Default: codingTools [read, bash, edit, write] */
tools?: Tool[]; tools?: Tool[];

View file

@ -15,7 +15,6 @@ import { listModels } from "./cli/list-models.js";
import { selectSession } from "./cli/session-picker.js"; import { selectSession } from "./cli/session-picker.js";
import { APP_NAME, getAgentDir, getModelsPath, VERSION } from "./config.js"; import { APP_NAME, getAgentDir, getModelsPath, VERSION } from "./config.js";
import { AuthStorage } from "./core/auth-storage.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 { exportFromFile } from "./core/export-html/index.js";
import type { LoadExtensionsResult } from "./core/extensions/index.js"; import type { LoadExtensionsResult } from "./core/extensions/index.js";
import { KeybindingsManager } from "./core/keybindings.js"; import { KeybindingsManager } from "./core/keybindings.js";
@ -488,12 +487,13 @@ function buildSessionOptions(
options.thinkingLevel = parsed.thinking; 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) { if (scopedModels.length > 0) {
const defaultThinkingLevel = settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;
options.scopedModels = scopedModels.map((sm) => ({ options.scopedModels = scopedModels.map((sm) => ({
model: sm.model, model: sm.model,
thinkingLevel: sm.thinkingLevel ?? defaultThinkingLevel, thinkingLevel: sm.thinkingLevel,
})); }));
} }

View file

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

View file

@ -3307,13 +3307,11 @@ export class InteractiveMode {
// Helper to update session's scoped models (session-only, no persist) // Helper to update session's scoped models (session-only, no persist)
const updateSessionModels = async (enabledIds: Set<string>) => { const updateSessionModels = async (enabledIds: Set<string>) => {
if (enabledIds.size > 0 && enabledIds.size < allModels.length) { 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); const newScopedModels = await resolveModelScope(Array.from(enabledIds), this.session.modelRegistry);
this.session.setScopedModels( this.session.setScopedModels(
newScopedModels.map((sm) => ({ newScopedModels.map((sm) => ({
model: sm.model, model: sm.model,
thinkingLevel: sm.thinkingLevel ?? currentThinkingLevel, thinkingLevel: sm.thinkingLevel,
})), })),
); );
} else { } else {