mirror of
https://github.com/stablyai/orca.git
synced 2026-09-25 00:02:35 +00:00
fix(quick-commands): flatten the settings list and give the command editor room (#13922)
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
+21
-36
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
+4
-1
@@ -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'
|
||||
|
||||
+38
@@ -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>
|
||||
)
|
||||
}
|
||||
+79
-109
@@ -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()
|
||||
|
||||
+8
-2
@@ -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 }
|
||||
@@ -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}}",
|
||||
|
||||
Reference in New Issue
Block a user