mirror of
https://github.com/stablyai/orca.git
synced 2026-11-14 18:07:45 +00:00
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.
This commit is contained in:
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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<HTMLTextAreaElement>('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 }
|
||||
|
||||
@@ -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<HTMLButtonElement>('button[aria-label="Configure codex"]')!
|
||||
const prompt = () =>
|
||||
document.querySelector<HTMLElement>('[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<HTMLTextAreaElement>('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<HTMLTextAreaElement>('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<HTMLTextAreaElement>('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)
|
||||
|
||||
@@ -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' ? <MessageSquare /> : <SquareTerminal />
|
||||
return (
|
||||
<div className="grid max-w-full auto-cols-fr grid-flow-col gap-2">
|
||||
<div className={cn('grid max-w-full auto-cols-fr grid-flow-col gap-2', prominent && 'w-full')}>
|
||||
{agentPickerButtonOrder(entry).map((next) => {
|
||||
const isPrimary = next === primary
|
||||
const label =
|
||||
@@ -71,7 +77,7 @@ export function AgentLaunchSurfaceButtons({
|
||||
<Button
|
||||
key={next}
|
||||
type="button"
|
||||
size="xs"
|
||||
size={prominent ? 'sm' : 'xs'}
|
||||
className="min-w-0 w-full"
|
||||
aria-label={label}
|
||||
aria-keyshortcuts={
|
||||
@@ -85,7 +91,8 @@ export function AgentLaunchSurfaceButtons({
|
||||
variant={isPrimary ? 'default' : 'outline'}
|
||||
onClick={() => onSelect(next)}
|
||||
>
|
||||
<span className="truncate">{label}</span>
|
||||
{prominent ? surfaceIcon(next) : null}
|
||||
<span className={cn('truncate', prominent && 'mr-auto')}>{label}</span>
|
||||
<span aria-hidden="true">
|
||||
<ShortcutKeyCombo
|
||||
keys={formatKeybinding(isPrimary ? 'Enter' : 'Mod+Enter', platform)}
|
||||
@@ -149,11 +156,12 @@ export function AgentLaunchSetupFooter({
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2 border-t border-border px-2.5 py-2">
|
||||
<div className="flex items-center px-3 pb-3">
|
||||
<AgentLaunchSurfaceButtons
|
||||
entry={entry}
|
||||
disabled={disabled}
|
||||
primary={entry.defaultSurface}
|
||||
prominent
|
||||
terminalBlocked={terminalBlocked}
|
||||
onSelect={launch}
|
||||
/>
|
||||
|
||||
@@ -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<HTMLElement>('[contenteditable]')!,
|
||||
'keep @/repo/exact-file.ts'
|
||||
@@ -100,41 +105,38 @@ it.each(['claude', 'codex'] as const)(
|
||||
)
|
||||
)
|
||||
fireEvent.click(configure())
|
||||
expect(panel().querySelector<HTMLTextAreaElement>('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('')
|
||||
)
|
||||
|
||||
@@ -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 ? (
|
||||
<NativeChatSessionOptionRows
|
||||
options={rows}
|
||||
shownReason={null}
|
||||
pendingIds={new Set()}
|
||||
divided={false}
|
||||
layout="settings"
|
||||
layout={layout ?? 'settings'}
|
||||
onSet={(option, value) => {
|
||||
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({
|
||||
</CommandList>
|
||||
</Command>
|
||||
<div className="grid min-w-0 grid-cols-1 gap-2.5 border-t border-border px-3 pt-2.5 pb-1">
|
||||
{renderOptions(
|
||||
optionRows.filter((row) => row.id !== 'fastMode' && row.id !== 'serviceTier')
|
||||
)}
|
||||
{!entry.textGeneration ? (
|
||||
{mode === 'launch' && renderOptions([...rowGroups.level, ...rowGroups.speed], 'inline')}
|
||||
{renderOptions(rowGroups.rest)}
|
||||
{editsDefaults ? (
|
||||
<div>
|
||||
<AgentPermissionMenu
|
||||
agentId={entry.agent}
|
||||
@@ -321,10 +324,8 @@ export function AgentLaunchSetupPanel({
|
||||
/>
|
||||
</div>
|
||||
) : 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') ? (
|
||||
<div>
|
||||
<Collapsible open={argumentsOpen} onOpenChange={setArgumentsOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<HTMLElement>(
|
||||
'[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(() => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
+38
-7
@@ -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: {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (descriptor.kind.type === 'boolean' && layout === 'inline' && descriptor.id === 'fastMode') {
|
||||
return (
|
||||
<Toggle
|
||||
variant="chip"
|
||||
size="xs"
|
||||
pressed={!gone && descriptor.kind.currentValue}
|
||||
disabled={off}
|
||||
onPressedChange={onSet}
|
||||
>
|
||||
<Zap className="size-3" />
|
||||
{say('fast')}
|
||||
</Toggle>
|
||||
)
|
||||
}
|
||||
if (descriptor.kind.type === 'boolean') {
|
||||
return (
|
||||
<label
|
||||
@@ -166,7 +182,13 @@ function OptionRow(props: {
|
||||
layout === 'menu' && 'pl-2'
|
||||
)}
|
||||
>
|
||||
<span id={labelId} className="text-xs text-muted-foreground">
|
||||
<span
|
||||
id={labelId}
|
||||
className={cn(
|
||||
'text-xs text-muted-foreground',
|
||||
layout === 'inline' && descriptor.category === 'thought_level' && 'sr-only'
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
<ToggleGroup
|
||||
@@ -186,7 +208,7 @@ function OptionRow(props: {
|
||||
}
|
||||
}}
|
||||
// Why -ml-2: a wrapped group lines its first label up under the row's.
|
||||
className="-ml-2 flex-wrap"
|
||||
className={cn('flex-wrap', layout !== 'inline' && '-ml-2')}
|
||||
>
|
||||
{descriptor.kind.choices.map((choice) => (
|
||||
<LevelSegment key={choice.value} optionId={descriptor.id} choice={choice} />
|
||||
@@ -233,19 +255,28 @@ export function NativeChatSessionOptionRows({
|
||||
ref={frame}
|
||||
className={cn(
|
||||
'h-(--option-rows-height) shrink-0 overflow-hidden transition-[height] duration-150 ease-out motion-reduce:transition-none',
|
||||
layout === 'settings' && rows.length === 0 && 'hidden'
|
||||
layout !== 'menu' && rows.length === 0 && 'hidden'
|
||||
)}
|
||||
>
|
||||
<div ref={content}>
|
||||
{rows.length > 0 ? (
|
||||
<div className={cn(divided && 'border-t border-border', layout === 'menu' && 'p-1')}>
|
||||
<div className={cn('flex flex-col', layout === 'menu' ? 'gap-0.5' : 'gap-2.5')}>
|
||||
<div
|
||||
className={cn(
|
||||
layout === 'inline' ? 'flex flex-wrap items-center gap-3' : 'flex flex-col',
|
||||
layout === 'menu' && 'gap-0.5',
|
||||
layout === 'settings' && 'gap-2.5'
|
||||
)}
|
||||
>
|
||||
{rows.map((descriptor) => {
|
||||
const reason = nativeChatSessionOptionDisabledReason(descriptor.disabledReason, say)
|
||||
return (
|
||||
<NativeChatSessionOptionWriteGroup
|
||||
key={descriptor.id}
|
||||
busy={pendingIds.has(descriptor.id)}
|
||||
className={cn(
|
||||
layout === 'inline' && descriptor.category !== 'thought_level' && 'ml-auto'
|
||||
)}
|
||||
>
|
||||
<div data-option-id={descriptor.id}>
|
||||
<OptionRow
|
||||
|
||||
@@ -14,7 +14,9 @@ const toggleVariants = cva(
|
||||
'border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground',
|
||||
// Why not bg-accent: on a light popover it is nearly the surface colour (STYLEGUIDE, list rows).
|
||||
segmented:
|
||||
'bg-transparent text-muted-foreground transition-[color,background-color,box-shadow] hover:bg-foreground/6 hover:text-foreground data-[state=on]:bg-foreground/12 data-[state=on]:text-foreground dark:hover:bg-accent/50 dark:data-[state=on]:bg-accent'
|
||||
'bg-transparent text-muted-foreground transition-[color,background-color,box-shadow] hover:bg-foreground/6 hover:text-foreground data-[state=on]:bg-foreground/12 data-[state=on]:text-foreground dark:hover:bg-accent/50 dark:data-[state=on]:bg-accent',
|
||||
// An outlined segmented toggle that stands alone; pressed trades its border for the fill.
|
||||
chip: 'border border-input bg-transparent text-muted-foreground transition-[color,background-color,box-shadow] hover:bg-foreground/6 hover:text-foreground data-[state=on]:border-transparent data-[state=on]:bg-foreground/12 data-[state=on]:text-foreground dark:hover:bg-accent/50 dark:data-[state=on]:bg-accent'
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 min-w-9 px-2',
|
||||
|
||||
@@ -23427,6 +23427,7 @@
|
||||
},
|
||||
"agentPicker": {
|
||||
"catalogUnsupported": "Modelle für {{agent}} sind auf diesem Host nicht verfügbar. Du kannst deine Auswahl beibehalten oder Argumente verwenden.",
|
||||
"catalogUnsupportedLaunch": "Modelle für {{agent}} sind auf diesem Host nicht verfügbar. Deine aktuelle Auswahl gilt weiterhin.",
|
||||
"catalogFailed": "Modelle für {{agent}} konnten nicht geladen werden. Schließe die Agentenauswahl und öffne sie erneut.",
|
||||
"catalogSignIn": "Melde dich bei {{agent}} an, um die Modelle zu laden, und öffne die Agentenauswahl erneut.",
|
||||
"catalogInstall": "Installiere {{agent}} auf diesem Host, um die Modelle zu laden, und öffne die Agentenauswahl erneut.",
|
||||
|
||||
@@ -23839,6 +23839,7 @@
|
||||
},
|
||||
"agentPicker": {
|
||||
"catalogUnsupported": "{{agent}} models are unavailable on this host. You can keep your current choices or use Arguments.",
|
||||
"catalogUnsupportedLaunch": "{{agent}} models are unavailable on this host. Your current choices still apply.",
|
||||
"catalogFailed": "Could not load {{agent}} models. Close and reopen the agent picker to try again.",
|
||||
"catalogSignIn": "Sign in to {{agent}} to load its models, then close and reopen the agent picker.",
|
||||
"catalogInstall": "Install {{agent}} on this host to load its models, then close and reopen the agent picker.",
|
||||
|
||||
@@ -23289,6 +23289,7 @@
|
||||
},
|
||||
"agentPicker": {
|
||||
"catalogUnsupported": "Los modelos de {{agent}} no están disponibles en este equipo. Puedes conservar tus opciones o usar Argumentos.",
|
||||
"catalogUnsupportedLaunch": "Los modelos de {{agent}} no están disponibles en este equipo. Tus opciones actuales se siguen aplicando.",
|
||||
"catalogFailed": "No se pudieron cargar los modelos de {{agent}}. Cierra y vuelve a abrir el selector de agentes para intentarlo de nuevo.",
|
||||
"catalogSignIn": "Inicia sesión en {{agent}} para cargar sus modelos y vuelve a abrir el selector de agentes.",
|
||||
"catalogInstall": "Instala {{agent}} en este equipo para cargar sus modelos y vuelve a abrir el selector de agentes.",
|
||||
|
||||
@@ -23289,6 +23289,7 @@
|
||||
},
|
||||
"agentPicker": {
|
||||
"catalogUnsupported": "Les modèles de {{agent}} sont indisponibles sur cet hôte. Vous pouvez conserver vos choix ou utiliser les arguments.",
|
||||
"catalogUnsupportedLaunch": "Les modèles de {{agent}} sont indisponibles sur cet hôte. Vos choix actuels s'appliquent toujours.",
|
||||
"catalogFailed": "Impossible de charger les modèles de {{agent}}. Fermez puis rouvrez le sélecteur d’agents pour réessayer.",
|
||||
"catalogSignIn": "Connectez-vous à {{agent}} pour charger ses modèles, puis rouvrez le sélecteur d’agents.",
|
||||
"catalogInstall": "Installez {{agent}} sur cet hôte pour charger ses modèles, puis rouvrez le sélecteur d’agents.",
|
||||
|
||||
@@ -22598,6 +22598,7 @@
|
||||
},
|
||||
"agentPicker": {
|
||||
"catalogUnsupported": "このホストでは{{agent}}のモデルを利用できません。現在の選択を保持するか、引数を使用できます。",
|
||||
"catalogUnsupportedLaunch": "このホストでは{{agent}}のモデルを利用できません。現在の選択はそのまま適用されます。",
|
||||
"catalogFailed": "{{agent}}のモデルを読み込めませんでした。Agent 選択を閉じて開き直してください。",
|
||||
"catalogSignIn": "{{agent}}にサインインしてモデルを読み込み、Agent 選択を開き直してください。",
|
||||
"catalogInstall": "このホストに{{agent}}をインストールしてモデルを読み込み、Agent 選択を開き直してください。",
|
||||
|
||||
@@ -22598,6 +22598,7 @@
|
||||
},
|
||||
"agentPicker": {
|
||||
"catalogUnsupported": "이 호스트에서는 {{agent}} 모델을 사용할 수 없습니다. 현재 선택을 유지하거나 인수를 사용할 수 있습니다.",
|
||||
"catalogUnsupportedLaunch": "이 호스트에서는 {{agent}} 모델을 사용할 수 없습니다. 현재 선택은 그대로 적용됩니다.",
|
||||
"catalogFailed": "{{agent}} 모델을 불러오지 못했습니다. 에이전트 선택기를 닫았다가 다시 열어 보세요.",
|
||||
"catalogSignIn": "{{agent}}에 로그인하여 모델을 불러온 후 에이전트 선택기를 다시 여세요.",
|
||||
"catalogInstall": "이 호스트에 {{agent}}를 설치하여 모델을 불러온 후 에이전트 선택기를 다시 여세요.",
|
||||
|
||||
@@ -23262,6 +23262,7 @@
|
||||
},
|
||||
"agentPicker": {
|
||||
"catalogUnsupported": "Os modelos de {{agent}} não estão disponíveis neste host. Você pode manter suas escolhas ou usar argumentos.",
|
||||
"catalogUnsupportedLaunch": "Os modelos de {{agent}} não estão disponíveis neste host. Suas escolhas atuais continuam valendo.",
|
||||
"catalogFailed": "Não foi possível carregar os modelos de {{agent}}. Feche e reabra o seletor de agentes para tentar novamente.",
|
||||
"catalogSignIn": "Entre em {{agent}} para carregar seus modelos e reabra o seletor de agentes.",
|
||||
"catalogInstall": "Instale {{agent}} neste host para carregar seus modelos e reabra o seletor de agentes.",
|
||||
|
||||
@@ -24183,6 +24183,7 @@
|
||||
},
|
||||
"agentPicker": {
|
||||
"catalogUnsupported": "Модели {{agent}} недоступны на этом сервере. Можно сохранить текущий выбор или использовать аргументы.",
|
||||
"catalogUnsupportedLaunch": "Модели {{agent}} недоступны на этом сервере. Текущий выбор по-прежнему применяется.",
|
||||
"catalogFailed": "Не удалось загрузить модели {{agent}}. Закройте и снова откройте выбор агента.",
|
||||
"catalogSignIn": "Войдите в {{agent}}, чтобы загрузить модели, затем снова откройте выбор агента.",
|
||||
"catalogInstall": "Установите {{agent}} на этом сервере, чтобы загрузить модели, затем снова откройте выбор агента.",
|
||||
|
||||
@@ -23408,6 +23408,7 @@
|
||||
},
|
||||
"agentPicker": {
|
||||
"catalogUnsupported": "{{agent}} modelleri bu sunucuda kullanılamıyor. Geçerli seçimlerinizi koruyabilir veya bağımsız değişkenleri kullanabilirsiniz.",
|
||||
"catalogUnsupportedLaunch": "{{agent}} modelleri bu sunucuda kullanılamıyor. Geçerli seçimleriniz uygulanmaya devam eder.",
|
||||
"catalogFailed": "{{agent}} modelleri yüklenemedi. Yeniden denemek için ajan seçiciyi kapatıp açın.",
|
||||
"catalogSignIn": "Modelleri yüklemek için {{agent}} oturumu açın, ardından ajan seçiciyi yeniden açın.",
|
||||
"catalogInstall": "Modelleri yüklemek için bu sunucuya {{agent}} yükleyin, ardından ajan seçiciyi yeniden açın.",
|
||||
|
||||
@@ -23093,6 +23093,7 @@
|
||||
},
|
||||
"agentPicker": {
|
||||
"catalogUnsupported": "此主機上無法使用 {{agent}} 模型。你可以保留目前的選擇或使用引數。",
|
||||
"catalogUnsupportedLaunch": "此主機上無法使用 {{agent}} 模型。目前的選擇仍會套用。",
|
||||
"catalogFailed": "無法載入 {{agent}} 模型。請關閉並重新開啟代理選擇器以重試。",
|
||||
"catalogSignIn": "請登入 {{agent}} 以載入模型,然後重新開啟代理選擇器。",
|
||||
"catalogInstall": "請在此主機上安裝 {{agent}} 以載入模型,然後重新開啟代理選擇器。",
|
||||
|
||||
@@ -22598,6 +22598,7 @@
|
||||
},
|
||||
"agentPicker": {
|
||||
"catalogUnsupported": "此主机上无法使用 {{agent}} 模型。你可以保留当前选择或使用参数。",
|
||||
"catalogUnsupportedLaunch": "此主机上无法使用 {{agent}} 模型。当前选择仍会生效。",
|
||||
"catalogFailed": "无法加载 {{agent}} 模型。请关闭并重新打开智能体选择器以重试。",
|
||||
"catalogSignIn": "请登录 {{agent}} 以加载模型,然后重新打开智能体选择器。",
|
||||
"catalogInstall": "请在此主机上安装 {{agent}} 以加载模型,然后重新打开智能体选择器。",
|
||||
|
||||
@@ -103,8 +103,10 @@ it('keeps pristine, edited and reopened historical-host picker launches free of
|
||||
? createElement(
|
||||
TooltipProvider,
|
||||
null,
|
||||
// Select mode still edits Arguments and Permissions; the launch menu leaves them to settings.
|
||||
createElement(AgentLaunchSetupPanel, {
|
||||
entry,
|
||||
mode: 'select',
|
||||
draft: setup.draft,
|
||||
getDraft: setup.getDraft,
|
||||
onDraftChange: setup.change,
|
||||
|
||||
@@ -42,3 +42,20 @@ export type AgentPickerRetainedSetup = {
|
||||
draft: AgentPickerDraft
|
||||
active: boolean
|
||||
}
|
||||
|
||||
/** Launch has no Arguments editor, so Arguments a speed edit copied from older settings yield to the current ones. */
|
||||
export function launchAgentPickerDraft(
|
||||
entry: AgentPickerEntry,
|
||||
draft: AgentPickerDraft
|
||||
): AgentPickerDraft {
|
||||
if (draft.argumentsOwner === undefined || draft.argumentsOwner === entry.ownerKey) {
|
||||
return draft
|
||||
}
|
||||
const {
|
||||
arguments: _arguments,
|
||||
pendingArguments: _pending,
|
||||
argumentsOwner: _owner,
|
||||
...rest
|
||||
} = draft
|
||||
return rest
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user