diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 92561529..145af044 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -11,6 +11,10 @@ - Extension UI dialogs (`ctx.ui.select()`, `ctx.ui.confirm()`, `ctx.ui.input()`) now support a `timeout` option that auto-dismisses the dialog with a live countdown display. Simpler alternative to `AbortSignal` for timed dialogs. - Extensions can now provide custom editor components via `ctx.ui.setEditorComponent((tui, theme, keybindings) => ...)`. Extend `CustomEditor` for full app keybinding support (escape, ctrl+d, model switching, etc.). See `examples/extensions/modal-editor.ts`, `examples/extensions/rainbow-editor.ts`, and `docs/tui.md` Pattern 7. +### Fixed + +- Default thinking level from settings now applies correctly when `enabledModels` is configured. Previously, models without explicit thinking level suffixes (e.g., `claude-opus-4-5` instead of `claude-opus-4-5:high`) would override `defaultThinkingLevel` with "off" + ## [0.37.8] - 2026-01-07 ## [0.37.7] - 2026-01-07 diff --git a/packages/coding-agent/src/core/model-resolver.ts b/packages/coding-agent/src/core/model-resolver.ts index 98c8fd60..b00e213a 100644 --- a/packages/coding-agent/src/core/model-resolver.ts +++ b/packages/coding-agent/src/core/model-resolver.ts @@ -29,7 +29,8 @@ export const defaultModelPerProvider: Record = { export interface ScopedModel { model: Model; - thinkingLevel: ThinkingLevel; + /** Thinking level if explicitly specified in pattern (e.g., "model:high"), undefined otherwise */ + thinkingLevel?: ThinkingLevel; } /** @@ -98,7 +99,8 @@ function tryMatchModel(modelPattern: string, availableModels: Model[]): Mod export interface ParsedModelResult { model: Model | undefined; - thinkingLevel: ThinkingLevel; + /** Thinking level if explicitly specified in pattern, undefined otherwise */ + thinkingLevel?: ThinkingLevel; warning: string | undefined; } @@ -119,14 +121,14 @@ export function parseModelPattern(pattern: string, availableModels: Model[] // Try exact match first const exactMatch = tryMatchModel(pattern, availableModels); if (exactMatch) { - return { model: exactMatch, thinkingLevel: "off", warning: undefined }; + return { model: exactMatch, thinkingLevel: undefined, warning: undefined }; } // No match - try splitting on last colon if present const lastColonIndex = pattern.lastIndexOf(":"); if (lastColonIndex === -1) { // No colons, pattern simply doesn't match any model - return { model: undefined, thinkingLevel: "off", warning: undefined }; + return { model: undefined, thinkingLevel: undefined, warning: undefined }; } const prefix = pattern.substring(0, lastColonIndex); @@ -137,22 +139,21 @@ export function parseModelPattern(pattern: string, availableModels: Model[] const result = parseModelPattern(prefix, availableModels); if (result.model) { // Only use this thinking level if no warning from inner recursion - // (if there was an invalid suffix deeper, we already have "off") return { model: result.model, - thinkingLevel: result.warning ? "off" : suffix, + thinkingLevel: result.warning ? undefined : suffix, warning: result.warning, }; } return result; } else { - // Invalid suffix - recurse on prefix with "off" and warn + // Invalid suffix - recurse on prefix and warn const result = parseModelPattern(prefix, availableModels); if (result.model) { return { model: result.model, - thinkingLevel: "off", - warning: `Invalid thinking level "${suffix}" in pattern "${pattern}". Using "off" instead.`, + thinkingLevel: undefined, + warning: `Invalid thinking level "${suffix}" in pattern "${pattern}". Using default instead.`, }; } return result; @@ -180,7 +181,7 @@ export async function resolveModelScope(patterns: string[], modelRegistry: Model // Extract optional thinking level suffix (e.g., "provider/*:high") const colonIdx = pattern.lastIndexOf(":"); let globPattern = pattern; - let thinkingLevel: ThinkingLevel = "off"; + let thinkingLevel: ThinkingLevel | undefined; if (colonIdx !== -1) { const suffix = pattern.substring(colonIdx + 1); @@ -282,7 +283,7 @@ export async function findInitialModel(options: { if (scopedModels.length > 0 && !isContinuing) { return { model: scopedModels[0].model, - thinkingLevel: scopedModels[0].thinkingLevel, + thinkingLevel: scopedModels[0].thinkingLevel ?? defaultThinkingLevel ?? "off", fallbackMessage: undefined, }; } diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index da8d0e6c..a05c46a8 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -235,6 +235,7 @@ function buildSessionOptions( scopedModels: ScopedModel[], sessionManager: SessionManager | undefined, modelRegistry: ModelRegistry, + settingsManager: SettingsManager, preloadedExtensions?: LoadedExtension[], ): CreateAgentSessionOptions { const options: CreateAgentSessionOptions = {}; @@ -261,15 +262,21 @@ function buildSessionOptions( } // Thinking level + // Only use scoped model's thinking level if it was explicitly specified (e.g., "model:high") + // Otherwise, let the SDK use defaultThinkingLevel from settings if (parsed.thinking) { options.thinkingLevel = parsed.thinking; - } else if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) { + } else if (scopedModels.length > 0 && scopedModels[0].thinkingLevel && !parsed.continue && !parsed.resume) { options.thinkingLevel = scopedModels[0].thinkingLevel; } - // Scoped models for Ctrl+P cycling + // Scoped models for Ctrl+P cycling - fill in default thinking level for models without explicit level if (scopedModels.length > 0) { - options.scopedModels = scopedModels; + const defaultThinkingLevel = settingsManager.getDefaultThinkingLevel() ?? "off"; + options.scopedModels = scopedModels.map((sm) => ({ + model: sm.model, + thinkingLevel: sm.thinkingLevel ?? defaultThinkingLevel, + })); } // API key from CLI - set in authStorage @@ -423,7 +430,14 @@ export async function main(args: string[]) { sessionManager = SessionManager.open(selectedPath); } - const sessionOptions = buildSessionOptions(parsed, scopedModels, sessionManager, modelRegistry, loadedExtensions); + const sessionOptions = buildSessionOptions( + parsed, + scopedModels, + sessionManager, + modelRegistry, + settingsManager, + loadedExtensions, + ); sessionOptions.authStorage = authStorage; sessionOptions.modelRegistry = modelRegistry; sessionOptions.eventBus = eventBus; @@ -471,7 +485,7 @@ export async function main(args: string[]) { if (scopedModels.length > 0) { const modelList = scopedModels .map((sm) => { - const thinkingStr = sm.thinkingLevel !== "off" ? `:${sm.thinkingLevel}` : ""; + const thinkingStr = sm.thinkingLevel ? `:${sm.thinkingLevel}` : ""; return `${sm.model.id}${thinkingStr}`; }) .join(", "); diff --git a/packages/coding-agent/test/model-resolver.test.ts b/packages/coding-agent/test/model-resolver.test.ts index 0b7b47ca..fdef4160 100644 --- a/packages/coding-agent/test/model-resolver.test.ts +++ b/packages/coding-agent/test/model-resolver.test.ts @@ -62,24 +62,24 @@ const allModels = [...mockModels, ...mockOpenRouterModels]; describe("parseModelPattern", () => { describe("simple patterns without colons", () => { - test("exact match returns model with off thinking level", () => { + test("exact match returns model with undefined thinking level", () => { const result = parseModelPattern("claude-sonnet-4-5", allModels); expect(result.model?.id).toBe("claude-sonnet-4-5"); - expect(result.thinkingLevel).toBe("off"); + expect(result.thinkingLevel).toBeUndefined(); expect(result.warning).toBeUndefined(); }); - test("partial match returns best model", () => { + test("partial match returns best model with undefined thinking level", () => { const result = parseModelPattern("sonnet", allModels); expect(result.model?.id).toBe("claude-sonnet-4-5"); - expect(result.thinkingLevel).toBe("off"); + expect(result.thinkingLevel).toBeUndefined(); expect(result.warning).toBeUndefined(); }); - test("no match returns null model", () => { + test("no match returns undefined model and thinking level", () => { const result = parseModelPattern("nonexistent", allModels); expect(result.model).toBeUndefined(); - expect(result.thinkingLevel).toBe("off"); + expect(result.thinkingLevel).toBeUndefined(); expect(result.warning).toBeUndefined(); }); }); @@ -110,27 +110,27 @@ describe("parseModelPattern", () => { }); describe("patterns with invalid thinking levels", () => { - test("sonnet:random returns sonnet with off and warning", () => { + test("sonnet:random returns sonnet with undefined thinking level and warning", () => { const result = parseModelPattern("sonnet:random", allModels); expect(result.model?.id).toBe("claude-sonnet-4-5"); - expect(result.thinkingLevel).toBe("off"); + expect(result.thinkingLevel).toBeUndefined(); expect(result.warning).toContain("Invalid thinking level"); expect(result.warning).toContain("random"); }); - test("gpt-4o:invalid returns gpt-4o with off and warning", () => { + test("gpt-4o:invalid returns gpt-4o with undefined thinking level and warning", () => { const result = parseModelPattern("gpt-4o:invalid", allModels); expect(result.model?.id).toBe("gpt-4o"); - expect(result.thinkingLevel).toBe("off"); + expect(result.thinkingLevel).toBeUndefined(); expect(result.warning).toContain("Invalid thinking level"); }); }); describe("OpenRouter models with colons in IDs", () => { - test("qwen3-coder:exacto matches the model with off", () => { + test("qwen3-coder:exacto matches the model with undefined thinking level", () => { const result = parseModelPattern("qwen/qwen3-coder:exacto", allModels); expect(result.model?.id).toBe("qwen/qwen3-coder:exacto"); - expect(result.thinkingLevel).toBe("off"); + expect(result.thinkingLevel).toBeUndefined(); expect(result.warning).toBeUndefined(); }); @@ -138,7 +138,7 @@ describe("parseModelPattern", () => { const result = parseModelPattern("openrouter/qwen/qwen3-coder:exacto", allModels); expect(result.model?.id).toBe("qwen/qwen3-coder:exacto"); expect(result.model?.provider).toBe("openrouter"); - expect(result.thinkingLevel).toBe("off"); + expect(result.thinkingLevel).toBeUndefined(); expect(result.warning).toBeUndefined(); }); @@ -157,27 +157,27 @@ describe("parseModelPattern", () => { expect(result.warning).toBeUndefined(); }); - test("gpt-4o:extended matches the extended model", () => { + test("gpt-4o:extended matches the extended model with undefined thinking level", () => { const result = parseModelPattern("openai/gpt-4o:extended", allModels); expect(result.model?.id).toBe("openai/gpt-4o:extended"); - expect(result.thinkingLevel).toBe("off"); + expect(result.thinkingLevel).toBeUndefined(); expect(result.warning).toBeUndefined(); }); }); describe("invalid thinking levels with OpenRouter models", () => { - test("qwen3-coder:exacto:random returns model with off and warning", () => { + test("qwen3-coder:exacto:random returns model with undefined thinking level and warning", () => { const result = parseModelPattern("qwen/qwen3-coder:exacto:random", allModels); expect(result.model?.id).toBe("qwen/qwen3-coder:exacto"); - expect(result.thinkingLevel).toBe("off"); + expect(result.thinkingLevel).toBeUndefined(); expect(result.warning).toContain("Invalid thinking level"); expect(result.warning).toContain("random"); }); - test("qwen3-coder:exacto:high:random returns model with off and warning", () => { + test("qwen3-coder:exacto:high:random returns model with undefined thinking level and warning", () => { const result = parseModelPattern("qwen/qwen3-coder:exacto:high:random", allModels); expect(result.model?.id).toBe("qwen/qwen3-coder:exacto"); - expect(result.thinkingLevel).toBe("off"); + expect(result.thinkingLevel).toBeUndefined(); expect(result.warning).toContain("Invalid thinking level"); expect(result.warning).toContain("random"); }); @@ -188,7 +188,7 @@ describe("parseModelPattern", () => { // Empty string is included in all model IDs, so partial matching finds a match const result = parseModelPattern("", allModels); expect(result.model).not.toBeNull(); - expect(result.thinkingLevel).toBe("off"); + expect(result.thinkingLevel).toBeUndefined(); }); test("pattern ending with colon treats empty suffix as invalid", () => {