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
110 changes: 110 additions & 0 deletions apps/web/src/components/chat/TraitsPicker.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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<ProviderOptionDescriptor, { type: "select" }> {
return { id, label: id, type: "select", options: [...options], currentValue };
}

function fastModeDescriptor(
currentValue: boolean,
): Extract<ProviderOptionDescriptor, { type: "boolean" }> {
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<ProviderOptionDescriptor>, 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<ProviderOptionDescriptor, { type: "boolean" }> = {
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("stays blank when descriptors resolve to no label and there is no fast mode", () => {
// A select with neither a currentValue nor an isDefault option yields no
// label. Without a fastMode descriptor present that must stay blank rather
// than falling through to a bogus "Normal".
const unresolved: Extract<ProviderOptionDescriptor, { type: "select" }> = {
id: "effort",
label: "effort",
type: "select",
options: [
{ id: "low", label: "Low" },
{ id: "high", label: "High" },
],
};
expect(display([unresolved], false)).toEqual({ label: "", 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 });
});
});
79 changes: 58 additions & 21 deletions apps/web/src/components/chat/TraitsPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -379,6 +379,46 @@ 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<ProviderOptionDescriptor>;
primarySelectDescriptorId: string | null;
ultrathinkPromptControlled: boolean;
fastModeEnabled: boolean;
}): { label: string; showFastModeIcon: boolean } {
let hasFastMode = false;
const labels: Array<string> = [];
for (const descriptor of input.descriptors) {
if (descriptor.id === "fastMode" && descriptor.type === "boolean") {
hasFastMode = true;
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);
}
}

// Only fall back to text when fast mode is genuinely the sole trait. Keying
// 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 && hasFastMode) {
return { label: input.fastModeEnabled ? "Fast" : "Normal", showFastModeIcon: false };
}
Comment thread
cursor[bot] marked this conversation as resolved.
Comment on lines +416 to +418

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium chat/TraitsPicker.tsx:416

When a model has fastMode plus another trait (e.g. a select descriptor) whose current label resolves to empty, buildTraitsTriggerDisplay incorrectly returns "Fast"/"Normal" and suppresses the bolt icon. The fallback labels.length === 0 && hasFastMode only checks that no label was produced — it does not confirm fast mode is the only descriptor. Any non-fast descriptor whose label is empty (a valid state covered by the new tests) triggers the sole-trait fallback, dropping the real trait's display and hiding the bolt. Consider checking the total descriptor count or the presence of non-fast descriptors instead of relying on labels.length.

Suggested change
if (labels.length === 0 && hasFastMode) {
return { label: input.fastModeEnabled ? "Fast" : "Normal", showFastModeIcon: false };
}
// Only fall back to text when fast mode is genuinely the sole trait.
const nonFastModeDescriptors = input.descriptors.filter(
(descriptor) => !(descriptor.id === "fastMode" && descriptor.type === "boolean"),
);
if (nonFastModeDescriptors.length === 0 && hasFastMode) {
return { label: input.fastModeEnabled ? "Fast" : "Normal", showFastModeIcon: false };
}
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/chat/TraitsPicker.tsx around lines 416-418:

When a model has `fastMode` plus another trait (e.g. a select descriptor) whose current label resolves to empty, `buildTraitsTriggerDisplay` incorrectly returns `"Fast"`/`"Normal"` and suppresses the bolt icon. The fallback `labels.length === 0 && hasFastMode` only checks that no label was produced — it does not confirm fast mode is the only descriptor. Any non-fast descriptor whose label is empty (a valid state covered by the new tests) triggers the sole-trait fallback, dropping the real trait's display and hiding the bolt. Consider checking the total descriptor count or the presence of non-fast descriptors instead of relying on `labels.length`.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked this one against the actual behavior and I'm going to decline the suggestion — it makes the case strictly worse.

The scenario is real: fastMode + a select whose label resolves to empty. But compare what the two versions render:

current suggested
fast on Fast `` (empty, bolt only)
fast off Normal `` (empty, bare chevron)

Since the other descriptor produced no label, there is nothing else to display. Gating on nonFastModeDescriptors.length rather than labels.length doesn't recover the missing trait — it just skips the fallback and emits an empty label. That's the degenerate trigger the fallback exists to prevent: with fast off you get a bare chevron with no text at all.

dropping the real trait's display and hiding the bolt

There is no real trait display to drop in this branch — labels is empty precisely because that descriptor resolved to nothing. Fast/Normal is a lossless description of the only state that actually resolved, and the bolt is redundant next to the word "Fast", which is why it's suppressed.

Verified both paths directly rather than reasoning from the diff: current returns {label: "Fast", showFastModeIcon: false}; the suggested condition returns {label: "", showFastModeIcon: true}.

Worth noting this is a doubly-degenerate state — a provider would have to ship a select with neither a currentValue nor an isDefault option. No provider in the repo does. Both versions are edge-case handling for something unreachable today; I'd rather the unreachable branch degrade to readable text than to an empty button.

return { label: labels.join(" · "), showFastModeIcon: input.fastModeEnabled };
}

export const TraitsPicker = memo(function TraitsPicker({
provider,
instanceId,
Expand All @@ -393,7 +433,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,
Expand All @@ -415,23 +455,18 @@ export const TraitsPicker = memo(function TraitsPicker({
return null;
}

const triggerLabels: Array<string> = [];
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 ? (
<>
<ZapIcon aria-hidden="true" className="size-3 shrink-0 text-foreground/80 opacity-100" />
<span className="sr-only">Fast mode on</span>
</>
) : null;

const isCodexStyle = provider === "codex";

Expand All @@ -457,12 +492,14 @@ export const TraitsPicker = memo(function TraitsPicker({
}
>
{isCodexStyle ? (
<span className="flex min-w-0 w-full items-center gap-2 overflow-hidden">
{triggerLabel}
<span className="flex min-w-0 w-full items-center gap-1.5 overflow-hidden">
{fastModeIcon}
<span className="min-w-0 truncate">{triggerLabel}</span>
<ChevronDownIcon aria-hidden="true" className="size-3 shrink-0 opacity-60" />
</span>
) : (
<>
{fastModeIcon}
<span>{triggerLabel}</span>
<ChevronDownIcon aria-hidden="true" className="size-3 opacity-60" />
</>
Expand Down
Loading