| ... | @@ -16,7 +16,7 @@ import { | ... | @@ -16,7 +16,7 @@ import { |
| 16 | } from "@t3tools/shared/model"; | 16 | } from "@t3tools/shared/model"; |
| 17 | import { memo, useCallback, useState } from "react"; | 17 | import { memo, useCallback, useState } from "react"; |
| 18 | import type { VariantProps } from "class-variance-authority"; | 18 | import type { VariantProps } from "class-variance-authority"; |
| 19 | import { ChevronDownIcon } from "lucide-react"; | 19 | import { ChevronDownIcon, ZapIcon } from "lucide-react"; |
| 20 | import { Button, buttonVariants } from "../ui/button"; | 20 | import { Button, buttonVariants } from "../ui/button"; |
| 21 | import { | 21 | import { |
| 22 | Menu, | 22 | Menu, |
| ... | @@ -379,6 +379,46 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ | ... | @@ -379,6 +379,46 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ |
| 379 | ); | 379 | ); |
| 380 | }); | 380 | }); |
| 381 | | 381 | |
| | 382 | /** |
| | 383 | * Build the traits trigger's text label plus whether the fast-mode bolt should |
| | 384 | * render. Fast mode is a lightning bolt when on and nothing at all when off — |
| | 385 | * "Normal" is the near-universal case and isn't worth the horizontal space. The |
| | 386 | * one exception is when fast mode is the only trait, where a bare bolt (or bare |
| | 387 | * chevron) would leave the trigger unreadable. |
| | 388 | */ |
| | 389 | export function buildTraitsTriggerDisplay(input: { |
| | 390 | descriptors: ReadonlyArray<ProviderOptionDescriptor>; |
| | 391 | primarySelectDescriptorId: string | null; |
| | 392 | ultrathinkPromptControlled: boolean; |
| | 393 | fastModeEnabled: boolean; |
| | 394 | }): { label: string; showFastModeIcon: boolean } { |
| | 395 | let hasFastMode = false; |
| | 396 | const labels: Array<string> = []; |
| | 397 | for (const descriptor of input.descriptors) { |
| | 398 | if (descriptor.id === "fastMode" && descriptor.type === "boolean") { |
| | 399 | hasFastMode = true; |
| | 400 | continue; |
| | 401 | } |
| | 402 | const label = |
| | 403 | input.ultrathinkPromptControlled && descriptor.id === input.primarySelectDescriptorId |
| | 404 | ? "Ultrathink" |
| | 405 | : descriptor.type === "boolean" |
| | 406 | ? `${descriptor.label} ${descriptor.currentValue === true ? "On" : "Off"}` |
| | 407 | : getProviderOptionCurrentLabel(descriptor); |
| | 408 | if (typeof label === "string" && label.length > 0) { |
| | 409 | labels.push(label); |
| | 410 | } |
| | 411 | } |
| | 412 | |
| | 413 | // Only fall back to text when fast mode is genuinely the sole trait. Keying |
| | 414 | // off an empty label list alone would also catch descriptors that resolved to |
| | 415 | // no label at all, printing a bogus "Normal" for a model without fast mode. |
| | 416 | if (labels.length === 0 && hasFastMode) { |
| | 417 | return { label: input.fastModeEnabled ? "Fast" : "Normal", showFastModeIcon: false }; |
| | 418 | } |
| | 419 | return { label: labels.join(" · "), showFastModeIcon: input.fastModeEnabled }; |
| | 420 | } |
| | 421 | |
| 382 | export const TraitsPicker = memo(function TraitsPicker({ | 422 | export const TraitsPicker = memo(function TraitsPicker({ |
| 383 | provider, | 423 | provider, |
| 384 | instanceId, | 424 | instanceId, |
| ... | @@ -393,7 +433,7 @@ export const TraitsPicker = memo(function TraitsPicker({ | ... | @@ -393,7 +433,7 @@ export const TraitsPicker = memo(function TraitsPicker({ |
| 393 | ...persistence | 433 | ...persistence |
| 394 | }: TraitsMenuContentProps & TraitsPersistence) { | 434 | }: TraitsMenuContentProps & TraitsPersistence) { |
| 395 | const [isMenuOpen, setIsMenuOpen] = useState(false); | 435 | const [isMenuOpen, setIsMenuOpen] = useState(false); |
| 396 | const { descriptors, primarySelectDescriptor, ultrathinkPromptControlled } = | 436 | const { descriptors, primarySelectDescriptor, ultrathinkPromptControlled, fastModeEnabled } = |
| 397 | getTraitsSectionVisibility({ | 437 | getTraitsSectionVisibility({ |
| 398 | provider, | 438 | provider, |
| 399 | models, | 439 | models, |
| ... | @@ -415,23 +455,18 @@ export const TraitsPicker = memo(function TraitsPicker({ | ... | @@ -415,23 +455,18 @@ export const TraitsPicker = memo(function TraitsPicker({ |
| 415 | return null; | 455 | return null; |
| 416 | } | 456 | } |
| 417 | | 457 | |
| 418 | const triggerLabels: Array<string> = []; | 458 | const { label: triggerLabel, showFastModeIcon } = buildTraitsTriggerDisplay({ |
| 419 | for (const descriptor of descriptors) { | 459 | descriptors, |
| 420 | const label = | 460 | primarySelectDescriptorId: primarySelectDescriptor?.id ?? null, |
| 421 | ultrathinkPromptControlled && descriptor.id === primarySelectDescriptor?.id | 461 | ultrathinkPromptControlled, |
| 422 | ? "Ultrathink" | 462 | fastModeEnabled, |
| 423 | : descriptor.type === "boolean" | 463 | }); |
| 424 | ? descriptor.id === "fastMode" | 464 | const fastModeIcon = showFastModeIcon ? ( |
| 425 | ? descriptor.currentValue === true | 465 | <> |
| 426 | ? "Fast" | 466 | <ZapIcon aria-hidden="true" className="size-3 shrink-0 text-foreground/80 opacity-100" /> |
| 427 | : "Normal" | 467 | <span className="sr-only">Fast mode on</span> |
| 428 | : `${descriptor.label} ${descriptor.currentValue === true ? "On" : "Off"}` | 468 | </> |
| 429 | : getProviderOptionCurrentLabel(descriptor); | 469 | ) : null; |
| 430 | if (typeof label === "string" && label.length > 0) { | | |
| 431 | triggerLabels.push(label); | | |
| 432 | } | | |
| 433 | } | | |
| 434 | const triggerLabel = triggerLabels.join(" · "); | | |
| 435 | | 470 | |
| 436 | const isCodexStyle = provider === "codex"; | 471 | const isCodexStyle = provider === "codex"; |
| 437 | | 472 | |
| ... | @@ -457,12 +492,14 @@ export const TraitsPicker = memo(function TraitsPicker({ | ... | @@ -457,12 +492,14 @@ export const TraitsPicker = memo(function TraitsPicker({ |
| 457 | } | 492 | } |
| 458 | > | 493 | > |
| 459 | {isCodexStyle ? ( | 494 | {isCodexStyle ? ( |
| 460 | <span className="flex min-w-0 w-full items-center gap-2 overflow-hidden"> | 495 | <span className="flex min-w-0 w-full items-center gap-1.5 overflow-hidden"> |
| 461 | {triggerLabel} | 496 | {fastModeIcon} |
| | 497 | <span className="min-w-0 truncate">{triggerLabel}</span> |
| 462 | <ChevronDownIcon aria-hidden="true" className="size-3 shrink-0 opacity-60" /> | 498 | <ChevronDownIcon aria-hidden="true" className="size-3 shrink-0 opacity-60" /> |
| 463 | </span> | 499 | </span> |
| 464 | ) : ( | 500 | ) : ( |
| 465 | <> | 501 | <> |
| | 502 | {fastModeIcon} |
| 466 | <span>{triggerLabel}</span> | 503 | <span>{triggerLabel}</span> |
| 467 | <ChevronDownIcon aria-hidden="true" className="size-3 opacity-60" /> | 504 | <ChevronDownIcon aria-hidden="true" className="size-3 opacity-60" /> |
| 468 | </> | 505 | </> |