From 80937f7eed33100b41c06995bf23e6c6ef9c9c2a Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 9 Nov 2026 18:11:52 -0800 Subject: [PATCH] Show a Fast-only speed as the Fast mode switch on mobile (#40523) On the phone, Codex's Standard/Fast speed was a Speed row opening a list while Claude's Fast mode was a switch. A Fast-only speed now uses the same switch row; more speeds, or a lone Flex, keep the list. The lone-Fast rule moves to shared code so desktop and mobile follow one rule. --- ...bileNativeChatSessionOptionPickers.test.ts | 25 +++++++++++++ .../MobileNativeChatSessionOptionPickers.tsx | 5 +-- .../MobileNativeChatSessionOptionRows.tsx | 36 ++++++++++++++++--- .../controls/NativeChatSessionOptionRows.tsx | 3 +- .../controls/NativeChatSessionSpeedButton.tsx | 20 ++--------- .../input/native-chat-session-speed-tiers.ts | 29 +++++++++++++++ 6 files changed, 94 insertions(+), 24 deletions(-) create mode 100644 src/shared/native-chat/input/native-chat-session-speed-tiers.ts diff --git a/mobile/src/session/native-chat/input/MobileNativeChatSessionOptionPickers.test.ts b/mobile/src/session/native-chat/input/MobileNativeChatSessionOptionPickers.test.ts index 676636c583d..1c4215f0839 100644 --- a/mobile/src/session/native-chat/input/MobileNativeChatSessionOptionPickers.test.ts +++ b/mobile/src/session/native-chat/input/MobileNativeChatSessionOptionPickers.test.ts @@ -160,6 +160,31 @@ describe('MobileNativeChatSessionOptionPickers', () => { renderer = null }) + it('toggles a Fast-only Codex speed on the same Fast mode switch as Claude', async () => { + const speed: SessionOptionDescriptor = { + id: 'serviceTier', + label: 'Speed', + kind: { + type: 'select', + currentValue: 'default', + choices: [ + { value: 'default', label: 'Standard' }, + { value: 'priority', label: 'Fast' } + ] + }, + valueSource: 'reported', + transport: 'catalog', + settable: true + } + mount([MODEL_DESCRIPTOR, speed], false, 'model') + const toggle = findAllByHostType(renderer!.root, 'Switch').find( + (node) => node.props.accessibilityLabel === 'Fast mode' + ) + expect(toggle?.props.value).toBe(false) + await act(async () => toggle!.props.onValueChange(true)) + expect(setOption).toHaveBeenCalledWith('serviceTier', 'priority') + }) + it('consumes a refused page without replay, then permits a fresh open and permission pick', async () => { const placeholder: SessionOptionDescriptor = { ...MODEL_DESCRIPTOR, diff --git a/mobile/src/session/native-chat/input/MobileNativeChatSessionOptionPickers.tsx b/mobile/src/session/native-chat/input/MobileNativeChatSessionOptionPickers.tsx index 64360176bd6..973ee4010c1 100644 --- a/mobile/src/session/native-chat/input/MobileNativeChatSessionOptionPickers.tsx +++ b/mobile/src/session/native-chat/input/MobileNativeChatSessionOptionPickers.tsx @@ -36,7 +36,8 @@ import { descriptorRowElements, ModelPill, SessionOptionCaption, - SessionOptionSummaryRow + SessionOptionSummaryRow, + showsSessionOptionAsSwitch } from './MobileNativeChatSessionOptionRows' import { SessionOptionSegments, @@ -383,7 +384,7 @@ function OptionRows({ {descriptors.flatMap((descriptor) => { const isPermission = descriptor === permission const disabled = locked(descriptor) || !descriptor.settable - if (descriptor.kind.type === 'boolean' && !descriptor.action) { + if (showsSessionOptionAsSwitch(descriptor)) { return descriptorRowElements( { descriptor, diff --git a/mobile/src/session/native-chat/input/MobileNativeChatSessionOptionRows.tsx b/mobile/src/session/native-chat/input/MobileNativeChatSessionOptionRows.tsx index e8dfb017a3d..63c69893624 100644 --- a/mobile/src/session/native-chat/input/MobileNativeChatSessionOptionRows.tsx +++ b/mobile/src/session/native-chat/input/MobileNativeChatSessionOptionRows.tsx @@ -17,6 +17,10 @@ import { nativeChatSessionOptionActionLabel, nativeChatSessionOptionLabel } from '../../../../../src/shared/native-chat/input/native-chat-session-option-labels' +import { + loneFastTier, + speedButtonTiers +} from '../../../../../src/shared/native-chat/input/native-chat-session-speed-tiers' import { sayMobileNativeChatSessionOption as say } from './native-chat-session-option-copy' import { MobileSwitch } from '../../../components/MobileSwitch' @@ -131,6 +135,20 @@ type DescriptorRowsProps = { moreChoices?: { shown: boolean; onToggle: () => void } } +/** A Fast-only speed, which toggles on one row like Claude's Fast mode. */ +function fastSpeedSwitch(descriptor: SessionOptionDescriptor) { + const speed = speedButtonTiers(descriptor) + const fast = speed ? loneFastTier(speed.tiers) : undefined + return speed && fast ? { standard: speed.standard, fast } : null +} + +export function showsSessionOptionAsSwitch(descriptor: SessionOptionDescriptor): boolean { + return ( + !descriptor.action && + (descriptor.kind.type === 'boolean' || fastSpeedSwitch(descriptor) !== null) + ) +} + /** One descriptor's rows as an array, so a ListGroup can draw the separators between them. */ export function descriptorRowElements( { @@ -156,8 +174,10 @@ export function descriptorRowElements( ] } // An unknown baseline still renders a switch; the source marker prevents implying the value was confirmed. - if (descriptor.kind.type === 'boolean') { - const label = nativeChatSessionOptionLabel(descriptor, say) + const fastSpeed = fastSpeedSwitch(descriptor) + if (descriptor.kind.type === 'boolean' || fastSpeed) { + const { kind } = descriptor + const label = fastSpeed ? say('fastMode') : nativeChatSessionOptionLabel(descriptor, say) const marker = sessionOptionValueMarker(descriptor) return [ onSetOption(next)} + value={ + kind.type === 'boolean' + ? kind.currentValue + : kind.currentValue === fastSpeed?.fast.value + } + onValueChange={(next) => + onSetOption( + fastSpeed ? (next ? fastSpeed.fast.value : fastSpeed.standard.value) : next + ) + } disabled={locked} /> } diff --git a/src/renderer/src/components/native-chat/session/options/controls/NativeChatSessionOptionRows.tsx b/src/renderer/src/components/native-chat/session/options/controls/NativeChatSessionOptionRows.tsx index 3c46c7fa97c..489c5087047 100644 --- a/src/renderer/src/components/native-chat/session/options/controls/NativeChatSessionOptionRows.tsx +++ b/src/renderer/src/components/native-chat/session/options/controls/NativeChatSessionOptionRows.tsx @@ -22,7 +22,8 @@ import { } from '../../../../../../../shared/native-chat/input/native-chat-session-option-labels' import { sayNativeChatSessionOptionTranslated as say } from './native-chat-session-option-labels-text' import { NativeChatSessionOptionWriteGroup } from './NativeChatSessionOptionWriteGroup' -import { NativeChatSessionSpeedButton, speedButtonTiers } from './NativeChatSessionSpeedButton' +import { NativeChatSessionSpeedButton } from './NativeChatSessionSpeedButton' +import { speedButtonTiers } from '../../../../../../../shared/native-chat/input/native-chat-session-speed-tiers' type NativeChatSessionOptionRowsProps = { /** The current model's options, in display order. */ diff --git a/src/renderer/src/components/native-chat/session/options/controls/NativeChatSessionSpeedButton.tsx b/src/renderer/src/components/native-chat/session/options/controls/NativeChatSessionSpeedButton.tsx index c4cdcdba29f..c39dd9fe0fa 100644 --- a/src/renderer/src/components/native-chat/session/options/controls/NativeChatSessionSpeedButton.tsx +++ b/src/renderer/src/components/native-chat/session/options/controls/NativeChatSessionSpeedButton.tsx @@ -8,28 +8,14 @@ import { DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import type { - SessionOptionDescriptor, SessionOptionSelectChoice, SessionOptionValue } from '../../../../../../../shared/native-chat/input/native-chat-session-options' import { nativeChatSessionChoiceLabel } from '../../../../../../../shared/native-chat/input/native-chat-session-option-labels' import { isFastServiceTier } from '../../../../../../../shared/agent-session/options/structured-agent-session-discovered-model' +import { loneFastTier } from '../../../../../../../shared/native-chat/input/native-chat-session-speed-tiers' import { sayNativeChatSessionOptionTranslated as say } from './native-chat-session-option-labels-text' -const STANDARD_TIER = 'default' - -/** Standard plus the other tiers, or null when the select is not a speed choice with any. */ -export function speedButtonTiers( - descriptor: SessionOptionDescriptor -): { standard: SessionOptionSelectChoice; tiers: SessionOptionSelectChoice[] } | null { - if (descriptor.id !== 'serviceTier' || descriptor.kind.type !== 'select') { - return null - } - const standard = descriptor.kind.choices.find((choice) => choice.value === STANDARD_TIER) - const tiers = descriptor.kind.choices.filter((choice) => choice.value !== STANDARD_TIER) - return standard && tiers.length > 0 ? { standard, tiers } : null -} - /** A lone Fast tier toggles like Claude's Fast; anything else opens a list instead of a segmented switcher. */ export function NativeChatSessionSpeedButton(props: { label: string @@ -46,8 +32,8 @@ export function NativeChatSessionSpeedButton(props: { const nameOf = (choice: SessionOptionSelectChoice) => nativeChatSessionChoiceLabel(choice, say, 'serviceTier') // Only Fast takes the toggle and bolt; Flex and other tiers are picked from the list. - if (tiers.length === 1 && isFastServiceTier(tiers[0])) { - const only = tiers[0] + const only = loneFastTier(tiers) + if (only) { return ( choice.value === STANDARD_TIER) + const tiers = descriptor.kind.choices.filter((choice) => choice.value !== STANDARD_TIER) + return standard && tiers.length > 0 ? { standard, tiers } : null +} + +/** A lone Fast tier toggles like Claude's Fast; Flex and several tiers are picked from a list. */ +export function loneFastTier( + tiers: readonly SessionOptionSelectChoice[] +): SessionOptionSelectChoice | undefined { + return tiers.length === 1 && isFastServiceTier(tiers[0]) ? tiers[0] : undefined +}