Skip to content
Open
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
61 changes: 52 additions & 9 deletions apps/web/src/components/chat/TraitsPicker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@ function fastModeDescriptor(
return { id: "fastMode", label: "Fast Mode", type: "boolean", currentValue };
}

function serviceTierDescriptor(
currentValue: "default" | "priority" | "flex",
): Extract<ProviderOptionDescriptor, { type: "select" }> {
return {
id: "serviceTier",
label: "Service Tier",
type: "select",
options: [
{ id: "default", label: "Standard", isDefault: true },
{ id: "priority", label: "Fast" },
{ id: "flex", label: "Flex" },
],
currentValue,
};
}

const EFFORT = selectDescriptor(
"effort",
[
Expand All @@ -33,49 +49,77 @@ const CONTEXT_WINDOW = selectDescriptor(
"1m",
);

function display(descriptors: ReadonlyArray<ProviderOptionDescriptor>, fastModeEnabled: boolean) {
function display(descriptors: ReadonlyArray<ProviderOptionDescriptor>) {
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({
expect(display([EFFORT, fastModeDescriptor(false), CONTEXT_WINDOW])).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({
expect(display([EFFORT, fastModeDescriptor(true), CONTEXT_WINDOW])).toEqual({
label: "High · 1M",
showFastModeIcon: true,
});
});

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

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

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

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({
expect(display([EFFORT, thinking])).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({
expect(display([fastModeDescriptor(true)])).toEqual({
label: "Fast",
showFastModeIcon: false,
});
expect(display([fastModeDescriptor(false)], false)).toEqual({
expect(display([fastModeDescriptor(false)])).toEqual({
label: "Normal",
showFastModeIcon: false,
});
Expand All @@ -94,7 +138,7 @@ describe("buildTraitsTriggerDisplay", () => {
{ id: "high", label: "High" },
],
};
expect(display([unresolved], false)).toEqual({ label: "", showFastModeIcon: false });
expect(display([unresolved])).toEqual({ label: "", showFastModeIcon: false });
});

it("still renders the prompt-controlled ultrathink label alongside the bolt", () => {
Expand All @@ -103,7 +147,6 @@ describe("buildTraitsTriggerDisplay", () => {
descriptors: [EFFORT, fastModeDescriptor(true)],
primarySelectDescriptorId: "effort",
ultrathinkPromptControlled: true,
fastModeEnabled: true,
}),
).toEqual({ label: "Ultrathink", showFastModeIcon: true });
});
Expand Down
42 changes: 27 additions & 15 deletions apps/web/src/components/chat/TraitsPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,6 @@ function getSelectedTraits(
: getDescriptorStringValue(primarySelectDescriptor)) ?? null;
const thinkingEnabled =
typeof thinkingDescriptor?.currentValue === "boolean" ? thinkingDescriptor.currentValue : null;
const fastModeEnabled =
typeof fastModeDescriptor?.currentValue === "boolean" ? fastModeDescriptor.currentValue : false;
const contextWindow = getDescriptorStringValue(contextWindowDescriptor);
const selectedAgent = getDescriptorStringValue(agentDescriptor);
const selectedAgentLabel = agentDescriptor
Expand All @@ -153,7 +151,6 @@ function getSelectedTraits(
thinkingDescriptor,
effort,
thinkingEnabled,
fastModeEnabled,
contextWindow,
ultrathinkPromptControlled,
ultrathinkInBodyText,
Expand Down Expand Up @@ -381,24 +378,40 @@ 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.
* 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.
*/
export function buildTraitsTriggerDisplay(input: {
descriptors: ReadonlyArray<ProviderOptionDescriptor>;
primarySelectDescriptorId: string | null;
ultrathinkPromptControlled: boolean;
fastModeEnabled: boolean;
}): { label: string; showFastModeIcon: boolean } {
let hasFastMode = false;
let fastModeFallbackLabel: string | null = null;
let fastModeEnabled = false;
const labels: Array<string> = [];
for (const descriptor of input.descriptors) {
if (descriptor.id === "fastMode" && descriptor.type === "boolean") {
hasFastMode = true;
fastModeEnabled = descriptor.currentValue === true;
fastModeFallbackLabel = fastModeEnabled ? "Fast" : "Normal";
continue;
}
if (descriptor.id === "serviceTier" && descriptor.type === "select") {
const currentOption =
descriptor.options.find((option) => option.id === descriptor.currentValue) ??
descriptor.options.find((option) => option.isDefault);
const isFastTier =
currentOption?.id === "fast" || currentOption?.label.toLowerCase() === "fast";
const isStandardTier =
currentOption?.id === "default" || currentOption?.label.toLowerCase() === "standard";
if (isFastTier || isStandardTier) {
fastModeEnabled = isFastTier;
fastModeFallbackLabel = currentOption.label;
continue;
}
}
const label =
input.ultrathinkPromptControlled && descriptor.id === input.primarySelectDescriptorId
? "Ultrathink"
Expand All @@ -413,10 +426,10 @@ export function buildTraitsTriggerDisplay(input: {
// 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 };
if (labels.length === 0 && fastModeFallbackLabel !== null) {
return { label: fastModeFallbackLabel, showFastModeIcon: false };
}
return { label: labels.join(" · "), showFastModeIcon: input.fastModeEnabled };
return { label: labels.join(" · "), showFastModeIcon: fastModeEnabled };
}

export const TraitsPicker = memo(function TraitsPicker({
Expand All @@ -433,7 +446,7 @@ export const TraitsPicker = memo(function TraitsPicker({
...persistence
}: TraitsMenuContentProps & TraitsPersistence) {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const { descriptors, primarySelectDescriptor, ultrathinkPromptControlled, fastModeEnabled } =
const { descriptors, primarySelectDescriptor, ultrathinkPromptControlled } =
getTraitsSectionVisibility({
provider,
models,
Expand All @@ -459,7 +472,6 @@ export const TraitsPicker = memo(function TraitsPicker({
descriptors,
primarySelectDescriptorId: primarySelectDescriptor?.id ?? null,
ultrathinkPromptControlled,
fastModeEnabled,
});
const fastModeIcon = showFastModeIcon ? (
<>
Expand Down
Loading