diff --git a/src/components/command-palette/CommandPalette.tsx b/src/components/command-palette/CommandPalette.tsx index 65a98943..c50e0b78 100644 --- a/src/components/command-palette/CommandPalette.tsx +++ b/src/components/command-palette/CommandPalette.tsx @@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'; import { ArrowDownToLine, ArrowUpFromLine, + ChevronRight, FileText, GitCommit, GitMerge, @@ -11,6 +12,7 @@ import { RefreshCw, Settings, SunMoon, + X, } from 'lucide-react'; import { @@ -36,7 +38,15 @@ import { useSessionMessageSearch } from './sources/useSessionMessageSearch'; import { useBranchesSource } from './sources/useBranchesSource'; import { useGitActions } from './sources/useGitActions'; -type Mode = 'mixed' | 'actions' | 'files' | 'commits'; +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; @@ -45,13 +55,6 @@ type CommandPaletteProps = { onShowTab?: (tab: AppTab) => void; }; -function parseMode(input: string): { mode: Mode; query: string } { - if (input.startsWith('> ')) return { mode: 'actions', query: input.slice(2) }; - if (input.startsWith('/')) return { mode: 'files', query: input.slice(1) }; - if (input.startsWith('#')) return { mode: 'commits', query: input.slice(1) }; - return { mode: 'mixed', query: input }; -} - 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' }, @@ -68,10 +71,13 @@ export default function CommandPalette({ }: 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'; @@ -84,22 +90,25 @@ export default function CommandPalette({ }, []); React.useEffect(() => { - if (!open) setSearch(''); + if (!open) { + setSearch(''); + setPages([]); + } }, [open]); - const { mode, query } = parseMode(search); const projectId = selectedProject?.projectId; - const showActions = mode === 'mixed' || mode === 'actions'; - const showSessions = mode === 'mixed'; - const showFiles = mode === 'mixed' || mode === 'files'; - const showCommits = mode === 'mixed' || mode === 'commits'; + 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, query, 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 && showActions); + const branches = useBranchesSource(projectId, open && showBranches); const git = useGitActions(projectId); const sessionRows = React.useMemo(() => { @@ -125,26 +134,58 @@ export default function CommandPalette({ return Array.from(byId.values()); }, [sessions, messageMatches, showSessions]); - const filter = React.useCallback((value: string, rawSearch: string) => { - const stripped = parseMode(rawSearch).query.trim().toLowerCase(); - if (!stripped) return 1; - return value.toLowerCase().includes(stripped) ? 1 : 0; - }, []); - 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 +
+ )} @@ -215,19 +256,6 @@ export default function CommandPalette({ Git: Push - {branches - .filter((b) => !b.isCurrent && !b.isRemote) - .slice(0, 30) - .map((b) => ( - run(() => { void git.checkout(b.name); onShowTab?.('git'); })} - > - - Switch to branch: {b.name} - - ))} )} @@ -246,12 +274,12 @@ export default function CommandPalette({ )} - {showSessions && projectId && sessionRows.length > 0 && ( + {showSessions && projectId && sessionsShown.length > 0 && ( - {sessionRows.map((s) => ( + {sessionsShown.map((s) => ( run(() => navigate(`/session/${s.id}`))} > @@ -266,15 +294,18 @@ export default function CommandPalette({ )} ))} + {!page && sessionRows.length > browseLimit && ( + pushPage('sessions')} /> + )} )} - {showFiles && projectId && files.length > 0 && ( + {showFiles && projectId && filesShown.length > 0 && ( - {files.map((f) => ( + {filesShown.map((f) => ( run(() => ops.openFile(f.path))} > @@ -282,15 +313,18 @@ export default function CommandPalette({ {f.path} ))} + {!page && files.length > browseLimit && ( + pushPage('files')} /> + )} )} - {showCommits && projectId && commits.length > 0 && ( + {showCommits && projectId && commitsShown.length > 0 && ( - {commits.map((c) => ( + {commitsShown.map((c) => ( run(() => onShowTab?.('git'))} > @@ -299,6 +333,27 @@ export default function CommandPalette({ {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')} /> + )} )} @@ -307,3 +362,12 @@ export default function CommandPalette({
); } + +function BrowseAllItem({ label, onSelect }: { label: string; onSelect: () => void }) { + return ( + + + {label} + + ); +} diff --git a/src/components/command-palette/sources/useBranchesSource.ts b/src/components/command-palette/sources/useBranchesSource.ts index e16ca447..33a30870 100644 --- a/src/components/command-palette/sources/useBranchesSource.ts +++ b/src/components/command-palette/sources/useBranchesSource.ts @@ -2,14 +2,10 @@ import { authenticatedFetch } from '../../../utils/api'; import { useApiSource } from './useApiSource'; -export type BranchResult = { - name: string; - isCurrent: boolean; - isRemote: boolean; -}; +export type BranchResult = { name: string }; interface BranchesResponse { - branches?: Array<{ name: string; current?: boolean; isRemote?: boolean }>; + localBranches?: string[]; } export function useBranchesSource(projectId: string | undefined, enabled: boolean) { @@ -20,13 +16,6 @@ export function useBranchesSource(projectId: string | undefined, enabled: boolea const params = new URLSearchParams({ project: projectId! }); return authenticatedFetch(`/api/git/branches?${params.toString()}`, { signal }); }, - parse: (data) => { - const list = data.branches ?? []; - return list.map((b) => ({ - name: b.name, - isCurrent: Boolean(b.current), - isRemote: Boolean(b.isRemote), - })); - }, + parse: (data) => (data.localBranches ?? []).map((name) => ({ name })), }); }