diff --git a/apps/web/src/components/chat/CompactComposerControlsMenu.browser.tsx b/apps/web/src/components/chat/CompactComposerControlsMenu.browser.tsx index aa3550e007..eee6f885e9 100644 --- a/apps/web/src/components/chat/CompactComposerControlsMenu.browser.tsx +++ b/apps/web/src/components/chat/CompactComposerControlsMenu.browser.tsx @@ -220,7 +220,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", @@ -235,8 +235,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 99d09fd634..9dea3651ea 100644 --- a/apps/web/src/components/chat/TraitsPicker.browser.tsx +++ b/apps/web/src/components/chat/TraitsPicker.browser.tsx @@ -296,7 +296,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" }, @@ -312,8 +312,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 a3b6cbb48f..061594ad53 100644 --- a/apps/web/src/components/chat/TraitsPicker.tsx +++ b/apps/web/src/components/chat/TraitsPicker.tsx @@ -120,6 +120,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, @@ -130,6 +134,7 @@ function getSelectedTraits( contextWindow, defaultContextWindow, ultrathinkPromptControlled, + ultrathinkInBodyText, }; } @@ -176,12 +181,12 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ contextWindow, defaultContextWindow, 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; @@ -193,6 +198,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 }), @@ -200,6 +210,7 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ }, [ ultrathinkPromptControlled, + ultrathinkInBodyText, modelOptions, onPromptChange, updateModelOptions, @@ -220,17 +231,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)" : ""}