diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 7de11cc4d42..025c5327386 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -6,6 +6,7 @@ - Improved search reliability for Perplexity provider by forcing retrieval for all queries - Fixed JS eval cells losing top-level `function` and `var` declarations across cells when the defining cell contained top-level `await` — the async wrapper scoped them to the cell's IIFE instead of publishing them to the worker global +- Fixed temporary model picks (`Alt+P`, `/switch`, `/model --temporary`) ignoring explicit thinking suffixes from matching configured model roles. ([#5290](https://github.com/can1357/oh-my-pi/issues/5290)) ## [16.4.7] - 2026-07-12 diff --git a/packages/coding-agent/src/modes/controllers/selector-controller.ts b/packages/coding-agent/src/modes/controllers/selector-controller.ts index e101bbc5fa3..e89cd7e436a 100644 --- a/packages/coding-agent/src/modes/controllers/selector-controller.ts +++ b/packages/coding-agent/src/modes/controllers/selector-controller.ts @@ -688,8 +688,8 @@ export class SelectorController { }, onPick: async (model, selector) => { try { - // Session-only: update agent state but don't persist the model to settings. - await this.ctx.session.setModelTemporary(model); + const roleThinkingLevel = this.ctx.session.resolveTemporaryModelThinkingLevel(model); + await this.ctx.session.setModelTemporary(model, roleThinkingLevel); this.ctx.statusLine.invalidate(); this.ctx.updateEditorBorderColor(); const roleSelectorHint = this.ctx.keybindings.getKeys("app.model.select")[0] ?? "Alt+M"; diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index 31d6b138343..8e4b2a2d7b7 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -182,7 +182,7 @@ import { resolveModelOverride, resolveModelRoleValue, } from "../config/model-resolver"; -import { MODEL_ROLE_IDS, MODEL_ROLES } from "../config/model-roles"; +import { getKnownRoleIds, MODEL_ROLE_IDS, MODEL_ROLES } from "../config/model-roles"; import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates"; import { buildServiceTierByFamily, serviceTierForAllFamilies, serviceTierSettingToTier } from "../config/service-tier"; import type { Settings, SkillsSettings } from "../config/settings"; @@ -7321,6 +7321,30 @@ export class AgentSession { return this.#resolveRoleModelFull(role, this.#modelRegistry.getAvailable(), this.model); } + /** + * Resolve the explicit thinking suffix that should apply when a temporary + * picker selects a model already assigned to a configured role. + */ + resolveTemporaryModelThinkingLevel(model: Model): ConfiguredThinkingLevel | undefined { + const availableModels = this.#modelRegistry.getAvailable(); + if (availableModels.length === 0) return undefined; + + const matchPreferences = getModelMatchPreferences(this.settings); + for (const role of getKnownRoleIds(this.settings)) { + const roleValue = this.settings.getModelRole(role); + if (!roleValue) continue; + + const resolved = resolveModelRoleValue(roleValue, availableModels, { + settings: this.settings, + matchPreferences, + }); + if (!resolved.explicitThinkingLevel || resolved.thinkingLevel === undefined || !resolved.model) continue; + if (modelsAreEqual(resolved.model, model)) return resolved.thinkingLevel; + } + + return undefined; + } + get promptTemplates(): ReadonlyArray { return this.#promptTemplates; } diff --git a/packages/coding-agent/test/agent-session-role-thinking.test.ts b/packages/coding-agent/test/agent-session-role-thinking.test.ts index 91090832524..7a69cfe7c1e 100644 --- a/packages/coding-agent/test/agent-session-role-thinking.test.ts +++ b/packages/coding-agent/test/agent-session-role-thinking.test.ts @@ -48,6 +48,7 @@ describe("AgentSession role model thinking behavior", () => { initialModelId: string; initialThinkingLevel: Effort; modelRoles: Record; + runtimeApiKeys?: Record; }) { const model = getAnthropicModelOrThrow(options.initialModelId); const agent = new Agent({ @@ -62,6 +63,10 @@ describe("AgentSession role model thinking behavior", () => { const authStorage = await AuthStorage.create(path.join(tempDir.path(), "testauth.db")); authStorages.push(authStorage); authStorage.setRuntimeApiKey("anthropic", "test-key"); + const runtimeApiKeys = options.runtimeApiKeys ?? {}; + for (const provider in runtimeApiKeys) { + authStorage.setRuntimeApiKey(provider, runtimeApiKeys[provider]); + } const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir.path(), "models.yml")); sessionSettings = Settings.isolated(); @@ -617,6 +622,34 @@ describe("AgentSession role model thinking behavior", () => { expect(session.autoResolvedThinkingLevel()).toBeUndefined(); }); + it("applies matching role thinking to temporary model picks", async () => { + const defaultModel = getAnthropicModelOrThrow("claude-sonnet-4-5"); + const temporaryModel = getBundledModel("google-antigravity", "gemini-3.5-flash"); + if (!temporaryModel) throw new Error("Expected google-antigravity model gemini-3.5-flash to exist"); + + await createSession({ + initialModelId: defaultModel.id, + initialThinkingLevel: Effort.Low, + modelRoles: { + smol: `${temporaryModel.provider}/${temporaryModel.id}:high`, + }, + runtimeApiKeys: { + [temporaryModel.provider]: "test-key", + }, + }); + + const roleResolved = session.resolveRoleModelWithThinking("smol"); + expect(roleResolved.model?.id).toBe(temporaryModel.id); + expect(roleResolved.thinkingLevel).toBe(Effort.High); + + const roleThinkingLevel = session.resolveTemporaryModelThinkingLevel(temporaryModel); + await session.setModelTemporary(temporaryModel, roleThinkingLevel); + + expect(session.model?.provider).toBe(temporaryModel.provider); + expect(session.model?.id).toBe(temporaryModel.id); + expect(session.thinkingLevel).toBe(Effort.High); + }); + it("ignores a stale recorded role and cycles from the active model", async () => { const defaultModel = getAnthropicModelOrThrow("claude-sonnet-4-5"); const slowModel = getAnthropicModelOrThrow("claude-sonnet-4-6");