From b51ef400ec74b3dc761809b7cd4a8ef7489e96dd Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:53:22 -0700 Subject: [PATCH] fix(quick-commands): flatten the settings list and give the command editor room (#13922) --- .../components/settings/QuickCommandsList.tsx | 208 +++++++++++------- .../components/settings/QuickCommandsPane.tsx | 93 +++----- .../settings/QuickCommandsToolbar.tsx | 116 ++++++++++ .../TerminalQuickCommandAdvancedSection.tsx | 57 ++--- .../TerminalQuickCommandAppendEnterSwitch.tsx | 5 +- .../TerminalQuickCommandCollapsibleRow.tsx | 38 ++++ .../TerminalQuickCommandContentSection.tsx | 188 +++++++--------- .../TerminalQuickCommandDialog.tsx | 22 +- .../TerminalQuickCommandDialogFooter.tsx | 10 +- src/renderer/src/components/ui/textarea.tsx | 28 +++ src/renderer/src/i18n/locales/en.json | 6 + 11 files changed, 470 insertions(+), 301 deletions(-) create mode 100644 src/renderer/src/components/settings/QuickCommandsToolbar.tsx create mode 100644 src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandCollapsibleRow.tsx create mode 100644 src/renderer/src/components/ui/textarea.tsx diff --git a/src/renderer/src/components/settings/QuickCommandsList.tsx b/src/renderer/src/components/settings/QuickCommandsList.tsx index ec377677ee5..3a0042b32a8 100644 --- a/src/renderer/src/components/settings/QuickCommandsList.tsx +++ b/src/renderer/src/components/settings/QuickCommandsList.tsx @@ -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 ( -
+
{command.label || translate('auto.components.settings.QuickCommandsPane.2bb9e38e93', 'Untitled')}
- + {scope.type === 'repo' ? ( <> @@ -74,9 +89,9 @@ function QuickCommandRow({ )}
-
+
{isTerminalAgentQuickCommand(command) ? ( - + ) : null} @@ -91,59 +106,95 @@ function QuickCommandRow({
-
- {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')} +
+ {getRunModeLabel(command)} +
+ {/* Why can-hover: touch devices never hover, so the actions must stay visible there. */} +
+ + + +
+
+ ) +} + +function QuickCommandsEmptyState({ + hasCommands, + hasQuery +}: { + hasCommands: boolean + hasQuery: boolean +}): React.JSX.Element { + if (hasCommands) { + return ( +
+ {hasQuery + ? translate( + 'auto.components.settings.QuickCommandsList.noSearchMatches', + 'No commands match this search.' + ) + : translate( + 'auto.components.settings.QuickCommandsPane.3eb9897ab0', + 'No commands in the selected scopes.' + )} +
+ ) + } + // Why no action here: the toolbar's Add Command sits directly above. + return ( +
+ +
+

+ {translate( + 'auto.components.settings.QuickCommandsPane.38d61927e6', + 'No quick commands saved.' + )} +

+

+ {translate( + 'auto.components.settings.QuickCommandsPane.c36912efd5', + 'Run them from the Quick Commands button in the tab bar, or right-click inside any terminal.' + )} +

- - -
) } @@ -151,43 +202,32 @@ function QuickCommandRow({ export function QuickCommandsList({ commands, visibleCommands, + hasQuery, repoById, onEdit, onRemove }: { commands: TerminalQuickCommand[] visibleCommands: TerminalQuickCommand[] + hasQuery: boolean repoById: Map> onEdit: (command: TerminalQuickCommand) => void onRemove: (command: TerminalQuickCommand) => void }): React.JSX.Element { + if (visibleCommands.length === 0) { + return 0} hasQuery={hasQuery} /> + } return ( -
- {visibleCommands.length === 0 ? ( -
- {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.' - )} -
- ) : ( -
- {visibleCommands.map((command) => ( - - ))} -
- )} +
+ {visibleCommands.map((command) => ( + + ))}
) } diff --git a/src/renderer/src/components/settings/QuickCommandsPane.tsx b/src/renderer/src/components/settings/QuickCommandsPane.tsx index f7f703e7ac1..a4f1ea85fe4 100644 --- a/src/renderer/src/components/settings/QuickCommandsPane.tsx +++ b/src/renderer/src/components/settings/QuickCommandsPane.tsx @@ -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 | 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 = 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 ( -
-
-
- -

- {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.' - )} -

-
- -
+ const openAddDialog = (): void => + setEditor({ + mode: 'add', + command: createDraftForCurrentFilter(), + connectionGeneration: selectedRuntimeConnectionGeneration, + hostId: selectedHostId + }) + return ( +
{shouldShowTerminalQuickCommandHostOwnership(hostOptions) ? ( -
- - -
+

{ownership.description}

) : null} - { + setSelectedHostId(hostId) + setScopeSelection(null) + }} + canAdd={canManageSelectedHost} + onAdd={openAddDialog} repos={hostRepos} effectiveSelection={effectiveSelection} showAll={showAll} @@ -408,6 +373,7 @@ export function QuickCommandsPane({ 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} /> diff --git a/src/renderer/src/components/settings/QuickCommandsToolbar.tsx b/src/renderer/src/components/settings/QuickCommandsToolbar.tsx new file mode 100644 index 00000000000..65e034eeb3f --- /dev/null +++ b/src/renderer/src/components/settings/QuickCommandsToolbar.tsx @@ -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 + showAll: boolean + scopePopoverOpen: boolean + setScopePopoverOpen: Dispatch> + 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 ( +
+
+ + setQuery(event.target.value)} + placeholder={searchLabel} + aria-label={searchLabel} + className="h-8 pl-8 text-xs" + /> +
+ + + + {showHostSelect ? ( + + ) : null} + + +
+ ) +} diff --git a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandAdvancedSection.tsx b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandAdvancedSection.tsx index d49be9c9616..af196b2c628 100644 --- a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandAdvancedSection.tsx +++ b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandAdvancedSection.tsx @@ -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({ -
-
-
- {!isTerminalAgentQuickCommand(draft) ? ( - - ) : null} - { - lastRepoScopeIdRef.current = repoId - }} - setDraft={setDraft} - /> -
-
-
+ + { + lastRepoScopeIdRef.current = repoId + }} + setDraft={setDraft} + /> + {!isTerminalAgentQuickCommand(draft) ? ( + + ) : null} +
) } diff --git a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandAppendEnterSwitch.tsx b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandAppendEnterSwitch.tsx index 4450562729e..73f69ebf5d8 100644 --- a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandAppendEnterSwitch.tsx +++ b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandAppendEnterSwitch.tsx @@ -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 (
@@ -27,6 +29,7 @@ export function TerminalQuickCommandAppendEnterSwitch({
+
+
+ {children} +
+
+
+ ) +} diff --git a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandContentSection.tsx b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandContentSection.tsx index 34035a6233f..19967b0daba 100644 --- a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandContentSection.tsx +++ b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandContentSection.tsx @@ -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 (
- {/* Why: action changes add/remove agent-only fields; animating rows here - keeps the fixed dialog from snapping between content heights. */} -
-
-
-
-
-
+ ) : null} + + + + ) + })} + + +
-