From 104d01b686b60212e45b9fd807ef8a40bf759ea4 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Sat, 7 Nov 2026 22:37:41 -0800 Subject: [PATCH] Simplify the new-tab agent launch menu (#39279) * feat(agent-launch): simplify the new-tab agent launch menu Drop Permissions and Arguments from the launch-mode setup panel; they are agent defaults edited in Agent settings. Put effort levels (unlabeled) and Fast mode on one row, and make the Launch chat/terminal buttons fill the row at a larger size with icons. Select-mode pickers (automations, new workspace, source control, continuation) keep both controls. * fix(agent-launch): let current Agent settings win over launch-menu speed edits A Fast or speed-tier edit copies the current Permissions and Arguments into the launch draft. With no Arguments editor in the launch menu, a later settings change was silently ignored. Launch drafts now drop Arguments stamped by older settings; the speed choice still applies. Also stop pointing hosts without a model list at the removed Arguments editor. * fix(agent-launch): show Fast as a button in the launch menu Matches the mock: an outlined Fast chip with the lightning icon in place of the "Fast mode" switch. The chat composer keeps its switch. --- .../agent/AgentLaunchPicker.form.test.tsx | 12 ++++- .../AgentLaunchPickerMenu.pending.test.tsx | 4 +- .../AgentLaunchSetupButton.retention.test.tsx | 39 ++++++++++++---- .../agent/AgentLaunchSetupFooter.tsx | 18 +++++--- .../AgentLaunchSetupPanel.prompt.test.tsx | 43 +++++++++--------- .../agent/AgentLaunchSetupPanel.tsx | 25 ++++++----- .../agent/agent-launch-picker-drafts.ts | 8 +++- ...ent-launch-picker-omnibox.test-fixture.tsx | 25 +++++------ .../agent/agent-launch-setup-option-rows.ts | 30 ++++++++++--- .../controls/NativeChatSessionOptionRows.tsx | 45 ++++++++++++++++--- src/renderer/src/components/ui/toggle.tsx | 4 +- src/renderer/src/i18n/locales/de.json | 1 + src/renderer/src/i18n/locales/en.json | 1 + src/renderer/src/i18n/locales/es.json | 1 + src/renderer/src/i18n/locales/fr.json | 1 + src/renderer/src/i18n/locales/ja.json | 1 + src/renderer/src/i18n/locales/ko.json | 1 + src/renderer/src/i18n/locales/pt.json | 1 + src/renderer/src/i18n/locales/ru.json | 1 + src/renderer/src/i18n/locales/tr.json | 1 + src/renderer/src/i18n/locales/zh-Hant.json | 1 + src/renderer/src/i18n/locales/zh.json | 1 + .../launch-agent-picker-contract.test.ts | 2 + .../picker/agent-launch-picker-authority.ts | 17 +++++++ 24 files changed, 202 insertions(+), 81 deletions(-) diff --git a/src/renderer/src/components/agent/AgentLaunchPicker.form.test.tsx b/src/renderer/src/components/agent/AgentLaunchPicker.form.test.tsx index 444e87416fd..bc47d65feb5 100644 --- a/src/renderer/src/components/agent/AgentLaunchPicker.form.test.tsx +++ b/src/renderer/src/components/agent/AgentLaunchPicker.form.test.tsx @@ -26,7 +26,15 @@ const entry: AgentPickerEntry = { origin: 'probe', fetchedAt: 1, models: [ - { id: 'astra', label: 'Astra', isDefault: true, efforts: [{ value: 'low', label: 'Low' }] } + { + id: 'astra', + label: 'Astra', + isDefault: true, + efforts: [ + { value: 'low', label: 'Low' }, + { value: 'high', label: 'High' } + ] + } ] } } @@ -48,6 +56,6 @@ afterEach(() => { vi.unstubAllGlobals() }) -it('keeps + ordinary and actual omnibox original presets independent after editing their models', async () => { +it('keeps + ordinary and actual omnibox original presets independent after editing the ordinary effort', async () => { await verifyOmniboxPresetIdentity(root, launch) }) diff --git a/src/renderer/src/components/agent/AgentLaunchPickerMenu.pending.test.tsx b/src/renderer/src/components/agent/AgentLaunchPickerMenu.pending.test.tsx index 01c82e2b116..adf26aa9bef 100644 --- a/src/renderer/src/components/agent/AgentLaunchPickerMenu.pending.test.tsx +++ b/src/renderer/src/components/agent/AgentLaunchPickerMenu.pending.test.tsx @@ -107,11 +107,9 @@ it('keeps the terminal launch shown but waiting while the host connects, with ch { name: 'Launch chat', disabled: false }, { name: 'Launch terminal', disabled: true } ]) - const args = () => document.querySelector('textarea[aria-label="Arguments"]') - fireEvent.change(args()!, { target: { value: '--search' } }) view.rerender(menu(false)) expect(reason()).toBe('ClaudeNo chat over SSH') - expect(args()?.value).toBe('--search') + expect(document.querySelector('[contenteditable]')?.textContent).toBe('first line') expect(buttons()).toEqual([ { name: 'Launch chat', disabled: false }, { name: 'Launch terminal', disabled: false } diff --git a/src/renderer/src/components/agent/AgentLaunchSetupButton.retention.test.tsx b/src/renderer/src/components/agent/AgentLaunchSetupButton.retention.test.tsx index 48aed679ce4..5bcb7caa207 100644 --- a/src/renderer/src/components/agent/AgentLaunchSetupButton.retention.test.tsx +++ b/src/renderer/src/components/agent/AgentLaunchSetupButton.retention.test.tsx @@ -56,10 +56,25 @@ afterEach(() => { describe('setup records exist only for authored setups', () => { it('keeps typed prompt and options through close, reopen and a rename', () => { - const entry = { + const entry: AgentPickerEntry = { ...row(0, 'codex'), worktreeId: 'repo::/old', - arguments: '--add-dir docs' + arguments: '--add-dir docs', + catalog: { + origin: 'probe', + fetchedAt: 1, + models: [ + { + id: 'gpt', + label: 'gpt', + isDefault: true, + efforts: [ + { value: 'low', label: 'Low' }, + { value: 'high', label: 'High' } + ] + } + ] + } } const show = (value: AgentPickerEntry) => act(() => @@ -73,7 +88,10 @@ describe('setup records exist only for authored setups', () => { document.querySelector('button[aria-label="Configure codex"]')! const prompt = () => document.querySelector('[contenteditable][aria-multiline="true"]')! - const close = () => fireEvent.keyDown(document.querySelector('textarea')!, { key: 'Escape' }) + const effort = (label: string) => + getByRole(document.body, 'radio', { name: label }).getAttribute('data-state') + const close = () => + fireEvent.keyDown(document.querySelector('[cmdk-input]')!, { key: 'Escape' }) show(entry) fireEvent.click(configure()) close() @@ -81,15 +99,17 @@ describe('setup records exist only for authored setups', () => { // The open panel's first write is an option change; it shows at once and retains the setup. fireEvent.click(configure()) - fireEvent.change(document.querySelector('textarea')!, { target: { value: '--add-dir src' } }) - expect(document.querySelector('textarea')?.value).toBe('--add-dir src') + // Arguments are agent settings, so the launch panel offers no editor for them. + expect(document.querySelector('textarea[aria-label="Arguments"]')).toBeNull() + fireEvent.click(getByRole(document.body, 'radio', { name: 'Low' })) + expect(effort('Low')).toBe('on') expect(sourceCount()).toBe(1) changePrompt(prompt(), 'fix the flaky test') close() - expect(document.querySelector('textarea')).toBeNull() + expect(document.querySelector('[cmdk-input]')).toBeNull() fireEvent.click(configure()) expect(prompt().textContent).toBe('fix the flaky test') - expect(document.querySelector('textarea')?.value).toBe('--add-dir src') + expect(effort('Low')).toBe('on') close() act(() => @@ -102,7 +122,7 @@ describe('setup records exist only for authored setups', () => { show({ ...entry, worktreeId: 'repo::/new' }) fireEvent.click(configure()) expect(prompt().textContent).toBe('fix the flaky test') - expect(document.querySelector('textarea')?.value).toBe('--add-dir src') + expect(effort('Low')).toBe('on') }) it('keeps a search preset in its own options slot, saving only real changes, with one shared prompt', async () => { @@ -146,7 +166,8 @@ describe('setup records exist only for authored setups', () => { const effort = (label: string) => getByRole(document.body, 'radio', { name: label }).getAttribute('data-state') const model = () => document.querySelector('[cmdk-item][data-current="true"]')?.textContent - const close = () => fireEvent.keyDown(document.querySelector('textarea')!, { key: 'Escape' }) + const close = () => + fireEvent.keyDown(document.querySelector('[cmdk-input]')!, { key: 'Escape' }) // Opening a preset saves nothing, and the plain row still shows defaults. fireEvent.click(searched) diff --git a/src/renderer/src/components/agent/AgentLaunchSetupFooter.tsx b/src/renderer/src/components/agent/AgentLaunchSetupFooter.tsx index 5392d7079ca..a8d9c48a463 100644 --- a/src/renderer/src/components/agent/AgentLaunchSetupFooter.tsx +++ b/src/renderer/src/components/agent/AgentLaunchSetupFooter.tsx @@ -1,7 +1,8 @@ import { terminalPendingLabel } from '../tab-bar/new-tab-agent-menu-model' import { useImperativeHandle, useRef, type Ref } from 'react' -import { CornerDownLeft } from 'lucide-react' +import { CornerDownLeft, MessageSquare, SquareTerminal } from 'lucide-react' import { Button } from '@/components/ui/button' +import { cn } from '@/lib/utils' import { ShortcutKeyCombo } from '@/components/ShortcutKeyCombo' import { translate } from '@/i18n/i18n' import { useTranslate, type Translate } from '@/i18n/use-translate' @@ -49,18 +50,23 @@ export function AgentLaunchSurfaceButtons({ disabled, terminalBlocked, primary, + prominent, onSelect }: { entry: AgentPickerEntry disabled?: boolean terminalBlocked?: boolean primary: AgentPickerSurface + /** Launch is the popover's main action, so its buttons split the full row. */ + prominent?: boolean onSelect: (surface: AgentPickerSurface) => void }): React.JSX.Element { const t = useTranslate() const platform = getShortcutPlatform() + const surfaceIcon = (surface: AgentPickerSurface) => + surface === 'chat' ? : return ( -
+
{agentPickerButtonOrder(entry).map((next) => { const isPrimary = next === primary const label = @@ -71,7 +77,7 @@ export function AgentLaunchSurfaceButtons({
-
+
diff --git a/src/renderer/src/components/agent/AgentLaunchSetupPanel.prompt.test.tsx b/src/renderer/src/components/agent/AgentLaunchSetupPanel.prompt.test.tsx index a206fd64544..3c1c44ab861 100644 --- a/src/renderer/src/components/agent/AgentLaunchSetupPanel.prompt.test.tsx +++ b/src/renderer/src/components/agent/AgentLaunchSetupPanel.prompt.test.tsx @@ -41,7 +41,9 @@ it.each(['claude', 'codex'] as const)( id: 'selected', label: 'Selected', efforts: [{ value: 'high', label: 'High' }], - isDefault: true + isDefault: true, + supportsFastMode: true, + serviceTiers: [{ value: 'fast', label: 'Fast' }] } ] } @@ -68,9 +70,12 @@ it.each(['claude', 'codex'] as const)( )! fireEvent.click(panel().querySelector('[cmdk-item]')!) fireEvent.click(getByRole(document.body, 'radio', { name: 'High' })) - fireEvent.change(panel().querySelector('textarea')!, { - target: { value: '--add-dir "authored exact path"' } - }) + // A speed edit copies the current Arguments into the draft; later settings must still win. + fireEvent.click( + agent === 'claude' + ? getByRole(document.body, 'button', { name: 'Fast' }) + : getByRole(document.body, 'radio', { name: 'Fast' }) + ) changePrompt( panel().querySelector('[contenteditable]')!, 'keep @/repo/exact-file.ts' @@ -100,41 +105,38 @@ it.each(['claude', 'codex'] as const)( ) ) fireEvent.click(configure()) - expect(panel().querySelector('textarea')?.value).toBe( - '--add-dir "authored exact path"' - ) + expect(panel().querySelector('textarea[aria-label="Arguments"]')).toBeNull() expect(panel().querySelector('[contenteditable]')?.textContent).toContain('exact-file.ts') launch.mockReturnValueOnce(false) - fireEvent.keyDown(panel().querySelector('textarea')!, { key: 'Enter' }) + fireEvent.click(getByRole(document.body, 'button', { name: 'Launch terminal' })) await waitFor(() => expect(launch).toHaveBeenCalledOnce()) const inputs = agentPickerLaunchInputs(launch.mock.lastCall![1], refreshed) - expect(inputs).toMatchObject({ - agentArgs: '--add-dir "authored exact path"', - sessionOptions: { model: 'selected', effort: 'high' } - }) + expect(inputs).toMatchObject({ sessionOptions: { model: 'selected', effort: 'high' } }) expect(compilePickerTerminalLaunch(launch.mock.lastCall![1], refreshed)).toMatchObject({ ok: true, tokens: agent === 'claude' ? [ 'claude', - '--add-dir', - 'authored exact path', + '--dangerously-skip-permissions', '--model', 'selected', '--effort', - 'high' + 'high', + '--settings', + '{"fastMode":true}' ] : [ 'codex', '-c', 'disable_paste_burst=true', - '--add-dir', - 'authored exact path', + '--dangerously-bypass-approvals-and-sandbox', '-m', 'selected', '-c', - 'model_reasoning_effort=high' + 'model_reasoning_effort=high', + '-c', + 'service_tier=fast' ] }) await waitFor(() => @@ -144,10 +146,7 @@ it.each(['claude', 'codex'] as const)( ) fireEvent.click(getByRole(document.body, 'button', { name: 'Launch terminal' })) await waitFor(() => expect(launch).toHaveBeenCalledTimes(2)) - expect(launch.mock.lastCall![1]).toMatchObject({ - prompt: 'keep @/repo/exact-file.ts', - argumentsOwner: refreshed.ownerKey - }) + expect(launch.mock.lastCall![1]).toMatchObject({ prompt: 'keep @/repo/exact-file.ts' }) await waitFor(() => expect(readNativeChatComposerDraft(agentLaunchPromptScopeKey(refreshed)).text).toBe('') ) diff --git a/src/renderer/src/components/agent/AgentLaunchSetupPanel.tsx b/src/renderer/src/components/agent/AgentLaunchSetupPanel.tsx index b2aece07a90..2e9ef971d67 100644 --- a/src/renderer/src/components/agent/AgentLaunchSetupPanel.tsx +++ b/src/renderer/src/components/agent/AgentLaunchSetupPanel.tsx @@ -3,7 +3,8 @@ import { textActionArgumentsOwnModel } from '../../../../shared/agent-launch/pic import type { AgentLaunchSetupPanelProps } from './AgentLaunchSetupButton' import { agentLaunchSetupOptionRows, - agentLaunchSetupCatalogState as catalogState + agentLaunchSetupRowGroups, + agentLaunchSetupCatalogState as catalogOf } from './agent-launch-setup-option-rows' import { useRef, useState } from 'react' import { Check, ChevronDown, ChevronLeft, ChevronRight } from 'lucide-react' @@ -83,7 +84,7 @@ export function AgentLaunchSetupPanel({ ? models.find((model) => model.isDefault)?.id : undefined) // Settled with nothing chosen still lets the user pick; nothing is checked and the agent keeps its own default. - const { hasModels, catalogPending, catalogMessage } = catalogState(entry, models, currentModel) + const { hasModels, catalogPending, catalogMessage } = catalogOf(entry, models, currentModel, mode) const modelInArguments = textActionArgumentsOwnModel(entry, draft) const modelDisabled = disabled || defaultsPending || catalogPending const options = agentPickerEntryOptions(entry, currentModel, draft.surface) @@ -98,14 +99,14 @@ export function AgentLaunchSetupPanel({ disabled, argumentsKnown }) - const renderOptions = (rows: SessionOptionDescriptor[]) => + const renderOptions = (rows: SessionOptionDescriptor[], layout?: 'inline') => rows.length ? ( { if (entry.textGeneration && value === 'arguments') { return @@ -144,6 +145,9 @@ export function AgentLaunchSetupPanel({ }, argumentsView.context ) + const rowGroups = agentLaunchSetupRowGroups(optionRows, mode === 'launch') + // Launch leaves Permissions and Arguments to Agent settings; select mode edits a saved setup. + const editsDefaults = mode === 'select' && !entry.textGeneration const note = agentLaunchSetupArgumentsNote(entry.label, options, !seed?.modelApply.cli?.length) const legacyCount = models.filter((model) => model.isLegacy && !model.hidden).length return ( @@ -288,10 +292,9 @@ export function AgentLaunchSetupPanel({
- {renderOptions( - optionRows.filter((row) => row.id !== 'fastMode' && row.id !== 'serviceTier') - )} - {!entry.textGeneration ? ( + {mode === 'launch' && renderOptions([...rowGroups.level, ...rowGroups.speed], 'inline')} + {renderOptions(rowGroups.rest)} + {editsDefaults ? (
) : null} - {renderOptions( - optionRows.filter((row) => row.id === 'fastMode' || row.id === 'serviceTier') - )} - {!entry.textGeneration && entry.surfaces.includes('terminal') ? ( + {mode === 'select' ? renderOptions(rowGroups.speed) : null} + {editsDefaults && entry.surfaces.includes('terminal') ? (
diff --git a/src/renderer/src/components/agent/agent-launch-picker-drafts.ts b/src/renderer/src/components/agent/agent-launch-picker-drafts.ts index 223173e147a..96d828773dd 100644 --- a/src/renderer/src/components/agent/agent-launch-picker-drafts.ts +++ b/src/renderer/src/components/agent/agent-launch-picker-drafts.ts @@ -18,7 +18,8 @@ import { import { type AgentPickerRetainedSetup as RetainedSetup, - currentAgentPickerDraft + currentAgentPickerDraft, + launchAgentPickerDraft } from '../../../../shared/agent-launch/picker/agent-launch-picker-authority' import { retireAgentPickerSetups } from './agent-launch-picker-retirement' import { @@ -228,7 +229,10 @@ export function useAgentPickerSetup({ }, [open, read, acquire, retained, mode]) let record = retained && matches(retained) ? retained : undefined const currentDraft = (draft: AgentPickerDraft) => { - const current = currentAgentPickerDraft(entry, draft) + const current = + mode === 'launch' + ? launchAgentPickerDraft(entry, draft) + : currentAgentPickerDraft(entry, draft) return currentSurface && currentSurface !== current.surface ? { ...current, surface: currentSurface } : current diff --git a/src/renderer/src/components/agent/agent-launch-picker-omnibox.test-fixture.tsx b/src/renderer/src/components/agent/agent-launch-picker-omnibox.test-fixture.tsx index 86a17de96b8..1fade9d9ad0 100644 --- a/src/renderer/src/components/agent/agent-launch-picker-omnibox.test-fixture.tsx +++ b/src/renderer/src/components/agent/agent-launch-picker-omnibox.test-fixture.tsx @@ -80,34 +80,33 @@ export async function verifyOmniboxPresetIdentity( ) ) fireEvent.click(document.querySelector('button[aria-label="Configure Codex"]')!) - await waitFor(() => expect(document.querySelector('textarea')).not.toBeNull()) + await waitFor(() => expect(effort('Low')).not.toBeNull()) } - const close = () => fireEvent.keyDown(document.querySelector('textarea')!, { key: 'Escape' }) + const panel = () => + document.querySelector( + '[data-slot="dropdown-menu-sub-content"], [data-slot="popover-content"]' + )! + const close = () => fireEvent.keyDown(panel(), { key: 'Escape' }) + const effort = (label: string) => + getByRole(document.body, 'radio', { name: label }).getAttribute('data-state') try { await showEntries(false) await configure() - fireEvent.change(document.querySelector('textarea')!, { - target: { value: '--ordinary-pending' } - }) + fireEvent.click(getByRole(document.body, 'radio', { name: 'High' })) close() await showEntries(true) fireEvent.change(document.querySelector('input')!, { target: { value: 'Astra low' } }) await configure() - expect(document.querySelector('textarea')).not.toHaveProperty('value', '--ordinary-pending') - fireEvent.change(document.querySelector('textarea')!, { - target: { value: '--model private/preset --preset-pending' } - }) - fireEvent.click(getByRole(document.body, 'radio', { name: 'Low' })) + expect(effort('Low')).toBe('on') close() await showEntries(false) await configure() - expect(document.querySelector('textarea')).toHaveProperty('value', '--ordinary-pending') + expect(effort('High')).toBe('on') close() await showEntries(true) fireEvent.change(document.querySelector('input')!, { target: { value: 'Astra low' } }) await configure() - expect(document.querySelector('textarea')).toHaveProperty('value', '--preset-pending') - expect(document.body.textContent).toContain('private/preset') + expect(effort('Low')).toBe('on') expect(launch).not.toHaveBeenCalled() } finally { act(() => { diff --git a/src/renderer/src/components/agent/agent-launch-setup-option-rows.ts b/src/renderer/src/components/agent/agent-launch-setup-option-rows.ts index 43f383aa04e..7a9e07ca2d1 100644 --- a/src/renderer/src/components/agent/agent-launch-setup-option-rows.ts +++ b/src/renderer/src/components/agent/agent-launch-setup-option-rows.ts @@ -163,10 +163,24 @@ export function agentLaunchSetupOptionRows( }) } +/** Speed rows sit apart from the rest; launch also lifts level rows onto speed's line. */ +export function agentLaunchSetupRowGroups(rows: SessionOptionDescriptor[], launch: boolean) { + const isSpeed = (row: SessionOptionDescriptor) => + row.id === 'fastMode' || row.id === 'serviceTier' + const isLevel = (row: SessionOptionDescriptor) => + launch && !isSpeed(row) && row.category === 'thought_level' && row.kind.type === 'select' + return { + level: rows.filter(isLevel), + speed: rows.filter(isSpeed), + rest: rows.filter((row) => !isSpeed(row) && !isLevel(row)) + } +} + export function agentLaunchSetupCatalogState( entry: AgentPickerEntry, models: readonly { id: string; hidden?: true }[], currentModel: string | undefined, + mode: 'launch' | 'select', t: Translate = translate ) { const hasModels = models.some((model) => !model.hidden) @@ -178,11 +192,17 @@ export function agentLaunchSetupCatalogState( ? null : entry.catalogRead === 'unsupported' || (!hasModels && entry.catalog.origin === 'unknown' && entry.catalogRead === undefined) - ? t( - 'agentPicker.catalogUnsupported', - '{{agent}} models are unavailable on this host. You can keep your current choices or use Arguments.', - { agent: entry.label } - ) + ? mode === 'launch' + ? t( + 'agentPicker.catalogUnsupportedLaunch', + '{{agent}} models are unavailable on this host. Your current choices still apply.', + { agent: entry.label } + ) + : t( + 'agentPicker.catalogUnsupported', + '{{agent}} models are unavailable on this host. You can keep your current choices or use Arguments.', + { agent: entry.label } + ) : entry.catalogRead === 'failed' ? t( 'agentPicker.catalogFailed', 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 761c6d0568a..67b99859a4b 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 @@ -1,7 +1,9 @@ import { useId, useLayoutEffect, useRef } from 'react' +import { Zap } from 'lucide-react' import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button' import { Switch } from '@/components/ui/switch' +import { Toggle } from '@/components/ui/toggle' import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { sortNativeChatSessionOptions } from '../../../../../../../shared/native-chat/input/native-chat-session-option-snapshot' @@ -33,8 +35,8 @@ type NativeChatSessionOptionRowsProps = { onAction: (descriptor: SessionOptionDescriptor) => void /** False when the caller already separates these rows from what sits above them. */ divided?: boolean - /** Settings grids provide their own inset and spacing between option blocks. */ - layout?: 'menu' | 'settings' + /** Settings grids provide their own inset and spacing; inline puts levels, unlabeled, left of the rest on one line. */ + layout?: 'menu' | 'settings' | 'inline' } // Retain missing rows disabled for this visit so model changes cannot slide the picker under the pointer. @@ -83,7 +85,7 @@ function LevelSegment(props: { function OptionRow(props: { descriptor: SessionOptionDescriptor - layout: 'menu' | 'settings' + layout: 'menu' | 'settings' | 'inline' /** Kept from an earlier model in this visit; the current one does not offer it. */ gone: boolean onSet: (value: SessionOptionValue) => void @@ -122,6 +124,20 @@ function OptionRow(props: {
) } + if (descriptor.kind.type === 'boolean' && layout === 'inline' && descriptor.id === 'fastMode') { + return ( + + + {say('fast')} + + ) + } if (descriptor.kind.type === 'boolean') { return (