{
// Why: a seeded query must keep the caret after the typed key.
const end = event.currentTarget.value.length
@@ -253,159 +226,3 @@ export function SubscribedAgentLaunchPickerMenu({
const entries = useAgentPickerInventoryEntries(inventory)
return
}
-
-// Why: catalog and seed reads land one agent at a time after the menu opens; only that row re-renders.
-const AgentPickerMenuRow = memo(function AgentPickerMenuRow({
- entry,
- draftScope,
- draftId,
- initialModel,
- initialEffort,
- openSetup,
- onSetupOpenChange,
- label = entry.label,
- source,
- selected,
- mode,
- setupDisabled,
- disabled,
- runLaunch,
- shortcut,
- onSelectAgent
-}: {
- entry: AgentPickerEntry
- draftScope?: object
- draftId?: string
- initialModel?: string
- initialEffort?: string
- openSetup: string | null
- onSetupOpenChange: (
- setup: {
- id: string
- agent: AgentPickerEntry['agent']
- preset?: AgentPickerSetupIdentity
- label: string
- } | null
- ) => void
- label?: string
- source?: AgentPickerDraft
- selected?: AgentPickerDraft
- mode?: 'launch' | 'select'
- setupDisabled?: boolean
- disabled?: boolean
- runLaunch: Launch
- shortcut?: string
- onSelectAgent?: (agent: AgentPickerEntry['agent']) => void
-}) {
- useUiLanguageRevision()
- const setupId = useId()
- const preset = initialModel ? { model: initialModel, effort: initialEffort } : undefined
- const initial = preset
- ? applyAgentPickerEdit(entry, selected ?? source ?? agentPickerDraft(entry), {
- kind: 'choice',
- patch: {
- model: preset.model,
- ...(preset.effort ? { options: { effort: preset.effort } } : {})
- }
- })
- : undefined
- const closeMenu = useDropdownMenuClose()
- const unavailable = entry.rows.find((row) => row.kind === 'chat-unavailable')
- if (setupDisabled) {
- return (
-
{
- onSelectAgent?.(entry.agent)
- runLaunch(pickerLaunchRow(entry, selected?.surface ?? entry.defaultSurface))
- }}
- >
-
- {label}
- {shortcut ? {shortcut} : null}
-
- )
- }
- return (
-
{
- if (next || openSetup === setupId) {
- onSetupOpenChange(next ? { id: setupId, agent: entry.agent, preset, label } : null)
- }
- if (next && openSetup !== setupId) {
- onSelectAgent?.(entry.agent)
- }
- }}
- onUse={(draft) => {
- const chosen = mode === 'select' ? draft.surface : undefined
- const result =
- chosen === undefined
- ? runLaunch(pickerLaunchRow(entry, draft.surface), draft, entry)
- : runLaunch(pickerLaunchRow(entry, draft.surface), draft, entry, chosen)
- if (result !== false) {
- closeMenu()
- }
- return result
- }}
- renderAnchor={(trigger) => (
- {
- event.preventDefault()
- if (disabled || !entry.surfaces.length) {
- return
- }
- const draft = initial ?? (mode === 'select' ? selected : undefined)
- const surface = draft?.surface ?? entry.defaultSurface
- const row = draft
- ? pickerLaunchRow(entry, surface)
- : (entry.rows.find((row) =>
- surface === 'chat'
- ? row.kind === 'chat'
- : row.kind === 'terminal' || row.kind === 'cli'
- ) ?? pickerLaunchRow(entry, surface))
- const result = draft ? runLaunch(row, draft, entry) : runLaunch(row, undefined)
- if (result !== false) {
- closeMenu()
- }
- }}
- onKeyDown={(event) => {
- if (
- event.target === event.currentTarget &&
- (event.key === 'Enter' || event.key === ' ')
- ) {
- event.preventDefault()
- event.currentTarget.click()
- }
- }}
- >
-
- {trigger}
-
- )}
- />
- )
-})
diff --git a/src/renderer/src/components/agent/AgentLaunchPickerMenuRow.tsx b/src/renderer/src/components/agent/AgentLaunchPickerMenuRow.tsx
new file mode 100644
index 00000000000..8603aec070c
--- /dev/null
+++ b/src/renderer/src/components/agent/AgentLaunchPickerMenuRow.tsx
@@ -0,0 +1,196 @@
+import { memo, useId } from 'react'
+import { Button } from '@/components/ui/button'
+import {
+ DropdownMenuItem,
+ DropdownMenuShortcut,
+ useDropdownMenuClose
+} from '@/components/ui/dropdown-menu'
+import { AgentIcon } from '@/lib/agent-catalog'
+import { useUiLanguageRevision } from '@/i18n/use-ui-language-revision'
+import { AgentLaunchSetupButton, type AgentPickerSetupIdentity } from './AgentLaunchSetupButton'
+import { applyAgentPickerEdit } from '../../../../shared/agent-launch/picker/agent-launch-picker-edits'
+import { chatUnavailableLabel } from '../tab-bar/tab-agent-row-presentation'
+import type { NewTabAgentMenuRow } from '../../../../shared/agent-launch/picker/agent-launch-picker-rows'
+import {
+ agentPickerDraft,
+ type AgentPickerDraft,
+ type AgentPickerEntry,
+ type AgentPickerSurface
+} from '../../../../shared/agent-launch/picker/agent-launch-picker'
+
+function pickerLaunchRow(entry: AgentPickerEntry, surface: AgentPickerSurface): NewTabAgentMenuRow {
+ return {
+ agent: entry.agent,
+ kind: surface === 'chat' ? 'chat' : 'cli',
+ executionHostId: entry.rows[0]?.executionHostId
+ }
+}
+
+export type AgentPickerMenuLaunch = (
+ row: NewTabAgentMenuRow,
+ draft?: AgentPickerDraft,
+ entry?: AgentPickerEntry,
+ /** Select mode: the surface explicitly accepted by a named launch action. */
+ surface?: AgentPickerSurface
+) => boolean | void | Promise
+
+// Why: catalog and seed reads land one agent at a time after the menu opens; only that row re-renders.
+export const AgentPickerMenuRow = memo(function AgentPickerMenuRow({
+ entry,
+ draftScope,
+ draftId,
+ initialModel,
+ initialEffort,
+ openSetup,
+ onSetupOpenChange,
+ label = entry.label,
+ source,
+ selected,
+ mode,
+ setupDisabled,
+ disabled,
+ runLaunch,
+ shortcut,
+ onSelectAgent
+}: {
+ entry: AgentPickerEntry
+ draftScope?: object
+ draftId?: string
+ initialModel?: string
+ initialEffort?: string
+ openSetup: string | null
+ onSetupOpenChange: (
+ setup: {
+ id: string
+ agent: AgentPickerEntry['agent']
+ preset?: AgentPickerSetupIdentity
+ label: string
+ } | null
+ ) => void
+ label?: string
+ source?: AgentPickerDraft
+ selected?: AgentPickerDraft
+ mode?: 'launch' | 'select'
+ setupDisabled?: boolean
+ disabled?: boolean
+ runLaunch: AgentPickerMenuLaunch
+ shortcut?: string
+ onSelectAgent?: (agent: AgentPickerEntry['agent']) => void
+}) {
+ useUiLanguageRevision()
+ const setupId = useId()
+ const preset = initialModel ? { model: initialModel, effort: initialEffort } : undefined
+ const initial = preset
+ ? applyAgentPickerEdit(entry, selected ?? source ?? agentPickerDraft(entry), {
+ kind: 'choice',
+ patch: {
+ model: preset.model,
+ ...(preset.effort ? { options: { effort: preset.effort } } : {})
+ }
+ })
+ : undefined
+ const closeMenu = useDropdownMenuClose()
+ const unavailable = entry.rows.find((row) => row.kind === 'chat-unavailable')
+ if (setupDisabled) {
+ return (
+ {
+ onSelectAgent?.(entry.agent)
+ runLaunch(pickerLaunchRow(entry, selected?.surface ?? entry.defaultSurface))
+ }}
+ >
+
+
+ {label}
+ {entry.description ? (
+ {entry.description}
+ ) : null}
+
+ {shortcut ? {shortcut} : null}
+
+ )
+ }
+ return (
+ {
+ if (next || openSetup === setupId) {
+ onSetupOpenChange(next ? { id: setupId, agent: entry.agent, preset, label } : null)
+ }
+ if (next && openSetup !== setupId) {
+ onSelectAgent?.(entry.agent)
+ }
+ }}
+ onUse={(draft) => {
+ const chosen = mode === 'select' ? draft.surface : undefined
+ const result =
+ chosen === undefined
+ ? runLaunch(pickerLaunchRow(entry, draft.surface), draft, entry)
+ : runLaunch(pickerLaunchRow(entry, draft.surface), draft, entry, chosen)
+ if (result !== false) {
+ closeMenu()
+ }
+ return result
+ }}
+ renderAnchor={(trigger) => (
+ {
+ event.preventDefault()
+ if (disabled || !entry.surfaces.length) {
+ return
+ }
+ const draft = initial ?? (mode === 'select' ? selected : undefined)
+ const surface = draft?.surface ?? entry.defaultSurface
+ const row = draft
+ ? pickerLaunchRow(entry, surface)
+ : (entry.rows.find((row) =>
+ surface === 'chat'
+ ? row.kind === 'chat'
+ : row.kind === 'terminal' || row.kind === 'cli'
+ ) ?? pickerLaunchRow(entry, surface))
+ const result = draft ? runLaunch(row, draft, entry) : runLaunch(row, undefined)
+ if (result !== false) {
+ closeMenu()
+ }
+ }}
+ onKeyDown={(event) => {
+ if (
+ event.target === event.currentTarget &&
+ (event.key === 'Enter' || event.key === ' ')
+ ) {
+ event.preventDefault()
+ event.currentTarget.click()
+ }
+ }}
+ >
+
+ {trigger}
+
+ )}
+ />
+ )
+})
diff --git a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandContentSection.tsx b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandContentSection.tsx
index 9d708cfa813..ec66016fd95 100644
--- a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandContentSection.tsx
+++ b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandContentSection.tsx
@@ -1,4 +1,11 @@
-import { useId, type Dispatch, type MutableRefObject, type SetStateAction } from 'react'
+import {
+ useCallback,
+ useId,
+ useMemo,
+ type Dispatch,
+ type MutableRefObject,
+ type SetStateAction
+} from 'react'
import type { TerminalQuickCommand } from '../../../../shared/terminal/quick-commands/terminal-quick-command-types'
import type { TuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent'
import {
@@ -10,14 +17,10 @@ import {
type TerminalQuickCommandBodySize
} from '../../../../shared/terminal/quick-commands/terminal-quick-command-prompt-limit'
import { Label } from '@/components/ui/label'
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue
-} from '@/components/ui/select'
-import { AgentIcon } from '@/lib/agent-catalog'
+import { AgentLaunchPicker } from '@/components/agent/AgentLaunchPicker'
+import type { AgentPickerEntry } from '../../../../shared/agent-launch/picker/agent-launch-picker'
+import type { NewTabAgentMenuModel } from '../tab-bar/new-tab-agent-menu-model'
+import { useUiLanguageRevision } from '@/i18n/use-ui-language-revision'
import { cn } from '@/lib/utils'
import { getIntlLocale, translate } from '@/i18n/i18n'
import { getTerminalQuickCommandAgentOptions } from './terminal-quick-command-agent-options'
@@ -25,6 +28,18 @@ import type { TerminalQuickCommandDialogDraftMemory } from './terminal-quick-com
import { TerminalQuickCommandAppendEnterSwitch } from './TerminalQuickCommandAppendEnterSwitch'
const QUICK_COMMAND_AGENT_OPTIONS = getTerminalQuickCommandAgentOptions()
+const QUICK_COMMAND_PICKER_MODEL: NewTabAgentMenuModel = {
+ state: 'settled',
+ rows: QUICK_COMMAND_AGENT_OPTIONS.map(({ id }) => ({ agent: id, kind: 'cli' })),
+ detection: {
+ detectedIds: [],
+ isLoading: false,
+ isRefreshing: false,
+ detectionFailed: false,
+ needsServerUpdate: false,
+ refresh: async () => []
+ }
+}
// Why: long pastes are the common way to hit the cap, so the count appears before Save disables.
const BODY_LENGTH_COUNTER_THRESHOLD = 0.8
@@ -79,6 +94,38 @@ export function TerminalQuickCommandContentSection({
setDraft,
toggleAppendEnter
}: TerminalQuickCommandContentSectionProps): React.JSX.Element {
+ useUiLanguageRevision()
+ const unsupportedLabel = translate(
+ 'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.026cfb232a',
+ 'Does not support prompt commands'
+ )
+ const entries = useMemo(
+ () =>
+ QUICK_COMMAND_AGENT_OPTIONS.map(({ id, label }) => {
+ const supported = supportsTerminalAgentQuickCommand(id)
+ return {
+ agent: id,
+ label,
+ description: supported ? undefined : unsupportedLabel,
+ rows: [{ agent: id, kind: 'cli' }],
+ surfaces: supported ? ['terminal'] : [],
+ defaultSurface: 'terminal',
+ terminal: {},
+ structured: {},
+ catalog: { origin: 'unknown' }
+ }
+ }),
+ [unsupportedLabel]
+ )
+ const selectAgent = useCallback(
+ (nextAgent: TuiAgent) => {
+ draftMemoryRef.current = { ...draftMemoryRef.current, agent: nextAgent }
+ setDraft((current) =>
+ isTerminalAgentQuickCommand(current) ? { ...current, agent: nextAgent } : current
+ )
+ },
+ [draftMemoryRef, setDraft]
+ )
const bodyTooLong = bodySize.used > bodySize.max
const showBodyLength = bodySize.used >= bodySize.max * BODY_LENGTH_COUNTER_THRESHOLD
const lengthId = useId()
@@ -124,58 +171,18 @@ export function TerminalQuickCommandContentSection({
'Agent'
)}
-
+ onPick={selectAgent}
+ />
diff --git a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandDialog.test.tsx b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandDialog.test.tsx
index 6f27d439ff7..61b1e119d3a 100644
--- a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandDialog.test.tsx
+++ b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandDialog.test.tsx
@@ -2,10 +2,18 @@
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
+import { fireEvent, getByRole } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { TerminalQuickCommand } from '../../../../shared/terminal/quick-commands/terminal-quick-command-types'
import { LEGACY_MAX_QUICK_COMMAND_AGENT_PROMPT_LENGTH } from '../../../../shared/terminal/quick-commands/terminal-quick-command-prompt-limit'
import { TerminalQuickCommandDialog } from './TerminalQuickCommandDialog'
+import { useAppStore } from '@/store'
+import {
+ makeFolderWorkspace,
+ makeWorktree,
+ TEST_REPO
+} from '@/store/slices/worktrees-slice-test-fixtures'
const mountedRoots: Root[] = []
@@ -192,13 +200,13 @@ describe('TerminalQuickCommandDialog animation structure', () => {
})
}
- const agentSelect = document.body.querySelector