From 530bc42569b477d13c7dd94dde8ac198bb86bfed Mon Sep 17 00:00:00 2001 From: Marve10s Date: Wed, 25 Mar 2026 11:40:29 +0300 Subject: [PATCH] fix(web): allow switching away from Ultrathink without manual prompt editing When Ultrathink was active, the effort dropdown was fully disabled, requiring the user to manually remove the "Ultrathink:" prefix from the prompt. Now selecting a different effort level automatically strips the prefix and applies the new effort. If "ultrathink" also appears in the user's body text, the controls stay disabled with a warning to remove it manually, preventing prompt mangling. --- .../CompactComposerControlsMenu.browser.tsx | 25 ++++++++++++++++--- .../components/chat/TraitsPicker.browser.tsx | 22 +++++++++++++--- apps/web/src/components/chat/TraitsPicker.tsx | 24 ++++++++++++++---- 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/apps/web/src/components/chat/CompactComposerControlsMenu.browser.tsx b/apps/web/src/components/chat/CompactComposerControlsMenu.browser.tsx index 8770e58138..e64aacb006 100644 --- a/apps/web/src/components/chat/CompactComposerControlsMenu.browser.tsx +++ b/apps/web/src/components/chat/CompactComposerControlsMenu.browser.tsx @@ -216,7 +216,7 @@ describe("CompactComposerControlsMenu", () => { }); }); - it("shows prompt-controlled Ultrathink messaging with disabled effort controls", async () => { + it("shows prompt-controlled Ultrathink state with selectable effort controls", async () => { await using _ = await mountMenu({ modelSelection: { provider: "claudeAgent", @@ -231,8 +231,27 @@ describe("CompactComposerControlsMenu", () => { await vi.waitFor(() => { const text = document.body.textContent ?? ""; expect(text).toContain("Effort"); - expect(text).toContain("Remove Ultrathink from the prompt to change effort."); - expect(text).not.toContain("Fallback Effort"); + expect(text).not.toContain("ultrathink"); + }); + }); + + it("warns when ultrathink appears in prompt body text", async () => { + await using _ = await mountMenu({ + modelSelection: { + provider: "claudeAgent", + model: "claude-opus-4-6", + options: { effort: "high" }, + }, + prompt: "Ultrathink:\nplease ultrathink about this problem", + }); + + await page.getByLabelText("More composer controls").click(); + + await vi.waitFor(() => { + const text = document.body.textContent ?? ""; + expect(text).toContain( + 'Your prompt contains "ultrathink" in the text. Remove it to change effort.', + ); }); }); }); diff --git a/apps/web/src/components/chat/TraitsPicker.browser.tsx b/apps/web/src/components/chat/TraitsPicker.browser.tsx index bd8c61ee56..4fa890d357 100644 --- a/apps/web/src/components/chat/TraitsPicker.browser.tsx +++ b/apps/web/src/components/chat/TraitsPicker.browser.tsx @@ -292,7 +292,7 @@ describe("TraitsPicker (Claude)", () => { }); }); - it("shows prompt-controlled Ultrathink state with disabled effort controls", async () => { + it("shows prompt-controlled Ultrathink state with selectable effort controls", async () => { await using _ = await mountClaudePicker({ model: "claude-opus-4-6", options: { effort: "high" }, @@ -308,8 +308,24 @@ describe("TraitsPicker (Claude)", () => { await vi.waitFor(() => { const text = document.body.textContent ?? ""; expect(text).toContain("Effort"); - expect(text).toContain("Remove Ultrathink from the prompt to change effort."); - expect(text).not.toContain("Fallback Effort"); + expect(text).not.toContain("ultrathink"); + }); + }); + + it("warns when ultrathink appears in prompt body text", async () => { + await using _ = await mountClaudePicker({ + model: "claude-opus-4-6", + options: { effort: "high" }, + prompt: "Ultrathink:\nplease ultrathink about this problem", + }); + + await page.getByRole("button").click(); + + await vi.waitFor(() => { + const text = document.body.textContent ?? ""; + expect(text).toContain( + 'Your prompt contains "ultrathink" in the text. Remove it to change effort.', + ); }); }); diff --git a/apps/web/src/components/chat/TraitsPicker.tsx b/apps/web/src/components/chat/TraitsPicker.tsx index 5fd97b8cde..09d487e479 100644 --- a/apps/web/src/components/chat/TraitsPicker.tsx +++ b/apps/web/src/components/chat/TraitsPicker.tsx @@ -110,6 +110,10 @@ function getSelectedTraits( caps.promptInjectedEffortLevels.length > 0 && isClaudeUltrathinkPrompt(prompt); + // Check if "ultrathink" appears in the body text (not just our prefix) + const ultrathinkInBodyText = + ultrathinkPromptControlled && isClaudeUltrathinkPrompt(prompt.replace(/^Ultrathink:\s*/i, "")); + return { caps, effort, @@ -117,6 +121,7 @@ function getSelectedTraits( thinkingEnabled, fastModeEnabled, ultrathinkPromptControlled, + ultrathinkInBodyText, }; } @@ -160,12 +165,12 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ thinkingEnabled, fastModeEnabled, ultrathinkPromptControlled, + ultrathinkInBodyText, } = getSelectedTraits(provider, models, model, prompt, modelOptions, allowPromptInjectedEffort); const defaultEffort = getDefaultEffort(caps); const handleEffortChange = useCallback( (value: string) => { - if (ultrathinkPromptControlled) return; if (!value) return; const nextOption = effortLevels.find((option) => option.value === value); if (!nextOption) return; @@ -177,6 +182,11 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ onPromptChange(nextPrompt); return; } + if (ultrathinkInBodyText) return; + if (ultrathinkPromptControlled) { + const stripped = prompt.replace(/^Ultrathink:\s*/i, ""); + onPromptChange(stripped); + } const effortKey = provider === "codex" ? "reasoningEffort" : "effort"; updateModelOptions( buildNextOptions(provider, modelOptions, { [effortKey]: nextOption.value }), @@ -184,6 +194,7 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ }, [ ultrathinkPromptControlled, + ultrathinkInBodyText, modelOptions, onPromptChange, updateModelOptions, @@ -204,17 +215,20 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ <>
Effort
- {ultrathinkPromptControlled ? ( + {ultrathinkInBodyText ? (
- Remove Ultrathink from the prompt to change effort. + Your prompt contains "ultrathink" in the text. Remove it to change effort.
) : null} - + {effortLevels.map((option) => ( {option.label} {option.value === defaultEffort ? " (default)" : ""}