Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/web/src/components/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import React, { type SVGProps, useId } from "react";
import { cn } from "~/lib/utils";
export type Icon = React.FC<SVGProps<SVGSVGElement>>;

export const UltrafastIcon: Icon = (props) => (
<svg viewBox="0 0 24 24" fill="currentColor" {...props}>
<path d="m17 2-10 12h7l-1 8 10-12h-7l1-8Z" opacity="0.4" />
<path d="m11 2-10 12h7l-1 8 10-12h-7l1-8Z" />
</svg>
);

export const FinderIcon: Icon = (props) => (
<svg viewBox="0 0 24 24" fill="none" {...props}>
<rect x="2" y="2" width="20" height="20" rx="4" fill="#36A9F5" />
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/components/chat/ComposerControl.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { mergeProps } from "@base-ui/react/merge-props";
import { Select as SelectPrimitive } from "@base-ui/react/select";
import { useRender } from "@base-ui/react/use-render";
import type { ComponentProps } from "react";
import { ChevronDownIcon, type LucideIcon } from "lucide-react";
import type { ComponentProps, ComponentType, SVGProps } from "react";
import { ChevronDownIcon } from "lucide-react";

import { cn } from "~/lib/utils";
import { Separator } from "../ui/separator";
Expand Down Expand Up @@ -52,7 +52,7 @@ export function ComposerControlIcon({
opticalSize = "default",
size = "sm",
}: {
icon: LucideIcon;
icon: ComponentType<SVGProps<SVGSVGElement>>;
className?: string | undefined;
opticalSize?: "default" | "large";
size?: ComposerControlSize;
Expand Down
70 changes: 57 additions & 13 deletions apps/web/src/components/chat/TraitsPicker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function fastModeDescriptor(
}

function serviceTierDescriptor(
currentValue: "default" | "priority" | "flex",
currentValue: "default" | "priority" | "ultrafast" | "flex",
): Extract<ProviderOptionDescriptor, { type: "select" }> {
return {
id: "serviceTier",
Expand All @@ -26,6 +26,7 @@ function serviceTierDescriptor(
options: [
{ id: "default", label: "Standard", isDefault: true },
{ id: "priority", label: "Fast" },
{ id: "ultrafast", label: "Ultrafast" },
{ id: "flex", label: "Flex" },
],
currentValue,
Expand Down Expand Up @@ -64,43 +65,86 @@ describe("buildTraitsTriggerDisplay", () => {
it("omits fast mode from the label entirely when it is off", () => {
expect(display([EFFORT, fastModeDescriptor(false), CONTEXT_WINDOW])).toEqual({
label: "High · 1M",
showFastModeIcon: false,
speedIcon: null,
});
});

it("shows the bolt instead of a text label when fast mode is on", () => {
expect(display([EFFORT, fastModeDescriptor(true), CONTEXT_WINDOW])).toEqual({
label: "High · 1M",
showFastModeIcon: true,
speedIcon: "fast",
});
});

it("treats Codex standard and fast service tiers as fast mode states", () => {
expect(display([EFFORT, serviceTierDescriptor("default")])).toEqual({
label: "High",
showFastModeIcon: false,
speedIcon: null,
});
expect(display([EFFORT, serviceTierDescriptor("priority")])).toEqual({
label: "High",
showFastModeIcon: true,
speedIcon: "fast",
});
});

it("uses a distinct double bolt for Codex Ultrafast", () => {
expect(display([EFFORT, serviceTierDescriptor("ultrafast")])).toEqual({
label: "High",
speedIcon: "ultrafast",
});
});

it("uses Ultrafast without requiring a Fast tier", () => {
const descriptor = serviceTierDescriptor("ultrafast");
expect(
display([
EFFORT,
{ ...descriptor, options: descriptor.options.filter(({ id }) => id !== "priority") },
]),
).toEqual({
label: "High",
speedIcon: "ultrafast",
});
});

it("keeps other Codex service tiers in the label", () => {
expect(display([EFFORT, serviceTierDescriptor("flex")])).toEqual({
label: "High · Flex",
showFastModeIcon: false,
speedIcon: null,
});
});

it("keeps Standard as text for models without speed tiers", () => {
const descriptor = serviceTierDescriptor("default");
const nonSpeedDescriptor = {
...descriptor,
options: descriptor.options.filter(({ id }) => id === "default" || id === "flex"),
};
expect(display([EFFORT, nonSpeedDescriptor])).toEqual({
label: "High · Standard",
speedIcon: null,
});
expect(display([nonSpeedDescriptor])).toEqual({
label: "Standard",
speedIcon: null,
});
});

it("keeps the Codex service tier readable when it is the only trait", () => {
expect(display([serviceTierDescriptor("default")])).toEqual({
label: "Standard",
showFastModeIcon: false,
speedIcon: null,
});
expect(display([serviceTierDescriptor("priority")])).toEqual({
label: "Fast",
showFastModeIcon: false,
speedIcon: null,
});
});

it("keeps Ultrafast readable when it is the only trait", () => {
expect(display([serviceTierDescriptor("ultrafast")])).toEqual({
label: "Ultrafast",
speedIcon: null,
});
});

Expand All @@ -113,18 +157,18 @@ describe("buildTraitsTriggerDisplay", () => {
};
expect(display([EFFORT, thinking])).toEqual({
label: "High · Thinking On",
showFastModeIcon: false,
speedIcon: null,
});
});

it("falls back to a text label when fast mode is the only trait", () => {
expect(display([fastModeDescriptor(true)])).toEqual({
label: "Fast",
showFastModeIcon: false,
speedIcon: null,
});
expect(display([fastModeDescriptor(false)])).toEqual({
label: "Normal",
showFastModeIcon: false,
speedIcon: null,
});
});

Expand All @@ -141,7 +185,7 @@ describe("buildTraitsTriggerDisplay", () => {
{ id: "high", label: "High" },
],
};
expect(display([unresolved])).toEqual({ label: "", showFastModeIcon: false });
expect(display([unresolved])).toEqual({ label: "", speedIcon: null });
});

it("still renders the prompt-controlled ultrathink label alongside the bolt", () => {
Expand All @@ -152,7 +196,7 @@ describe("buildTraitsTriggerDisplay", () => {
primarySelectDescriptorId: "reasoningEffort",
ultrathinkPromptControlled: true,
}),
).toEqual({ label: "Ultrathink", showFastModeIcon: true });
).toEqual({ label: "Ultrathink", speedIcon: "fast" });
});
});

Expand Down
49 changes: 28 additions & 21 deletions apps/web/src/components/chat/TraitsPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from "@t3tools/shared/model";
import { memo, useCallback } from "react";
import { BrainIcon, ZapIcon } from "lucide-react";
import { UltrafastIcon } from "../Icons";
import {
Menu,
MenuGroup,
Expand Down Expand Up @@ -474,26 +475,22 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({
});

/**
* Build the traits trigger's text label plus whether the fast-mode bolt should
* render. Claude and Cursor expose fast mode as a boolean, while Codex exposes
* it through the Standard/Fast service tiers. In either form, fast mode is a
* lightning bolt when on and nothing at all when off. The one exception is when
* fast mode is the only trait, where a bare bolt (or bare chevron) would leave
* the trigger unreadable.
* Fast mode uses one bolt; Codex Ultrafast uses two. Keep a text label when
* speed is the only trait so the trigger remains readable.
*/
export function buildTraitsTriggerDisplay(input: {
provider: ProviderDriverKind;
descriptors: ReadonlyArray<ProviderOptionDescriptor>;
primarySelectDescriptorId: string | null;
ultrathinkPromptControlled: boolean;
}): { label: string; showFastModeIcon: boolean } {
}): { label: string; speedIcon: "fast" | "ultrafast" | null } {
let fastModeFallbackLabel: string | null = null;
let fastModeEnabled = false;
let speedIcon: "fast" | "ultrafast" | null = null;
const labels: Array<string> = [];
for (const descriptor of input.descriptors) {
if (descriptor.id === "fastMode" && descriptor.type === "boolean") {
fastModeEnabled = descriptor.currentValue === true;
fastModeFallbackLabel = fastModeEnabled ? "Fast" : "Normal";
speedIcon = descriptor.currentValue === true ? "fast" : null;
fastModeFallbackLabel = speedIcon ? "Fast" : "Normal";
continue;
}
if (
Expand All @@ -503,11 +500,20 @@ export function buildTraitsTriggerDisplay(input: {
) {
const currentValue = getProviderOptionCurrentValue(descriptor);
const fastTier = descriptor.options.find(({ label }) => label === "Fast");
if (fastTier && (currentValue === "default" || currentValue === fastTier.id)) {
fastModeEnabled = currentValue === fastTier.id;
const ultrafastTier = descriptor.options.find(({ label }) => label === "Ultrafast");
if (
((fastTier || ultrafastTier) && currentValue === "default") ||
(fastTier && currentValue === fastTier.id) ||
(ultrafastTier && currentValue === ultrafastTier.id)
) {
speedIcon =
ultrafastTier && currentValue === ultrafastTier.id
? "ultrafast"
: fastTier && currentValue === fastTier.id
? "fast"
: null;
fastModeFallbackLabel =
descriptor.options.find(({ id }) => id === currentValue)?.label ??
(fastModeEnabled ? "Fast" : "Normal");
descriptor.options.find(({ id }) => id === currentValue)?.label ?? "Normal";
continue;
}
}
Expand All @@ -526,9 +532,9 @@ export function buildTraitsTriggerDisplay(input: {
// off an empty label list alone would also catch descriptors that resolved to
// no label at all, printing a bogus "Normal" for a model without fast mode.
if (labels.length === 0 && fastModeFallbackLabel !== null) {
return { label: fastModeFallbackLabel, showFastModeIcon: false };
return { label: fastModeFallbackLabel, speedIcon: null };
}
return { label: labels.join(" · "), showFastModeIcon: fastModeEnabled };
return { label: labels.join(" · "), speedIcon };
}

export const TraitsPicker = memo(function TraitsPicker({
Expand Down Expand Up @@ -577,17 +583,18 @@ export const TraitsPicker = memo(function TraitsPicker({
return null;
}

const { label: triggerLabel, showFastModeIcon } = buildTraitsTriggerDisplay({
const { label: triggerLabel, speedIcon } = buildTraitsTriggerDisplay({
provider,
descriptors,
primarySelectDescriptorId: primarySelectDescriptor?.id ?? null,
ultrathinkPromptControlled,
});
const accessibleLabel = showFastModeIcon ? `${triggerLabel}, Fast mode on` : triggerLabel;
const fastModeIcon = showFastModeIcon ? (
const speedLabel = speedIcon === "ultrafast" ? "Ultrafast mode on" : "Fast mode on";
const accessibleLabel = speedIcon ? `${triggerLabel}, ${speedLabel}` : triggerLabel;
const fastModeIcon = speedIcon ? (
<>
<ComposerControlIcon
icon={ZapIcon}
icon={speedIcon === "ultrafast" ? UltrafastIcon : ZapIcon}
size={size}
className={cn(
"fill-current opacity-80",
Expand All @@ -598,7 +605,7 @@ export const TraitsPicker = memo(function TraitsPicker({
: "text-foreground",
)}
/>
<span className="sr-only">Fast mode on</span>
<span className="sr-only">{speedLabel}</span>
</>
) : null;

Expand Down
Loading