| author | |
| committer | |
| log | 792c7dd127865fc542a453ad944b94ae85ead10a |
| tree | 4b1e8bee80d726d63663783533685669d2725fbe |
| parent | 35be904f2fc40aa6d7a42778b6895e8274f3097f |
| signature | Signed by PGP key B5690EEEBB952194 |
4 files changed, 95 insertions(+), 37 deletions(-)
apps/web/src/components/Icons.tsx+7| ... | @@ -2,6 +2,13 @@ import React, { type SVGProps, useId } from "react"; | ... | @@ -2,6 +2,13 @@ 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 | |||
| 5 | export const FinderIcon: Icon = (props) => ( | 12 | export const FinderIcon: Icon = (props) => ( |
| 6 | <svg viewBox="0 0 24 24" fill="none" {...props}> | 13 | <svg viewBox="0 0 24 24" fill="none" {...props}> |
| 7 | <rect x="2" y="2" width="20" height="20" rx="4" fill="#36A9F5" /> | 14 | <rect x="2" y="2" width="20" height="20" rx="4" fill="#36A9F5" /> |
apps/web/src/components/chat/ComposerControl.tsx+3-3| ... | @@ -1,8 +1,8 @@ | ... | @@ -1,8 +1,8 @@ |
| 1 | import { mergeProps } from "@base-ui/react/merge-props"; | 1 | import { mergeProps } from "@base-ui/react/merge-props"; |
| 2 | import { Select as SelectPrimitive } from "@base-ui/react/select"; | 2 | import { Select as SelectPrimitive } from "@base-ui/react/select"; |
| 3 | import { useRender } from "@base-ui/react/use-render"; | 3 | import { useRender } from "@base-ui/react/use-render"; |
| 4 | import type { ComponentProps } from "react"; | 4 | import type { ComponentProps, ComponentType, SVGProps } from "react"; |
| 5 | import { ChevronDownIcon, type LucideIcon } from "lucide-react"; | 5 | import { ChevronDownIcon } from "lucide-react"; |
| 6 | 6 | ||
| 7 | import { cn } from "~/lib/utils"; | 7 | import { cn } from "~/lib/utils"; |
| 8 | import { Separator } from "../ui/separator"; | 8 | import { Separator } from "../ui/separator"; |
| ... | @@ -52,7 +52,7 @@ export function ComposerControlIcon({ | ... | @@ -52,7 +52,7 @@ export function ComposerControlIcon({ |
| 52 | opticalSize = "default", | 52 | opticalSize = "default", |
| 53 | size = "sm", | 53 | size = "sm", |
| 54 | }: { | 54 | }: { |
| 55 | icon: LucideIcon; | 55 | icon: ComponentType<SVGProps<SVGSVGElement>>; |
| 56 | className?: string | undefined; | 56 | className?: string | undefined; |
| 57 | opticalSize?: "default" | "large"; | 57 | opticalSize?: "default" | "large"; |
| 58 | size?: ComposerControlSize; | 58 | size?: ComposerControlSize; |
apps/web/src/components/chat/TraitsPicker.test.ts+57-13| ... | @@ -17,7 +17,7 @@ function fastModeDescriptor( | ... | @@ -17,7 +17,7 @@ function fastModeDescriptor( |
| 17 | } | 17 | } |
| 18 | 18 | ||
| 19 | function serviceTierDescriptor( | 19 | function serviceTierDescriptor( |
| 20 | currentValue: "default" | "priority" | "flex", | 20 | currentValue: "default" | "priority" | "ultrafast" | "flex", |
| 21 | ): Extract<ProviderOptionDescriptor, { type: "select" }> { | 21 | ): Extract<ProviderOptionDescriptor, { type: "select" }> { |
| 22 | return { | 22 | return { |
| 23 | id: "serviceTier", | 23 | id: "serviceTier", |
| ... | @@ -26,6 +26,7 @@ function serviceTierDescriptor( | ... | @@ -26,6 +26,7 @@ function serviceTierDescriptor( |
| 26 | options: [ | 26 | options: [ |
| 27 | { id: "default", label: "Standard", isDefault: true }, | 27 | { id: "default", label: "Standard", isDefault: true }, |
| 28 | { id: "priority", label: "Fast" }, | 28 | { id: "priority", label: "Fast" }, |
| 29 | { id: "ultrafast", label: "Ultrafast" }, | ||
| 29 | { id: "flex", label: "Flex" }, | 30 | { id: "flex", label: "Flex" }, |
| 30 | ], | 31 | ], |
| 31 | currentValue, | 32 | currentValue, |
| ... | @@ -64,43 +65,86 @@ describe("buildTraitsTriggerDisplay", () => { | ... | @@ -64,43 +65,86 @@ describe("buildTraitsTriggerDisplay", () => { |
| 64 | it("omits fast mode from the label entirely when it is off", () => { | 65 | it("omits fast mode from the label entirely when it is off", () => { |
| 65 | expect(display([EFFORT, fastModeDescriptor(false), CONTEXT_WINDOW])).toEqual({ | 66 | expect(display([EFFORT, fastModeDescriptor(false), CONTEXT_WINDOW])).toEqual({ |
| 66 | label: "High · 1M", | 67 | label: "High · 1M", |
| 67 | showFastModeIcon: false, | 68 | speedIcon: null, |
| 68 | }); | 69 | }); |
| 69 | }); | 70 | }); |
| 70 | 71 | ||
| 71 | it("shows the bolt instead of a text label when fast mode is on", () => { | 72 | it("shows the bolt instead of a text label when fast mode is on", () => { |
| 72 | expect(display([EFFORT, fastModeDescriptor(true), CONTEXT_WINDOW])).toEqual({ | 73 | expect(display([EFFORT, fastModeDescriptor(true), CONTEXT_WINDOW])).toEqual({ |
| 73 | label: "High · 1M", | 74 | label: "High · 1M", |
| 74 | showFastModeIcon: true, | 75 | speedIcon: "fast", |
| 75 | }); | 76 | }); |
| 76 | }); | 77 | }); |
| 77 | 78 | ||
| 78 | it("treats Codex standard and fast service tiers as fast mode states", () => { | 79 | it("treats Codex standard and fast service tiers as fast mode states", () => { |
| 79 | expect(display([EFFORT, serviceTierDescriptor("default")])).toEqual({ | 80 | expect(display([EFFORT, serviceTierDescriptor("default")])).toEqual({ |
| 80 | label: "High", | 81 | label: "High", |
| 81 | showFastModeIcon: false, | 82 | speedIcon: null, |
| 82 | }); | 83 | }); |
| 83 | expect(display([EFFORT, serviceTierDescriptor("priority")])).toEqual({ | 84 | expect(display([EFFORT, serviceTierDescriptor("priority")])).toEqual({ |
| 84 | label: "High", | 85 | label: "High", |
| 85 | showFastModeIcon: true, | 86 | speedIcon: "fast", |
| 87 | }); | ||
| 88 | }); | ||
| 89 | |||
| 90 | it("uses a distinct double bolt for Codex Ultrafast", () => { | ||
| 91 | expect(display([EFFORT, serviceTierDescriptor("ultrafast")])).toEqual({ | ||
| 92 | label: "High", | ||
| 93 | speedIcon: "ultrafast", | ||
| 94 | }); | ||
| 95 | }); | ||
| 96 | |||
| 97 | it("uses Ultrafast without requiring a Fast tier", () => { | ||
| 98 | const descriptor = serviceTierDescriptor("ultrafast"); | ||
| 99 | expect( | ||
| 100 | display([ | ||
| 101 | EFFORT, | ||
| 102 | { ...descriptor, options: descriptor.options.filter(({ id }) => id !== "priority") }, | ||
| 103 | ]), | ||
| 104 | ).toEqual({ | ||
| 105 | label: "High", | ||
| 106 | speedIcon: "ultrafast", | ||
| 86 | }); | 107 | }); |
| 87 | }); | 108 | }); |
| 88 | 109 | ||
| 89 | it("keeps other Codex service tiers in the label", () => { | 110 | it("keeps other Codex service tiers in the label", () => { |
| 90 | expect(display([EFFORT, serviceTierDescriptor("flex")])).toEqual({ | 111 | expect(display([EFFORT, serviceTierDescriptor("flex")])).toEqual({ |
| 91 | label: "High · Flex", | 112 | label: "High · Flex", |
| 92 | showFastModeIcon: false, | 113 | speedIcon: null, |
| 114 | }); | ||
| 115 | }); | ||
| 116 | |||
| 117 | it("keeps Standard as text for models without speed tiers", () => { | ||
| 118 | const descriptor = serviceTierDescriptor("default"); | ||
| 119 | const nonSpeedDescriptor = { | ||
| 120 | ...descriptor, | ||
| 121 | options: descriptor.options.filter(({ id }) => id === "default" || id === "flex"), | ||
| 122 | }; | ||
| 123 | expect(display([EFFORT, nonSpeedDescriptor])).toEqual({ | ||
| 124 | label: "High · Standard", | ||
| 125 | speedIcon: null, | ||
| 126 | }); | ||
| 127 | expect(display([nonSpeedDescriptor])).toEqual({ | ||
| 128 | label: "Standard", | ||
| 129 | speedIcon: null, | ||
| 93 | }); | 130 | }); |
| 94 | }); | 131 | }); |
| 95 | 132 | ||
| 96 | it("keeps the Codex service tier readable when it is the only trait", () => { | 133 | it("keeps the Codex service tier readable when it is the only trait", () => { |
| 97 | expect(display([serviceTierDescriptor("default")])).toEqual({ | 134 | expect(display([serviceTierDescriptor("default")])).toEqual({ |
| 98 | label: "Standard", | 135 | label: "Standard", |
| 99 | showFastModeIcon: false, | 136 | speedIcon: null, |
| 100 | }); | 137 | }); |
| 101 | expect(display([serviceTierDescriptor("priority")])).toEqual({ | 138 | expect(display([serviceTierDescriptor("priority")])).toEqual({ |
| 102 | label: "Fast", | 139 | label: "Fast", |
| 103 | showFastModeIcon: false, | 140 | speedIcon: null, |
| 141 | }); | ||
| 142 | }); | ||
| 143 | |||
| 144 | it("keeps Ultrafast readable when it is the only trait", () => { | ||
| 145 | expect(display([serviceTierDescriptor("ultrafast")])).toEqual({ | ||
| 146 | label: "Ultrafast", | ||
| 147 | speedIcon: null, | ||
| 104 | }); | 148 | }); |
| 105 | }); | 149 | }); |
| 106 | 150 | ||
| ... | @@ -113,18 +157,18 @@ describe("buildTraitsTriggerDisplay", () => { | ... | @@ -113,18 +157,18 @@ describe("buildTraitsTriggerDisplay", () => { |
| 113 | }; | 157 | }; |
| 114 | expect(display([EFFORT, thinking])).toEqual({ | 158 | expect(display([EFFORT, thinking])).toEqual({ |
| 115 | label: "High · Thinking On", | 159 | label: "High · Thinking On", |
| 116 | showFastModeIcon: false, | 160 | speedIcon: null, |
| 117 | }); | 161 | }); |
| 118 | }); | 162 | }); |
| 119 | 163 | ||
| 120 | it("falls back to a text label when fast mode is the only trait", () => { | 164 | it("falls back to a text label when fast mode is the only trait", () => { |
| 121 | expect(display([fastModeDescriptor(true)])).toEqual({ | 165 | expect(display([fastModeDescriptor(true)])).toEqual({ |
| 122 | label: "Fast", | 166 | label: "Fast", |
| 123 | showFastModeIcon: false, | 167 | speedIcon: null, |
| 124 | }); | 168 | }); |
| 125 | expect(display([fastModeDescriptor(false)])).toEqual({ | 169 | expect(display([fastModeDescriptor(false)])).toEqual({ |
| 126 | label: "Normal", | 170 | label: "Normal", |
| 127 | showFastModeIcon: false, | 171 | speedIcon: null, |
| 128 | }); | 172 | }); |
| 129 | }); | 173 | }); |
| 130 | 174 | ||
| ... | @@ -141,7 +185,7 @@ describe("buildTraitsTriggerDisplay", () => { | ... | @@ -141,7 +185,7 @@ describe("buildTraitsTriggerDisplay", () => { |
| 141 | { id: "high", label: "High" }, | 185 | { id: "high", label: "High" }, |
| 142 | ], | 186 | ], |
| 143 | }; | 187 | }; |
| 144 | expect(display([unresolved])).toEqual({ label: "", showFastModeIcon: false }); | 188 | expect(display([unresolved])).toEqual({ label: "", speedIcon: null }); |
| 145 | }); | 189 | }); |
| 146 | 190 | ||
| 147 | it("still renders the prompt-controlled ultrathink label alongside the bolt", () => { | 191 | it("still renders the prompt-controlled ultrathink label alongside the bolt", () => { |
| ... | @@ -152,7 +196,7 @@ describe("buildTraitsTriggerDisplay", () => { | ... | @@ -152,7 +196,7 @@ describe("buildTraitsTriggerDisplay", () => { |
| 152 | primarySelectDescriptorId: "reasoningEffort", | 196 | primarySelectDescriptorId: "reasoningEffort", |
| 153 | ultrathinkPromptControlled: true, | 197 | ultrathinkPromptControlled: true, |
| 154 | }), | 198 | }), |
| 155 | ).toEqual({ label: "Ultrathink", showFastModeIcon: true }); | 199 | ).toEqual({ label: "Ultrathink", speedIcon: "fast" }); |
| 156 | }); | 200 | }); |
| 157 | }); | 201 | }); |
| 158 | 202 |
apps/web/src/components/chat/TraitsPicker.tsx+28-21| ... | @@ -17,6 +17,7 @@ import { | ... | @@ -17,6 +17,7 @@ import { |
| 17 | } from "@t3tools/shared/model"; | 17 | } from "@t3tools/shared/model"; |
| 18 | import { memo, useCallback } from "react"; | 18 | import { memo, useCallback } from "react"; |
| 19 | import { BrainIcon, ZapIcon } from "lucide-react"; | 19 | import { BrainIcon, ZapIcon } from "lucide-react"; |
| 20 | import { UltrafastIcon } from "../Icons"; | ||
| 20 | import { | 21 | import { |
| 21 | Menu, | 22 | Menu, |
| 22 | MenuGroup, | 23 | MenuGroup, |
| ... | @@ -474,26 +475,22 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ | ... | @@ -474,26 +475,22 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ |
| 474 | }); | 475 | }); |
| 475 | 476 | ||
| 476 | /** | 477 | /** |
| 477 | * Build the traits trigger's text label plus whether the fast-mode bolt should | 478 | * Fast mode uses one bolt; Codex Ultrafast uses two. Keep a text label when |
| 478 | * render. Claude and Cursor expose fast mode as a boolean, while Codex exposes | 479 | * speed is the only trait so the trigger remains readable. |
| 479 | * it through the Standard/Fast service tiers. In either form, fast mode is a | ||
| 480 | * lightning bolt when on and nothing at all when off. The one exception is when | ||
| 481 | * fast mode is the only trait, where a bare bolt (or bare chevron) would leave | ||
| 482 | * the trigger unreadable. | ||
| 483 | */ | 480 | */ |
| 484 | export function buildTraitsTriggerDisplay(input: { | 481 | export function buildTraitsTriggerDisplay(input: { |
| 485 | provider: ProviderDriverKind; | 482 | provider: ProviderDriverKind; |
| 486 | descriptors: ReadonlyArray<ProviderOptionDescriptor>; | 483 | descriptors: ReadonlyArray<ProviderOptionDescriptor>; |
| 487 | primarySelectDescriptorId: string | null; | 484 | primarySelectDescriptorId: string | null; |
| 488 | ultrathinkPromptControlled: boolean; | 485 | ultrathinkPromptControlled: boolean; |
| 489 | }): { label: string; showFastModeIcon: boolean } { | 486 | }): { label: string; speedIcon: "fast" | "ultrafast" | null } { |
| 490 | let fastModeFallbackLabel: string | null = null; | 487 | let fastModeFallbackLabel: string | null = null; |
| 491 | let fastModeEnabled = false; | 488 | let speedIcon: "fast" | "ultrafast" | null = null; |
| 492 | const labels: Array<string> = []; | 489 | const labels: Array<string> = []; |
| 493 | for (const descriptor of input.descriptors) { | 490 | for (const descriptor of input.descriptors) { |
| 494 | if (descriptor.id === "fastMode" && descriptor.type === "boolean") { | 491 | if (descriptor.id === "fastMode" && descriptor.type === "boolean") { |
| 495 | fastModeEnabled = descriptor.currentValue === true; | 492 | speedIcon = descriptor.currentValue === true ? "fast" : null; |
| 496 | fastModeFallbackLabel = fastModeEnabled ? "Fast" : "Normal"; | 493 | fastModeFallbackLabel = speedIcon ? "Fast" : "Normal"; |
| 497 | continue; | 494 | continue; |
| 498 | } | 495 | } |
| 499 | if ( | 496 | if ( |
| ... | @@ -503,11 +500,20 @@ export function buildTraitsTriggerDisplay(input: { | ... | @@ -503,11 +500,20 @@ export function buildTraitsTriggerDisplay(input: { |
| 503 | ) { | 500 | ) { |
| 504 | const currentValue = getProviderOptionCurrentValue(descriptor); | 501 | const currentValue = getProviderOptionCurrentValue(descriptor); |
| 505 | const fastTier = descriptor.options.find(({ label }) => label === "Fast"); | 502 | const fastTier = descriptor.options.find(({ label }) => label === "Fast"); |
| 506 | if (fastTier && (currentValue === "default" || currentValue === fastTier.id)) { | 503 | const ultrafastTier = descriptor.options.find(({ label }) => label === "Ultrafast"); |
| 507 | fastModeEnabled = currentValue === fastTier.id; | 504 | if ( |
| 505 | ((fastTier || ultrafastTier) && currentValue === "default") || | ||
| 506 | (fastTier && currentValue === fastTier.id) || | ||
| 507 | (ultrafastTier && currentValue === ultrafastTier.id) | ||
| 508 | ) { | ||
| 509 | speedIcon = | ||
| 510 | ultrafastTier && currentValue === ultrafastTier.id | ||
| 511 | ? "ultrafast" | ||
| 512 | : fastTier && currentValue === fastTier.id | ||
| 513 | ? "fast" | ||
| 514 | : null; | ||
| 508 | fastModeFallbackLabel = | 515 | fastModeFallbackLabel = |
| 509 | descriptor.options.find(({ id }) => id === currentValue)?.label ?? | 516 | descriptor.options.find(({ id }) => id === currentValue)?.label ?? "Normal"; |
| 510 | (fastModeEnabled ? "Fast" : "Normal"); | ||
| 511 | continue; | 517 | continue; |
| 512 | } | 518 | } |
| 513 | } | 519 | } |
| ... | @@ -526,9 +532,9 @@ export function buildTraitsTriggerDisplay(input: { | ... | @@ -526,9 +532,9 @@ export function buildTraitsTriggerDisplay(input: { |
| 526 | // off an empty label list alone would also catch descriptors that resolved to | 532 | // off an empty label list alone would also catch descriptors that resolved to |
| 527 | // no label at all, printing a bogus "Normal" for a model without fast mode. | 533 | // no label at all, printing a bogus "Normal" for a model without fast mode. |
| 528 | if (labels.length === 0 && fastModeFallbackLabel !== null) { | 534 | if (labels.length === 0 && fastModeFallbackLabel !== null) { |
| 529 | return { label: fastModeFallbackLabel, showFastModeIcon: false }; | 535 | return { label: fastModeFallbackLabel, speedIcon: null }; |
| 530 | } | 536 | } |
| 531 | return { label: labels.join(" · "), showFastModeIcon: fastModeEnabled }; | 537 | return { label: labels.join(" · "), speedIcon }; |
| 532 | } | 538 | } |
| 533 | 539 | ||
| 534 | export const TraitsPicker = memo(function TraitsPicker({ | 540 | export const TraitsPicker = memo(function TraitsPicker({ |
| ... | @@ -577,17 +583,18 @@ export const TraitsPicker = memo(function TraitsPicker({ | ... | @@ -577,17 +583,18 @@ export const TraitsPicker = memo(function TraitsPicker({ |
| 577 | return null; | 583 | return null; |
| 578 | } | 584 | } |
| 579 | 585 | ||
| 580 | const { label: triggerLabel, showFastModeIcon } = buildTraitsTriggerDisplay({ | 586 | const { label: triggerLabel, speedIcon } = buildTraitsTriggerDisplay({ |
| 581 | provider, | 587 | provider, |
| 582 | descriptors, | 588 | descriptors, |
| 583 | primarySelectDescriptorId: primarySelectDescriptor?.id ?? null, | 589 | primarySelectDescriptorId: primarySelectDescriptor?.id ?? null, |
| 584 | ultrathinkPromptControlled, | 590 | ultrathinkPromptControlled, |
| 585 | }); | 591 | }); |
| 586 | const accessibleLabel = showFastModeIcon ? `${triggerLabel}, Fast mode on` : triggerLabel; | 592 | const speedLabel = speedIcon === "ultrafast" ? "Ultrafast mode on" : "Fast mode on"; |
| 587 | const fastModeIcon = showFastModeIcon ? ( | 593 | const accessibleLabel = speedIcon ? `${triggerLabel}, ${speedLabel}` : triggerLabel; |
| 594 | const fastModeIcon = speedIcon ? ( | ||
| 588 | <> | 595 | <> |
| 589 | <ComposerControlIcon | 596 | <ComposerControlIcon |
| 590 | icon={ZapIcon} | 597 | icon={speedIcon === "ultrafast" ? UltrafastIcon : ZapIcon} |
| 591 | size={size} | 598 | size={size} |
| 592 | className={cn( | 599 | className={cn( |
| 593 | "fill-current opacity-80", | 600 | "fill-current opacity-80", |
| ... | @@ -598,7 +605,7 @@ export const TraitsPicker = memo(function TraitsPicker({ | ... | @@ -598,7 +605,7 @@ export const TraitsPicker = memo(function TraitsPicker({ |
| 598 | : "text-foreground", | 605 | : "text-foreground", |
| 599 | )} | 606 | )} |
| 600 | /> | 607 | /> |
| 601 | <span className="sr-only">Fast mode on</span> | 608 | <span className="sr-only">{speedLabel}</span> |
| 602 | </> | 609 | </> |
| 603 | ) : null; | 610 | ) : null; |
| 604 | 611 |