From d505694a9cf778a3840d969626d990d355aaeb34 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Fri, 24 Jul 2026 16:55:46 -0700 Subject: [PATCH 1/2] feat(web): show fast mode as a bolt instead of a "Normal" label The traits trigger spelled out the fast-mode state as text, so the near-universal off case burned horizontal space on the word "Normal". Drop fastMode from the trigger's text label entirely and render a lightning bolt only when it's on, matching how Codex presents it. The label-building logic moves into an exported pure function so it can be tested without mounting the menu. When fast mode is the only trait, fall back to the old Fast/Normal text so the trigger doesn't render as a bare bolt or bare chevron. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/components/chat/TraitsPicker.test.ts | 94 +++++++++++++++++++ apps/web/src/components/chat/TraitsPicker.tsx | 74 ++++++++++----- 2 files changed, 147 insertions(+), 21 deletions(-) create mode 100644 apps/web/src/components/chat/TraitsPicker.test.ts diff --git a/apps/web/src/components/chat/TraitsPicker.test.ts b/apps/web/src/components/chat/TraitsPicker.test.ts new file mode 100644 index 00000000000..1e5c581e0ad --- /dev/null +++ b/apps/web/src/components/chat/TraitsPicker.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, it } from "vite-plus/test"; +import type { ProviderOptionDescriptor } from "@t3tools/contracts"; +import { buildTraitsTriggerDisplay } from "./TraitsPicker"; + +function selectDescriptor( + id: string, + options: ReadonlyArray<{ id: string; label: string }>, + currentValue: string, +): Extract { + return { id, label: id, type: "select", options: [...options], currentValue }; +} + +function fastModeDescriptor( + currentValue: boolean, +): Extract { + return { id: "fastMode", label: "Fast Mode", type: "boolean", currentValue }; +} + +const EFFORT = selectDescriptor( + "effort", + [ + { id: "high", label: "High" }, + { id: "max", label: "Max" }, + ], + "high", +); +const CONTEXT_WINDOW = selectDescriptor( + "contextWindow", + [ + { id: "200k", label: "200k" }, + { id: "1m", label: "1M" }, + ], + "1m", +); + +function display(descriptors: ReadonlyArray, fastModeEnabled: boolean) { + return buildTraitsTriggerDisplay({ + descriptors, + primarySelectDescriptorId: "effort", + ultrathinkPromptControlled: false, + fastModeEnabled, + }); +} + +describe("buildTraitsTriggerDisplay", () => { + it("omits fast mode from the label entirely when it is off", () => { + expect(display([EFFORT, fastModeDescriptor(false), CONTEXT_WINDOW], false)).toEqual({ + label: "High · 1M", + showFastModeIcon: false, + }); + }); + + it("shows the bolt instead of a text label when fast mode is on", () => { + expect(display([EFFORT, fastModeDescriptor(true), CONTEXT_WINDOW], true)).toEqual({ + label: "High · 1M", + showFastModeIcon: true, + }); + }); + + it("keeps non-fastMode booleans as text labels", () => { + const thinking: Extract = { + id: "thinking", + label: "Thinking", + type: "boolean", + currentValue: true, + }; + expect(display([EFFORT, thinking], false)).toEqual({ + label: "High · Thinking On", + showFastModeIcon: false, + }); + }); + + it("falls back to a text label when fast mode is the only trait", () => { + expect(display([fastModeDescriptor(true)], true)).toEqual({ + label: "Fast", + showFastModeIcon: false, + }); + expect(display([fastModeDescriptor(false)], false)).toEqual({ + label: "Normal", + showFastModeIcon: false, + }); + }); + + it("still renders the prompt-controlled ultrathink label alongside the bolt", () => { + expect( + buildTraitsTriggerDisplay({ + descriptors: [EFFORT, fastModeDescriptor(true)], + primarySelectDescriptorId: "effort", + ultrathinkPromptControlled: true, + fastModeEnabled: true, + }), + ).toEqual({ label: "Ultrathink", showFastModeIcon: true }); + }); +}); diff --git a/apps/web/src/components/chat/TraitsPicker.tsx b/apps/web/src/components/chat/TraitsPicker.tsx index 60f7f828ad4..8a9630f32d5 100644 --- a/apps/web/src/components/chat/TraitsPicker.tsx +++ b/apps/web/src/components/chat/TraitsPicker.tsx @@ -16,7 +16,7 @@ import { } from "@t3tools/shared/model"; import { memo, useCallback, useState } from "react"; import type { VariantProps } from "class-variance-authority"; -import { ChevronDownIcon } from "lucide-react"; +import { ChevronDownIcon, ZapIcon } from "lucide-react"; import { Button, buttonVariants } from "../ui/button"; import { Menu, @@ -379,6 +379,41 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ ); }); +/** + * Build the traits trigger's text label plus whether the fast-mode bolt should + * render. Fast mode is a lightning bolt when on and nothing at all when off — + * "Normal" is the near-universal case and isn't worth the horizontal space. The + * one exception is when fast mode is the only trait, where a bare bolt (or bare + * chevron) would leave the trigger unreadable. + */ +export function buildTraitsTriggerDisplay(input: { + descriptors: ReadonlyArray; + primarySelectDescriptorId: string | null; + ultrathinkPromptControlled: boolean; + fastModeEnabled: boolean; +}): { label: string; showFastModeIcon: boolean } { + const labels: Array = []; + for (const descriptor of input.descriptors) { + if (descriptor.id === "fastMode" && descriptor.type === "boolean") { + continue; + } + const label = + input.ultrathinkPromptControlled && descriptor.id === input.primarySelectDescriptorId + ? "Ultrathink" + : descriptor.type === "boolean" + ? `${descriptor.label} ${descriptor.currentValue === true ? "On" : "Off"}` + : getProviderOptionCurrentLabel(descriptor); + if (typeof label === "string" && label.length > 0) { + labels.push(label); + } + } + + if (labels.length === 0) { + return { label: input.fastModeEnabled ? "Fast" : "Normal", showFastModeIcon: false }; + } + return { label: labels.join(" · "), showFastModeIcon: input.fastModeEnabled }; +} + export const TraitsPicker = memo(function TraitsPicker({ provider, instanceId, @@ -393,7 +428,7 @@ export const TraitsPicker = memo(function TraitsPicker({ ...persistence }: TraitsMenuContentProps & TraitsPersistence) { const [isMenuOpen, setIsMenuOpen] = useState(false); - const { descriptors, primarySelectDescriptor, ultrathinkPromptControlled } = + const { descriptors, primarySelectDescriptor, ultrathinkPromptControlled, fastModeEnabled } = getTraitsSectionVisibility({ provider, models, @@ -415,23 +450,18 @@ export const TraitsPicker = memo(function TraitsPicker({ return null; } - const triggerLabels: Array = []; - for (const descriptor of descriptors) { - const label = - ultrathinkPromptControlled && descriptor.id === primarySelectDescriptor?.id - ? "Ultrathink" - : descriptor.type === "boolean" - ? descriptor.id === "fastMode" - ? descriptor.currentValue === true - ? "Fast" - : "Normal" - : `${descriptor.label} ${descriptor.currentValue === true ? "On" : "Off"}` - : getProviderOptionCurrentLabel(descriptor); - if (typeof label === "string" && label.length > 0) { - triggerLabels.push(label); - } - } - const triggerLabel = triggerLabels.join(" · "); + const { label: triggerLabel, showFastModeIcon } = buildTraitsTriggerDisplay({ + descriptors, + primarySelectDescriptorId: primarySelectDescriptor?.id ?? null, + ultrathinkPromptControlled, + fastModeEnabled, + }); + const fastModeIcon = showFastModeIcon ? ( + <> +