From 9b5d554a31ff52e509ea14743d6a8342ca30f54e Mon Sep 17 00:00:00 2001 From: Adam Buchweitz <312235+AdamBuchweitz@users.noreply.github.com> Date: Mon, 18 May 2026 16:42:54 -0500 Subject: [PATCH] fix: maintain reasoning selections for multiple providers --- apps/web/src/components/chat/ChatComposer.tsx | 17 +++++++-- apps/web/src/components/chat/TraitsPicker.tsx | 8 +++- .../components/chat/composerProviderState.tsx | 16 +++++++- apps/web/src/composerDraftStore.test.ts | 38 +++++++++++++++++++ apps/web/src/composerDraftStore.ts | 8 +++- 5 files changed, 78 insertions(+), 9 deletions(-) diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 96fc34c1013..b19e1240364 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -727,9 +727,16 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) model: selectedModel, models: selectedProviderModels, prompt, - modelOptions: composerModelOptions?.[selectedProvider], + modelOptions: composerModelOptions?.[selectedInstanceId], }), - [composerModelOptions, prompt, selectedModel, selectedProvider, selectedProviderModels], + [ + composerModelOptions, + prompt, + selectedInstanceId, + selectedModel, + selectedProvider, + selectedProviderModels, + ], ); const selectedPromptEffort = composerProviderState.promptEffort; @@ -1018,21 +1025,23 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) const providerTraitsMenuContent = renderProviderTraitsMenuContent({ provider: selectedProvider, + instanceId: selectedInstanceId, ...(routeKind === "server" ? { threadRef: routeThreadRef } : {}), ...(routeKind === "draft" && draftId ? { draftId } : {}), model: selectedModel, models: selectedProviderModels, - modelOptions: composerModelOptions?.[selectedProvider], + modelOptions: composerModelOptions?.[selectedInstanceId], prompt, onPromptChange: setPromptFromTraits, }); const providerTraitsPicker = renderProviderTraitsPicker({ provider: selectedProvider, + instanceId: selectedInstanceId, ...(routeKind === "server" ? { threadRef: routeThreadRef } : {}), ...(routeKind === "draft" && draftId ? { draftId } : {}), model: selectedModel, models: selectedProviderModels, - modelOptions: composerModelOptions?.[selectedProvider], + modelOptions: composerModelOptions?.[selectedInstanceId], prompt, onPromptChange: setPromptFromTraits, }); diff --git a/apps/web/src/components/chat/TraitsPicker.tsx b/apps/web/src/components/chat/TraitsPicker.tsx index 62caa323fd3..76ff055a594 100644 --- a/apps/web/src/components/chat/TraitsPicker.tsx +++ b/apps/web/src/components/chat/TraitsPicker.tsx @@ -1,5 +1,6 @@ import { type ProviderDriverKind, + type ProviderInstanceId, type ProviderOptionDescriptor, type ProviderOptionSelection, type ScopedThreadRef, @@ -196,6 +197,7 @@ export function shouldRenderTraitsControls(input: { export interface TraitsMenuContentProps { provider: ProviderDriverKind; + instanceId?: ProviderInstanceId; models: ReadonlyArray; model: string | null | undefined; prompt: string; @@ -208,6 +210,7 @@ export interface TraitsMenuContentProps { export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ provider, + instanceId, models, model, prompt, @@ -228,11 +231,12 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ return; } setProviderModelOptions(threadTarget, provider, nextOptions, { + ...(instanceId ? { instanceId } : {}), model, persistSticky: true, }); }, - [model, persistence, provider, setProviderModelOptions], + [instanceId, model, persistence, provider, setProviderModelOptions], ); const { descriptors, @@ -343,6 +347,7 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ export const TraitsPicker = memo(function TraitsPicker({ provider, + instanceId, models, model, prompt, @@ -431,6 +436,7 @@ export const TraitsPicker = memo(function TraitsPicker({ { ); }); + it("stores provider option changes on a selected custom instance", () => { + const store = useComposerDraftStore.getState(); + + store.setProviderModelOptions( + threadRef, + CODEX_DRIVER, + toSelections({ reasoningEffort: "low" }), + { + instanceId: CODEX_SECONDARY_INSTANCE, + model: "gpt-5-codex", + persistSticky: true, + }, + ); + + expect( + draftFor(threadId, TEST_ENVIRONMENT_ID)?.modelSelectionByProvider[CODEX_SECONDARY_INSTANCE], + ).toEqual( + expect.objectContaining({ + instanceId: CODEX_SECONDARY_INSTANCE, + options: [{ id: "reasoningEffort", value: "low" }], + }), + ); + expect(draftFor(threadId, TEST_ENVIRONMENT_ID)?.activeProvider).toBe(CODEX_SECONDARY_INSTANCE); + expect(useComposerDraftStore.getState().stickyActiveProvider).toBe(CODEX_SECONDARY_INSTANCE); + expect(useComposerDraftStore.getState().stickyModelSelectionByProvider[CODEX_INSTANCE]).toBe( + undefined, + ); + expect( + useComposerDraftStore.getState().stickyModelSelectionByProvider[CODEX_SECONDARY_INSTANCE], + ).toEqual( + expect.objectContaining({ + instanceId: CODEX_SECONDARY_INSTANCE, + options: [{ id: "reasoningEffort", value: "low" }], + }), + ); + }); + it("updates only the draft when sticky persistence is disabled", () => { const store = useComposerDraftStore.getState(); diff --git a/apps/web/src/composerDraftStore.ts b/apps/web/src/composerDraftStore.ts index d554ad7b0d3..5c90197a406 100644 --- a/apps/web/src/composerDraftStore.ts +++ b/apps/web/src/composerDraftStore.ts @@ -371,6 +371,7 @@ interface ComposerDraftStoreState { provider: ProviderDriverKind, nextProviderOptions: ReadonlyArray | null | undefined, options?: { + instanceId?: ProviderInstanceId | null | undefined; model?: string | null | undefined; persistSticky?: boolean; }, @@ -2456,7 +2457,7 @@ const composerDraftStore = create()( if (normalizedProvider === null) { return; } - const instanceKey = defaultInstanceIdForDriver(normalizedProvider); + const instanceKey = options?.instanceId ?? defaultInstanceIdForDriver(normalizedProvider); const fallbackModel = normalizeModelSlug(options?.model, normalizedProvider) ?? DEFAULT_MODEL_BY_PROVIDER[normalizedProvider] ?? @@ -2501,7 +2502,9 @@ const composerDraftStore = create()( const { options: _, ...rest } = stickyBase; nextStickyMap[instanceKey] = rest as ModelSelection; } - nextStickyActiveProvider = base.activeProvider ?? instanceKey; + nextStickyActiveProvider = options.instanceId + ? instanceKey + : (base.activeProvider ?? instanceKey); } if ( @@ -2514,6 +2517,7 @@ const composerDraftStore = create()( const nextDraft: ComposerThreadDraftState = { ...base, + ...(options?.instanceId ? { activeProvider: instanceKey } : {}), modelSelectionByProvider: nextMap, }; const nextDraftsByThreadKey = { ...state.draftsByThreadKey };