fix(quick-commands): flatten the settings list and give the command editor room (#13922)

This commit is contained in:
Jinwoo Hong
2026-08-11 19:53:22 -07:00
committed by GitHub
parent 64aec94cb2
commit b51ef400ec
11 changed files with 470 additions and 301 deletions
@@ -1,4 +1,4 @@
import { Check, Copy, Pencil, Trash2 } from 'lucide-react'
import { Check, Copy, Pencil, TerminalSquare, Trash2 } from 'lucide-react'
import type {
Repo,
TerminalQuickCommand,
@@ -29,6 +29,15 @@ function getScopeLabel(
return repo ? getQuickCommandRepoLabel(repo) : 'Missing project'
}
function getRunModeLabel(command: TerminalQuickCommand): string {
if (isTerminalAgentQuickCommand(command)) {
return translate('auto.components.settings.QuickCommandsPane.4ccc63da87', 'Agent')
}
return command.appendEnter
? translate('auto.components.settings.QuickCommandsPane.9b3e338d62', 'Enter')
: translate('auto.components.settings.QuickCommandsPane.9fcfc29519', 'Insert')
}
function QuickCommandRow({
command,
repoById,
@@ -43,6 +52,7 @@ function QuickCommandRow({
const scope = getTerminalQuickCommandScope(command)
const body = getTerminalQuickCommandBody(command)
const { canCopy, copyText, status } = useClipboardTextCopyFeedback(body)
const commandName = command.label || 'quick command'
const copyLabel =
status === 'copied'
@@ -51,19 +61,24 @@ function QuickCommandRow({
? translate('auto.components.settings.QuickCommandsPane.53b17a4b1b', "Couldn't copy")
: canCopy
? translate('auto.components.settings.QuickCommandsPane.a9a564b7e7', 'Copy {{value0}}', {
value0: command.label || 'quick command'
value0: commandName
})
: translate('auto.components.settings.QuickCommandsPane.69a1441a21', 'Nothing to copy')
const editLabel = translate(
'auto.components.settings.QuickCommandsPane.7d90fd5299',
'Edit {{value0}}',
{ value0: commandName }
)
return (
<div className="flex items-center gap-3 rounded-md border border-border/60 bg-background px-3 py-2 shadow-xs">
<div className="group/qc flex items-center gap-3 rounded-md px-2 py-2.5 transition-colors hover:bg-accent/60 focus-within:bg-accent/60">
<div className="min-w-0 flex-1">
<div className="flex min-w-0 items-center gap-2">
<div className="truncate text-sm font-medium">
{command.label ||
translate('auto.components.settings.QuickCommandsPane.2bb9e38e93', 'Untitled')}
</div>
<Badge variant="outline" className="max-w-44 gap-1.5">
<Badge variant="outline" className="max-w-44 gap-1.5 text-[11px] font-normal">
{scope.type === 'repo' ? (
<>
<RepoBadgeMark color={repoById.get(scope.repoId)?.badgeColor} />
@@ -74,9 +89,9 @@ function QuickCommandRow({
)}
</Badge>
</div>
<div className="flex min-w-0 items-center gap-1.5 text-xs text-foreground/80">
<div className="mt-0.5 flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground">
{isTerminalAgentQuickCommand(command) ? (
<span className="shrink-0 text-muted-foreground">
<span className="shrink-0">
<AgentIcon agent={command.agent} size={12} />
</span>
) : null}
@@ -91,59 +106,95 @@ function QuickCommandRow({
</span>
</div>
</div>
<div className="shrink-0 text-[11px] font-medium text-foreground/75">
{isTerminalAgentQuickCommand(command)
? translate('auto.components.settings.QuickCommandsPane.4ccc63da87', 'Agent')
: command.appendEnter
? translate('auto.components.settings.QuickCommandsPane.9b3e338d62', 'Enter')
: translate('auto.components.settings.QuickCommandsPane.9fcfc29519', 'Insert')}
<div className="w-12 shrink-0 text-right text-[11px] text-muted-foreground">
{getRunModeLabel(command)}
</div>
{/* Why can-hover: touch devices never hover, so the actions must stay visible there. */}
<div className="flex shrink-0 items-center gap-0.5 transition-opacity can-hover:opacity-0 group-hover/qc:opacity-100 group-focus-within/qc:opacity-100">
<Button
type="button"
variant="ghost"
size="icon-sm"
aria-label={editLabel}
title={editLabel}
onClick={() => onEdit(command)}
>
<Pencil />
</Button>
<Button
type="button"
variant="ghost"
size="icon-sm"
disabled={!canCopy}
aria-label={copyLabel}
title={copyLabel}
onClick={() => void copyText()}
className={cn(
status === 'copied' && 'text-status-success',
status === 'failed' && 'text-destructive'
)}
>
{status === 'copied' ? <Check /> : <Copy />}
</Button>
<Button
type="button"
variant="ghost"
size="icon-sm"
aria-label={translate(
'auto.components.settings.QuickCommandsPane.8764c6e9e4',
'Remove {{value0}}',
{ value0: commandName }
)}
onClick={() => onRemove(command)}
className="text-muted-foreground hover:text-destructive"
>
<Trash2 />
</Button>
</div>
</div>
)
}
function QuickCommandsEmptyState({
hasCommands,
hasQuery
}: {
hasCommands: boolean
hasQuery: boolean
}): React.JSX.Element {
if (hasCommands) {
return (
<div className="px-2 py-10 text-center text-sm text-muted-foreground">
{hasQuery
? translate(
'auto.components.settings.QuickCommandsList.noSearchMatches',
'No commands match this search.'
)
: translate(
'auto.components.settings.QuickCommandsPane.3eb9897ab0',
'No commands in the selected scopes.'
)}
</div>
)
}
// Why no action here: the toolbar's Add Command sits directly above.
return (
<div className="flex flex-col items-center gap-3 px-2 py-10 text-center">
<TerminalSquare className="size-7 text-muted-foreground/50" />
<div className="space-y-1">
<p className="text-sm text-muted-foreground">
{translate(
'auto.components.settings.QuickCommandsPane.38d61927e6',
'No quick commands saved.'
)}
</p>
<p className="text-xs text-muted-foreground/80">
{translate(
'auto.components.settings.QuickCommandsPane.c36912efd5',
'Run them from the Quick Commands button in the tab bar, or right-click inside any terminal.'
)}
</p>
</div>
<Button
type="button"
variant="ghost"
size="icon-sm"
disabled={!canCopy}
aria-label={copyLabel}
title={copyLabel}
onClick={() => void copyText()}
className={cn(
status === 'copied' && 'text-status-success',
status === 'failed' && 'text-destructive'
)}
>
{status === 'copied' ? <Check /> : <Copy />}
</Button>
<Button
type="button"
variant="ghost"
size="icon-sm"
aria-label={translate(
'auto.components.settings.QuickCommandsPane.7d90fd5299',
'Edit {{value0}}',
{
value0: command.label || 'quick command'
}
)}
onClick={() => onEdit(command)}
>
<Pencil />
</Button>
<Button
type="button"
variant="ghost"
size="icon-sm"
aria-label={translate(
'auto.components.settings.QuickCommandsPane.8764c6e9e4',
'Remove {{value0}}',
{
value0: command.label || 'quick command'
}
)}
onClick={() => onRemove(command)}
className="text-muted-foreground hover:text-destructive"
>
<Trash2 />
</Button>
</div>
)
}
@@ -151,43 +202,32 @@ function QuickCommandRow({
export function QuickCommandsList({
commands,
visibleCommands,
hasQuery,
repoById,
onEdit,
onRemove
}: {
commands: TerminalQuickCommand[]
visibleCommands: TerminalQuickCommand[]
hasQuery: boolean
repoById: Map<string, Pick<Repo, 'displayName' | 'path' | 'badgeColor'>>
onEdit: (command: TerminalQuickCommand) => void
onRemove: (command: TerminalQuickCommand) => void
}): React.JSX.Element {
if (visibleCommands.length === 0) {
return <QuickCommandsEmptyState hasCommands={commands.length > 0} hasQuery={hasQuery} />
}
return (
<div className="overflow-hidden rounded-lg border border-border/50 bg-muted/20">
{visibleCommands.length === 0 ? (
<div className="px-3 py-6 text-sm text-muted-foreground">
{commands.length === 0
? translate(
'auto.components.settings.QuickCommandsPane.38d61927e6',
'No quick commands saved.'
)
: translate(
'auto.components.settings.QuickCommandsPane.3eb9897ab0',
'No commands in the selected scopes.'
)}
</div>
) : (
<div className="max-h-[60vh] space-y-2 overflow-y-auto p-2 scrollbar-sleek">
{visibleCommands.map((command) => (
<QuickCommandRow
key={command.id}
command={command}
repoById={repoById}
onEdit={onEdit}
onRemove={onRemove}
/>
))}
</div>
)}
<div className="-mx-2 divide-y divide-border/50">
{visibleCommands.map((command) => (
<QuickCommandRow
key={command.id}
command={command}
repoById={repoById}
onEdit={onEdit}
onRemove={onRemove}
/>
))}
</div>
)
}
@@ -1,26 +1,25 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Plus } from 'lucide-react'
import type { GlobalSettings, TerminalQuickCommand } from '../../../../shared/types'
import { getTerminalQuickCommandScope } from '../../../../shared/terminal-quick-commands'
import {
createTerminalQuickCommandDraft,
TerminalQuickCommandDialog
} from '@/components/terminal-quick-commands/TerminalQuickCommandDialog'
import { searchTerminalQuickCommands } from '@/lib/terminal-quick-command-search'
import { useAppStore } from '../../store'
import { Button } from '../ui/button'
import { Label } from '../ui/label'
import { useConfirmationDialog } from '@/components/confirmation-dialog-context'
import { getSettingOwnershipSummary } from './setting-ownership'
import { translate } from '@/i18n/i18n'
import { QuickCommandsList } from './QuickCommandsList'
import { GLOBAL_SCOPE_KEY, QuickCommandsScopeFilter } from './QuickCommandsScopeFilter'
import { QuickCommandsToolbar } from './QuickCommandsToolbar'
import { GLOBAL_SCOPE_KEY } from './QuickCommandsScopeFilter'
import {
getRepoExecutionHostId,
LOCAL_EXECUTION_HOST_ID,
parseExecutionHostId,
type ExecutionHostId
} from '../../../../shared/execution-host'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
import {
getTerminalQuickCommandHostOptions,
shouldShowTerminalQuickCommandHostOwnership
@@ -130,6 +129,7 @@ export function QuickCommandsPane({
// automatically rather than being silently excluded.
const [scopeSelection, setScopeSelection] = useState<ReadonlySet<string> | null>(null)
const [scopePopoverOpen, setScopePopoverOpen] = useState(false)
const [query, setQuery] = useState('')
const availableHostId = getAvailableQuickCommandHostId(selectedHostId, hostOptions)
const editorHostIsCurrent =
@@ -164,7 +164,7 @@ export function QuickCommandsPane({
const effectiveSelection: ReadonlySet<string> = scopeSelection ?? allScopeKeys
const showAll = scopeSelection === null
const visibleCommands = commands.filter((command) => {
const scopedCommands = commands.filter((command) => {
const scope = getTerminalQuickCommandScope(command)
if (showAll) {
return true
@@ -174,6 +174,7 @@ export function QuickCommandsPane({
}
return effectiveSelection.has(scope.repoId)
})
const visibleCommands = searchTerminalQuickCommands(scopedCommands, query)
const createDraftForCurrentFilter = useCallback((): TerminalQuickCommand => {
// Why: when the user has narrowed to a single repo scope, the natural
@@ -273,68 +274,32 @@ export function QuickCommandsPane({
void useAppStore.getState().deleteTerminalQuickCommand(selectedHostId, command.id)
}
return (
<div className="space-y-3">
<div className="flex items-center justify-between gap-3 py-2">
<div className="space-y-1">
<Label>
{translate('auto.components.settings.QuickCommandsPane.f91b649324', 'Saved Commands')}
</Label>
<p className="text-xs text-muted-foreground">
{shouldShowTerminalQuickCommandHostOwnership(hostOptions)
? ownership.description
: translate(
'auto.components.settings.settingOwnership.terminalQuickCommands',
'Commands are saved on this client, then scoped globally or to a project setup so they run from the selected terminal context.'
)}
</p>
</div>
<Button
type="button"
variant="outline"
size="sm"
disabled={!canManageSelectedHost}
onClick={() =>
setEditor({
mode: 'add',
command: createDraftForCurrentFilter(),
connectionGeneration: selectedRuntimeConnectionGeneration,
hostId: selectedHostId
})
}
>
<Plus />
{translate('auto.components.settings.QuickCommandsPane.5aacc8f7dc', 'Add Command')}
</Button>
</div>
const openAddDialog = (): void =>
setEditor({
mode: 'add',
command: createDraftForCurrentFilter(),
connectionGeneration: selectedRuntimeConnectionGeneration,
hostId: selectedHostId
})
return (
<div className="space-y-4">
{shouldShowTerminalQuickCommandHostOwnership(hostOptions) ? (
<div className="space-y-2">
<Label htmlFor="quick-command-storage-host">
{translate('auto.components.settings.QuickCommandsPane.89f7e57fcc', 'Saved on')}
</Label>
<Select
value={selectedHostId}
onValueChange={(value) => {
setSelectedHostId(value as ExecutionHostId)
setScopeSelection(null)
}}
>
<SelectTrigger id="quick-command-storage-host" size="sm" className="w-56">
<SelectValue />
</SelectTrigger>
<SelectContent>
{hostOptions.map((host) => (
<SelectItem key={host.id} value={host.id}>
{host.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<p className="text-xs text-muted-foreground">{ownership.description}</p>
) : null}
<QuickCommandsScopeFilter
<QuickCommandsToolbar
query={query}
setQuery={setQuery}
hostOptions={hostOptions}
showHostSelect={shouldShowTerminalQuickCommandHostOwnership(hostOptions)}
selectedHostId={selectedHostId}
onHostChange={(hostId) => {
setSelectedHostId(hostId)
setScopeSelection(null)
}}
canAdd={canManageSelectedHost}
onAdd={openAddDialog}
repos={hostRepos}
effectiveSelection={effectiveSelection}
showAll={showAll}
@@ -408,6 +373,7 @@ export function QuickCommandsPane({
<QuickCommandsList
commands={commands}
visibleCommands={visibleCommands}
hasQuery={query.trim().length > 0}
repoById={repoById}
onEdit={(command) =>
setEditor({
@@ -428,6 +394,7 @@ export function QuickCommandsPane({
mode={editor.mode}
command={editor.command}
repos={hostRepos}
defaultAdvancedOpen
onOpenChange={(open) => !open && setEditor(null)}
onSave={saveCommand}
/>
@@ -0,0 +1,116 @@
import type { Dispatch, SetStateAction } from 'react'
import { Plus, Search } from 'lucide-react'
import type { Repo } from '../../../../shared/types'
import type { ExecutionHostId } from '../../../../shared/execution-host'
import { Button } from '../ui/button'
import { Input } from '../ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
import { translate } from '@/i18n/i18n'
import { QuickCommandsScopeFilter } from './QuickCommandsScopeFilter'
type QuickCommandsToolbarProps = {
query: string
setQuery: (query: string) => void
hostOptions: readonly { id: ExecutionHostId; label: string }[]
showHostSelect: boolean
selectedHostId: ExecutionHostId
onHostChange: (hostId: ExecutionHostId) => void
canAdd: boolean
onAdd: () => void
repos: readonly Repo[]
effectiveSelection: ReadonlySet<string>
showAll: boolean
scopePopoverOpen: boolean
setScopePopoverOpen: Dispatch<SetStateAction<boolean>>
handleSelectAll: () => void
toggleScope: (key: string) => void
}
export function QuickCommandsToolbar({
query,
setQuery,
hostOptions,
showHostSelect,
selectedHostId,
onHostChange,
canAdd,
onAdd,
repos,
effectiveSelection,
showAll,
scopePopoverOpen,
setScopePopoverOpen,
handleSelectAll,
toggleScope
}: QuickCommandsToolbarProps): React.JSX.Element {
const searchLabel = translate(
'auto.components.settings.QuickCommandsToolbar.searchLabel',
'Search commands'
)
return (
<div className="flex flex-wrap items-center gap-2">
<div className="relative min-w-52 flex-1">
<Search className="pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-muted-foreground" />
<Input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder={searchLabel}
aria-label={searchLabel}
className="h-8 pl-8 text-xs"
/>
</div>
<QuickCommandsScopeFilter
repos={repos}
effectiveSelection={effectiveSelection}
showAll={showAll}
scopePopoverOpen={scopePopoverOpen}
setScopePopoverOpen={setScopePopoverOpen}
handleSelectAll={handleSelectAll}
toggleScope={toggleScope}
/>
{showHostSelect ? (
<Select
value={selectedHostId}
onValueChange={(value) => onHostChange(value as ExecutionHostId)}
>
<SelectTrigger
id="quick-command-storage-host"
size="sm"
aria-label={translate(
'auto.components.settings.QuickCommandsPane.89f7e57fcc',
'Saved on'
)}
className="text-xs"
>
<span className="text-muted-foreground">
{translate('auto.components.settings.QuickCommandsPane.89f7e57fcc', 'Saved on')}
</span>
<SelectValue />
</SelectTrigger>
<SelectContent>
{hostOptions.map((host) => (
<SelectItem key={host.id} value={host.id}>
{host.label}
</SelectItem>
))}
</SelectContent>
</Select>
) : null}
<Button
type="button"
variant="outline"
size="sm"
disabled={!canAdd}
onClick={onAdd}
className="ml-auto"
>
<Plus />
{translate('auto.components.settings.QuickCommandsPane.5aacc8f7dc', 'Add Command')}
</Button>
</div>
)
}
@@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { TerminalQuickCommandAppendEnterSwitch } from './TerminalQuickCommandAppendEnterSwitch'
import { TerminalQuickCommandCollapsibleRow } from './TerminalQuickCommandCollapsibleRow'
import { TerminalQuickCommandScopeField } from './TerminalQuickCommandScopeField'
type TerminalQuickCommandAdvancedSectionProps = {
@@ -50,42 +51,26 @@ export function TerminalQuickCommandAdvancedSection({
<ChevronDown className={cn('size-4 transition-transform', advancedOpen && 'rotate-180')} />
</Button>
<div
className={cn(
'grid overflow-hidden transition-[grid-template-rows] duration-200 ease-out',
advancedOpen ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'
)}
aria-hidden={!advancedOpen}
>
<div className="min-h-0">
<div
className={cn(
'space-y-4 px-1 pt-1 pb-1 transition-[opacity,transform] duration-150 ease-out',
advancedOpen
? 'translate-y-0 opacity-100 delay-200'
: '-translate-y-1 opacity-0 delay-0'
)}
>
{!isTerminalAgentQuickCommand(draft) ? (
<TerminalQuickCommandAppendEnterSwitch
appendEnter={draft.appendEnter}
onToggle={toggleAppendEnter}
/>
) : null}
<TerminalQuickCommandScopeField
repos={repos}
selectedScope={selectedScope}
selectedRepoId={selectedRepoId}
selectedRepoMissing={selectedRepoMissing}
lastRepoScopeId={lastRepoScopeIdRef.current}
rememberRepoScopeId={(repoId) => {
lastRepoScopeIdRef.current = repoId
}}
setDraft={setDraft}
/>
</div>
</div>
</div>
<TerminalQuickCommandCollapsibleRow open={advancedOpen} className="space-y-4 px-1 pt-2 pb-1">
<TerminalQuickCommandScopeField
repos={repos}
selectedScope={selectedScope}
selectedRepoId={selectedRepoId}
selectedRepoMissing={selectedRepoMissing}
lastRepoScopeId={lastRepoScopeIdRef.current}
rememberRepoScopeId={(repoId) => {
lastRepoScopeIdRef.current = repoId
}}
setDraft={setDraft}
/>
{!isTerminalAgentQuickCommand(draft) ? (
<TerminalQuickCommandAppendEnterSwitch
appendEnter={draft.appendEnter}
disabled={!advancedOpen}
onToggle={toggleAppendEnter}
/>
) : null}
</TerminalQuickCommandCollapsibleRow>
</div>
)
}
@@ -3,11 +3,13 @@ import { Switch } from '@/components/ui/switch'
type TerminalQuickCommandAppendEnterSwitchProps = {
appendEnter: boolean
onToggle: () => void
disabled?: boolean
}
export function TerminalQuickCommandAppendEnterSwitch({
appendEnter,
onToggle
onToggle,
disabled = false
}: TerminalQuickCommandAppendEnterSwitchProps): React.JSX.Element {
return (
<div className="flex items-start justify-between gap-4">
@@ -27,6 +29,7 @@ export function TerminalQuickCommandAppendEnterSwitch({
</div>
<Switch
checked={appendEnter}
disabled={disabled}
aria-label={translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandAppendEnterSwitch.e4e5fed3b3',
'Toggle append Enter'
@@ -0,0 +1,38 @@
import type { ReactNode } from 'react'
import { cn } from '@/lib/utils'
type TerminalQuickCommandCollapsibleRowProps = {
open: boolean
className?: string
children: ReactNode
}
/** Why: switching action adds/removes fields; animating the rows keeps the
* dialog from snapping between content heights. */
export function TerminalQuickCommandCollapsibleRow({
open,
className,
children
}: TerminalQuickCommandCollapsibleRowProps): React.JSX.Element {
return (
<div
className={cn(
'grid overflow-hidden transition-[grid-template-rows] duration-200 ease-out',
open ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'
)}
aria-hidden={!open}
>
<div className="min-h-0">
<div
className={cn(
'transition-[opacity,transform] duration-150 ease-out',
open ? 'translate-y-0 opacity-100 delay-200' : '-translate-y-1 opacity-0 delay-0',
className
)}
>
{children}
</div>
</div>
</div>
)
}
@@ -12,9 +12,11 @@ import {
SelectTrigger,
SelectValue
} from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea'
import { AgentIcon } from '@/lib/agent-catalog'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { TerminalQuickCommandCollapsibleRow } from './TerminalQuickCommandCollapsibleRow'
import { getTerminalQuickCommandAgentOptions } from './terminal-quick-command-agent-options'
import type { TerminalQuickCommandDialogDraftMemory } from './terminal-quick-command-dialog-draft'
@@ -37,85 +39,66 @@ export function TerminalQuickCommandContentSection({
}: TerminalQuickCommandContentSectionProps): React.JSX.Element {
return (
<div>
{/* Why: action changes add/remove agent-only fields; animating rows here
keeps the fixed dialog from snapping between content heights. */}
<div
className={cn(
'grid overflow-hidden transition-[grid-template-rows] duration-200 ease-out',
isAgentAction ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'
)}
aria-hidden={!isAgentAction}
>
<div className="min-h-0">
<div
className={cn(
'space-y-2 px-1 pt-1 pb-4 transition-[opacity,transform] duration-150 ease-out',
isAgentAction
? 'translate-y-0 opacity-100 delay-200'
: '-translate-y-1 opacity-0 delay-0'
)}
>
<Label>
{translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.0adba8fa0c',
'Agent'
<TerminalQuickCommandCollapsibleRow open={isAgentAction} className="space-y-2 px-1 pt-1 pb-4">
<Label>
{translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.0adba8fa0c',
'Agent'
)}
</Label>
<Select
value={selectedAgent}
disabled={!isAgentAction}
onValueChange={(agent) => {
const nextAgent = agent as TuiAgent
draftMemoryRef.current = {
...draftMemoryRef.current,
agent: nextAgent
}
setDraft((current) =>
isTerminalAgentQuickCommand(current) ? { ...current, agent: nextAgent } : current
)
}}
>
<SelectTrigger>
<SelectValue
placeholder={translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.346d409ab2',
'Choose agent'
)}
</Label>
<Select
value={selectedAgent}
disabled={!isAgentAction}
onValueChange={(agent) => {
const nextAgent = agent as TuiAgent
draftMemoryRef.current = {
...draftMemoryRef.current,
agent: nextAgent
}
setDraft((current) =>
isTerminalAgentQuickCommand(current) ? { ...current, agent: nextAgent } : current
)
}}
>
<SelectTrigger>
<SelectValue
placeholder={translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.346d409ab2',
'Choose agent'
)}
/>
</SelectTrigger>
<SelectContent
position="popper"
side="bottom"
align="start"
sideOffset={4}
className="max-h-[min(20rem,var(--radix-select-content-available-height))] w-[--radix-select-trigger-width]"
>
{QUICK_COMMAND_AGENT_OPTIONS.map((entry) => {
const supported = supportsTerminalAgentQuickCommand(entry.id)
return (
<SelectItem key={entry.id} value={entry.id} disabled={!supported}>
<span className="flex min-w-0 items-center gap-2">
<AgentIcon agent={entry.id} size={16} />
<span className="flex min-w-0 flex-col">
<span className="truncate">{entry.label}</span>
{!supported ? (
<span className="truncate text-xs text-muted-foreground">
{translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.026cfb232a',
'Does not support prompt commands'
)}
</span>
) : null}
/>
</SelectTrigger>
<SelectContent
position="popper"
side="bottom"
align="start"
sideOffset={4}
className="max-h-[min(20rem,var(--radix-select-content-available-height))] w-[--radix-select-trigger-width]"
>
{QUICK_COMMAND_AGENT_OPTIONS.map((entry) => {
const supported = supportsTerminalAgentQuickCommand(entry.id)
return (
<SelectItem key={entry.id} value={entry.id} disabled={!supported}>
<span className="flex min-w-0 items-center gap-2">
<AgentIcon agent={entry.id} size={16} />
<span className="flex min-w-0 flex-col">
<span className="truncate">{entry.label}</span>
{!supported ? (
<span className="truncate text-xs text-muted-foreground">
{translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.026cfb232a',
'Does not support prompt commands'
)}
</span>
</span>
</SelectItem>
)
})}
</SelectContent>
</Select>
</div>
</div>
</div>
) : null}
</span>
</span>
</SelectItem>
)
})}
</SelectContent>
</Select>
</TerminalQuickCommandCollapsibleRow>
<div className="space-y-2">
<Label>
@@ -129,7 +112,7 @@ export function TerminalQuickCommandContentSection({
'Command Text'
)}
</Label>
<textarea
<Textarea
value={isTerminalAgentQuickCommand(draft) ? draft.prompt : draft.command}
onChange={(event) => {
const text = event.target.value
@@ -159,44 +142,31 @@ export function TerminalQuickCommandContentSection({
'npm run dev'
)
}
rows={4}
rows={8}
spellCheck={isAgentAction}
className={cn(
'min-h-24 w-full resize-y rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50',
'max-h-[40vh] min-h-40 resize-y text-sm leading-relaxed',
!isAgentAction && 'font-mono'
)}
/>
</div>
<div
className={cn(
'grid overflow-hidden transition-[grid-template-rows] duration-200 ease-out',
isAgentAction ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'
)}
aria-hidden={!isAgentAction}
<TerminalQuickCommandCollapsibleRow
open={isAgentAction}
className="px-1 pt-2 text-xs text-muted-foreground"
>
<div className="min-h-0">
<p
className={cn(
'px-1 pt-2 text-xs text-muted-foreground transition-[opacity,transform] duration-150 ease-out',
isAgentAction
? 'translate-y-0 opacity-100 delay-200'
: '-translate-y-1 opacity-0 delay-0'
)}
>
{translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.e604bd40d6',
'Supports skills, file paths, and built-in commands like'
)}{' '}
<code className="rounded bg-muted px-1 font-mono text-[11px]">
{translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.97e96cc027',
'/goal'
)}
</code>
.
</p>
</div>
</div>
{translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.e604bd40d6',
'Supports skills, file paths, and built-in commands like'
)}{' '}
<code className="rounded bg-muted px-1 font-mono text-[11px]">
{translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.97e96cc027',
'/goal'
)}
</code>
.
</TerminalQuickCommandCollapsibleRow>
</div>
)
}
@@ -40,6 +40,9 @@ type TerminalQuickCommandDialogProps = {
mode: TerminalQuickCommandDialogMode
command: TerminalQuickCommand
repos?: readonly Pick<Repo, 'id' | 'displayName' | 'path' | 'badgeColor'>[]
/** Settings has no ambient workspace to imply scope from, so it opens the
* Advanced section up front. In-workspace entry points leave it collapsed. */
defaultAdvancedOpen?: boolean
onOpenChange: (open: boolean) => void
onSave: (command: TerminalQuickCommand) => void
}
@@ -63,6 +66,7 @@ export function TerminalQuickCommandDialog({
mode,
command,
repos = EMPTY_REPOS,
defaultAdvancedOpen = false,
onOpenChange,
onSave
}: TerminalQuickCommandDialogProps): React.JSX.Element {
@@ -76,7 +80,7 @@ export function TerminalQuickCommandDialog({
const lastRepoScopeIdRef = useRef<string | null>(
initialScope.type === 'repo' ? initialScope.repoId : null
)
const [advancedOpen, setAdvancedOpen] = useState(false)
const [advancedOpen, setAdvancedOpen] = useState(defaultAdvancedOpen)
const selectedAction = getTerminalQuickCommandAction(draft)
const selectedScope = getTerminalQuickCommandScope(draft)
const isAgentAction = isTerminalAgentQuickCommand(draft)
@@ -95,7 +99,7 @@ export function TerminalQuickCommandDialog({
draftMemoryRef.current = createTerminalQuickCommandDialogDraftMemory(command, fallbackAgent)
const commandScope = getTerminalQuickCommandScope(command)
lastRepoScopeIdRef.current = commandScope.type === 'repo' ? commandScope.repoId : null
setAdvancedOpen(false)
setAdvancedOpen(defaultAdvancedOpen)
setDraft({ ...command })
}
@@ -164,9 +168,12 @@ export function TerminalQuickCommandDialog({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md sm:max-w-md" showCloseButton={false}>
<DialogContent
className="max-h-[min(88vh,54rem)] grid-rows-[auto_minmax(0,1fr)_auto] sm:max-w-xl"
showCloseButton={false}
>
<DialogHeader>
<DialogTitle className="text-sm">
<DialogTitle>
{mode === 'edit'
? translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.f9b184fc16',
@@ -177,7 +184,7 @@ export function TerminalQuickCommandDialog({
'Add Quick Command'
)}
</DialogTitle>
<DialogDescription className="text-xs">
<DialogDescription>
{translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.ed04233b3e',
'Save terminal commands or agent prompts for quick access.'
@@ -185,8 +192,11 @@ export function TerminalQuickCommandDialog({
</DialogDescription>
</DialogHeader>
{/* Why -mx-3/px-3: overflow clips at the padding box, so the padding has
to cover the widest negative margin inside (Advanced's -ml-2) plus a
focus ring. The matching negative margin keeps children aligned. */}
<div
className="space-y-4"
className="-mx-3 min-h-0 space-y-4 overflow-y-auto px-3 py-1 scrollbar-sleek"
onKeyDown={(event) => {
if (isScreenSubmitShortcut(event) && canSave) {
event.preventDefault()
@@ -1,5 +1,7 @@
import { Button } from '@/components/ui/button'
import { DialogFooter } from '@/components/ui/dialog'
import { ShortcutKeyCombo } from '@/components/ShortcutKeyCombo'
import { getScreenSubmitModifierLabel } from '@/lib/screen-submit-shortcut'
import { translate } from '@/i18n/i18n'
type TerminalQuickCommandDialogFooterProps = {
@@ -16,7 +18,7 @@ export function TerminalQuickCommandDialogFooter({
onSave
}: TerminalQuickCommandDialogFooterProps): React.JSX.Element {
return (
<DialogFooter>
<DialogFooter className="sm:items-center">
<Button type="button" variant="outline" onClick={onCancel}>
{translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialogFooter.28370f16b9',
@@ -37,7 +39,11 @@ export function TerminalQuickCommandDialogFooter({
'auto.components.terminal.quick.commands.TerminalQuickCommandDialogFooter.2e2b958dfc',
'Save'
)}
<span className="ml-1 text-[10px] opacity-60">{submitShortcutLabel}</span>
<ShortcutKeyCombo
keys={[getScreenSubmitModifierLabel(), 'Enter']}
className="ml-1"
keyCapClassName="border-primary-foreground/25 bg-primary-foreground/15 text-primary-foreground/80 shadow-none"
/>
</Button>
</DialogFooter>
)
@@ -0,0 +1,28 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<'textarea'>>(
({ className, ...props }, ref) => {
return (
<textarea
ref={ref}
data-slot="textarea"
// Why scrollbar-sleek here: a textarea scrolls without an overflow class,
// so it escapes the scrollbar lint rule and paints Chromium's default
// light scrollbar on dark surfaces.
className={cn(
'scrollbar-sleek min-h-16 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground/60 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30',
'focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50',
'aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40',
className
)}
{...props}
/>
)
}
)
Textarea.displayName = 'Textarea'
export { Textarea }
+6
View File
@@ -6781,6 +6781,12 @@
"8bfdd23a88": "Help us figure out what to build next. Orca sends anonymous counts of which features you use and where things break.",
"afec8b03be": "ci"
},
"QuickCommandsList": {
"noSearchMatches": "No commands match this search."
},
"QuickCommandsToolbar": {
"searchLabel": "Search commands"
},
"QuickCommandsPane": {
"8764c6e9e4": "Remove {{value0}}",
"7d90fd5299": "Edit {{value0}}",