Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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.',
);
});
});
});
22 changes: 19 additions & 3 deletions apps/web/src/components/chat/TraitsPicker.browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand All @@ -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.',
);
});
});

Expand Down
24 changes: 19 additions & 5 deletions apps/web/src/components/chat/TraitsPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -130,6 +134,7 @@ function getSelectedTraits(
contextWindow,
defaultContextWindow,
ultrathinkPromptControlled,
ultrathinkInBodyText,
};
}

Expand Down Expand Up @@ -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;
Expand All @@ -193,13 +198,19 @@ 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 }),
);
},
[
ultrathinkPromptControlled,
ultrathinkInBodyText,
modelOptions,
onPromptChange,
updateModelOptions,
Expand All @@ -220,17 +231,20 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({
<>
<MenuGroup>
<div className="px-2 pt-1.5 pb-1 font-medium text-muted-foreground text-xs">Effort</div>
{ultrathinkPromptControlled ? (
{ultrathinkInBodyText ? (
<div className="px-2 pb-1.5 text-muted-foreground/80 text-xs">
Remove Ultrathink from the prompt to change effort.
Your prompt contains &quot;ultrathink&quot; in the text. Remove it to change effort.
</div>
) : null}
<MenuRadioGroup value={effort} onValueChange={handleEffortChange}>
<MenuRadioGroup
value={ultrathinkPromptControlled ? "ultrathink" : effort}
onValueChange={handleEffortChange}
>
{effortLevels.map((option) => (
<MenuRadioItem
key={option.value}
value={option.value}
disabled={ultrathinkPromptControlled}
disabled={ultrathinkInBodyText}
>
{option.label}
{option.value === defaultEffort ? " (default)" : ""}
Expand Down