From 1b512d0cba7414967fd7997ecf0b1b708d4a6d99 Mon Sep 17 00:00:00 2001 From: Arav Jain Date: Sat, 6 Jun 2026 08:24:27 -0500 Subject: [PATCH 1/2] fix(web): remember Composer Fast mode across new chats Default Cursor fastMode to Normal when the user has not chosen Fast, and preserve sticky fastMode options when switching models so new chats reuse the last manual Fast/Normal selection. Co-authored-by: Cursor --- .../chat/composerProviderState.test.tsx | 84 ++++++++++++++++++- .../components/chat/composerProviderState.tsx | 26 +++++- apps/web/src/composerDraftStore.test.ts | 19 +++++ apps/web/src/composerDraftStore.ts | 11 ++- 4 files changed, 136 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/chat/composerProviderState.test.tsx b/apps/web/src/components/chat/composerProviderState.test.tsx index cc86d2cbe50..12489d33eba 100644 --- a/apps/web/src/components/chat/composerProviderState.test.tsx +++ b/apps/web/src/components/chat/composerProviderState.test.tsx @@ -9,6 +9,7 @@ import { getComposerProviderState, renderProviderTraitsMenuContent, renderProviderTraitsPicker, + withImplicitFastModeDefault, } from "./composerProviderState"; // Everything in composerProviderState is now data-driven by the model's @@ -36,8 +37,16 @@ function selectDescriptor( }; } -function booleanDescriptor(id: string): Extract { - return { id, label: id, type: "boolean" }; +function booleanDescriptor( + id: string, + currentValue?: boolean, +): Extract { + return { + id, + label: id, + type: "boolean", + ...(typeof currentValue === "boolean" ? { currentValue } : {}), + }; } function modelWith( @@ -205,6 +214,77 @@ describe("getComposerProviderState", () => { }); }); + it("defaults fastMode to false when the provider reports true but the user has not selected it", () => { + const state = getComposerProviderState({ + provider: ProviderDriverKind.make("cursor"), + model: MODEL, + models: modelWith([booleanDescriptor("fastMode", true)]), + prompt: "", + modelOptions: undefined, + }); + + expect(state.modelOptionsForDispatch).toEqual(selections(["fastMode", false])); + }); + + it("keeps explicit fastMode true when the user selected Fast", () => { + const state = getComposerProviderState({ + provider: ProviderDriverKind.make("cursor"), + model: MODEL, + models: modelWith([booleanDescriptor("fastMode", true)]), + prompt: "", + modelOptions: selections(["fastMode", true]), + }); + + expect(state.modelOptionsForDispatch).toEqual(selections(["fastMode", true])); + }); + + it("keeps explicit fastMode false when the user selected Normal", () => { + const state = getComposerProviderState({ + provider: ProviderDriverKind.make("cursor"), + model: MODEL, + models: modelWith([booleanDescriptor("fastMode", true)]), + prompt: "", + modelOptions: selections(["fastMode", false]), + }); + + expect(state.modelOptionsForDispatch).toEqual(selections(["fastMode", false])); + }); +}); + +describe("withImplicitFastModeDefault", () => { + it("injects fastMode false only when the model exposes fastMode and no selection exists", () => { + expect( + withImplicitFastModeDefault( + { + optionDescriptors: [booleanDescriptor("fastMode", true)], + }, + undefined, + ), + ).toEqual(selections(["fastMode", false])); + + expect( + withImplicitFastModeDefault( + { + optionDescriptors: [booleanDescriptor("fastMode", true)], + }, + selections(["fastMode", true]), + ), + ).toEqual(selections(["fastMode", true])); + }); + + it("does not add fastMode when the model does not expose it", () => { + expect( + withImplicitFastModeDefault( + { + optionDescriptors: [booleanDescriptor("thinking", true)], + }, + undefined, + ), + ).toBeUndefined(); + }); +}); + +describe("getComposerProviderState ultrathink styling", () => { it("does not add ultrathink class names when the descriptor has no promptInjectedValues", () => { const state = getComposerProviderState({ provider: PROVIDER, diff --git a/apps/web/src/components/chat/composerProviderState.tsx b/apps/web/src/components/chat/composerProviderState.tsx index 77269d0bd90..3f19a5434b7 100644 --- a/apps/web/src/components/chat/composerProviderState.tsx +++ b/apps/web/src/components/chat/composerProviderState.tsx @@ -1,4 +1,5 @@ import { + type ModelCapabilities, type ProviderDriverKind, type ProviderInstanceId, type ProviderOptionSelection, @@ -46,10 +47,33 @@ type TraitsRenderInput = { onPromptChange: (prompt: string) => void; }; +/** + * Cursor ACP can report `fastMode: true` as the provider default. T3 should only + * use Fast when the user explicitly selected it (draft/sticky/settings); otherwise + * default to Normal so new chats do not inherit the provider default. + */ +export function withImplicitFastModeDefault( + caps: ModelCapabilities, + modelOptions: ReadonlyArray | null | undefined, +): ReadonlyArray | undefined { + const hasExplicitFastMode = modelOptions?.some((selection) => selection.id === "fastMode"); + if (hasExplicitFastMode) { + return modelOptions ?? undefined; + } + const hasFastModeDescriptor = caps.optionDescriptors?.some( + (descriptor) => descriptor.type === "boolean" && descriptor.id === "fastMode", + ); + if (!hasFastModeDescriptor) { + return modelOptions ?? undefined; + } + return [...(modelOptions ?? []), { id: "fastMode", value: false }]; +} + export function getComposerProviderState(input: ComposerProviderStateInput): ComposerProviderState { const { provider, model, models, prompt, modelOptions } = input; const caps = getProviderModelCapabilities(models, model, provider); - const descriptors = getProviderOptionDescriptors({ caps, selections: modelOptions }); + const selections = withImplicitFastModeDefault(caps, modelOptions); + const descriptors = getProviderOptionDescriptors({ caps, selections }); const primarySelectDescriptor = descriptors.find( (descriptor): descriptor is Extract<(typeof descriptors)[number], { type: "select" }> => descriptor.type === "select", diff --git a/apps/web/src/composerDraftStore.test.ts b/apps/web/src/composerDraftStore.test.ts index e6caedaa2f9..b33eecb31f5 100644 --- a/apps/web/src/composerDraftStore.test.ts +++ b/apps/web/src/composerDraftStore.test.ts @@ -1344,6 +1344,25 @@ describe("composerDraftStore sticky composer settings", () => { expect(useComposerDraftStore.getState().stickyActiveProvider).toBe("cursor"); }); + it("preserves sticky provider options when model selection omits options", () => { + const store = useComposerDraftStore.getState(); + + store.setStickyModelSelection( + modelSelection(CURSOR_DRIVER, "composer-2", { + fastMode: false, + }), + ); + store.setStickyModelSelection(modelSelection(CURSOR_DRIVER, "composer-2.5")); + + expect( + useComposerDraftStore.getState().stickyModelSelectionByProvider[CURSOR_INSTANCE], + ).toEqual( + modelSelection(CURSOR_DRIVER, "composer-2.5", { + fastMode: false, + }), + ); + }); + it("applies sticky activeProvider to new drafts", () => { const store = useComposerDraftStore.getState(); const threadId = ThreadId.make("thread-sticky-active-provider"); diff --git a/apps/web/src/composerDraftStore.ts b/apps/web/src/composerDraftStore.ts index 3de3b5d706d..31154dfd90e 100644 --- a/apps/web/src/composerDraftStore.ts +++ b/apps/web/src/composerDraftStore.ts @@ -2252,9 +2252,18 @@ const composerDraftStore = create()( if (!normalized) { return state; } + const current = state.stickyModelSelectionByProvider[normalized.instanceId]; + const nextSelection = + normalized.options !== undefined + ? normalized + : createModelSelection( + normalized.instanceId, + normalized.model, + current?.options, + ); const nextMap: Partial> = { ...state.stickyModelSelectionByProvider, - [normalized.instanceId]: normalized, + [normalized.instanceId]: nextSelection, }; if (Equal.equals(state.stickyModelSelectionByProvider, nextMap)) { return state.stickyActiveProvider === normalized.instanceId From c1a38deceb3ea09a63bceeeaa693492d1da79d04 Mon Sep 17 00:00:00 2001 From: Arav Jain Date: Sun, 7 Jun 2026 08:43:10 -0500 Subject: [PATCH 2/2] fix(web): align traits picker with implicit Composer Fast default Apply withImplicitFastModeDefault in trait controls so the Fast/Normal badge matches dispatch when Cursor reports fastMode true as the provider default but the user has not explicitly chosen Fast. Co-authored-by: Cursor --- .../chat/composerProviderState.test.tsx | 18 ++++++++++++++++++ .../components/chat/composerProviderState.tsx | 12 ++++++++++-- apps/web/src/composerDraftStore.ts | 6 +----- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/apps/web/src/components/chat/composerProviderState.test.tsx b/apps/web/src/components/chat/composerProviderState.test.tsx index 12489d33eba..d830c75a632 100644 --- a/apps/web/src/components/chat/composerProviderState.test.tsx +++ b/apps/web/src/components/chat/composerProviderState.test.tsx @@ -5,12 +5,14 @@ import { type ProviderOptionSelection, type ServerProviderModel, } from "@t3tools/contracts"; +import { getProviderOptionDescriptors } from "@t3tools/shared/model"; import { getComposerProviderState, renderProviderTraitsMenuContent, renderProviderTraitsPicker, withImplicitFastModeDefault, } from "./composerProviderState"; +import { getProviderModelCapabilities } from "../../providerModels"; // Everything in composerProviderState is now data-driven by the model's // optionDescriptors, so these tests use a single synthetic provider/model and @@ -302,6 +304,22 @@ describe("getComposerProviderState ultrathink styling", () => { }); }); +describe("trait controls fastMode display", () => { + it("resolves traits fastMode to Normal when the provider defaults to true without a user selection", () => { + const models = modelWith([booleanDescriptor("fastMode", true)]); + const provider = ProviderDriverKind.make("cursor"); + const caps = getProviderModelCapabilities(models, MODEL, provider); + const resolved = withImplicitFastModeDefault(caps, undefined); + const descriptors = getProviderOptionDescriptors({ caps, selections: resolved }); + const fastMode = descriptors.find((descriptor) => descriptor.id === "fastMode"); + + expect(fastMode?.type).toBe("boolean"); + if (fastMode?.type === "boolean") { + expect(fastMode.currentValue).toBe(false); + } + }); +}); + describe("provider traits render guards", () => { it("returns null when no thread target is provided", () => { const models = modelWith([ diff --git a/apps/web/src/components/chat/composerProviderState.tsx b/apps/web/src/components/chat/composerProviderState.tsx index 3f19a5434b7..083a017557e 100644 --- a/apps/web/src/components/chat/composerProviderState.tsx +++ b/apps/web/src/components/chat/composerProviderState.tsx @@ -114,9 +114,17 @@ function renderTraitsControl( onPromptChange, } = input; const hasTarget = threadRef !== undefined || draftId !== undefined; + const caps = getProviderModelCapabilities(models, model, provider); + const resolvedModelOptions = withImplicitFastModeDefault(caps, modelOptions); if ( !hasTarget || - !shouldRenderTraitsControls({ provider, models, model, modelOptions, prompt }) + !shouldRenderTraitsControls({ + provider, + models, + model, + modelOptions: resolvedModelOptions, + prompt, + }) ) { return null; } @@ -128,7 +136,7 @@ function renderTraitsControl( {...(threadRef ? { threadRef } : {})} {...(draftId ? { draftId } : {})} model={model} - modelOptions={modelOptions} + modelOptions={resolvedModelOptions} prompt={prompt} onPromptChange={onPromptChange} /> diff --git a/apps/web/src/composerDraftStore.ts b/apps/web/src/composerDraftStore.ts index 31154dfd90e..60bbc8196e4 100644 --- a/apps/web/src/composerDraftStore.ts +++ b/apps/web/src/composerDraftStore.ts @@ -2256,11 +2256,7 @@ const composerDraftStore = create()( const nextSelection = normalized.options !== undefined ? normalized - : createModelSelection( - normalized.instanceId, - normalized.model, - current?.options, - ); + : createModelSelection(normalized.instanceId, normalized.model, current?.options); const nextMap: Partial> = { ...state.stickyModelSelectionByProvider, [normalized.instanceId]: nextSelection,