import type { GitBranch } from "@t3tools/contracts"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useVirtualizer } from "@tanstack/react-virtual"; import { ChevronDownIcon } from "lucide-react"; import { type CSSProperties, useCallback, useDeferredValue, useEffect, useMemo, useOptimistic, useRef, useState, useTransition, } from "react"; import { gitBranchesQueryOptions, gitQueryKeys, gitStatusQueryOptions, invalidateGitQueries, } from "../lib/gitReactQuery"; import { readNativeApi } from "../nativeApi"; import { parsePullRequestReference } from "../pullRequestReference"; import { dedupeRemoteBranchesWithLocalMatches, deriveLocalBranchNameFromRemoteRef, EnvMode, resolveBranchSelectionTarget, resolveBranchToolbarValue, } from "./BranchToolbar.logic"; import { Button } from "./ui/button"; import { Combobox, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, ComboboxPopup, ComboboxTrigger, } from "./ui/combobox"; import { toastManager } from "./ui/toast"; interface BranchToolbarBranchSelectorProps { activeProjectCwd: string; activeThreadBranch: string | null; activeWorktreePath: string | null; branchCwd: string | null; effectiveEnvMode: EnvMode; envLocked: boolean; onSetThreadBranch: (branch: string | null, worktreePath: string | null) => void; onCheckoutPullRequestRequest?: (reference: string) => void; onComposerFocusRequest?: () => void; } function toBranchActionErrorMessage(error: unknown): string { return error instanceof Error ? error.message : "An error occurred."; } function getBranchTriggerLabel(input: { activeWorktreePath: string | null; effectiveEnvMode: EnvMode; resolvedActiveBranch: string | null; }): string { const { activeWorktreePath, effectiveEnvMode, resolvedActiveBranch } = input; if (!resolvedActiveBranch) { return "Select branch"; } if (effectiveEnvMode === "worktree" && !activeWorktreePath) { return `From ${resolvedActiveBranch}`; } return resolvedActiveBranch; } export function BranchToolbarBranchSelector({ activeProjectCwd, activeThreadBranch, activeWorktreePath, branchCwd, effectiveEnvMode, envLocked, onSetThreadBranch, onCheckoutPullRequestRequest, onComposerFocusRequest, }: BranchToolbarBranchSelectorProps) { const queryClient = useQueryClient(); const [isBranchMenuOpen, setIsBranchMenuOpen] = useState(false); const [branchQuery, setBranchQuery] = useState(""); const deferredBranchQuery = useDeferredValue(branchQuery); const branchesQuery = useQuery(gitBranchesQueryOptions(branchCwd)); const branchStatusQuery = useQuery(gitStatusQueryOptions(branchCwd)); const branches = useMemo( () => dedupeRemoteBranchesWithLocalMatches(branchesQuery.data?.branches ?? []), [branchesQuery.data?.branches], ); const currentGitBranch = branchStatusQuery.data?.branch ?? branches.find((branch) => branch.current)?.name ?? null; const canonicalActiveBranch = resolveBranchToolbarValue({ envMode: effectiveEnvMode, activeWorktreePath, activeThreadBranch, currentGitBranch, }); const branchNames = useMemo(() => branches.map((branch) => branch.name), [branches]); const branchByName = useMemo( () => new Map(branches.map((branch) => [branch.name, branch] as const)), [branches], ); const trimmedBranchQuery = branchQuery.trim(); const deferredTrimmedBranchQuery = deferredBranchQuery.trim(); const normalizedDeferredBranchQuery = deferredTrimmedBranchQuery.toLowerCase(); const prReference = parsePullRequestReference(trimmedBranchQuery); const isSelectingWorktreeBase = effectiveEnvMode === "worktree" && !envLocked && !activeWorktreePath; const checkoutPullRequestItemValue = prReference && onCheckoutPullRequestRequest ? `__checkout_pull_request__:${prReference}` : null; const canCreateBranch = !isSelectingWorktreeBase && trimmedBranchQuery.length > 0; const hasExactBranchMatch = branchByName.has(trimmedBranchQuery); const createBranchItemValue = canCreateBranch ? `__create_new_branch__:${trimmedBranchQuery}` : null; const branchPickerItems = useMemo(() => { const items = [...branchNames]; if (createBranchItemValue && !hasExactBranchMatch) { items.push(createBranchItemValue); } if (checkoutPullRequestItemValue) { items.unshift(checkoutPullRequestItemValue); } return items; }, [branchNames, checkoutPullRequestItemValue, createBranchItemValue, hasExactBranchMatch]); const filteredBranchPickerItems = useMemo( () => normalizedDeferredBranchQuery.length === 0 ? branchPickerItems : branchPickerItems.filter((itemValue) => { if (createBranchItemValue && itemValue === createBranchItemValue) return true; return itemValue.toLowerCase().includes(normalizedDeferredBranchQuery); }), [branchPickerItems, createBranchItemValue, normalizedDeferredBranchQuery], ); const [resolvedActiveBranch, setOptimisticBranch] = useOptimistic( canonicalActiveBranch, (_currentBranch: string | null, optimisticBranch: string | null) => optimisticBranch, ); const [isBranchActionPending, startBranchActionTransition] = useTransition(); const shouldVirtualizeBranchList = filteredBranchPickerItems.length > 40; const runBranchAction = (action: () => Promise) => { startBranchActionTransition(async () => { await action().catch(() => undefined); await invalidateGitQueries(queryClient).catch(() => undefined); }); }; const selectBranch = (branch: GitBranch) => { const api = readNativeApi(); if (!api || !branchCwd || isBranchActionPending) return; // In new-worktree mode, selecting a branch sets the base branch. if (isSelectingWorktreeBase) { onSetThreadBranch(branch.name, null); setIsBranchMenuOpen(false); onComposerFocusRequest?.(); return; } const selectionTarget = resolveBranchSelectionTarget({ activeProjectCwd, activeWorktreePath, branch, }); // If the branch already lives in a worktree, point the thread there. if (selectionTarget.reuseExistingWorktree) { onSetThreadBranch(branch.name, selectionTarget.nextWorktreePath); setIsBranchMenuOpen(false); onComposerFocusRequest?.(); return; } const selectedBranchName = branch.isRemote ? deriveLocalBranchNameFromRemoteRef(branch.name) : branch.name; setIsBranchMenuOpen(false); onComposerFocusRequest?.(); runBranchAction(async () => { setOptimisticBranch(selectedBranchName); try { await api.git.checkout({ cwd: selectionTarget.checkoutCwd, branch: branch.name }); await invalidateGitQueries(queryClient); } catch (error) { toastManager.add({ type: "error", title: "Failed to checkout branch.", description: toBranchActionErrorMessage(error), }); return; } let nextBranchName = selectedBranchName; if (branch.isRemote) { const status = await api.git.status({ cwd: branchCwd }).catch(() => null); if (status?.branch) { nextBranchName = status.branch; } } setOptimisticBranch(nextBranchName); onSetThreadBranch(nextBranchName, selectionTarget.nextWorktreePath); }); }; const createBranch = (rawName: string) => { const name = rawName.trim(); const api = readNativeApi(); if (!api || !branchCwd || !name || isBranchActionPending) return; setIsBranchMenuOpen(false); onComposerFocusRequest?.(); runBranchAction(async () => { setOptimisticBranch(name); try { await api.git.createBranch({ cwd: branchCwd, branch: name }); try { await api.git.checkout({ cwd: branchCwd, branch: name }); } catch (error) { toastManager.add({ type: "error", title: "Failed to checkout branch.", description: toBranchActionErrorMessage(error), }); return; } } catch (error) { toastManager.add({ type: "error", title: "Failed to create branch.", description: toBranchActionErrorMessage(error), }); return; } setOptimisticBranch(name); onSetThreadBranch(name, activeWorktreePath); setBranchQuery(""); }); }; useEffect(() => { if ( effectiveEnvMode !== "worktree" || activeWorktreePath || activeThreadBranch || !currentGitBranch ) { return; } onSetThreadBranch(currentGitBranch, null); }, [ activeThreadBranch, activeWorktreePath, currentGitBranch, effectiveEnvMode, onSetThreadBranch, ]); const handleOpenChange = useCallback( (open: boolean) => { setIsBranchMenuOpen(open); if (!open) { setBranchQuery(""); return; } void queryClient.invalidateQueries({ queryKey: gitQueryKeys.branches(branchCwd), }); }, [branchCwd, queryClient], ); const branchListScrollElementRef = useRef(null); const branchListVirtualizer = useVirtualizer({ count: filteredBranchPickerItems.length, estimateSize: (index) => filteredBranchPickerItems[index] === checkoutPullRequestItemValue ? 44 : 28, getScrollElement: () => branchListScrollElementRef.current, overscan: 12, enabled: isBranchMenuOpen && shouldVirtualizeBranchList, initialRect: { height: 224, width: 0, }, }); const virtualBranchRows = branchListVirtualizer.getVirtualItems(); const setBranchListRef = useCallback( (element: HTMLDivElement | null) => { branchListScrollElementRef.current = (element?.parentElement as HTMLDivElement | null) ?? null; if (element) { branchListVirtualizer.measure(); } }, [branchListVirtualizer], ); useEffect(() => { if (!isBranchMenuOpen || !shouldVirtualizeBranchList) return; queueMicrotask(() => { branchListVirtualizer.measure(); }); }, [ branchListVirtualizer, filteredBranchPickerItems.length, isBranchMenuOpen, shouldVirtualizeBranchList, ]); const triggerLabel = getBranchTriggerLabel({ activeWorktreePath, effectiveEnvMode, resolvedActiveBranch, }); function renderPickerItem(itemValue: string, index: number, style?: CSSProperties) { if (checkoutPullRequestItemValue && itemValue === checkoutPullRequestItemValue) { return ( { if (!prReference || !onCheckoutPullRequestRequest) { return; } setIsBranchMenuOpen(false); setBranchQuery(""); onComposerFocusRequest?.(); onCheckoutPullRequestRequest(prReference); }} >
Checkout Pull Request {prReference}
); } if (createBranchItemValue && itemValue === createBranchItemValue) { return ( createBranch(trimmedBranchQuery)} > Create new branch "{trimmedBranchQuery}" ); } const branch = branchByName.get(itemValue); if (!branch) return null; const hasSecondaryWorktree = branch.worktreePath && branch.worktreePath !== activeProjectCwd; const badge = branch.current ? "current" : hasSecondaryWorktree ? "worktree" : branch.isRemote ? "remote" : branch.isDefault ? "default" : null; return ( selectBranch(branch)} >
{itemValue} {badge && {badge}}
); } return ( { if (!isBranchMenuOpen || eventDetails.index < 0) return; branchListVirtualizer.scrollToIndex(eventDetails.index, { align: "auto" }); }} onOpenChange={handleOpenChange} open={isBranchMenuOpen} value={resolvedActiveBranch} > } className="text-muted-foreground/70 hover:text-foreground/80" disabled={(branchesQuery.isLoading && branches.length === 0) || isBranchActionPending} > {triggerLabel}
setBranchQuery(event.target.value)} />
No branches found. {shouldVirtualizeBranchList ? (
{virtualBranchRows.map((virtualRow) => { const itemValue = filteredBranchPickerItems[virtualRow.index]; if (!itemValue) return null; return renderPickerItem(itemValue, virtualRow.index, { position: "absolute", top: 0, left: 0, width: "100%", transform: `translateY(${virtualRow.start}px)`, }); })}
) : ( filteredBranchPickerItems.map((itemValue, index) => renderPickerItem(itemValue, index)) )}
); }