From 26555766644d955e69a2d62b7a9accb150679504 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 22 Jun 2026 16:43:09 +0000 Subject: [PATCH] Stabilize composer provider state while typing Co-authored-by: Julius Marminge --- apps/web/src/components/chat/ChatComposer.tsx | 9 ++++++-- .../chat/composerProviderState.test.tsx | 22 ++++++++++++------- .../components/chat/composerProviderState.tsx | 12 +++++++--- apps/web/src/modelSelection.ts | 2 -- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 3a5e06bce06..11feae3e501 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -78,6 +78,7 @@ import { ComposerPlanFollowUpBanner } from "./ComposerPlanFollowUpBanner"; import { resolveComposerMenuActiveItemId } from "./composerMenuHighlight"; import { searchSlashCommandItems } from "./composerSlashCommandSearch"; import { + getComposerPromptInjectionState, getComposerProviderState, renderProviderTraitsMenuContent, renderProviderTraitsPicker, @@ -791,18 +792,22 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) [selectedProviderEntry], ); + const composerPromptInjectionState = useMemo( + () => getComposerPromptInjectionState(prompt), + [prompt], + ); const composerProviderState = useMemo( () => getComposerProviderState({ provider: selectedProvider, model: selectedModel, models: selectedProviderModels, - prompt, + promptInjectionState: composerPromptInjectionState, modelOptions: composerModelOptions?.[selectedInstanceId], }), [ composerModelOptions, - prompt, + composerPromptInjectionState, selectedInstanceId, selectedModel, selectedProvider, diff --git a/apps/web/src/components/chat/composerProviderState.test.tsx b/apps/web/src/components/chat/composerProviderState.test.tsx index 07eec55de2c..067e71ef1bf 100644 --- a/apps/web/src/components/chat/composerProviderState.test.tsx +++ b/apps/web/src/components/chat/composerProviderState.test.tsx @@ -6,6 +6,7 @@ import { type ServerProviderModel, } from "@t3tools/contracts"; import { + getComposerPromptInjectionState, getComposerProviderState, renderProviderTraitsMenuContent, renderProviderTraitsPicker, @@ -61,6 +62,13 @@ const ULTRATHINK_FRAME_CLASSES = { } as const; describe("getComposerProviderState", () => { + it("derives a stable prompt injection state for ordinary prompt edits", () => { + expect(getComposerPromptInjectionState("Investigate this failure")).toBe("none"); + expect(getComposerPromptInjectionState("Ultrathink:\nInvestigate this failure")).toBe( + "ultrathink", + ); + }); + it("returns descriptor defaults when no selections are provided", () => { const state = getComposerProviderState({ provider: PROVIDER, @@ -71,7 +79,6 @@ describe("getComposerProviderState", () => { { id: "high", label: "High", isDefault: true }, ]), ]), - prompt: "", modelOptions: undefined, }); @@ -93,7 +100,6 @@ describe("getComposerProviderState", () => { ]), booleanDescriptor("fastMode"), ]), - prompt: "", modelOptions: selections(["effort", "low"], ["fastMode", true]), }); @@ -112,7 +118,6 @@ describe("getComposerProviderState", () => { selectDescriptor("effort", [{ id: "high", label: "High", isDefault: true }]), booleanDescriptor("fastMode"), ]), - prompt: "", modelOptions: selections(["effort", "high"], ["fastMode", false]), }); @@ -126,7 +131,6 @@ describe("getComposerProviderState", () => { provider: PROVIDER, model: MODEL, models: modelWith([booleanDescriptor("thinking")]), - prompt: "", modelOptions: selections(["effort", "max"], ["thinking", false]), }); @@ -152,7 +156,6 @@ describe("getComposerProviderState", () => { { id: "plan", label: "Plan" }, ]), ]), - prompt: "", modelOptions: selections(["agent", "plan"]), }); @@ -167,7 +170,6 @@ describe("getComposerProviderState", () => { provider: PROVIDER, model: MODEL, models: modelWith([]), - prompt: "", modelOptions: selections(["anything", "value"]), }); @@ -193,7 +195,9 @@ describe("getComposerProviderState", () => { ["ultrathink"], ), ]), - prompt: "Ultrathink:\nInvestigate this failure", + promptInjectionState: getComposerPromptInjectionState( + "Ultrathink:\nInvestigate this failure", + ), modelOptions: selections(["effort", "medium"]), }); @@ -212,7 +216,9 @@ describe("getComposerProviderState", () => { models: modelWith([ selectDescriptor("effort", [{ id: "high", label: "High", isDefault: true }]), ]), - prompt: "Ultrathink:\nInvestigate this failure", + promptInjectionState: getComposerPromptInjectionState( + "Ultrathink:\nInvestigate this failure", + ), modelOptions: undefined, }); diff --git a/apps/web/src/components/chat/composerProviderState.tsx b/apps/web/src/components/chat/composerProviderState.tsx index b5cc790538d..1349e2509b7 100644 --- a/apps/web/src/components/chat/composerProviderState.tsx +++ b/apps/web/src/components/chat/composerProviderState.tsx @@ -21,10 +21,12 @@ export type ComposerProviderStateInput = { provider: ProviderDriverKind; model: string; models: ReadonlyArray; - prompt: string; + promptInjectionState?: ComposerPromptInjectionState; modelOptions: ReadonlyArray | null | undefined; }; +export type ComposerPromptInjectionState = "none" | "ultrathink"; + export type ComposerProviderState = { provider: ProviderDriverKind; promptEffort: string | null; @@ -46,8 +48,12 @@ type TraitsRenderInput = { onPromptChange: (prompt: string) => void; }; +export function getComposerPromptInjectionState(prompt: string): ComposerPromptInjectionState { + return isClaudeUltrathinkPrompt(prompt) ? "ultrathink" : "none"; +} + export function getComposerProviderState(input: ComposerProviderStateInput): ComposerProviderState { - const { provider, model, models, prompt, modelOptions } = input; + const { provider, model, models, modelOptions, promptInjectionState = "none" } = input; const caps = getProviderModelCapabilities(models, model, provider); const descriptors = getProviderOptionDescriptors({ caps, selections: modelOptions }); const primarySelectDescriptor = descriptors.find( @@ -58,7 +64,7 @@ export function getComposerProviderState(input: ComposerProviderStateInput): Com const promptEffort = typeof primaryValue === "string" ? primaryValue : null; const ultrathinkActive = (primarySelectDescriptor?.promptInjectedValues?.length ?? 0) > 0 && - isClaudeUltrathinkPrompt(prompt); + promptInjectionState === "ultrathink"; return { provider, diff --git a/apps/web/src/modelSelection.ts b/apps/web/src/modelSelection.ts index 0fcf680b732..7ede9665ac9 100644 --- a/apps/web/src/modelSelection.ts +++ b/apps/web/src/modelSelection.ts @@ -303,7 +303,6 @@ export function resolveAppModelSelectionState( provider, model, models: entry.models, - prompt: "", modelOptions: selectedEntry ? selection.options : undefined, }); @@ -321,7 +320,6 @@ export function resolveAppModelSelectionState( provider, model, models: getProviderModels(providers, provider), - prompt: "", modelOptions: keptSelectedProvider ? selection.options : undefined, });