diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index a74f11d5d..b6c3ca456 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -30,6 +30,7 @@ - Fixed inconsistent history rendering when toggling the display setting for compacted items - Fixed discovered plugin `.mcp.json` stdio servers launching relative `command`/`cwd` values against the session cwd instead of the plugin's config directory, which broke bundled ChatGPT/Codex plugins such as Computer Use (`ENOENT` spawning `./…` from an unrelated cwd). Relative `cwd` and path-like `command` (`./`, `../`) discovered by the `claude-plugins`/`omp-plugins` providers now resolve against the `.mcp.json` directory; bare executables like `npx` are left untouched. ([#5330](https://github.com/can1357/oh-my-pi/issues/5330)) +- Fixed Model Hub DEFAULT role assignments with `auto` retaining a stale concrete reasoning suffix, which restored the old level after restart ([#5326](https://github.com/can1357/oh-my-pi/issues/5326)) - Fixed configured `retry.fallbackChains` never engaging on non-retryable provider errors (e.g. "Cloud Code Assist API returned an empty response"): a hard error on a model covered by a fallback chain now switches to the next candidate instead of failing the turn, while still never backoff-retrying the failing model itself - Fixed transcript rebuilds (compaction, `/compact`, and toggling history display) repainting content below stale scrollback when collapsing history; rebuilds now correctly clear the scrollback buffer when history is collapsed - Improved auto-compaction to automatically drop images and elide content when context is tight, and added persistent warning badges to the compaction divider when manual intervention is required diff --git a/packages/coding-agent/src/modes/controllers/selector-controller.ts b/packages/coding-agent/src/modes/controllers/selector-controller.ts index e516c67f0..9cb4c5717 100644 --- a/packages/coding-agent/src/modes/controllers/selector-controller.ts +++ b/packages/coding-agent/src/modes/controllers/selector-controller.ts @@ -719,7 +719,7 @@ export class SelectorController { if (role === "default") { const { switched } = await this.ctx.session.setModel(model, role, { selector, - thinkingLevel: concreteThinking, + thinkingLevel: isAuto ? ThinkingLevel.Inherit : concreteThinking, persist: true, currentContextTokens, }); diff --git a/packages/coding-agent/test/selector-settings-side-effects.test.ts b/packages/coding-agent/test/selector-settings-side-effects.test.ts index 15c9f3531..b6f80ed11 100644 --- a/packages/coding-agent/test/selector-settings-side-effects.test.ts +++ b/packages/coding-agent/test/selector-settings-side-effects.test.ts @@ -1,11 +1,15 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test"; import { stripVTControlCharacters } from "node:util"; +import { ThinkingLevel } from "@oh-my-pi/pi-agent-core"; import { buildModel } from "@oh-my-pi/pi-catalog/build"; +import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking"; +import { getBundledModel } from "@oh-my-pi/pi-catalog/models"; import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import { SelectorController } from "@oh-my-pi/pi-coding-agent/modes/controllers/selector-controller"; import { getThemeByName, setThemeInstance } from "@oh-my-pi/pi-coding-agent/modes/theme/theme"; import type { InteractiveModeContext } from "@oh-my-pi/pi-coding-agent/modes/types"; import type { ResolvedRoleModel } from "@oh-my-pi/pi-coding-agent/session/agent-session"; +import { AUTO_THINKING } from "@oh-my-pi/pi-coding-agent/thinking"; import { beginSettingsTest, restoreSettingsTestState, type SettingsTestState } from "./helpers/settings-test-state"; let settingsState: SettingsTestState | undefined; @@ -57,6 +61,98 @@ describe("selector setting side effects", () => { expect(requestRender).toHaveBeenCalledTimes(1); }); + it("clears stale default role thinking when auto is selected", async () => { + const testTheme = await getThemeByName("dark"); + if (!testTheme) throw new Error("Failed to load dark theme for model selector test"); + setThemeInstance(testTheme); + + const previousModel = getBundledModel("openai", "gpt-5.5"); + const nextModel = getBundledModel("openai", "gpt-5.6"); + if (!previousModel || !nextModel) throw new Error("Expected bundled OpenAI models for selector test"); + + const settings = Settings.isolated({ + defaultThinkingLevel: ThinkingLevel.High, + modelRoles: { default: `${previousModel.provider}/${previousModel.id}:high` }, + }); + const setModel = vi.fn(async () => ({ switched: true })); + const autoApplied = Promise.withResolvers(); + const setThinkingLevel = vi.fn((level: ThinkingLevel | typeof AUTO_THINKING, persist: boolean) => { + if (level === AUTO_THINKING && persist) { + settings.set("defaultThinkingLevel", level); + autoApplied.resolve(); + } + }); + let captured: unknown; + const controller = new SelectorController({ + ui: { + requestRender: vi.fn(), + setFocus: vi.fn(), + showOverlay: vi.fn((component: unknown) => { + captured = component; + return { hide: vi.fn() }; + }), + terminal: { rows: 40 }, + }, + editorContainer: { clear: vi.fn(), addChild: vi.fn(), children: [] }, + editor: {}, + settings, + session: { + model: nextModel, + modelRegistry: { + getAll: () => [previousModel, nextModel], + getAvailable: () => [previousModel, nextModel], + getError: () => undefined, + refresh: async () => {}, + refreshProvider: async () => {}, + getDiscoverableProviders: () => [], + getProviderDiscoveryState: () => undefined, + authStorage: { hasAuth: () => false }, + }, + scopedModels: [{ model: nextModel }], + getContextUsage: () => undefined, + setModel, + setThinkingLevel, + }, + statusLine: { invalidate: vi.fn() }, + updateEditorBorderColor: vi.fn(), + keybindings: { getKeys: () => [] }, + showStatus: vi.fn(), + showError: vi.fn(), + } as unknown as InteractiveModeContext); + + controller.showModelSelector(); + const hub = captured as + | { handleInput(data: string): void; render(width: number): string[]; dispose(): void } + | undefined; + if (!hub) throw new Error("Expected model hub overlay to be shown"); + try { + hub.handleInput("\x1b[A"); // All models → Roles. + hub.handleInput("\n"); // Enter the role rows. + hub.handleInput("\n"); // Assign DEFAULT. + hub.handleInput("\n"); // Pick the scoped replacement model. + + const levels = [ThinkingLevel.Inherit, ThinkingLevel.Off, AUTO_THINKING, ...getSupportedEfforts(nextModel)]; + const highIndex = levels.indexOf(ThinkingLevel.High); + const autoIndex = levels.indexOf(AUTO_THINKING); + if (highIndex < autoIndex) throw new Error("Expected auto before high in the thinking strip"); + for (let i = autoIndex; i < highIndex; i++) hub.handleInput("\x1b[D"); + hub.handleInput("\n"); + await autoApplied.promise; + + expect(setModel).toHaveBeenLastCalledWith( + nextModel, + "default", + expect.objectContaining({ + thinkingLevel: ThinkingLevel.Inherit, + persist: true, + }), + ); + expect(setThinkingLevel).toHaveBeenLastCalledWith(AUTO_THINKING, true); + } finally { + hub.dispose(); + } + }); + it("replaces malformed default retry fallback chains from the model selector action", async () => { const testTheme = await getThemeByName("dark"); if (!testTheme) throw new Error("Failed to load dark theme for model selector test");