From 95ad7272e94c6ff500a89b4016a2c11d7fa6ed89 Mon Sep 17 00:00:00 2001 From: simosmik Date: Thu, 30 Apr 2026 07:22:39 +0000 Subject: [PATCH] feat(command-palette): add git fetch/pull/push and branch switch actions --- .../command-palette/CommandPalette.tsx | 47 +++++++++++++++++++ .../sources/useBranchesSource.ts | 47 +++++++++++++++++++ .../command-palette/sources/useGitActions.ts | 38 +++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 src/components/command-palette/sources/useBranchesSource.ts create mode 100644 src/components/command-palette/sources/useGitActions.ts diff --git a/src/components/command-palette/CommandPalette.tsx b/src/components/command-palette/CommandPalette.tsx index 3634fe35..2c67271c 100644 --- a/src/components/command-palette/CommandPalette.tsx +++ b/src/components/command-palette/CommandPalette.tsx @@ -1,11 +1,14 @@ import * as React from 'react'; import { useNavigate } from 'react-router-dom'; import { + ArrowDownToLine, + ArrowUpFromLine, Bell, Bot, FileText, GitBranch, GitCommit, + GitMerge, Info, KeyRound, ListChecks, @@ -13,6 +16,7 @@ import { MessageSquarePlus, Palette, Plug, + RefreshCw, Settings, SunMoon, } from 'lucide-react'; @@ -35,6 +39,8 @@ 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 Mode = 'mixed' | 'actions' | 'files' | 'commits'; @@ -104,6 +110,8 @@ export default function CommandPalette({ const { items: messageMatches } = useSessionMessageSearch(projectId, query, open && mode === 'mixed'); const { items: files } = useFilesSource(projectId, open && (mode === 'mixed' || mode === 'files')); const { items: commits } = useCommitsSource(projectId, open && (mode === 'mixed' || mode === 'commits')); + const { items: branches } = useBranchesSource(projectId, open && (mode === 'mixed' || mode === 'actions')); + const git = useGitActions(projectId); const showActions = mode === 'mixed' || mode === 'actions'; const showSessions = mode === 'mixed'; @@ -206,6 +214,45 @@ export default function CommandPalette({ )} + {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 + + {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} + + ))} + + )} + {showActions && ( {SETTINGS_TABS.map(({ id, label, keywords, icon: Icon }) => ( diff --git a/src/components/command-palette/sources/useBranchesSource.ts b/src/components/command-palette/sources/useBranchesSource.ts new file mode 100644 index 00000000..3e276a4a --- /dev/null +++ b/src/components/command-palette/sources/useBranchesSource.ts @@ -0,0 +1,47 @@ +import { useEffect, useState } from 'react'; + +import { authenticatedFetch } from '../../../utils/api'; + +export type BranchResult = { + name: string; + isCurrent: boolean; + isRemote: boolean; +}; + +interface BranchesResponse { + branches?: Array<{ name: string; current?: boolean; isRemote?: boolean }>; +} + +export function useBranchesSource(projectId: string | undefined, enabled: boolean) { + const [items, setItems] = useState([]); + + useEffect(() => { + if (!enabled || !projectId) { + setItems([]); + return; + } + let cancelled = false; + const params = new URLSearchParams({ project: projectId }); + authenticatedFetch(`/api/git/branches?${params.toString()}`) + .then((r) => r.json() as Promise) + .then((data) => { + if (cancelled) return; + const list = data.branches ?? []; + setItems( + list.map((b) => ({ + name: b.name, + isCurrent: Boolean(b.current), + isRemote: Boolean(b.isRemote), + })), + ); + }) + .catch(() => { + if (!cancelled) setItems([]); + }); + return () => { + cancelled = true; + }; + }, [projectId, enabled]); + + return { items }; +} diff --git a/src/components/command-palette/sources/useGitActions.ts b/src/components/command-palette/sources/useGitActions.ts new file mode 100644 index 00000000..cf765f34 --- /dev/null +++ b/src/components/command-palette/sources/useGitActions.ts @@ -0,0 +1,38 @@ +import { useCallback } from 'react'; + +import { authenticatedFetch } from '../../../utils/api'; + +async function postGit(path: string, body: Record) { + const res = await authenticatedFetch(path, { + method: 'POST', + body: JSON.stringify(body), + }); + return res.json(); +} + +export function useGitActions(projectId: string | undefined) { + const fetch = useCallback(() => { + if (!projectId) return Promise.resolve(); + return postGit('/api/git/fetch', { project: projectId }); + }, [projectId]); + + const pull = useCallback(() => { + if (!projectId) return Promise.resolve(); + return postGit('/api/git/pull', { project: projectId }); + }, [projectId]); + + const push = useCallback(() => { + if (!projectId) return Promise.resolve(); + return postGit('/api/git/push', { project: projectId }); + }, [projectId]); + + const checkout = useCallback( + (branch: string) => { + if (!projectId) return Promise.resolve(); + return postGit('/api/git/checkout', { project: projectId, branch }); + }, + [projectId], + ); + + return { fetch, pull, push, checkout }; +}