import * as React from 'react'; import { useNavigate } from 'react-router-dom'; import { ArrowDownToLine, ArrowUpFromLine, ChevronRight, FileText, GitCommit, GitMerge, MessageSquare, MessageSquarePlus, RefreshCw, Settings, SunMoon, X, } from 'lucide-react'; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Dialog, DialogContent, DialogTitle, } from '../../shared/view/ui'; import { useTheme } from '../../contexts/ThemeContext'; import { usePaletteOps } from '../../contexts/PaletteOpsContext'; import { SETTINGS_MAIN_TABS } from '../settings/constants/constants'; import type { AppTab, Project } from '../../types/app'; import { useSessionsSource } from './sources/useSessionsSource'; import { useFilesSource } from './sources/useFilesSource'; import { useCommitsSource } from './sources/useCommitsSource'; import { useSessionMessageSearch } from './sources/useSessionMessageSearch'; import { useBranchesSource } from './sources/useBranchesSource'; import { useGitActions } from './sources/useGitActions'; type Page = 'actions' | 'files' | 'sessions' | 'commits' | 'branches'; const PAGE_LABELS: Record = { actions: 'Actions', files: 'Files', sessions: 'Sessions', commits: 'Commits', branches: 'Branches', }; type CommandPaletteProps = { selectedProject: Project | null; onStartNewChat: (project: Project) => void; onOpenSettings: (tab?: string) => void; onShowTab?: (tab: AppTab) => void; }; const NAV_TABS: Array<{ id: AppTab; label: string; keywords: string }> = [ { id: 'chat', label: 'Go to Chat', keywords: 'chat messages conversation' }, { id: 'files', label: 'Go to Files', keywords: 'files file tree explorer' }, { id: 'shell', label: 'Go to Shell', keywords: 'shell terminal console' }, { id: 'git', label: 'Go to Git', keywords: 'git diff branches' }, { id: 'tasks', label: 'Go to Tasks', keywords: 'tasks taskmaster' }, ]; export default function CommandPalette({ selectedProject, onStartNewChat, onOpenSettings, onShowTab, }: CommandPaletteProps) { const [open, setOpen] = React.useState(false); const [search, setSearch] = React.useState(''); const [pages, setPages] = React.useState([]); const { toggleDarkMode } = useTheme(); const navigate = useNavigate(); const ops = usePaletteOps(); const page = pages.at(-1); React.useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { const isCmdK = (e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 'k'; if (!isCmdK) return; e.preventDefault(); setOpen((prev) => !prev); }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, []); React.useEffect(() => { if (!open) { setSearch(''); setPages([]); } }, [open]); const projectId = selectedProject?.projectId; const showActions = !page || page === 'actions'; const showSessions = !page || page === 'sessions'; const showFiles = !page || page === 'files'; const showCommits = !page || page === 'commits'; const showBranches = !page || page === 'branches' || page === 'actions'; const sessions = useSessionsSource(projectId, open && showSessions); const messageMatches = useSessionMessageSearch(projectId, search, open && showSessions); const files = useFilesSource(projectId, open && showFiles); const commits = useCommitsSource(projectId, open && showCommits); const branches = useBranchesSource(projectId, open && showBranches); const git = useGitActions(projectId); const sessionRows = React.useMemo(() => { if (!showSessions) return []; type Row = { id: string; label: string; provider?: string; snippet?: string }; const byId = new Map(); for (const s of sessions) { byId.set(s.id, { id: s.id, label: s.label, provider: s.provider }); } for (const m of messageMatches) { const existing = byId.get(m.sessionId); if (existing) { existing.snippet = m.snippet; } else { byId.set(m.sessionId, { id: m.sessionId, label: m.label, provider: m.provider, snippet: m.snippet, }); } } return Array.from(byId.values()); }, [sessions, messageMatches, showSessions]); const run = React.useCallback((fn: () => void) => { setOpen(false); fn(); }, []); const pushPage = React.useCallback((next: Page) => { setSearch(''); setPages((prev) => [...prev, next]); }, []); const popPage = React.useCallback(() => { setSearch(''); setPages((prev) => prev.slice(0, -1)); }, []); const handleKeyDown = React.useCallback((e: React.KeyboardEvent) => { if (e.key === 'Backspace' && !search && pages.length > 0) { e.preventDefault(); popPage(); } }, [search, pages.length, popPage]); const startNewChatDisabled = !selectedProject; const browseLimit = 5; const filesShown = page === 'files' ? files : files.slice(0, browseLimit); const commitsShown = page === 'commits' ? commits : commits.slice(0, browseLimit); const sessionsShown = page === 'sessions' ? sessionRows : sessionRows.slice(0, browseLimit); const branchesShown = page === 'branches' ? branches : branches.slice(0, browseLimit); return ( Command palette {page && (
{PAGE_LABELS[page]} Backspace to go back
)} No results. {showActions && ( { if (!selectedProject) return; run(() => onStartNewChat(selectedProject)); }} > Start new chat {startNewChatDisabled && ( Select a project first )} run(() => onOpenSettings())}> Open settings run(toggleDarkMode)}> Toggle theme )} {showActions && ( {NAV_TABS.map((tab) => ( run(() => onShowTab?.(tab.id))} > {tab.label} ))} )} {showActions && projectId && ( run(() => { void git.fetch(); onShowTab?.('git'); })} > Git: Fetch run(() => { void git.pull(); onShowTab?.('git'); })} > Git: Pull run(() => { void git.push(); onShowTab?.('git'); })} > Git: Push )} {showActions && ( {SETTINGS_MAIN_TABS.map(({ id, label, keywords, icon: Icon }) => ( run(() => onOpenSettings(id))} > Settings: {label} ))} )} {showSessions && projectId && sessionsShown.length > 0 && ( {sessionsShown.map((s) => ( run(() => navigate(`/session/${s.id}`))} >
{s.label} {s.snippet && ( {s.snippet} )}
{s.provider && ( {s.provider} )}
))} {!page && sessionRows.length > browseLimit && ( pushPage('sessions')} /> )}
)} {showFiles && projectId && filesShown.length > 0 && ( {filesShown.map((f) => ( run(() => ops.openFile(f.path))} > {f.name} {f.path} ))} {!page && files.length > browseLimit && ( pushPage('files')} /> )} )} {showCommits && projectId && commitsShown.length > 0 && ( {commitsShown.map((c) => ( run(() => onShowTab?.('git'))} > {c.shortHash} {c.message} {c.author} ))} {!page && commits.length > browseLimit && ( pushPage('commits')} /> )} )} {showBranches && projectId && branchesShown.length > 0 && ( {branchesShown.map((b) => ( run(() => { void git.checkout(b.name); onShowTab?.('git'); })} > Switch to: {b.name} ))} {!page && branches.length > browseLimit && ( pushPage('branches')} /> )} )}
); } function BrowseAllItem({ label, onSelect }: { label: string; onSelect: () => void }) { return ( {label} ); }