feat(command-palette): add git fetch/pull/push and branch switch actions

This commit is contained in:
simosmik
2026-04-30 07:22:39 +00:00
parent 66dd81976f
commit 95ad7272e9
3 changed files with 132 additions and 0 deletions

View File

@@ -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({
</CommandGroup>
)}
{showActions && projectId && (
<CommandGroup heading="Git">
<CommandItem
value="git fetch remote"
onSelect={() => run(() => { void git.fetch(); onShowTab?.('git'); })}
>
<RefreshCw className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden />
<span className="flex-1">Git: Fetch</span>
</CommandItem>
<CommandItem
value="git pull merge upstream"
onSelect={() => run(() => { void git.pull(); onShowTab?.('git'); })}
>
<ArrowDownToLine className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden />
<span className="flex-1">Git: Pull</span>
</CommandItem>
<CommandItem
value="git push origin remote"
onSelect={() => run(() => { void git.push(); onShowTab?.('git'); })}
>
<ArrowUpFromLine className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden />
<span className="flex-1">Git: Push</span>
</CommandItem>
{branches
.filter((b) => !b.isCurrent && !b.isRemote)
.slice(0, 30)
.map((b) => (
<CommandItem
key={`branch-${b.name}`}
value={`git switch checkout branch ${b.name}`}
onSelect={() => run(() => { void git.checkout(b.name); onShowTab?.('git'); })}
>
<GitMerge className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden />
<span className="flex-1 truncate">Switch to branch: {b.name}</span>
</CommandItem>
))}
</CommandGroup>
)}
{showActions && (
<CommandGroup heading="Settings">
{SETTINGS_TABS.map(({ id, label, keywords, icon: Icon }) => (

View File

@@ -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<BranchResult[]>([]);
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<BranchesResponse>)
.then((data) => {
if (cancelled) return;
const list = data.branches ?? [];
setItems(
list.map<BranchResult>((b) => ({
name: b.name,
isCurrent: Boolean(b.current),
isRemote: Boolean(b.isRemote),
})),
);
})
.catch(() => {
if (!cancelled) setItems([]);
});
return () => {
cancelled = true;
};
}, [projectId, enabled]);
return { items };
}

View File

@@ -0,0 +1,38 @@
import { useCallback } from 'react';
import { authenticatedFetch } from '../../../utils/api';
async function postGit(path: string, body: Record<string, unknown>) {
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 };
}