Merge PR #5297: fix(coding-agent): respect role thinking for temporary model picks (@roboomp)

# Conflicts:
#	packages/coding-agent/src/modes/controllers/selector-controller.ts
This commit is contained in:
can1357
2026-07-14 18:47:28 +02:00
4 changed files with 62 additions and 2 deletions
+1
View File
@@ -94,6 +94,7 @@
- Fixed tab reuse issues where hung navigation or unhandled modals would cause initialization to stall and trigger a force-kill
- 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
@@ -634,7 +634,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";
@@ -777,6 +778,7 @@ export class SelectorController {
this.ctx.showError(error instanceof Error ? error.message : String(error));
}
},
onLoginRequest: providerId => {
done();
void this.#loginThenReopenModelHub(providerId);
@@ -187,7 +187,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";
@@ -7788,6 +7788,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<PromptTemplate> {
return this.#promptTemplates;
}
@@ -48,6 +48,7 @@ describe("AgentSession role model thinking behavior", () => {
initialModelId: string;
initialThinkingLevel: Effort;
modelRoles: Record<string, string>;
runtimeApiKeys?: Record<string, string>;
}) {
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");