| author | |
| committer | |
| log | e22c8804346d4bd45a733803eda826e04e3bbd74 |
| tree | b0e03254002d46c31b8ac6b6b06d3824013e7a3f |
| parent | 7b797f2b90918e7bae0f710dcf24018b8b0843dc |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>3 files changed, 140 insertions(+), 119 deletions(-)
apps/web/src/components/Icons.tsx-7| ... | @@ -2,13 +2,6 @@ import React, { type SVGProps, useId } from "react"; | ... | @@ -2,13 +2,6 @@ import React, { type SVGProps, useId } from "react"; |
| 2 | import { cn } from "~/lib/utils"; | 2 | import { cn } from "~/lib/utils"; |
| 3 | export type Icon = React.FC<SVGProps<SVGSVGElement>>; | 3 | export type Icon = React.FC<SVGProps<SVGSVGElement>>; |
| 4 | 4 | ||
| 5 | export const UltrafastIcon: Icon = (props) => ( | ||
| 6 | <svg viewBox="0 0 24 24" fill="currentColor" {...props}> | ||
| 7 | <path d="m17 2-10 12h7l-1 8 10-12h-7l1-8Z" opacity="0.4" /> | ||
| 8 | <path d="m11 2-10 12h7l-1 8 10-12h-7l1-8Z" /> | ||
| 9 | </svg> | ||
| 10 | ); | ||
| 11 | |||
| 12 | export const FinderIcon: Icon = (props) => ( | 5 | export const FinderIcon: Icon = (props) => ( |
| 13 | <svg viewBox="0 0 24 24" fill="none" {...props}> | 6 | <svg viewBox="0 0 24 24" fill="none" {...props}> |
| 14 | <rect x="2" y="2" width="20" height="20" rx="4" fill="#36A9F5" /> | 7 | <rect x="2" y="2" width="20" height="20" rx="4" fill="#36A9F5" /> |
apps/web/src/components/chat/TraitsPicker.test.ts+108-58| ... | @@ -69,32 +69,108 @@ describe("buildTraitsTriggerDisplay", () => { | ... | @@ -69,32 +69,108 @@ describe("buildTraitsTriggerDisplay", () => { |
| 69 | it("omits fast mode from the label entirely when it is off", () => { | 69 | it("omits fast mode from the label entirely when it is off", () => { |
| 70 | expect(display([EFFORT, fastModeDescriptor(false), CONTEXT_WINDOW])).toEqual({ | 70 | expect(display([EFFORT, fastModeDescriptor(false), CONTEXT_WINDOW])).toEqual({ |
| 71 | label: "High · 1M", | 71 | label: "High · 1M", |
| 72 | speedIcon: null, | ||
| 73 | }); | 72 | }); |
| 74 | }); | 73 | }); |
| 75 | 74 | ||
| 76 | it("shows the bolt instead of a text label when fast mode is on", () => { | 75 | it("pairs fast mode with reasoning before the context window", () => { |
| 77 | expect(display([EFFORT, fastModeDescriptor(true), CONTEXT_WINDOW])).toEqual({ | 76 | expect(display([EFFORT, fastModeDescriptor(true), CONTEXT_WINDOW])).toEqual({ |
| 78 | label: "High · 1M", | 77 | label: "High Fast · 1M", |
| 79 | speedIcon: "fast", | 78 | }); |
| 79 | expect(display([EFFORT, CONTEXT_WINDOW, fastModeDescriptor(true)])).toEqual({ | ||
| 80 | label: "High Fast · 1M", | ||
| 80 | }); | 81 | }); |
| 81 | }); | 82 | }); |
| 82 | 83 | ||
| 83 | it("treats Codex standard and fast service tiers as fast mode states", () => { | 84 | it.each(["reasoningEffort", "reasoning", "effort", "variant", "thinking"])( |
| 84 | expect(display([EFFORT, serviceTierDescriptor("default")])).toEqual({ | 85 | "pairs Fast with %s even when context is the primary select", |
| 85 | label: "High", | 86 | (id) => { |
| 86 | speedIcon: null, | 87 | for (const speed of [fastModeDescriptor(true), serviceTierDescriptor("priority")]) { |
| 87 | }); | 88 | expect( |
| 88 | expect(display([EFFORT, serviceTierDescriptor("priority")])).toEqual({ | 89 | buildTraitsTriggerDisplay({ |
| 89 | label: "High", | 90 | provider: CODEX, |
| 90 | speedIcon: "fast", | 91 | descriptors: [ |
| 92 | selectDescriptor("profile", [{ id: "balanced", label: "Balanced" }], "balanced"), | ||
| 93 | CONTEXT_WINDOW, | ||
| 94 | { ...EFFORT, id }, | ||
| 95 | speed, | ||
| 96 | ], | ||
| 97 | primarySelectDescriptorId: "profile", | ||
| 98 | ultrathinkPromptControlled: false, | ||
| 99 | }), | ||
| 100 | ).toEqual({ label: "Balanced · 1M · High Fast" }); | ||
| 101 | } | ||
| 102 | expect( | ||
| 103 | buildTraitsTriggerDisplay({ | ||
| 104 | provider: CODEX, | ||
| 105 | descriptors: [CONTEXT_WINDOW, { ...EFFORT, id }, serviceTierDescriptor("ultrafast")], | ||
| 106 | primarySelectDescriptorId: CONTEXT_WINDOW.id, | ||
| 107 | ultrathinkPromptControlled: false, | ||
| 108 | }), | ||
| 109 | ).toEqual({ label: "1M · High Ultrafast" }); | ||
| 110 | }, | ||
| 111 | ); | ||
| 112 | |||
| 113 | it("pairs Cursor Fast with reasoning rather than the thinking toggle", () => { | ||
| 114 | expect( | ||
| 115 | buildTraitsTriggerDisplay({ | ||
| 116 | provider: ProviderDriverKind.make("cursor"), | ||
| 117 | descriptors: [ | ||
| 118 | { ...EFFORT, id: "reasoning" }, | ||
| 119 | fastModeDescriptor(true), | ||
| 120 | { id: "thinking", label: "Thinking", type: "boolean", currentValue: true }, | ||
| 121 | ], | ||
| 122 | primarySelectDescriptorId: "reasoning", | ||
| 123 | ultrathinkPromptControlled: false, | ||
| 124 | }), | ||
| 125 | ).toEqual({ label: "High Fast · Thinking On" }); | ||
| 126 | }); | ||
| 127 | |||
| 128 | it.each([true, false])("keeps boolean thinking %s independent of Fast", (currentValue) => { | ||
| 129 | const thinking = { | ||
| 130 | id: "thinking", | ||
| 131 | label: "Thinking", | ||
| 132 | type: "boolean" as const, | ||
| 133 | currentValue, | ||
| 134 | }; | ||
| 135 | const label = `Thinking ${currentValue ? "On" : "Off"}`; | ||
| 136 | expect(display([thinking, fastModeDescriptor(true)])).toEqual({ label: `${label} · Fast` }); | ||
| 137 | expect(display([thinking, { ...EFFORT, id: "reasoning" }, fastModeDescriptor(true)])).toEqual({ | ||
| 138 | label: `${label} · High Fast`, | ||
| 91 | }); | 139 | }); |
| 140 | expect(display([thinking, fastModeDescriptor(false)])).toEqual({ label }); | ||
| 141 | }); | ||
| 142 | |||
| 143 | it("keeps speed separate when there is no reasoning descriptor", () => { | ||
| 144 | expect(display([CONTEXT_WINDOW, fastModeDescriptor(true)])).toEqual({ label: "1M · Fast" }); | ||
| 92 | }); | 145 | }); |
| 93 | 146 | ||
| 94 | it("uses a distinct double bolt for Codex Ultrafast", () => { | 147 | it.each(["Low", "Medium", "High", "Extra High", "Max", "Ultra"])( |
| 148 | "preserves %s reasoning with Fast across harnesses", | ||
| 149 | (label) => { | ||
| 150 | for (const provider of ["codex", "claudeAgent", "cursor"]) { | ||
| 151 | expect( | ||
| 152 | buildTraitsTriggerDisplay({ | ||
| 153 | provider: ProviderDriverKind.make(provider), | ||
| 154 | descriptors: [ | ||
| 155 | { ...EFFORT, options: [{ id: "effort", label }], currentValue: "effort" }, | ||
| 156 | fastModeDescriptor(true), | ||
| 157 | ], | ||
| 158 | primarySelectDescriptorId: EFFORT.id, | ||
| 159 | ultrathinkPromptControlled: false, | ||
| 160 | }), | ||
| 161 | ).toEqual({ label: `${label} Fast` }); | ||
| 162 | } | ||
| 163 | }, | ||
| 164 | ); | ||
| 165 | |||
| 166 | it("treats Codex standard and fast service tiers as fast mode states", () => { | ||
| 167 | expect(display([EFFORT, serviceTierDescriptor("default")])).toEqual({ label: "High" }); | ||
| 168 | expect(display([EFFORT, serviceTierDescriptor("priority")])).toEqual({ label: "High Fast" }); | ||
| 169 | }); | ||
| 170 | |||
| 171 | it("keeps Codex Ultrafast distinct from Fast", () => { | ||
| 95 | expect(display([EFFORT, serviceTierDescriptor("ultrafast")])).toEqual({ | 172 | expect(display([EFFORT, serviceTierDescriptor("ultrafast")])).toEqual({ |
| 96 | label: "High", | 173 | label: "High Ultrafast", |
| 97 | speedIcon: "ultrafast", | ||
| 98 | }); | 174 | }); |
| 99 | }); | 175 | }); |
| 100 | 176 | ||
| ... | @@ -105,17 +181,11 @@ describe("buildTraitsTriggerDisplay", () => { | ... | @@ -105,17 +181,11 @@ describe("buildTraitsTriggerDisplay", () => { |
| 105 | EFFORT, | 181 | EFFORT, |
| 106 | { ...descriptor, options: descriptor.options.filter(({ id }) => id !== "priority") }, | 182 | { ...descriptor, options: descriptor.options.filter(({ id }) => id !== "priority") }, |
| 107 | ]), | 183 | ]), |
| 108 | ).toEqual({ | 184 | ).toEqual({ label: "High Ultrafast" }); |
| 109 | label: "High", | ||
| 110 | speedIcon: "ultrafast", | ||
| 111 | }); | ||
| 112 | }); | 185 | }); |
| 113 | 186 | ||
| 114 | it("keeps other Codex service tiers in the label", () => { | 187 | it("keeps other Codex service tiers in the label", () => { |
| 115 | expect(display([EFFORT, serviceTierDescriptor("flex")])).toEqual({ | 188 | expect(display([EFFORT, serviceTierDescriptor("flex")])).toEqual({ label: "High · Flex" }); |
| 116 | label: "High · Flex", | ||
| 117 | speedIcon: null, | ||
| 118 | }); | ||
| 119 | }); | 189 | }); |
| 120 | 190 | ||
| 121 | it("keeps Standard as text for models without speed tiers", () => { | 191 | it("keeps Standard as text for models without speed tiers", () => { |
| ... | @@ -124,32 +194,17 @@ describe("buildTraitsTriggerDisplay", () => { | ... | @@ -124,32 +194,17 @@ describe("buildTraitsTriggerDisplay", () => { |
| 124 | ...descriptor, | 194 | ...descriptor, |
| 125 | options: descriptor.options.filter(({ id }) => id === "default" || id === "flex"), | 195 | options: descriptor.options.filter(({ id }) => id === "default" || id === "flex"), |
| 126 | }; | 196 | }; |
| 127 | expect(display([EFFORT, nonSpeedDescriptor])).toEqual({ | 197 | expect(display([EFFORT, nonSpeedDescriptor])).toEqual({ label: "High · Standard" }); |
| 128 | label: "High · Standard", | 198 | expect(display([nonSpeedDescriptor])).toEqual({ label: "Standard" }); |
| 129 | speedIcon: null, | ||
| 130 | }); | ||
| 131 | expect(display([nonSpeedDescriptor])).toEqual({ | ||
| 132 | label: "Standard", | ||
| 133 | speedIcon: null, | ||
| 134 | }); | ||
| 135 | }); | 199 | }); |
| 136 | 200 | ||
| 137 | it("keeps the Codex service tier readable when it is the only trait", () => { | 201 | it("keeps the Codex service tier readable when it is the only trait", () => { |
| 138 | expect(display([serviceTierDescriptor("default")])).toEqual({ | 202 | expect(display([serviceTierDescriptor("default")])).toEqual({ label: "Standard" }); |
| 139 | label: "Standard", | 203 | expect(display([serviceTierDescriptor("priority")])).toEqual({ label: "Fast" }); |
| 140 | speedIcon: null, | ||
| 141 | }); | ||
| 142 | expect(display([serviceTierDescriptor("priority")])).toEqual({ | ||
| 143 | label: "Fast", | ||
| 144 | speedIcon: null, | ||
| 145 | }); | ||
| 146 | }); | 204 | }); |
| 147 | 205 | ||
| 148 | it("keeps Ultrafast readable when it is the only trait", () => { | 206 | it("keeps Ultrafast readable when it is the only trait", () => { |
| 149 | expect(display([serviceTierDescriptor("ultrafast")])).toEqual({ | 207 | expect(display([serviceTierDescriptor("ultrafast")])).toEqual({ label: "Ultrafast" }); |
| 150 | label: "Ultrafast", | ||
| 151 | speedIcon: null, | ||
| 152 | }); | ||
| 153 | }); | 208 | }); |
| 154 | 209 | ||
| 155 | it("keeps non-fastMode booleans as text labels", () => { | 210 | it("keeps non-fastMode booleans as text labels", () => { |
| ... | @@ -159,21 +214,16 @@ describe("buildTraitsTriggerDisplay", () => { | ... | @@ -159,21 +214,16 @@ describe("buildTraitsTriggerDisplay", () => { |
| 159 | type: "boolean", | 214 | type: "boolean", |
| 160 | currentValue: true, | 215 | currentValue: true, |
| 161 | }; | 216 | }; |
| 162 | expect(display([EFFORT, thinking])).toEqual({ | 217 | expect(display([EFFORT, thinking])).toEqual({ label: "High · Thinking On" }); |
| 163 | label: "High · Thinking On", | ||
| 164 | speedIcon: null, | ||
| 165 | }); | ||
| 166 | }); | 218 | }); |
| 167 | 219 | ||
| 168 | it("falls back to a text label when fast mode is the only trait", () => { | 220 | it("falls back to a text label when fast mode is the only trait", () => { |
| 169 | expect(display([fastModeDescriptor(true)])).toEqual({ | 221 | expect(display([fastModeDescriptor(true)])).toEqual({ label: "Fast" }); |
| 170 | label: "Fast", | 222 | expect(display([fastModeDescriptor(false)])).toEqual({ label: "Normal" }); |
| 171 | speedIcon: null, | 223 | }); |
| 172 | }); | 224 | |
| 173 | expect(display([fastModeDescriptor(false)])).toEqual({ | 225 | it("does not add Fast to a model without a speed option", () => { |
| 174 | label: "Normal", | 226 | expect(display([EFFORT, CONTEXT_WINDOW])).toEqual({ label: "High · 1M" }); |
| 175 | speedIcon: null, | ||
| 176 | }); | ||
| 177 | }); | 227 | }); |
| 178 | 228 | ||
| 179 | it("stays blank when descriptors resolve to no label and there is no fast mode", () => { | 229 | it("stays blank when descriptors resolve to no label and there is no fast mode", () => { |
| ... | @@ -189,10 +239,10 @@ describe("buildTraitsTriggerDisplay", () => { | ... | @@ -189,10 +239,10 @@ describe("buildTraitsTriggerDisplay", () => { |
| 189 | { id: "high", label: "High" }, | 239 | { id: "high", label: "High" }, |
| 190 | ], | 240 | ], |
| 191 | }; | 241 | }; |
| 192 | expect(display([unresolved])).toEqual({ label: "", speedIcon: null }); | 242 | expect(display([unresolved])).toEqual({ label: "" }); |
| 193 | }); | 243 | }); |
| 194 | 244 | ||
| 195 | it("still renders the prompt-controlled ultrathink label alongside the bolt", () => { | 245 | it("still renders prompt-controlled ultrathink with Fast", () => { |
| 196 | expect( | 246 | expect( |
| 197 | buildTraitsTriggerDisplay({ | 247 | buildTraitsTriggerDisplay({ |
| 198 | provider: CODEX, | 248 | provider: CODEX, |
| ... | @@ -200,7 +250,7 @@ describe("buildTraitsTriggerDisplay", () => { | ... | @@ -200,7 +250,7 @@ describe("buildTraitsTriggerDisplay", () => { |
| 200 | primarySelectDescriptorId: "reasoningEffort", | 250 | primarySelectDescriptorId: "reasoningEffort", |
| 201 | ultrathinkPromptControlled: true, | 251 | ultrathinkPromptControlled: true, |
| 202 | }), | 252 | }), |
| 203 | ).toEqual({ label: "Ultrathink", speedIcon: "fast" }); | 253 | ).toEqual({ label: "Ultrathink Fast" }); |
| 204 | }); | 254 | }); |
| 205 | }); | 255 | }); |
| 206 | 256 |
apps/web/src/components/chat/TraitsPicker.tsx+32-54| ... | @@ -17,8 +17,7 @@ import { | ... | @@ -17,8 +17,7 @@ import { |
| 17 | normalizeModelSlug, | 17 | normalizeModelSlug, |
| 18 | } from "@t3tools/shared/model"; | 18 | } from "@t3tools/shared/model"; |
| 19 | import { memo, useCallback } from "react"; | 19 | import { memo, useCallback } from "react"; |
| 20 | import { BrainIcon, ZapIcon } from "lucide-react"; | 20 | import { BrainIcon } from "lucide-react"; |
| 21 | import { UltrafastIcon } from "../Icons"; | ||
| 22 | import { | 21 | import { |
| 23 | Menu, | 22 | Menu, |
| 24 | MenuGroup, | 23 | MenuGroup, |
| ... | @@ -485,10 +484,7 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ | ... | @@ -485,10 +484,7 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ |
| 485 | ); | 484 | ); |
| 486 | }); | 485 | }); |
| 487 | 486 | ||
| 488 | /** | 487 | /** Pair speed with reasoning while keeping other traits separated. */ |
| 489 | * Fast mode uses one bolt; Codex Ultrafast uses two. Keep a text label when | ||
| 490 | * speed is the only trait so the trigger remains readable. | ||
| 491 | */ | ||
| 492 | export function buildTraitsTriggerDisplay(input: { | 488 | export function buildTraitsTriggerDisplay(input: { |
| 493 | provider: ProviderDriverKind; | 489 | provider: ProviderDriverKind; |
| 494 | descriptors: ReadonlyArray<ProviderOptionDescriptor>; | 490 | descriptors: ReadonlyArray<ProviderOptionDescriptor>; |
| ... | @@ -496,14 +492,15 @@ export function buildTraitsTriggerDisplay(input: { | ... | @@ -496,14 +492,15 @@ export function buildTraitsTriggerDisplay(input: { |
| 496 | ultrathinkPromptControlled: boolean; | 492 | ultrathinkPromptControlled: boolean; |
| 497 | modelSelection?: ModelSelection | null; | 493 | modelSelection?: ModelSelection | null; |
| 498 | reportedModelSelection?: ModelSelection | null | undefined; | 494 | reportedModelSelection?: ModelSelection | null | undefined; |
| 499 | }): { label: string; speedIcon: "fast" | "ultrafast" | null } { | 495 | }): { label: string } { |
| 500 | let fastModeFallbackLabel: string | null = null; | 496 | let fastModeFallbackLabel: string | null = null; |
| 501 | let speedIcon: "fast" | "ultrafast" | null = null; | 497 | let speedLabel: string | null = null; |
| 498 | let reasoningLabelIndex = -1; | ||
| 502 | const labels: Array<string> = []; | 499 | const labels: Array<string> = []; |
| 503 | for (const descriptor of input.descriptors) { | 500 | for (const descriptor of input.descriptors) { |
| 504 | if (descriptor.id === "fastMode" && descriptor.type === "boolean") { | 501 | if (descriptor.id === "fastMode" && descriptor.type === "boolean") { |
| 505 | speedIcon = descriptor.currentValue === true ? "fast" : null; | 502 | speedLabel = descriptor.currentValue === true ? "Fast" : null; |
| 506 | fastModeFallbackLabel = speedIcon ? "Fast" : "Normal"; | 503 | fastModeFallbackLabel = speedLabel ?? "Normal"; |
| 507 | continue; | 504 | continue; |
| 508 | } | 505 | } |
| 509 | if ( | 506 | if ( |
| ... | @@ -519,11 +516,11 @@ export function buildTraitsTriggerDisplay(input: { | ... | @@ -519,11 +516,11 @@ export function buildTraitsTriggerDisplay(input: { |
| 519 | (fastTier && currentValue === fastTier.id) || | 516 | (fastTier && currentValue === fastTier.id) || |
| 520 | (ultrafastTier && currentValue === ultrafastTier.id) | 517 | (ultrafastTier && currentValue === ultrafastTier.id) |
| 521 | ) { | 518 | ) { |
| 522 | speedIcon = | 519 | speedLabel = |
| 523 | ultrafastTier && currentValue === ultrafastTier.id | 520 | ultrafastTier && currentValue === ultrafastTier.id |
| 524 | ? "ultrafast" | 521 | ? "Ultrafast" |
| 525 | : fastTier && currentValue === fastTier.id | 522 | : fastTier && currentValue === fastTier.id |
| 526 | ? "fast" | 523 | ? "Fast" |
| 527 | : null; | 524 | : null; |
| 528 | fastModeFallbackLabel = | 525 | fastModeFallbackLabel = |
| 529 | descriptor.options.find(({ id }) => id === currentValue)?.label ?? "Normal"; | 526 | descriptor.options.find(({ id }) => id === currentValue)?.label ?? "Normal"; |
| ... | @@ -541,6 +538,14 @@ export function buildTraitsTriggerDisplay(input: { | ... | @@ -541,6 +538,14 @@ export function buildTraitsTriggerDisplay(input: { |
| 541 | input.reportedModelSelection, | 538 | input.reportedModelSelection, |
| 542 | ); | 539 | ); |
| 543 | if (typeof label === "string" && label.length > 0) { | 540 | if (typeof label === "string" && label.length > 0) { |
| 541 | // Custom models retain descriptor order, so the primary select can be context. | ||
| 542 | if ( | ||
| 543 | reasoningLabelIndex === -1 && | ||
| 544 | descriptor.type === "select" && | ||
| 545 | ["reasoningEffort", "reasoning", "effort", "variant", "thinking"].includes(descriptor.id) | ||
| 546 | ) { | ||
| 547 | reasoningLabelIndex = labels.length; | ||
| 548 | } | ||
| 544 | labels.push(label); | 549 | labels.push(label); |
| 545 | } | 550 | } |
| 546 | } | 551 | } |
| ... | @@ -549,9 +554,16 @@ export function buildTraitsTriggerDisplay(input: { | ... | @@ -549,9 +554,16 @@ export function buildTraitsTriggerDisplay(input: { |
| 549 | // off an empty label list alone would also catch descriptors that resolved to | 554 | // off an empty label list alone would also catch descriptors that resolved to |
| 550 | // no label at all, printing a bogus "Normal" for a model without fast mode. | 555 | // no label at all, printing a bogus "Normal" for a model without fast mode. |
| 551 | if (labels.length === 0 && fastModeFallbackLabel !== null) { | 556 | if (labels.length === 0 && fastModeFallbackLabel !== null) { |
| 552 | return { label: fastModeFallbackLabel, speedIcon: null }; | 557 | return { label: fastModeFallbackLabel }; |
| 558 | } | ||
| 559 | if (speedLabel) { | ||
| 560 | if (reasoningLabelIndex >= 0) { | ||
| 561 | labels[reasoningLabelIndex] = `${labels[reasoningLabelIndex]} ${speedLabel}`; | ||
| 562 | } else { | ||
| 563 | labels.push(speedLabel); | ||
| 564 | } | ||
| 553 | } | 565 | } |
| 554 | return { label: labels.join(" · "), speedIcon }; | 566 | return { label: labels.join(" · ") }; |
| 555 | } | 567 | } |
| 556 | 568 | ||
| 557 | export const TraitsPicker = memo(function TraitsPicker({ | 569 | export const TraitsPicker = memo(function TraitsPicker({ |
| ... | @@ -601,7 +613,7 @@ export const TraitsPicker = memo(function TraitsPicker({ | ... | @@ -601,7 +613,7 @@ export const TraitsPicker = memo(function TraitsPicker({ |
| 601 | return null; | 613 | return null; |
| 602 | } | 614 | } |
| 603 | 615 | ||
| 604 | const { label: triggerLabel, speedIcon } = buildTraitsTriggerDisplay({ | 616 | const { label: triggerLabel } = buildTraitsTriggerDisplay({ |
| 605 | provider, | 617 | provider, |
| 606 | descriptors, | 618 | descriptors, |
| 607 | primarySelectDescriptorId: primarySelectDescriptor?.id ?? null, | 619 | primarySelectDescriptorId: primarySelectDescriptor?.id ?? null, |
| ... | @@ -609,26 +621,6 @@ export const TraitsPicker = memo(function TraitsPicker({ | ... | @@ -609,26 +621,6 @@ export const TraitsPicker = memo(function TraitsPicker({ |
| 609 | modelSelection: instanceId && model ? { instanceId, model, options: modelOptions ?? [] } : null, | 621 | modelSelection: instanceId && model ? { instanceId, model, options: modelOptions ?? [] } : null, |
| 610 | reportedModelSelection, | 622 | reportedModelSelection, |
| 611 | }); | 623 | }); |
| 612 | const speedLabel = speedIcon === "ultrafast" ? "Ultrafast mode on" : "Fast mode on"; | ||
| 613 | const accessibleLabel = speedIcon ? `${triggerLabel}, ${speedLabel}` : triggerLabel; | ||
| 614 | const fastModeIcon = speedIcon ? ( | ||
| 615 | <> | ||
| 616 | <ComposerControlIcon | ||
| 617 | icon={speedIcon === "ultrafast" ? UltrafastIcon : ZapIcon} | ||
| 618 | size={size} | ||
| 619 | className={cn( | ||
| 620 | "fill-current opacity-80", | ||
| 621 | size === "xs" | ||
| 622 | ? "text-current" | ||
| 623 | : provider === "claudeAgent" | ||
| 624 | ? "text-[#d97757]" | ||
| 625 | : "text-foreground", | ||
| 626 | )} | ||
| 627 | /> | ||
| 628 | <span className="sr-only">{speedLabel}</span> | ||
| 629 | </> | ||
| 630 | ) : null; | ||
| 631 | |||
| 632 | const isCodexStyle = provider === "codex"; | 624 | const isCodexStyle = provider === "codex"; |
| 633 | 625 | ||
| 634 | return ( | 626 | return ( |
| ... | @@ -644,7 +636,7 @@ export const TraitsPicker = memo(function TraitsPicker({ | ... | @@ -644,7 +636,7 @@ export const TraitsPicker = memo(function TraitsPicker({ |
| 644 | <MenuTrigger | 636 | <MenuTrigger |
| 645 | render={ | 637 | render={ |
| 646 | <ComposerControl | 638 | <ComposerControl |
| 647 | aria-label={accessibleLabel} | 639 | aria-label={triggerLabel} |
| 648 | data-composer-shortcut={isComposerOwned ? "composer.effort" : undefined} | 640 | data-composer-shortcut={isComposerOwned ? "composer.effort" : undefined} |
| 649 | size={size} | 641 | size={size} |
| 650 | className={cn( | 642 | className={cn( |
| ... | @@ -667,14 +659,7 @@ export const TraitsPicker = memo(function TraitsPicker({ | ... | @@ -667,14 +659,7 @@ export const TraitsPicker = memo(function TraitsPicker({ |
| 667 | size === "xs" ? "gap-1" : "gap-1.5", | 659 | size === "xs" ? "gap-1" : "gap-1.5", |
| 668 | )} | 660 | )} |
| 669 | > | 661 | > |
| 670 | {fastModeIcon ?? ( | 662 | <ComposerControlIcon icon={BrainIcon} size={size} /> |
| 671 | <span | ||
| 672 | data-composer-control-compact-icon | ||
| 673 | className="pointer-events-none invisible absolute" | ||
| 674 | > | ||
| 675 | <ComposerControlIcon icon={BrainIcon} size={size} /> | ||
| 676 | </span> | ||
| 677 | )} | ||
| 678 | <span data-composer-control-label className="min-w-0 truncate"> | 663 | <span data-composer-control-label className="min-w-0 truncate"> |
| 679 | {triggerLabel} | 664 | {triggerLabel} |
| 680 | </span> | 665 | </span> |
| ... | @@ -682,20 +667,13 @@ export const TraitsPicker = memo(function TraitsPicker({ | ... | @@ -682,20 +667,13 @@ export const TraitsPicker = memo(function TraitsPicker({ |
| 682 | </span> | 667 | </span> |
| 683 | ) : ( | 668 | ) : ( |
| 684 | <> | 669 | <> |
| 685 | {fastModeIcon ?? ( | 670 | <ComposerControlIcon icon={BrainIcon} size={size} /> |
| 686 | <span | ||
| 687 | data-composer-control-compact-icon | ||
| 688 | className="pointer-events-none invisible absolute" | ||
| 689 | > | ||
| 690 | <ComposerControlIcon icon={BrainIcon} size={size} /> | ||
| 691 | </span> | ||
| 692 | )} | ||
| 693 | <span data-composer-control-label>{triggerLabel}</span> | 671 | <span data-composer-control-label>{triggerLabel}</span> |
| 694 | <ComposerControlChevron size={size} /> | 672 | <ComposerControlChevron size={size} /> |
| 695 | </> | 673 | </> |
| 696 | )} | 674 | )} |
| 697 | </TooltipTrigger> | 675 | </TooltipTrigger> |
| 698 | <TooltipPopup side="top">{accessibleLabel}</TooltipPopup> | 676 | <TooltipPopup side="top">{triggerLabel}</TooltipPopup> |
| 699 | </Tooltip> | 677 | </Tooltip> |
| 700 | <MenuPopup align="start" {...(isComposerOwned ? composerFloatingLayerProps : {})}> | 678 | <MenuPopup align="start" {...(isComposerOwned ? composerFloatingLayerProps : {})}> |
| 701 | <TraitsMenuContent | 679 | <TraitsMenuContent |