From 614918ec6554c345632ac68f7eced1304d30539a Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Thu, 8 Oct 2026 08:26:33 +0000 Subject: [PATCH] refactor: resolve the workspace reasoning mode from metadata and an unsent pick The composer, sends, palette and plan handoff read the reasoning mode through the resolver, and a reasoning pick stays in memory until the next send saves it. Agent switches, the plan handoff and workspace creation no longer write a reasoning mode, and the unused metadata and composer-model helpers go away. Seeding of the old key stays until a later commit deletes it. Stack-Layer: A2e --- src/browser/App.tsx | 20 ++--- .../WorkspaceModeAISync.tsx | 47 ++--------- src/browser/contexts/ThinkingContext.test.tsx | 26 +++--- src/browser/contexts/ThinkingContext.tsx | 68 ++++----------- .../ChatInput/useCreationWorkspace.ts | 18 +--- .../features/Tools/ProposePlanToolCall.tsx | 82 +++---------------- src/browser/stories/App.astraPro.stories.tsx | 3 - src/browser/utils/messages/sendOptions.ts | 14 +--- src/browser/utils/workspaceAiSettingsSync.ts | 29 ------- 9 files changed, 58 insertions(+), 249 deletions(-) diff --git a/src/browser/App.tsx b/src/browser/App.tsx index a8e5d51930..3b14755838 100644 --- a/src/browser/App.tsx +++ b/src/browser/App.tsx @@ -64,7 +64,6 @@ import { } from "@/common/utils/subProjects"; import { THINKING_LEVELS, - coerceOpenAIReasoningMode, type OpenAIReasoningMode, type ThinkingLevel, } from "@/common/types/thinking"; @@ -74,7 +73,6 @@ import { getAgentIdKey, getAgentsInitNudgeKey, getProjectScopeId, - getReasoningModeKey, getWorkspaceAISettingsByAgentKey, getWorkspaceLastReadKey, EXPANDED_PROJECTS_KEY, @@ -517,6 +515,11 @@ function AppInner() { workspaceId ? getAiSelectionForWorkspace(workspaceId).thinkingLevel : "off", [getAiSelectionForWorkspace] ); + const getReasoningModeForWorkspace = useCallback( + (workspaceId: string): OpenAIReasoningMode => + workspaceId ? getAiSelectionForWorkspace(workspaceId).reasoningMode : "standard", + [getAiSelectionForWorkspace] + ); // Pro mode is Responses-only; the palette command hides under chatCompletions // and on non-passthrough routes (mirroring the send path's header gating). @@ -531,18 +534,6 @@ function AppInner() { [routing] ); - const getReasoningModeForWorkspace = useCallback((workspaceId: string): OpenAIReasoningMode => { - if (!workspaceId) { - return "standard"; - } - const stored = readPersistedState( - getReasoningModeKey(workspaceId), - null - ); - // Coerce untrusted persisted values so corrupt entries self-heal to "standard". - return coerceOpenAIReasoningMode(stored) ?? "standard"; - }, []); - // Palette picks stay in memory until a user message sends them. const setThinkingLevelFromPalette = useCallback( (workspaceId: string, level: ThinkingLevel) => { @@ -613,7 +604,6 @@ function AppInner() { const model = getModelForWorkspace(workspaceId); const thinkingLevel = getThinkingLevelForWorkspace(workspaceId); - updatePersistedState(getReasoningModeKey(workspaceId), next); markAiSelectionIntent(workspaceId, "reasoningMode", next); type WorkspaceAISettingsByAgentCache = Partial< diff --git a/src/browser/components/WorkspaceModeAISync/WorkspaceModeAISync.tsx b/src/browser/components/WorkspaceModeAISync/WorkspaceModeAISync.tsx index b51af4bfc9..60b0283165 100644 --- a/src/browser/components/WorkspaceModeAISync/WorkspaceModeAISync.tsx +++ b/src/browser/components/WorkspaceModeAISync/WorkspaceModeAISync.tsx @@ -1,24 +1,20 @@ import { useEffect, useRef } from "react"; import { useAgent } from "@/browser/contexts/AgentContext"; -import { readPersistedState, updatePersistedState } from "@/browser/hooks/usePersistedState"; +import { readPersistedState } from "@/browser/hooks/usePersistedState"; import { useAgentAiDefaults } from "@/browser/stores/AppConfigStore"; import { getAutoRoutingChoiceByAgentKey, - getReasoningModeKey, getWorkspaceAISettingsByAgentKey, } from "@/common/constants/storage"; -import { getDefaultModel } from "@/browser/hooks/useModelsFromSettings"; import { applyAutoRoutingOutcome, recordWorkspaceModelChange } from "@/browser/utils/modelChange"; import { resolveAutoRoutingForAgent, - resolveWorkspaceAiSettingsForAgent, type AutoRoutingChoiceByAgent, type WorkspaceAISettingsCache, } from "@/browser/utils/workspaceModeAi"; import { getWorkspaceAiSelection } from "@/browser/utils/workspaceAiSettingsSync"; import { useExperimentValue } from "@/browser/hooks/useExperiments"; import { EXPERIMENT_IDS } from "@/common/constants/experiments"; -import type { OpenAIReasoningMode } from "@/common/types/thinking"; import { normalizeAgentId } from "@/common/utils/agentIds"; export function WorkspaceModeAISync(props: { workspaceId: string }): null { @@ -35,8 +31,6 @@ export function WorkspaceModeAISync(props: { workspaceId: string }): null { const prevWorkspaceIdRef = useRef(null); useEffect(() => { - const fallbackModel = getDefaultModel(); - const normalizedAgentId = normalizeAgentId(agentId, "exec"); const previousAgentId = prevAgentIdRef.current; @@ -49,44 +43,17 @@ export function WorkspaceModeAISync(props: { workspaceId: string }): null { prevAgentIdRef.current = normalizedAgentId; prevWorkspaceIdRef.current = workspaceId; - // Read at call time rather than subscribing: this cache only feeds explicit agent - // switches, yet every model/thinking/pro-mode change rewrites it, so a subscription - // would re-run this effect and re-apply the mode default over the user's own pick. - const workspaceByAgent = readPersistedState( - getWorkspaceAISettingsByAgentKey(workspaceId), - {} - ); - - const reasoningKey = getReasoningModeKey(workspaceId); - const existingReasoning = readPersistedState(reasoningKey, "standard"); - const agentBaseById = new Map(agents.map((agent) => [agent.id, agent.base])); - const selection = getWorkspaceAiSelection(workspaceId, normalizedAgentId, agentBaseById); if (isExplicitAgentSwitch) { // Each agent resolves its own model, so the switch itself is the explicit model change. recordWorkspaceModelChange( workspaceId, - selection.model, + getWorkspaceAiSelection(workspaceId, normalizedAgentId, agentBaseById).model, "agent", getWorkspaceAiSelection(workspaceId, previousAgentId, agentBaseById).model ); } - // The resolver owns model and thinking; only reasoning comes from here. - const { resolvedReasoningMode } = resolveWorkspaceAiSettingsForAgent({ - agentId: normalizedAgentId, - agentAiDefaults, - // Keep deterministic handoff behavior: background sync should trust the - // currently active workspace settings, but explicit mode switches should - // restore the selected agent's per-workspace override (if any). - workspaceByAgent, - useWorkspaceByAgentFallback: isExplicitAgentSwitch, - fallbackModel, - existingModel: selection.model, - existingThinking: selection.thinkingLevel, - existingReasoningMode: existingReasoning, - agentBaseById, - }); const autoRoutingOutcome = resolveAutoRoutingForAgent({ agentId: normalizedAgentId, agentAiDefaults, @@ -97,13 +64,11 @@ export function WorkspaceModeAISync(props: { workspaceId: string }): null { getAutoRoutingChoiceByAgentKey(workspaceId), {} ), - workspaceByAgent, + workspaceByAgent: readPersistedState( + getWorkspaceAISettingsByAgentKey(workspaceId), + {} + ), }); - - if (existingReasoning !== resolvedReasoningMode) { - updatePersistedState(reasoningKey, resolvedReasoningMode); - } - applyAutoRoutingOutcome(workspaceId, autoRoutingOutcome); }, [agentAiDefaults, agentId, agents, autoRoutingEnabled, workspaceId]); diff --git a/src/browser/contexts/ThinkingContext.test.tsx b/src/browser/contexts/ThinkingContext.test.tsx index 9a226410ef..56c4594b29 100644 --- a/src/browser/contexts/ThinkingContext.test.tsx +++ b/src/browser/contexts/ThinkingContext.test.tsx @@ -12,11 +12,10 @@ import { } from "@/browser/contexts/WorkspaceContext"; import { useThinkingLevel } from "@/browser/hooks/useThinkingLevel"; import type { FrontendWorkspaceMetadata } from "@/common/types/workspace"; -import type { ThinkingLevel } from "@/common/types/thinking"; +import type { OpenAIReasoningMode, ThinkingLevel } from "@/common/types/thinking"; import { getAutoThinkingLevelKey, getProjectScopeId, - getReasoningModeKey, getWorkspaceAISettingsByAgentKey, } from "@/common/constants/storage"; import { useReasoningMode } from "@/browser/hooks/useReasoningMode"; @@ -354,7 +353,7 @@ describe("ThinkingContext", () => { test("setting thinking uses metadata model before global default", async () => { const workspaceId = "ws-set-thinking-metadata-model"; - updatePersistedState(getReasoningModeKey(workspaceId), "pro"); + markAiSelectionIntent(workspaceId, "reasoningMode", "pro"); const updateAgentAISettings = mock< (args: WorkspaceUpdateAgentAISettingsArgs) => Promise >(() => @@ -480,21 +479,24 @@ describe("ThinkingContext", () => { expect(updateAgentAISettings).not.toHaveBeenCalled(); }); - test("self-heals corrupt persisted reasoningMode to standard but keeps valid pro", async () => { - // Corrupt persisted values (e.g. from a future downgrade) must coerce to + test("self-heals a corrupt saved reasoningMode to standard but keeps valid pro", async () => { + // Corrupt saved values (e.g. from a future downgrade) must coerce to // "standard" instead of flowing into SendMessageOptionsSchema and bricking sends. const cases = [ - { workspaceId: "ws-reasoning-corrupt", persisted: "ultra", expected: "standard" }, - { workspaceId: "ws-reasoning-valid", persisted: "pro", expected: "pro" }, + { workspaceId: "ws-reasoning-corrupt", saved: "ultra", expected: "standard" }, + { workspaceId: "ws-reasoning-valid", saved: "pro", expected: "pro" }, ]; for (const testCase of cases) { - const metadata = createWorkspaceMetadata({ id: testCase.workspaceId }); + const metadata = createWorkspaceMetadata({ + id: testCase.workspaceId, + aiSettings: { + model: "openai:gpt-5.5", + thinkingLevel: "high", + reasoningMode: testCase.saved as OpenAIReasoningMode, + }, + }); setWorkspaceMetadata(metadata); - window.localStorage.setItem( - getReasoningModeKey(testCase.workspaceId), - JSON.stringify(testCase.persisted) - ); const view = renderWithWorkspaceMetadata({ workspaceId: testCase.workspaceId, diff --git a/src/browser/contexts/ThinkingContext.tsx b/src/browser/contexts/ThinkingContext.tsx index a54b33f2bf..a09d03942e 100644 --- a/src/browser/contexts/ThinkingContext.tsx +++ b/src/browser/contexts/ThinkingContext.tsx @@ -6,11 +6,7 @@ import { type OpenAIReasoningMode, type ThinkingLevel, } from "@/common/types/thinking"; -import { - readPersistedState, - updatePersistedState, - usePersistedState, -} from "@/browser/hooks/usePersistedState"; +import { updatePersistedState, usePersistedState } from "@/browser/hooks/usePersistedState"; import { getProjectScopeId, getReasoningModeKey, @@ -24,12 +20,8 @@ import { useMinThinkingLevels } from "@/browser/hooks/useMinThinkingLevels"; import { useProvidersConfig } from "@/browser/hooks/useProvidersConfig"; import { useAPI } from "@/browser/contexts/API"; import { requestActiveTurnThinkingLevel } from "@/browser/utils/activeTurnThinking"; -import { - getWorkspaceAiSettingsFromMetadata, - useWorkspaceAiSelection, -} from "@/browser/utils/workspaceAiSettingsSync"; +import { useWorkspaceAiSelection } from "@/browser/utils/workspaceAiSettingsSync"; import { useOptionalAgent } from "@/browser/contexts/AgentContext"; -import { useOptionalWorkspaceContext } from "@/browser/contexts/WorkspaceContext"; import { KEYBINDS, matchesKeybind } from "@/browser/utils/ui/keybinds"; import { WORKSPACE_DEFAULTS } from "@/constants/workspaceDefaults"; import { markAiSelectionIntent } from "@/browser/utils/aiSelectionIntent"; @@ -62,7 +54,6 @@ function getScopeId(workspaceId: string | undefined, projectPath: string | undef export const ThinkingProvider: React.FC = (props) => { const { api } = useAPI(); - const workspaceContext = useOptionalWorkspaceContext(); const { getMinimum } = useMinThinkingLevels(); // Resolve mapped aliases so keybind stepping walks the target model's ladder. const { config: providersConfig } = useProvidersConfig(); @@ -79,30 +70,22 @@ export const ThinkingProvider: React.FC = (props) => { ); const scopedModel = useScopedAiDefault(defaultsScopeId, "model"); const scopedThinkingLevel = useScopedAiDefault(defaultsScopeId, "thinkingLevel"); + const [scopedReasoningMode, setScopedReasoningMode] = + usePersistedState(getReasoningModeKey(defaultsScopeId), null, { + listener: true, + }); + // normalizeSelectedModel (not normalizeToCanonical): explicit gateway identities must // survive; thinking policy lookups resolve gateway-scoped strings themselves. const model = workspaceId != null ? selection.model : normalizeSelectedModel(scopedModel ?? defaultModel); const thinkingLevel = workspaceId != null ? selection.thinkingLevel : (scopedThinkingLevel ?? THINKING_LEVEL_OFF); - const metadataAgentId = readScopedAiDefault(scopeId, "agentId") ?? WORKSPACE_DEFAULTS.agentId; - const metadataSettings = getWorkspaceAiSettingsFromMetadata( - props.workspaceId ? workspaceContext?.workspaceMetadata.get(props.workspaceId) : undefined, - metadataAgentId - ); - - // Workspace-scoped OpenAI pro reasoning mode. Null = no explicit user choice yet; - // absent everywhere means "standard" (the API default). - const reasoningKey = getReasoningModeKey(scopeId); - const [persistedReasoningMode, setReasoningModeInternal] = - usePersistedState(reasoningKey, null, { listener: true }); - // Coerce untrusted persisted values (corrupt entries or a future downgrade) so - // bad state self-heals to "standard" instead of failing SendMessageOptionsSchema - // validation and bricking sends until storage is cleared. + // Coerce untrusted persisted values so bad state self-heals to "standard". const reasoningMode = - coerceOpenAIReasoningMode(persistedReasoningMode) ?? - coerceOpenAIReasoningMode(metadataSettings.reasoningMode) ?? - "standard"; + workspaceId != null + ? selection.reasoningMode + : (coerceOpenAIReasoningMode(scopedReasoningMode) ?? "standard"); // Keep picker choices local until a user message sends the full settings. const persistAgentAiSettings = useCallback( @@ -145,18 +128,6 @@ export const ThinkingProvider: React.FC = (props) => { [props.workspaceId, scopeId] ); - // Read the sibling setting at call time (not from the render closure) so - // rapid interleaved updates cannot persist a stale counterpart value. - const getCurrentReasoningMode = useCallback( - (): OpenAIReasoningMode => - coerceOpenAIReasoningMode( - readPersistedState(reasoningKey, null) - ) ?? - coerceOpenAIReasoningMode(metadataSettings.reasoningMode) ?? - "standard", - [metadataSettings.reasoningMode, reasoningKey] - ); - // A workspace pick stays in memory until a user message sends it. const setThinkingLevel = useCallback( (level: ThinkingLevel) => { @@ -169,28 +140,25 @@ export const ThinkingProvider: React.FC = (props) => { // A concrete pick (selector row or keybind step) leaves thinking Auto, // mirroring setWorkspaceModelWithOrigin for the model dimension. setAutoRoutingChoice(scopeId, "thinkingLevel", false); - persistAgentAiSettings({ - model, - thinkingLevel: level, - reasoningMode: getCurrentReasoningMode(), - }); + persistAgentAiSettings({ model, thinkingLevel: level, reasoningMode }); // Mid-turn change: also request the new level for the active turn's next model step. if (workspaceId != null) { requestActiveTurnThinkingLevel(api, workspaceId, level); } }, - [api, getCurrentReasoningMode, model, persistAgentAiSettings, scopeId, workspaceId] + [api, model, persistAgentAiSettings, reasoningMode, scopeId, workspaceId] ); const setReasoningMode = useCallback( (mode: OpenAIReasoningMode) => { - setReasoningModeInternal(mode); - if (props.workspaceId) { - markAiSelectionIntent(props.workspaceId, "reasoningMode", mode); + if (workspaceId != null) { + markAiSelectionIntent(workspaceId, "reasoningMode", mode); + } else { + setScopedReasoningMode(mode); } persistAgentAiSettings({ model, thinkingLevel, reasoningMode: mode }); }, - [model, persistAgentAiSettings, props.workspaceId, setReasoningModeInternal, thinkingLevel] + [model, persistAgentAiSettings, setScopedReasoningMode, thinkingLevel, workspaceId] ); // Global keybinds for adjusting the thinking level. diff --git a/src/browser/features/ChatInput/useCreationWorkspace.ts b/src/browser/features/ChatInput/useCreationWorkspace.ts index e45bf5bab3..146181cb27 100644 --- a/src/browser/features/ChatInput/useCreationWorkspace.ts +++ b/src/browser/features/ChatInput/useCreationWorkspace.ts @@ -9,11 +9,7 @@ import type { } from "@/common/types/runtime"; import type { RuntimeChoice } from "@/browser/utils/runtimeUi"; import { buildRuntimeConfig, RUNTIME_MODE } from "@/common/types/runtime"; -import { - coerceOpenAIReasoningMode, - type OpenAIReasoningMode, - type ThinkingLevel, -} from "@/common/types/thinking"; +import type { OpenAIReasoningMode, ThinkingLevel } from "@/common/types/thinking"; import { useDraftWorkspaceSettings } from "@/browser/hooks/useDraftWorkspaceSettings"; import { getAutoRoutingKey, @@ -25,7 +21,6 @@ import { readPersistedState, updatePersistedState } from "@/browser/hooks/usePer import { getSendOptionsFromStorage } from "@/browser/utils/messages/sendOptions"; import { getAgentIdKey, - getReasoningModeKey, getWorkspaceAISettingsByAgentKey, getPendingScopeId, getDraftScopeId, @@ -178,16 +173,6 @@ function syncCreationPreferences( recordAutoRoutingChoiceForAgent(workspaceId, effectiveAgentId, routingChoice); } - // Carry the creation-time pro reasoning-mode choice into the new workspace's - // scope so it survives the project→workspace transition. - // Coerced so a corrupt persisted value is dropped instead of copied forward. - const projectReasoningMode = coerceOpenAIReasoningMode( - readPersistedState(getReasoningModeKey(projectScopeId), null) - ); - if (projectReasoningMode != null) { - updatePersistedState(getReasoningModeKey(workspaceId), projectReasoningMode); - } - const projectModel = projectDefaults?.model; if (projectModel) { const effectiveThinking: ThinkingLevel = projectDefaults?.thinkingLevel ?? "off"; @@ -207,7 +192,6 @@ function syncCreationPreferences( [effectiveAgentId]: { model: projectModel, thinkingLevel: effectiveThinking, - ...(projectReasoningMode != null ? { reasoningMode: projectReasoningMode } : {}), }, }; }, diff --git a/src/browser/features/Tools/ProposePlanToolCall.tsx b/src/browser/features/Tools/ProposePlanToolCall.tsx index ba66b66849..1825598497 100644 --- a/src/browser/features/Tools/ProposePlanToolCall.tsx +++ b/src/browser/features/Tools/ProposePlanToolCall.tsx @@ -41,17 +41,10 @@ import { useOpenInEditor } from "@/browser/hooks/useOpenInEditor"; import { useOptionalWorkspaceContext } from "@/browser/contexts/WorkspaceContext"; import { usePopoverError } from "@/browser/hooks/usePopoverError"; import { PopoverError } from "@/browser/components/PopoverError/PopoverError"; -import { - getAgentIdKey, - getReasoningModeKey, - getAutoRoutingChoiceByAgentKey, - getWorkspaceAISettingsByAgentKey, -} from "@/common/constants/storage"; -import { getDefaultModel } from "@/browser/hooks/useModelsFromSettings"; +import { getAgentIdKey, getAutoRoutingChoiceByAgentKey } from "@/common/constants/storage"; import { readPersistedState, updatePersistedState } from "@/browser/hooks/usePersistedState"; import { getSendOptionsFromStorage } from "@/browser/utils/messages/sendOptions"; import { applyAutoRoutingOutcome } from "@/browser/utils/modelChange"; -import { getWorkspaceAiSelection } from "@/browser/utils/workspaceAiSettingsSync"; import { useExperimentValue } from "@/browser/hooks/useExperiments"; import { EXPERIMENT_IDS } from "@/common/constants/experiments"; import { @@ -67,15 +60,11 @@ import { import { TRANSCRIPT_NOT_CAUGHT_UP_MESSAGE } from "@/constants/transcriptBarrier"; import { resolveAutoRoutingForAgent, - resolveWorkspaceAiSettingsForAgent, type AutoRoutingChoiceByAgent, type AutoRoutingOutcome, - type WorkspaceAISettingsCache, } from "@/browser/utils/workspaceModeAi"; -import type { AgentAiDefaults } from "@/common/types/agentAiDefaults"; import type { ReviewActionCallbacks } from "../Shared/InlineReviewNote"; import { isPlanFilePath, normalizePlanFilePath } from "@/common/types/review"; -import type { OpenAIReasoningMode } from "@/common/types/thinking"; import { Check, Clipboard, @@ -163,13 +152,6 @@ function isTypingInNonEmptyField(element: Element | null): boolean { return (element.textContent ?? "").trim() !== ""; } -/** Resolved (not yet persisted) AI settings for a plan action's target agent. */ -interface TargetAgentSettings { - resolvedReasoningMode: OpenAIReasoningMode; - existingReasoning: OpenAIReasoningMode; - autoRouting: AutoRoutingOutcome; -} - interface ProposePlanToolCallProps { args: Record; result?: unknown; @@ -519,49 +501,19 @@ export const ProposePlanToolCall: React.FC = (props) = } }; - // User request: propose_plan primary actions send immediately after agent switch. - // Resolve and persist reasoning synchronously here so the follow-up message - // uses the target agent defaults instead of stale planning-mode preferences. /** - * The AI settings a plan action switches the workspace to. Pure read: nothing is persisted - * until `persistTargetAgentSettings` runs, so the handlers can await their preconditions + * The target agent's auto-routing outcome. Pure read: nothing is persisted until + * `persistTargetAgentSettings` runs, so the handlers can await their preconditions * (preference persistence, the transcript barrier) before any side effect. */ - const resolveTargetAgentSettings = (args: { + const resolveTargetAgentRouting = (args: { workspaceId: string; targetAgentId: "auto" | "exec"; - }): TargetAgentSettings => { - const reasoningKey = getReasoningModeKey(args.workspaceId); - const fallbackModel = getDefaultModel(); - - const existingReasoning = readPersistedState(reasoningKey, "standard"); - const agentAiDefaults: AgentAiDefaults = - getAppConfigStore().getSnapshot()?.agentAiDefaults ?? {}; - const workspaceByAgent = readPersistedState( - getWorkspaceAISettingsByAgentKey(args.workspaceId), - {} - ); - - const agentBaseById = new Map(agents.map((agent) => [agent.id, agent.base])); - const target = getWorkspaceAiSelection(args.workspaceId, args.targetAgentId, agentBaseById); - // The resolver owns model and thinking; only reasoning comes from here. - const { resolvedReasoningMode } = resolveWorkspaceAiSettingsForAgent({ + }): AutoRoutingOutcome => + resolveAutoRoutingForAgent({ agentId: args.targetAgentId, - agentAiDefaults, - // Propose-plan actions are explicit mode switches; honor any per-agent - // workspace override before inheriting the previously active plan settings. - workspaceByAgent, - useWorkspaceByAgentFallback: true, - fallbackModel, - existingModel: target.model, - existingThinking: target.thinkingLevel, - existingReasoningMode: existingReasoning, - agentBaseById, - }); - const autoRouting = resolveAutoRoutingForAgent({ - agentId: args.targetAgentId, - agentAiDefaults, - agentBaseById, + agentAiDefaults: getAppConfigStore().getSnapshot()?.agentAiDefaults ?? {}, + agentBaseById: new Map(agents.map((agent) => [agent.id, agent.base])), explicitSwitch: true, experimentEnabled: autoRoutingEnabled, routingChoices: readPersistedState( @@ -570,23 +522,15 @@ export const ProposePlanToolCall: React.FC = (props) = ), }); - return { resolvedReasoningMode, existingReasoning, autoRouting }; - }; - - /** Switch the workspace to the resolved agent settings (synchronous, right before the send). */ + /** Switch the workspace to the target agent (synchronous, right before the send). */ const persistTargetAgentSettings = (args: { workspaceId: string; targetAgentId: "auto" | "exec"; - settings: TargetAgentSettings; + autoRouting: AutoRoutingOutcome; }): void => { - const { settings } = args; updatePersistedState(getAgentIdKey(args.workspaceId), args.targetAgentId); - // Persist before getSendOptionsFromStorage reads the key for the follow-up send. - if (settings.existingReasoning !== settings.resolvedReasoningMode) { - updatePersistedState(getReasoningModeKey(args.workspaceId), settings.resolvedReasoningMode); - } // Persist routing for later composer sends; the immediate action below disables routing. - applyAutoRoutingOutcome(args.workspaceId, settings.autoRouting); + applyAutoRoutingOutcome(args.workspaceId, args.autoRouting); }; /** @@ -614,7 +558,7 @@ export const ProposePlanToolCall: React.FC = (props) = } if (shouldReplaceChatHistory && !canReplaceChatHistory) return null; - const settings = resolveTargetAgentSettings({ + const autoRouting = resolveTargetAgentRouting({ workspaceId, targetAgentId: args.targetAgentId, }); @@ -631,7 +575,7 @@ export const ProposePlanToolCall: React.FC = (props) = errorContext: args.replacementErrorContext, }); } - persistTargetAgentSettings({ workspaceId, targetAgentId: args.targetAgentId, settings }); + persistTargetAgentSettings({ workspaceId, targetAgentId: args.targetAgentId, autoRouting }); // The target agent's resolved model and thinking level (its saved or configured settings). const sendMessageOptions = getSendOptionsFromStorage(workspaceId); const sendResult = await api.workspace.sendMessage({ diff --git a/src/browser/stories/App.astraPro.stories.tsx b/src/browser/stories/App.astraPro.stories.tsx index 21ea543777..78a06f7006 100644 --- a/src/browser/stories/App.astraPro.stories.tsx +++ b/src/browser/stories/App.astraPro.stories.tsx @@ -3,8 +3,6 @@ import { appMeta, AppWithMocks, type AppStory } from "./meta.js"; import { setupSimpleChatStory } from "./helpers/chatSetup"; import { collapseLeftSidebar } from "./helpers/uiState"; import { blurActiveElement, waitForChatInputAutofocusDone } from "./storyPlayHelpers"; -import { readPersistedState } from "@/browser/hooks/usePersistedState"; -import { getReasoningModeKey } from "@/common/constants/storage"; export default { ...appMeta, title: "App/Astra Pro" }; @@ -60,7 +58,6 @@ export const CoderGateway: AppStory = { await expect(pro).toHaveAttribute("aria-pressed", "false"); await userEvent.click(pro); await expect(pro).toHaveAttribute("aria-pressed", "true"); - await expect(readPersistedState(getReasoningModeKey(workspaceId), "standard")).toBe("pro"); await expect(trigger).toHaveAccessibleName("Thinking: high, pro mode"); // Keyboard activation must also disable Pro without changing reasoning effort. pro.focus(); diff --git a/src/browser/utils/messages/sendOptions.ts b/src/browser/utils/messages/sendOptions.ts index ee43e07268..b524baec2a 100644 --- a/src/browser/utils/messages/sendOptions.ts +++ b/src/browser/utils/messages/sendOptions.ts @@ -1,13 +1,11 @@ import { getAutoModelRoutingKey, getAutoThinkingLevelKey, - getReasoningModeKey, getDisableWorkspaceAgentsKey, } from "@/common/constants/storage"; import { readPersistedState } from "@/browser/hooks/usePersistedState"; import { buildSendMessageOptions } from "@/browser/utils/messages/buildSendMessageOptions"; import type { SendMessageOptions } from "@/common/orpc/types"; -import { coerceOpenAIReasoningMode, type OpenAIReasoningMode } from "@/common/types/thinking"; import type { MuxProviderOptions } from "@/common/types/providerOptions"; import { WORKSPACE_DEFAULTS } from "@/constants/workspaceDefaults"; import { getAppConfigStore, getUserPreferences } from "@/browser/stores/AppConfigStore"; @@ -32,14 +30,6 @@ export function getSendOptionsFromStorage(workspaceId: string): SendMessageOptio const agentId = readScopedAiDefault(workspaceId, "agentId") ?? WORKSPACE_DEFAULTS.agentId; const selection = getWorkspaceAiSelection(workspaceId, agentId); - // OpenAI pro reasoning mode (workspace-scoped); absent = standard. - // Coerce untrusted persisted values so corrupt entries self-heal to "standard" - // instead of failing SendMessageOptionsSchema on retry/resume/creation flows. - const reasoningMode = - coerceOpenAIReasoningMode( - readPersistedState(getReasoningModeKey(workspaceId), null) - ) ?? "standard"; - const providerOptions = getProviderOptions(); const disableWorkspaceAgents = readPersistedState( @@ -59,10 +49,8 @@ export function getSendOptionsFromStorage(workspaceId: string): SendMessageOptio readPersistedState(getAutoThinkingLevelKey(workspaceId), false) === true; return buildSendMessageOptions({ - model: selection.model, + ...selection, agentId, - thinkingLevel: selection.thinkingLevel, - reasoningMode, providerOptions, disableWorkspaceAgents, autoModelRouting, diff --git a/src/browser/utils/workspaceAiSettingsSync.ts b/src/browser/utils/workspaceAiSettingsSync.ts index b882faa903..d8a069b718 100644 --- a/src/browser/utils/workspaceAiSettingsSync.ts +++ b/src/browser/utils/workspaceAiSettingsSync.ts @@ -26,7 +26,6 @@ import { type OpenAIReasoningMode, type ThinkingLevel, } from "@/common/types/thinking"; -import type { FrontendWorkspaceMetadata } from "@/common/types/workspace"; import { isValidModelFormat } from "@/common/utils/ai/models"; import { normalizeAgentId } from "@/common/utils/agentIds"; import { WORKSPACE_DEFAULTS } from "@/constants/workspaceDefaults"; @@ -177,31 +176,3 @@ export function useWorkspaceAiSelection( agentBaseById, }); } - -export function getWorkspaceAiSettingsFromMetadata( - metadata: FrontendWorkspaceMetadata | undefined, - agentId: string | undefined -): { - model: string | undefined; - thinkingLevel: ThinkingLevel | undefined; - reasoningMode: OpenAIReasoningMode | undefined; -} { - const settings = - (agentId ? metadata?.aiSettingsByAgent?.[agentId] : undefined) ?? metadata?.aiSettings; - return { - model: settings?.model, - thinkingLevel: settings?.thinkingLevel, - reasoningMode: settings?.reasoningMode, - }; -} - -export function resolveEffectiveComposerModel( - preferredModel: unknown, - metadata: FrontendWorkspaceMetadata | undefined, - agentId: string | undefined, - defaultModel: string -): string { - const metadataModel = getWorkspaceAiSettingsFromMetadata(metadata, agentId).model; - // Match ChatInput precedence so shortcuts and palette actions gate on the model users see. - return normalizeModelPreference(preferredModel, metadataModel ?? defaultModel); -}