| author | |
| committer | |
| log | 61f983098be44316944e77e3bc81538a6a8fb0e9 |
| tree | 585b5ef2e9dbeb07ba43cb17372ef4b4b53f598b |
| parent | 32c1f98a02d5cf8e42d70dbd65822f9b08151024 |
| signature | Signed by PGP key B5690EEEBB952194 |
3 files changed, 60 insertions(+), 11 deletions(-)
apps/web/src/components/chat/CompactComposerControlsMenu.browser.tsx+22-3| ... | ... | @@ -220,7 +220,7 @@ describe("CompactComposerControlsMenu", () => { |
| 220 | 220 | }); |
| 221 | 221 | }); |
| 222 | 222 | |
| 223 | it("shows prompt-controlled Ultrathink messaging with disabled effort controls", async () => { | |
| 223 | it("shows prompt-controlled Ultrathink state with selectable effort controls", async () => { | |
| 224 | 224 | await using _ = await mountMenu({ |
| 225 | 225 | modelSelection: { |
| 226 | 226 | provider: "claudeAgent", |
| ... | ... | @@ -235,8 +235,27 @@ describe("CompactComposerControlsMenu", () => { |
| 235 | 235 | await vi.waitFor(() => { |
| 236 | 236 | const text = document.body.textContent ?? ""; |
| 237 | 237 | expect(text).toContain("Effort"); |
| 238 | expect(text).toContain("Remove Ultrathink from the prompt to change effort."); | |
| 239 | expect(text).not.toContain("Fallback Effort"); | |
| 238 | expect(text).not.toContain("ultrathink"); | |
| 239 | }); | |
| 240 | }); | |
| 241 | ||
| 242 | it("warns when ultrathink appears in prompt body text", async () => { | |
| 243 | await using _ = await mountMenu({ | |
| 244 | modelSelection: { | |
| 245 | provider: "claudeAgent", | |
| 246 | model: "claude-opus-4-6", | |
| 247 | options: { effort: "high" }, | |
| 248 | }, | |
| 249 | prompt: "Ultrathink:\nplease ultrathink about this problem", | |
| 250 | }); | |
| 251 | ||
| 252 | await page.getByLabelText("More composer controls").click(); | |
| 253 | ||
| 254 | await vi.waitFor(() => { | |
| 255 | const text = document.body.textContent ?? ""; | |
| 256 | expect(text).toContain( | |
| 257 | 'Your prompt contains "ultrathink" in the text. Remove it to change effort.', | |
| 258 | ); | |
| 240 | 259 | }); |
| 241 | 260 | }); |
| 242 | 261 | }); |
apps/web/src/components/chat/TraitsPicker.browser.tsx+19-3| ... | ... | @@ -296,7 +296,7 @@ describe("TraitsPicker (Claude)", () => { |
| 296 | 296 | }); |
| 297 | 297 | }); |
| 298 | 298 | |
| 299 | it("shows prompt-controlled Ultrathink state with disabled effort controls", async () => { | |
| 299 | it("shows prompt-controlled Ultrathink state with selectable effort controls", async () => { | |
| 300 | 300 | await using _ = await mountClaudePicker({ |
| 301 | 301 | model: "claude-opus-4-6", |
| 302 | 302 | options: { effort: "high" }, |
| ... | ... | @@ -312,8 +312,24 @@ describe("TraitsPicker (Claude)", () => { |
| 312 | 312 | await vi.waitFor(() => { |
| 313 | 313 | const text = document.body.textContent ?? ""; |
| 314 | 314 | expect(text).toContain("Effort"); |
| 315 | expect(text).toContain("Remove Ultrathink from the prompt to change effort."); | |
| 316 | expect(text).not.toContain("Fallback Effort"); | |
| 315 | expect(text).not.toContain("ultrathink"); | |
| 316 | }); | |
| 317 | }); | |
| 318 | ||
| 319 | it("warns when ultrathink appears in prompt body text", async () => { | |
| 320 | await using _ = await mountClaudePicker({ | |
| 321 | model: "claude-opus-4-6", | |
| 322 | options: { effort: "high" }, | |
| 323 | prompt: "Ultrathink:\nplease ultrathink about this problem", | |
| 324 | }); | |
| 325 | ||
| 326 | await page.getByRole("button").click(); | |
| 327 | ||
| 328 | await vi.waitFor(() => { | |
| 329 | const text = document.body.textContent ?? ""; | |
| 330 | expect(text).toContain( | |
| 331 | 'Your prompt contains "ultrathink" in the text. Remove it to change effort.', | |
| 332 | ); | |
| 317 | 333 | }); |
| 318 | 334 | }); |
| 319 | 335 |
apps/web/src/components/chat/TraitsPicker.tsx+19-5| ... | ... | @@ -120,6 +120,10 @@ function getSelectedTraits( |
| 120 | 120 | caps.promptInjectedEffortLevels.length > 0 && |
| 121 | 121 | isClaudeUltrathinkPrompt(prompt); |
| 122 | 122 | |
| 123 | // Check if "ultrathink" appears in the body text (not just our prefix) | |
| 124 | const ultrathinkInBodyText = | |
| 125 | ultrathinkPromptControlled && isClaudeUltrathinkPrompt(prompt.replace(/^Ultrathink:\s*/i, "")); | |
| 126 | ||
| 123 | 127 | return { |
| 124 | 128 | caps, |
| 125 | 129 | effort, |
| ... | ... | @@ -130,6 +134,7 @@ function getSelectedTraits( |
| 130 | 134 | contextWindow, |
| 131 | 135 | defaultContextWindow, |
| 132 | 136 | ultrathinkPromptControlled, |
| 137 | ultrathinkInBodyText, | |
| 133 | 138 | }; |
| 134 | 139 | } |
| 135 | 140 | |
| ... | ... | @@ -176,12 +181,12 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ |
| 176 | 181 | contextWindow, |
| 177 | 182 | defaultContextWindow, |
| 178 | 183 | ultrathinkPromptControlled, |
| 184 | ultrathinkInBodyText, | |
| 179 | 185 | } = getSelectedTraits(provider, models, model, prompt, modelOptions, allowPromptInjectedEffort); |
| 180 | 186 | const defaultEffort = getDefaultEffort(caps); |
| 181 | 187 | |
| 182 | 188 | const handleEffortChange = useCallback( |
| 183 | 189 | (value: string) => { |
| 184 | if (ultrathinkPromptControlled) return; | |
| 185 | 190 | if (!value) return; |
| 186 | 191 | const nextOption = effortLevels.find((option) => option.value === value); |
| 187 | 192 | if (!nextOption) return; |
| ... | ... | @@ -193,6 +198,11 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ |
| 193 | 198 | onPromptChange(nextPrompt); |
| 194 | 199 | return; |
| 195 | 200 | } |
| 201 | if (ultrathinkInBodyText) return; | |
| 202 | if (ultrathinkPromptControlled) { | |
| 203 | const stripped = prompt.replace(/^Ultrathink:\s*/i, ""); | |
| 204 | onPromptChange(stripped); | |
| 205 | } | |
| 196 | 206 | const effortKey = provider === "codex" ? "reasoningEffort" : "effort"; |
| 197 | 207 | updateModelOptions( |
| 198 | 208 | buildNextOptions(provider, modelOptions, { [effortKey]: nextOption.value }), |
| ... | ... | @@ -200,6 +210,7 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ |
| 200 | 210 | }, |
| 201 | 211 | [ |
| 202 | 212 | ultrathinkPromptControlled, |
| 213 | ultrathinkInBodyText, | |
| 203 | 214 | modelOptions, |
| 204 | 215 | onPromptChange, |
| 205 | 216 | updateModelOptions, |
| ... | ... | @@ -220,17 +231,20 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ |
| 220 | 231 | <> |
| 221 | 232 | <MenuGroup> |
| 222 | 233 | <div className="px-2 pt-1.5 pb-1 font-medium text-muted-foreground text-xs">Effort</div> |
| 223 | {ultrathinkPromptControlled ? ( | |
| 234 | {ultrathinkInBodyText ? ( | |
| 224 | 235 | <div className="px-2 pb-1.5 text-muted-foreground/80 text-xs"> |
| 225 | Remove Ultrathink from the prompt to change effort. | |
| 236 | Your prompt contains &quot;ultrathink&quot; in the text. Remove it to change effort. | |
| 226 | 237 | </div> |
| 227 | 238 | ) : null} |
| 228 | <MenuRadioGroup value={effort} onValueChange={handleEffortChange}> | |
| 239 | <MenuRadioGroup | |
| 240 | value={ultrathinkPromptControlled ? "ultrathink" : effort} | |
| 241 | onValueChange={handleEffortChange} | |
| 242 | > | |
| 229 | 243 | {effortLevels.map((option) => ( |
| 230 | 244 | <MenuRadioItem |
| 231 | 245 | key={option.value} |
| 232 | 246 | value={option.value} |
| 233 | disabled={ultrathinkPromptControlled} | |
| 247 | disabled={ultrathinkInBodyText} | |
| 234 | 248 | > |
| 235 | 249 | {option.label} |
| 236 | 250 | {option.value === defaultEffort ? " (default)" : ""} |