authorgravatar for 206951365+cursor[bot]@users.noreply.github.comcursor[bot] <206951365+cursor[bot]@users.noreply.github.com> 2026-06-22 12:52:13-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-22 12:52:13-07:00
logf5f98cf0ae48a14b524480a0942f9ce37190656c
treeb09dc4a52ee5ed6d52c92f06b39bbaa19ed6858f
parent37ac970e289f6a79940c00e17c5a58114d94d4cf
signature Signed by PGP key B5690EEEBB952194

Stabilize composer provider state while typing (#3507)

Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Julius Marminge <juliusmarminge@users.noreply.github.com>

4 files changed, 30 insertions(+), 15 deletions(-)

apps/web/src/components/chat/ChatComposer.tsx+7-2
...@@ -78,6 +78,7 @@ import { ComposerPlanFollowUpBanner } from "./ComposerPlanFollowUpBanner";...@@ -78,6 +78,7 @@ import { ComposerPlanFollowUpBanner } from "./ComposerPlanFollowUpBanner";
78import { resolveComposerMenuActiveItemId } from "./composerMenuHighlight";78import { resolveComposerMenuActiveItemId } from "./composerMenuHighlight";
79import { searchSlashCommandItems } from "./composerSlashCommandSearch";79import { searchSlashCommandItems } from "./composerSlashCommandSearch";
80import {80import {
81 getComposerPromptInjectionState,
81 getComposerProviderState,82 getComposerProviderState,
82 renderProviderTraitsMenuContent,83 renderProviderTraitsMenuContent,
83 renderProviderTraitsPicker,84 renderProviderTraitsPicker,
...@@ -791,18 +792,22 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -791,18 +792,22 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
791 [selectedProviderEntry],792 [selectedProviderEntry],
792 );793 );
793794
795 const composerPromptInjectionState = useMemo(
796 () => getComposerPromptInjectionState(prompt),
797 [prompt],
798 );
794 const composerProviderState = useMemo(799 const composerProviderState = useMemo(
795 () =>800 () =>
796 getComposerProviderState({801 getComposerProviderState({
797 provider: selectedProvider,802 provider: selectedProvider,
798 model: selectedModel,803 model: selectedModel,
799 models: selectedProviderModels,804 models: selectedProviderModels,
800 prompt,805 promptInjectionState: composerPromptInjectionState,
801 modelOptions: composerModelOptions?.[selectedInstanceId],806 modelOptions: composerModelOptions?.[selectedInstanceId],
802 }),807 }),
803 [808 [
804 composerModelOptions,809 composerModelOptions,
805 prompt,810 composerPromptInjectionState,
806 selectedInstanceId,811 selectedInstanceId,
807 selectedModel,812 selectedModel,
808 selectedProvider,813 selectedProvider,
apps/web/src/components/chat/composerProviderState.test.tsx+14-8
...@@ -6,6 +6,7 @@ import {...@@ -6,6 +6,7 @@ import {
6 type ServerProviderModel,6 type ServerProviderModel,
7} from "@t3tools/contracts";7} from "@t3tools/contracts";
8import {8import {
9 getComposerPromptInjectionState,
9 getComposerProviderState,10 getComposerProviderState,
10 renderProviderTraitsMenuContent,11 renderProviderTraitsMenuContent,
11 renderProviderTraitsPicker,12 renderProviderTraitsPicker,
...@@ -61,6 +62,13 @@ const ULTRATHINK_FRAME_CLASSES = {...@@ -61,6 +62,13 @@ const ULTRATHINK_FRAME_CLASSES = {
61} as const;62} as const;
6263
63describe("getComposerProviderState", () => {64describe("getComposerProviderState", () => {
65 it("derives a stable prompt injection state for ordinary prompt edits", () => {
66 expect(getComposerPromptInjectionState("Investigate this failure")).toBe("none");
67 expect(getComposerPromptInjectionState("Ultrathink:\nInvestigate this failure")).toBe(
68 "ultrathink",
69 );
70 });
71
64 it("returns descriptor defaults when no selections are provided", () => {72 it("returns descriptor defaults when no selections are provided", () => {
65 const state = getComposerProviderState({73 const state = getComposerProviderState({
66 provider: PROVIDER,74 provider: PROVIDER,
...@@ -71,7 +79,6 @@ describe("getComposerProviderState", () => {...@@ -71,7 +79,6 @@ describe("getComposerProviderState", () => {
71 { id: "high", label: "High", isDefault: true },79 { id: "high", label: "High", isDefault: true },
72 ]),80 ]),
73 ]),81 ]),
74 prompt: "",
75 modelOptions: undefined,82 modelOptions: undefined,
76 });83 });
7784
...@@ -93,7 +100,6 @@ describe("getComposerProviderState", () => {...@@ -93,7 +100,6 @@ describe("getComposerProviderState", () => {
93 ]),100 ]),
94 booleanDescriptor("fastMode"),101 booleanDescriptor("fastMode"),
95 ]),102 ]),
96 prompt: "",
97 modelOptions: selections(["effort", "low"], ["fastMode", true]),103 modelOptions: selections(["effort", "low"], ["fastMode", true]),
98 });104 });
99105
...@@ -112,7 +118,6 @@ describe("getComposerProviderState", () => {...@@ -112,7 +118,6 @@ describe("getComposerProviderState", () => {
112 selectDescriptor("effort", [{ id: "high", label: "High", isDefault: true }]),118 selectDescriptor("effort", [{ id: "high", label: "High", isDefault: true }]),
113 booleanDescriptor("fastMode"),119 booleanDescriptor("fastMode"),
114 ]),120 ]),
115 prompt: "",
116 modelOptions: selections(["effort", "high"], ["fastMode", false]),121 modelOptions: selections(["effort", "high"], ["fastMode", false]),
117 });122 });
118123
...@@ -126,7 +131,6 @@ describe("getComposerProviderState", () => {...@@ -126,7 +131,6 @@ describe("getComposerProviderState", () => {
126 provider: PROVIDER,131 provider: PROVIDER,
127 model: MODEL,132 model: MODEL,
128 models: modelWith([booleanDescriptor("thinking")]),133 models: modelWith([booleanDescriptor("thinking")]),
129 prompt: "",
130 modelOptions: selections(["effort", "max"], ["thinking", false]),134 modelOptions: selections(["effort", "max"], ["thinking", false]),
131 });135 });
132136
...@@ -152,7 +156,6 @@ describe("getComposerProviderState", () => {...@@ -152,7 +156,6 @@ describe("getComposerProviderState", () => {
152 { id: "plan", label: "Plan" },156 { id: "plan", label: "Plan" },
153 ]),157 ]),
154 ]),158 ]),
155 prompt: "",
156 modelOptions: selections(["agent", "plan"]),159 modelOptions: selections(["agent", "plan"]),
157 });160 });
158161
...@@ -167,7 +170,6 @@ describe("getComposerProviderState", () => {...@@ -167,7 +170,6 @@ describe("getComposerProviderState", () => {
167 provider: PROVIDER,170 provider: PROVIDER,
168 model: MODEL,171 model: MODEL,
169 models: modelWith([]),172 models: modelWith([]),
170 prompt: "",
171 modelOptions: selections(["anything", "value"]),173 modelOptions: selections(["anything", "value"]),
172 });174 });
173175
...@@ -193,7 +195,9 @@ describe("getComposerProviderState", () => {...@@ -193,7 +195,9 @@ describe("getComposerProviderState", () => {
193 ["ultrathink"],195 ["ultrathink"],
194 ),196 ),
195 ]),197 ]),
196 prompt: "Ultrathink:\nInvestigate this failure",198 promptInjectionState: getComposerPromptInjectionState(
199 "Ultrathink:\nInvestigate this failure",
200 ),
197 modelOptions: selections(["effort", "medium"]),201 modelOptions: selections(["effort", "medium"]),
198 });202 });
199203
...@@ -212,7 +216,9 @@ describe("getComposerProviderState", () => {...@@ -212,7 +216,9 @@ describe("getComposerProviderState", () => {
212 models: modelWith([216 models: modelWith([
213 selectDescriptor("effort", [{ id: "high", label: "High", isDefault: true }]),217 selectDescriptor("effort", [{ id: "high", label: "High", isDefault: true }]),
214 ]),218 ]),
215 prompt: "Ultrathink:\nInvestigate this failure",219 promptInjectionState: getComposerPromptInjectionState(
220 "Ultrathink:\nInvestigate this failure",
221 ),
216 modelOptions: undefined,222 modelOptions: undefined,
217 });223 });
218224
apps/web/src/components/chat/composerProviderState.tsx+9-3
...@@ -21,10 +21,12 @@ export type ComposerProviderStateInput = {...@@ -21,10 +21,12 @@ export type ComposerProviderStateInput = {
21 provider: ProviderDriverKind;21 provider: ProviderDriverKind;
22 model: string;22 model: string;
23 models: ReadonlyArray<ServerProviderModel>;23 models: ReadonlyArray<ServerProviderModel>;
24 prompt: string;24 promptInjectionState?: ComposerPromptInjectionState;
25 modelOptions: ReadonlyArray<ProviderOptionSelection> | null | undefined;25 modelOptions: ReadonlyArray<ProviderOptionSelection> | null | undefined;
26};26};
2727
28export type ComposerPromptInjectionState = "none" | "ultrathink";
29
28export type ComposerProviderState = {30export type ComposerProviderState = {
29 provider: ProviderDriverKind;31 provider: ProviderDriverKind;
30 promptEffort: string | null;32 promptEffort: string | null;
...@@ -46,8 +48,12 @@ type TraitsRenderInput = {...@@ -46,8 +48,12 @@ type TraitsRenderInput = {
46 onPromptChange: (prompt: string) => void;48 onPromptChange: (prompt: string) => void;
47};49};
4850
51export function getComposerPromptInjectionState(prompt: string): ComposerPromptInjectionState {
52 return isClaudeUltrathinkPrompt(prompt) ? "ultrathink" : "none";
53}
54
49export function getComposerProviderState(input: ComposerProviderStateInput): ComposerProviderState {55export function getComposerProviderState(input: ComposerProviderStateInput): ComposerProviderState {
50 const { provider, model, models, prompt, modelOptions } = input;56 const { provider, model, models, modelOptions, promptInjectionState = "none" } = input;
51 const caps = getProviderModelCapabilities(models, model, provider);57 const caps = getProviderModelCapabilities(models, model, provider);
52 const descriptors = getProviderOptionDescriptors({ caps, selections: modelOptions });58 const descriptors = getProviderOptionDescriptors({ caps, selections: modelOptions });
53 const primarySelectDescriptor = descriptors.find(59 const primarySelectDescriptor = descriptors.find(
...@@ -58,7 +64,7 @@ export function getComposerProviderState(input: ComposerProviderStateInput): Com...@@ -58,7 +64,7 @@ export function getComposerProviderState(input: ComposerProviderStateInput): Com
58 const promptEffort = typeof primaryValue === "string" ? primaryValue : null;64 const promptEffort = typeof primaryValue === "string" ? primaryValue : null;
59 const ultrathinkActive =65 const ultrathinkActive =
60 (primarySelectDescriptor?.promptInjectedValues?.length ?? 0) > 0 &&66 (primarySelectDescriptor?.promptInjectedValues?.length ?? 0) > 0 &&
61 isClaudeUltrathinkPrompt(prompt);67 promptInjectionState === "ultrathink";
6268
63 return {69 return {
64 provider,70 provider,
apps/web/src/modelSelection.ts-2
...@@ -303,7 +303,6 @@ export function resolveAppModelSelectionState(...@@ -303,7 +303,6 @@ export function resolveAppModelSelectionState(
303 provider,303 provider,
304 model,304 model,
305 models: entry.models,305 models: entry.models,
306 prompt: "",
307 modelOptions: selectedEntry ? selection.options : undefined,306 modelOptions: selectedEntry ? selection.options : undefined,
308 });307 });
309308
...@@ -321,7 +320,6 @@ export function resolveAppModelSelectionState(...@@ -321,7 +320,6 @@ export function resolveAppModelSelectionState(
321 provider,320 provider,
322 model,321 model,
323 models: getProviderModels(providers, provider),322 models: getProviderModels(providers, provider),
324 prompt: "",
325 modelOptions: keptSelectedProvider ? selection.options : undefined,323 modelOptions: keptSelectedProvider ? selection.options : undefined,
326 });324 });
327325