diff --git a/src/browser/contexts/ThinkingContext.test.tsx b/src/browser/contexts/ThinkingContext.test.tsx index 6888be53f04..7c6761ec85e 100644 --- a/src/browser/contexts/ThinkingContext.test.tsx +++ b/src/browser/contexts/ThinkingContext.test.tsx @@ -33,6 +33,11 @@ import { } from "@/browser/testUtils"; import type { UserPreferences } from "@/common/config/schemas/userPreferences"; import { getAppConfigStore, getUserPreferences } from "@/browser/stores/AppConfigStore"; +import { + markAiSelectionIntent, + resetAiSelectionIntentForTests, + setWorkspaceAiMetadata, +} from "@/browser/utils/aiSelectionIntent"; let currentClientMock: TestApiOverrides = {}; let metadataMap = new Map(); @@ -148,6 +153,7 @@ function createWorkspaceMetadata( function setWorkspaceMetadata(metadata: FrontendWorkspaceMetadata) { metadataMap = new Map([[metadata.id, metadata]]); + setWorkspaceAiMetadata(metadata.id, metadata); } function createEmptyAsyncIterable(): AsyncIterable { @@ -168,12 +174,12 @@ function applyWorkspaceStorageOverrides(props: { modelOverride?: string | null; thinkingOverride?: "off" | null; }) { - if (props.modelOverride !== undefined) { - if (props.modelOverride == null) { - window.localStorage.removeItem(getModelKey(props.workspaceId)); - } else { - updatePersistedState(getModelKey(props.workspaceId), props.modelOverride); - } + if (props.modelOverride === null) { + window.localStorage.removeItem(getModelKey(props.workspaceId)); + } + if (props.modelOverride != null) { + updatePersistedState(getModelKey(props.workspaceId), props.modelOverride); + markAiSelectionIntent(props.workspaceId, "model", props.modelOverride); } if (props.thinkingOverride !== undefined) { @@ -315,6 +321,7 @@ describe("ThinkingContext", () => { defaultModel: undefined, }); metadataMap = new Map(); + resetAiSelectionIntentForTests(); currentClientMock = {}; }); @@ -563,6 +570,7 @@ describe("ThinkingContext", () => { const workspaceId = "ws-1"; updatePersistedState(getModelKey(workspaceId), "openai:gpt-5.2"); + markAiSelectionIntent(workspaceId, "model", "openai:gpt-5.2"); updatePersistedState(getThinkingLevelKey(workspaceId), "high"); let unmounts = 0; @@ -590,6 +598,7 @@ describe("ThinkingContext", () => { act(() => { updatePersistedState(getModelKey(workspaceId), "anthropic:claude-3.5"); + markAiSelectionIntent(workspaceId, "model", "anthropic:claude-3.5"); }); // Thinking is workspace-scoped (not per-model), so switching models should not change it. @@ -603,6 +612,7 @@ describe("ThinkingContext", () => { const workspaceId = "ws-1"; updatePersistedState(getModelKey(workspaceId), "openai:gpt-5.2"); + markAiSelectionIntent(workspaceId, "model", "openai:gpt-5.2"); updatePersistedState(getThinkingLevelByModelKey("openai:gpt-5.2"), "low"); const view = renderWithAPI( @@ -623,6 +633,7 @@ describe("ThinkingContext", () => { // Switching models should not change the workspace-scoped value. act(() => { updatePersistedState(getModelKey(workspaceId), "anthropic:claude-3.5"); + markAiSelectionIntent(workspaceId, "model", "anthropic:claude-3.5"); }); await waitFor(() => { diff --git a/src/browser/features/Settings/Sections/TasksSection.ui.test.tsx b/src/browser/features/Settings/Sections/TasksSection.ui.test.tsx index 827682c9dc7..94016e4861f 100644 --- a/src/browser/features/Settings/Sections/TasksSection.ui.test.tsx +++ b/src/browser/features/Settings/Sections/TasksSection.ui.test.tsx @@ -22,6 +22,10 @@ import type { AgentDefinitionDescriptor } from "@/common/types/agentDefinition"; import { EXPERIMENT_IDS } from "@/common/constants/experiments"; import { getModelKey } from "@/common/constants/storage"; import { updatePersistedState } from "@/browser/hooks/usePersistedState"; +import { + markAiSelectionIntent, + resetAiSelectionIntentForTests, +} from "@/browser/utils/aiSelectionIntent"; import { getAppConfigStore } from "@/browser/stores/AppConfigStore"; import { getProvidersConfigStore } from "@/browser/stores/ProvidersConfigStore"; import { FALLBACK_AGENTS } from "./TasksSection.agents"; @@ -168,8 +172,10 @@ function renderTasksSection(options: RenderTasksSectionOptions = {}) { // Discovery only runs for a selected workspace's project. selectedWorkspaceMock = options.agents || options.workspaceModel ? { projectPath: "/proj", workspaceId: "ws-1" } : null; + resetAiSelectionIntentForTests(); if (options.workspaceModel) { updatePersistedState(getModelKey("ws-1"), options.workspaceModel); + markAiSelectionIntent("ws-1", "model", options.workspaceModel); } // Inject the per-test client through the real provider; mocking the API module leaks into diff --git a/src/browser/features/Tools/ProposePlanToolCall.test.tsx b/src/browser/features/Tools/ProposePlanToolCall.test.tsx index 2177781591a..e9d20d95851 100644 --- a/src/browser/features/Tools/ProposePlanToolCall.test.tsx +++ b/src/browser/features/Tools/ProposePlanToolCall.test.tsx @@ -23,6 +23,11 @@ import { getThinkingLevelKey, getWorkspaceAISettingsByAgentKey, } from "@/common/constants/storage"; +import { + resetAiSelectionIntentForTests, + setWorkspaceAiMetadata, +} from "@/browser/utils/aiSelectionIntent"; +import type { ThinkingLevel } from "@/common/types/thinking"; import { TooltipProvider } from "@/browser/components/Tooltip/Tooltip"; import { createTestApiClient, @@ -319,9 +324,18 @@ function renderCompletedPlan(props: Partial = {}) { }); } -function startInPlanMode(workspaceId = WORKSPACE_ID, model?: string, thinkingLevel?: string) { +function startInPlanMode( + workspaceId = WORKSPACE_ID, + model?: string, + thinkingLevel?: ThinkingLevel +) { window.localStorage.setItem(getAgentIdKey(workspaceId), JSON.stringify("plan")); if (model) updatePersistedState(getModelKey(workspaceId), model); + if (model) { + setWorkspaceAiMetadata(workspaceId, { + aiSettingsByAgent: { plan: { model, thinkingLevel: thinkingLevel ?? "off" } }, + }); + } if (thinkingLevel) updatePersistedState(getThinkingLevelKey(workspaceId), thinkingLevel); } @@ -370,6 +384,7 @@ describe("ProposePlanToolCall", () => { afterEach(async () => { resetTestExperiments(); + resetAiSelectionIntentForTests(); setTestAgentAiDefaults(undefined); cleanup(); await restoreProposePlanModuleMocks(); @@ -656,9 +671,15 @@ describe("ProposePlanToolCall", () => { startInPlanMode(WORKSPACE_ID, "anthropic:claude-sonnet-4-5", "high"); setTestAgentAiDefaults({}); - updatePersistedState(getWorkspaceAISettingsByAgentKey(WORKSPACE_ID), { - exec: { model: execWorkspaceModel, thinkingLevel: execWorkspaceThinking }, + const exec = { model: execWorkspaceModel, thinkingLevel: execWorkspaceThinking } as const; + setWorkspaceAiMetadata(WORKSPACE_ID, { + aiSettingsByAgent: { + plan: { model: "anthropic:claude-sonnet-4-5", thinkingLevel: "high" }, + exec, + }, }); + // Thinking still restores from the seeded per-agent cache. + updatePersistedState(getWorkspaceAISettingsByAgentKey(WORKSPACE_ID), { exec }); const sendMessageCalls: SendMessageArgs[] = []; mockApi = createMockApi({ sendMessage: recordSendMessage(sendMessageCalls) }); diff --git a/src/browser/utils/commands/sources.test.ts b/src/browser/utils/commands/sources.test.ts index f6e76294c91..1ef495e57a3 100644 --- a/src/browser/utils/commands/sources.test.ts +++ b/src/browser/utils/commands/sources.test.ts @@ -9,6 +9,10 @@ import type { FrontendWorkspaceMetadata } from "@/common/types/workspace"; import { DEFAULT_RUNTIME_CONFIG } from "@/common/constants/workspace"; import { GlobalWindow } from "happy-dom"; import { getModelKey } from "@/common/constants/storage"; +import { + markAiSelectionIntent, + resetAiSelectionIntentForTests, +} from "@/browser/utils/aiSelectionIntent"; import { getAppConfigStore, getUserPreferences } from "@/browser/stores/AppConfigStore"; import { normalizeUserPreferences } from "@/common/config/schemas/userPreferences"; import { applyMergePatch } from "@/common/utils/applyMergePatch"; @@ -1162,6 +1166,7 @@ test("goal set objective prompt blocks budgeted goals on unpriced selected model globalThis.window = testWindow as unknown as Window & typeof globalThis; globalThis.document = testWindow.document as unknown as Document; window.localStorage.setItem(getModelKey("w1"), JSON.stringify("custom:unpriced-model")); + markAiSelectionIntent("w1", "model", "custom:unpriced-model"); try { const getGoal = mock(() => Promise.resolve({ goal: null })); @@ -1187,6 +1192,7 @@ test("goal set objective prompt blocks budgeted goals on unpriced selected model expect(setGoal).not.toHaveBeenCalled(); } finally { + resetAiSelectionIntentForTests(); globalThis.window = originalWindow; globalThis.document = originalDocument; } diff --git a/src/browser/utils/messages/sendOptions.test.ts b/src/browser/utils/messages/sendOptions.test.ts index c307aab0df7..9c12a667377 100644 --- a/src/browser/utils/messages/sendOptions.test.ts +++ b/src/browser/utils/messages/sendOptions.test.ts @@ -11,6 +11,10 @@ import { getThinkingLevelByModelKey, getThinkingLevelKey, } from "@/common/constants/storage"; +import { + resetAiSelectionIntentForTests, + setWorkspaceAiMetadata, +} from "@/browser/utils/aiSelectionIntent"; import { WORKSPACE_DEFAULTS } from "@/constants/workspaceDefaults"; import { installDom } from "../../../../tests/ui/dom"; import { getSendOptionsFromStorage } from "./sendOptions"; @@ -28,6 +32,7 @@ describe("getSendOptionsFromStorage", () => { afterEach(() => { resetTestExperiments(); + resetAiSelectionIntentForTests(); getAppConfigStore().updateOptimistically({ userPreferences: undefined, defaultModel: undefined, @@ -70,11 +75,14 @@ describe("getSendOptionsFromStorage", () => { ); }); - test("preserves explicit gateway-scoped stored model preferences", () => { + test("preserves explicit gateway-scoped saved workspace models", () => { const workspaceId = "ws-1"; const rawModel = "mux-gateway:anthropic/claude-haiku-4-5"; window.localStorage.setItem(getModelKey(workspaceId), JSON.stringify(rawModel)); + setWorkspaceAiMetadata(workspaceId, { + aiSettingsByAgent: { exec: { model: rawModel, thinkingLevel: "off" } }, + }); const options = getSendOptionsFromStorage(workspaceId); diff --git a/src/browser/utils/modelChange.test.ts b/src/browser/utils/modelChange.test.ts index 3779e2ca220..aedf99c4090 100644 --- a/src/browser/utils/modelChange.test.ts +++ b/src/browser/utils/modelChange.test.ts @@ -3,6 +3,7 @@ import { restoreDomGlobals, saveDomGlobals } from "../../../tests/ui/domGlobals" import { GlobalWindow } from "happy-dom"; import { readPersistedState, updatePersistedState } from "@/browser/hooks/usePersistedState"; +import { markAiSelectionIntent } from "@/browser/utils/aiSelectionIntent"; import { consumeWorkspaceModelChange, recordAutoRoutingChoiceForAgent, @@ -70,6 +71,7 @@ describe("modelChange", () => { const otherModel = "anthropic:claude-sonnet-4-5"; setWorkspaceModelWithOrigin(workspaceId, model, "sync"); + markAiSelectionIntent(workspaceId, "model", model); // Simulate user selecting the already-active model. setWorkspaceModelWithOrigin(workspaceId, model, "user");