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";
7878import { resolveComposerMenuActiveItemId } from "./composerMenuHighlight";
7979import { searchSlashCommandItems } from "./composerSlashCommandSearch";
8080import {
81 getComposerPromptInjectionState,
8182 getComposerProviderState,
8283 renderProviderTraitsMenuContent,
8384 renderProviderTraitsPicker,
......@@ -791,18 +792,22 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
791792 [selectedProviderEntry],
792793 );
793794
795 const composerPromptInjectionState = useMemo(
796 () => getComposerPromptInjectionState(prompt),
797 [prompt],
798 );
794799 const composerProviderState = useMemo(
795800 () =>
796801 getComposerProviderState({
797802 provider: selectedProvider,
798803 model: selectedModel,
799804 models: selectedProviderModels,
800 prompt,
805 promptInjectionState: composerPromptInjectionState,
801806 modelOptions: composerModelOptions?.[selectedInstanceId],
802807 }),
803808 [
804809 composerModelOptions,
805 prompt,
810 composerPromptInjectionState,
806811 selectedInstanceId,
807812 selectedModel,
808813 selectedProvider,
apps/web/src/components/chat/composerProviderState.test.tsx+14-8
......@@ -6,6 +6,7 @@ import {
66 type ServerProviderModel,
77} from "@t3tools/contracts";
88import {
9 getComposerPromptInjectionState,
910 getComposerProviderState,
1011 renderProviderTraitsMenuContent,
1112 renderProviderTraitsPicker,
......@@ -61,6 +62,13 @@ const ULTRATHINK_FRAME_CLASSES = {
6162} as const;
6263
6364describe("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
6472 it("returns descriptor defaults when no selections are provided", () => {
6573 const state = getComposerProviderState({
6674 provider: PROVIDER,
......@@ -71,7 +79,6 @@ describe("getComposerProviderState", () => {
7179 { id: "high", label: "High", isDefault: true },
7280 ]),
7381 ]),
74 prompt: "",
7582 modelOptions: undefined,
7683 });
7784
......@@ -93,7 +100,6 @@ describe("getComposerProviderState", () => {
93100 ]),
94101 booleanDescriptor("fastMode"),
95102 ]),
96 prompt: "",
97103 modelOptions: selections(["effort", "low"], ["fastMode", true]),
98104 });
99105
......@@ -112,7 +118,6 @@ describe("getComposerProviderState", () => {
112118 selectDescriptor("effort", [{ id: "high", label: "High", isDefault: true }]),
113119 booleanDescriptor("fastMode"),
114120 ]),
115 prompt: "",
116121 modelOptions: selections(["effort", "high"], ["fastMode", false]),
117122 });
118123
......@@ -126,7 +131,6 @@ describe("getComposerProviderState", () => {
126131 provider: PROVIDER,
127132 model: MODEL,
128133 models: modelWith([booleanDescriptor("thinking")]),
129 prompt: "",
130134 modelOptions: selections(["effort", "max"], ["thinking", false]),
131135 });
132136
......@@ -152,7 +156,6 @@ describe("getComposerProviderState", () => {
152156 { id: "plan", label: "Plan" },
153157 ]),
154158 ]),
155 prompt: "",
156159 modelOptions: selections(["agent", "plan"]),
157160 });
158161
......@@ -167,7 +170,6 @@ describe("getComposerProviderState", () => {
167170 provider: PROVIDER,
168171 model: MODEL,
169172 models: modelWith([]),
170 prompt: "",
171173 modelOptions: selections(["anything", "value"]),
172174 });
173175
......@@ -193,7 +195,9 @@ describe("getComposerProviderState", () => {
193195 ["ultrathink"],
194196 ),
195197 ]),
196 prompt: "Ultrathink:\nInvestigate this failure",
198 promptInjectionState: getComposerPromptInjectionState(
199 "Ultrathink:\nInvestigate this failure",
200 ),
197201 modelOptions: selections(["effort", "medium"]),
198202 });
199203
......@@ -212,7 +216,9 @@ describe("getComposerProviderState", () => {
212216 models: modelWith([
213217 selectDescriptor("effort", [{ id: "high", label: "High", isDefault: true }]),
214218 ]),
215 prompt: "Ultrathink:\nInvestigate this failure",
219 promptInjectionState: getComposerPromptInjectionState(
220 "Ultrathink:\nInvestigate this failure",
221 ),
216222 modelOptions: undefined,
217223 });
218224
apps/web/src/components/chat/composerProviderState.tsx+9-3
......@@ -21,10 +21,12 @@ export type ComposerProviderStateInput = {
2121 provider: ProviderDriverKind;
2222 model: string;
2323 models: ReadonlyArray<ServerProviderModel>;
24 prompt: string;
24 promptInjectionState?: ComposerPromptInjectionState;
2525 modelOptions: ReadonlyArray<ProviderOptionSelection> | null | undefined;
2626};
2727
28export type ComposerPromptInjectionState = "none" | "ultrathink";
29
2830export type ComposerProviderState = {
2931 provider: ProviderDriverKind;
3032 promptEffort: string | null;
......@@ -46,8 +48,12 @@ type TraitsRenderInput = {
4648 onPromptChange: (prompt: string) => void;
4749};
4850
51export function getComposerPromptInjectionState(prompt: string): ComposerPromptInjectionState {
52 return isClaudeUltrathinkPrompt(prompt) ? "ultrathink" : "none";
53}
54
4955export function getComposerProviderState(input: ComposerProviderStateInput): ComposerProviderState {
50 const { provider, model, models, prompt, modelOptions } = input;
56 const { provider, model, models, modelOptions, promptInjectionState = "none" } = input;
5157 const caps = getProviderModelCapabilities(models, model, provider);
5258 const descriptors = getProviderOptionDescriptors({ caps, selections: modelOptions });
5359 const primarySelectDescriptor = descriptors.find(
......@@ -58,7 +64,7 @@ export function getComposerProviderState(input: ComposerProviderStateInput): Com
5864 const promptEffort = typeof primaryValue === "string" ? primaryValue : null;
5965 const ultrathinkActive =
6066 (primarySelectDescriptor?.promptInjectedValues?.length ?? 0) > 0 &&
61 isClaudeUltrathinkPrompt(prompt);
67 promptInjectionState === "ultrathink";
6268
6369 return {
6470 provider,
apps/web/src/modelSelection.ts-2
......@@ -303,7 +303,6 @@ export function resolveAppModelSelectionState(
303303 provider,
304304 model,
305305 models: entry.models,
306 prompt: "",
307306 modelOptions: selectedEntry ? selection.options : undefined,
308307 });
309308
......@@ -321,7 +320,6 @@ export function resolveAppModelSelectionState(
321320 provider,
322321 model,
323322 models: getProviderModels(providers, provider),
324 prompt: "",
325323 modelOptions: keptSelectedProvider ? selection.options : undefined,
326324 });
327325