From ad80210907cba7fe54e063a891877ac7a0caf7f8 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 01a5d32d6..f8093e872 100644 --- a/apps/web/src/components/chat/CompactComposerControlsMenu.browser.tsx +++ b/apps/web/src/components/chat/CompactComposerControlsMenu.browser.tsx @@ -151,7 +151,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", @@ -166,8 +166,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 811ad5bb3..806eff3af 100644 --- a/apps/web/src/components/chat/TraitsPicker.browser.tsx +++ b/apps/web/src/components/chat/TraitsPicker.browser.tsx @@ -195,7 +195,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" }, @@ -211,8 +211,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 e43c09428..b0dc4a3cb 100644 --- a/apps/web/src/components/chat/TraitsPicker.tsx +++ b/apps/web/src/components/chat/TraitsPicker.tsx @@ -90,6 +90,10 @@ function getSelectedTraits( const ultrathinkPromptControlled = 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, @@ -97,6 +101,7 @@ function getSelectedTraits( thinkingEnabled, fastModeEnabled, ultrathinkPromptControlled, + ultrathinkInBodyText, }; } @@ -125,12 +130,12 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ thinkingEnabled, fastModeEnabled, ultrathinkPromptControlled, + ultrathinkInBodyText, } = getSelectedTraits(provider, model, prompt, modelOptions); 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; @@ -142,6 +147,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"; setProviderModelOptions( threadId, @@ -152,6 +162,7 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ }, [ ultrathinkPromptControlled, + ultrathinkInBodyText, modelOptions, onPromptChange, threadId, @@ -173,17 +184,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)" : ""}