diff --git a/src/renderer/src/components/settings/TerminalPane.tsx b/src/renderer/src/components/settings/TerminalPane.tsx index 582f7bdaa..dbd9a1c28 100644 --- a/src/renderer/src/components/settings/TerminalPane.tsx +++ b/src/renderer/src/components/settings/TerminalPane.tsx @@ -63,6 +63,7 @@ import { GhosttyImportModal } from './GhosttyImportModal' import type { UseGhosttyImportReturn } from './useGhosttyImport' import { ManageSessionsSection } from './ManageSessionsSection' import { TerminalQuickCommandsSection } from './TerminalQuickCommandsSection' +import { getRepoIdFromWorktreeId } from '../../../../shared/worktree-id' type TerminalPaneProps = { settings: GlobalSettings @@ -93,6 +94,9 @@ export function TerminalPane({ pwshAvailable }: TerminalPaneProps): React.JSX.Element { const searchQuery = useAppStore((state) => state.settingsSearchQuery) + const repos = useAppStore((state) => state.repos) + const activeWorktreeId = useAppStore((state) => state.activeWorktreeId) + const activeRepoId = activeWorktreeId ? getRepoIdFromWorktreeId(activeWorktreeId) : null const isWindows = isWindowsUserAgent() const isMac = isMacUserAgent() const [themeSearchDark, setThemeSearchDark] = useState('') @@ -276,18 +280,29 @@ export function TerminalPane({

Quick Commands

- Save terminal input snippets for the terminal right-click menu. + Save global and repository-specific terminal snippets for the right-click menu.

updateSettings({ terminalQuickCommands })} /> diff --git a/src/renderer/src/components/settings/TerminalQuickCommandsSection.tsx b/src/renderer/src/components/settings/TerminalQuickCommandsSection.tsx index a8e90a5c3..4416818d1 100644 --- a/src/renderer/src/components/settings/TerminalQuickCommandsSection.tsx +++ b/src/renderer/src/components/settings/TerminalQuickCommandsSection.tsx @@ -1,24 +1,31 @@ import { useEffect, useState } from 'react' import { Pencil, Plus, Trash2 } from 'lucide-react' -import type { TerminalQuickCommand } from '../../../../shared/types' -import { createBrowserUuid } from '@/lib/browser-uuid' -import { Button } from '../ui/button' +import type { + Repo, + TerminalQuickCommand, + TerminalQuickCommandScope +} from '../../../../shared/types' +import { getTerminalQuickCommandScope } from '../../../../shared/terminal-quick-commands' import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle -} from '../ui/dialog' -import { Input } from '../ui/input' + createTerminalQuickCommandDraft, + TerminalQuickCommandDialog +} from '@/components/terminal-quick-commands/TerminalQuickCommandDialog' +import { Badge } from '../ui/badge' +import { Button } from '../ui/button' import { Label } from '../ui/label' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select' +import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group' +import RepoDotLabel from '../repo/RepoDotLabel' type TerminalQuickCommandsSectionProps = { commands: TerminalQuickCommand[] + repos: Pick[] + activeRepoId: string | null onChange: (commands: TerminalQuickCommand[]) => void } +type ScopeFilter = 'all' | 'global' | 'repo' + type EditorState = | { mode: 'add' @@ -30,51 +37,94 @@ type EditorState = } | null -function createQuickCommand(): TerminalQuickCommand { - return { - id: `quick-command-${createBrowserUuid()}`, - label: '', - command: '', - appendEnter: true +function getRepoLabel(repo: Pick): string { + return repo.displayName || repo.path +} + +function getScopeLabel( + scope: TerminalQuickCommandScope, + repoById: Map> +): string { + if (scope.type === 'global') { + return 'Global' } + const repo = repoById.get(scope.repoId) + return repo ? getRepoLabel(repo) : 'Missing repo' } export function TerminalQuickCommandsSection({ commands, + repos, + activeRepoId, onChange }: TerminalQuickCommandsSectionProps): React.JSX.Element { const [editor, setEditor] = useState(null) - const [draft, setDraft] = useState(createQuickCommand) + const [scopeFilter, setScopeFilter] = useState('all') + const [repoFilterId, setRepoFilterId] = useState(activeRepoId ?? '') + const [repoFilterManuallyChanged, setRepoFilterManuallyChanged] = useState(false) + const repoById = new Map(repos.map((repo) => [repo.id, repo])) + const activeRepoFilterId = activeRepoId && repoById.has(activeRepoId) ? activeRepoId : '' + const repoFilterIsValid = repoFilterId !== '' && repoById.has(repoFilterId) + const selectedRepoId = + activeRepoFilterId && (!repoFilterManuallyChanged || !repoFilterIsValid) + ? activeRepoFilterId + : repoFilterIsValid + ? repoFilterId + : (repos[0]?.id ?? '') + const visibleCommands = commands.filter((command) => { + const scope = getTerminalQuickCommandScope(command) + if (scopeFilter === 'global') { + return scope.type === 'global' + } + if (scopeFilter === 'repo') { + return scope.type === 'repo' && (!selectedRepoId || scope.repoId === selectedRepoId) + } + return true + }) + const createDraftForCurrentFilter = (): TerminalQuickCommand => { + if (scopeFilter === 'repo' && selectedRepoId) { + return createTerminalQuickCommandDraft({ type: 'repo', repoId: selectedRepoId }) + } + return createTerminalQuickCommandDraft({ type: 'global' }) + } + + // Follow the active worktree until the user picks a repo; resume if that repo disappears. useEffect(() => { - if (editor) { - setDraft({ ...editor.command }) - } - }, [editor]) - - const saveDraft = (): void => { - const next = { - ...draft, - label: draft.label.trim(), - command: draft.command.trimEnd() - } - if (!next.label || !next.command) { + if (!activeRepoFilterId) { return } + + if (!repoFilterManuallyChanged) { + if (repoFilterId !== activeRepoFilterId) { + setRepoFilterId(activeRepoFilterId) + } + return + } + + if (!repoFilterIsValid) { + setRepoFilterId(activeRepoFilterId) + setRepoFilterManuallyChanged(false) + } + }, [activeRepoFilterId, repoFilterId, repoFilterIsValid, repoFilterManuallyChanged]) + + const changeRepoFilter = (nextRepoId: string): void => { + setRepoFilterManuallyChanged(true) + setRepoFilterId(nextRepoId) + } + + const saveCommand = (next: TerminalQuickCommand): void => { if (editor?.mode === 'edit') { onChange(commands.map((command) => (command.id === next.id ? next : command))) } else { onChange([...commands, next]) } - setEditor(null) } const removeCommand = (id: string): void => { onChange(commands.filter((command) => command.id !== id)) } - const canSave = draft.label.trim().length > 0 && draft.command.trimEnd().length > 0 - return (
@@ -88,128 +138,111 @@ export function TerminalQuickCommandsSection({ type="button" variant="outline" size="sm" - onClick={() => setEditor({ mode: 'add', command: createQuickCommand() })} + onClick={() => setEditor({ mode: 'add', command: createDraftForCurrentFilter() })} > Add Command
+
+ { + if (value === 'all' || value === 'global' || value === 'repo') { + setScopeFilter(value) + } + }} + className="justify-start" + > + All + Global + + Repository + + + {scopeFilter === 'repo' && repos.length > 0 ? ( + + ) : null} +
+
- {commands.length === 0 ? ( -
No quick commands saved.
+ {visibleCommands.length === 0 ? ( +
+ {commands.length === 0 ? 'No quick commands saved.' : 'No commands match this scope.'} +
) : (
- {commands.map((command) => ( -
-
-
{command.label || 'Untitled'}
-
- {command.command || 'No command text'} + {visibleCommands.map((command) => { + const scope = getTerminalQuickCommandScope(command) + return ( +
+
+
+
+ {command.label || 'Untitled'} +
+ + {getScopeLabel(scope, repoById)} + +
+
+ {command.command || 'No command text'} +
+
+ {command.appendEnter ? 'Enter' : 'Insert'} +
+ +
-
- {command.appendEnter ? 'Enter' : 'Insert'} -
- - -
- ))} + ) + })}
)}
- !open && setEditor(null)}> - - - - {editor?.mode === 'edit' ? 'Edit Quick Command' : 'Add Quick Command'} - - - Save terminal input text for the context menu. - - - -
-
- - - setDraft((current) => ({ ...current, label: event.target.value })) - } - placeholder="Restart server" - /> -
- -
- -