import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime";import type { EnvironmentId, GitBranch, ThreadId } from "@t3tools/contracts";import { useInfiniteQuery, 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 { useComposerDraftStore, type DraftId } from "../composerDraftStore";import { readEnvironmentApi } from "../environmentApi";import { gitBranchSearchInfiniteQueryOptions, gitQueryKeys } from "../lib/gitReactQuery";import { useGitStatus } from "../lib/gitStatusState";import { newCommandId } from "../lib/utils";import { parsePullRequestReference } from "../pullRequestReference";import { useStore } from "../store";import { createProjectSelectorByRef, createThreadSelectorByRef } from "../storeSelectors";import { deriveLocalBranchNameFromRemoteRef, resolveBranchSelectionTarget, resolveBranchToolbarValue, resolveDraftEnvModeAfterBranchChange, resolveEffectiveEnvMode, shouldIncludeBranchPickerItem,} from "./BranchToolbar.logic";import { Button } from "./ui/button";import { Combobox, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, ComboboxPopup, ComboboxStatus, ComboboxTrigger,} from "./ui/combobox";import { toastManager } from "./ui/toast"; interface BranchToolbarBranchSelectorProps { environmentId: EnvironmentId; threadId: ThreadId; draftId?: DraftId; envLocked: boolean; 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: "local" | "worktree"; 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({ environmentId, threadId, draftId, envLocked, onCheckoutPullRequestRequest, onComposerFocusRequest,}: BranchToolbarBranchSelectorProps) { // --------------------------------------------------------------------------- // Thread / project state (pushed down from parent to colocate with mutation) // --------------------------------------------------------------------------- const threadRef = useMemo( () => scopeThreadRef(environmentId, threadId), [environmentId, threadId], ); const serverThreadSelector = useMemo(() => createThreadSelectorByRef(threadRef), [threadRef]); const serverThread = useStore(serverThreadSelector); const serverSession = serverThread?.session ?? null; const setThreadBranchAction = useStore((store) => store.setThreadBranch); const draftThread = useComposerDraftStore((store) => store.getDraftThreadByRef(threadRef)); const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext); const activeProjectRef = serverThread ? scopeProjectRef(serverThread.environmentId, serverThread.projectId) : draftThread ? scopeProjectRef(draftThread.environmentId, draftThread.projectId) : null; const activeProjectSelector = useMemo( () => createProjectSelectorByRef(activeProjectRef), [activeProjectRef], ); const activeProject = useStore(activeProjectSelector); const activeThreadId = serverThread?.id ?? (draftThread ? threadId : undefined); const activeThreadBranch = serverThread?.branch ?? draftThread?.branch ?? null; const activeWorktreePath = serverThread?.worktreePath ?? draftThread?.worktreePath ?? null; const activeProjectCwd = activeProject?.cwd ?? null; const branchCwd = activeWorktreePath ?? activeProjectCwd; const hasServerThread = serverThread !== undefined; const effectiveEnvMode = resolveEffectiveEnvMode({ activeWorktreePath, hasServerThread, draftThreadEnvMode: draftThread?.envMode, }); // --------------------------------------------------------------------------- // Thread branch mutation (colocated — only this component calls it) // --------------------------------------------------------------------------- const setThreadBranch = useCallback( (branch: string | null, worktreePath: string | null) => { if (!activeThreadId || !activeProject) return; const api = readEnvironmentApi(environmentId); if (serverSession && worktreePath !== activeWorktreePath && api) { void api.orchestration .dispatchCommand({ type: "thread.session.stop", commandId: newCommandId(), threadId: activeThreadId, createdAt: new Date().toISOString(), }) .catch(() => undefined); } if (api && hasServerThread) { void api.orchestration.dispatchCommand({ type: "thread.meta.update", commandId: newCommandId(), threadId: activeThreadId, branch, worktreePath, }); } if (hasServerThread) { setThreadBranchAction(threadRef, branch, worktreePath); return; } const nextDraftEnvMode = resolveDraftEnvModeAfterBranchChange({ nextWorktreePath: worktreePath, currentWorktreePath: activeWorktreePath, effectiveEnvMode, }); setDraftThreadContext(draftId ?? threadRef, { branch, worktreePath, envMode: nextDraftEnvMode, projectRef: scopeProjectRef(environmentId, activeProject.id), }); }, [ activeThreadId, activeProject, serverSession, activeWorktreePath, hasServerThread, setThreadBranchAction, setDraftThreadContext, draftId, threadRef, environmentId, effectiveEnvMode, ], ); // --------------------------------------------------------------------------- // Git branch queries // --------------------------------------------------------------------------- const queryClient = useQueryClient(); const [isBranchMenuOpen, setIsBranchMenuOpen] = useState(false); const [branchQuery, setBranchQuery] = useState(""); const deferredBranchQuery = useDeferredValue(branchQuery); const branchStatusQuery = useGitStatus({ environmentId, cwd: branchCwd }); const trimmedBranchQuery = branchQuery.trim(); const deferredTrimmedBranchQuery = deferredBranchQuery.trim(); useEffect(() => { if (!branchCwd) return; void queryClient.prefetchInfiniteQuery( gitBranchSearchInfiniteQueryOptions({ environmentId, cwd: branchCwd, query: "" }), ); }, [branchCwd, environmentId, queryClient]); const { data: branchesSearchData, fetchNextPage, hasNextPage, isFetchingNextPage, isPending: isBranchesSearchPending, } = useInfiniteQuery( gitBranchSearchInfiniteQueryOptions({ environmentId, cwd: branchCwd, query: deferredTrimmedBranchQuery, }), ); const branches = useMemo( () => branchesSearchData?.pages.flatMap((page) => page.branches) ?? [], [branchesSearchData?.pages], ); 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 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) => shouldIncludeBranchPickerItem({ itemValue, normalizedQuery: normalizedDeferredBranchQuery, createBranchItemValue, checkoutPullRequestItemValue, }), ), [ branchPickerItems, checkoutPullRequestItemValue, createBranchItemValue, normalizedDeferredBranchQuery, ], ); const [resolvedActiveBranch, setOptimisticBranch] = useOptimistic( canonicalActiveBranch, (_currentBranch: string | null, optimisticBranch: string | null) => optimisticBranch, ); const [isBranchActionPending, startBranchActionTransition] = useTransition(); const shouldVirtualizeBranchList = filteredBranchPickerItems.length > 40; const totalBranchCount = branchesSearchData?.pages[0]?.totalCount ?? 0; const branchStatusText = isBranchesSearchPending ? "Loading branches..." : isFetchingNextPage ? "Loading more branches..." : hasNextPage ? `Showing ${branches.length} of ${totalBranchCount} branches` : null; // --------------------------------------------------------------------------- // Branch actions // --------------------------------------------------------------------------- const runBranchAction = (action: () => Promise<void>) => { startBranchActionTransition(async () => { await action().catch(() => undefined); await queryClient .invalidateQueries({ queryKey: gitQueryKeys.branches(environmentId, branchCwd) }) .catch(() => undefined); }); }; const selectBranch = (branch: GitBranch) => { const api = readEnvironmentApi(environmentId); if (!api || !branchCwd || !activeProjectCwd || isBranchActionPending) return; if (isSelectingWorktreeBase) { setThreadBranch(branch.name, null); setIsBranchMenuOpen(false); onComposerFocusRequest?.(); return; } const selectionTarget = resolveBranchSelectionTarget({ activeProjectCwd, activeWorktreePath, branch, }); if (selectionTarget.reuseExistingWorktree) { setThreadBranch(branch.name, selectionTarget.nextWorktreePath); setIsBranchMenuOpen(false); onComposerFocusRequest?.(); return; } const selectedBranchName = branch.isRemote ? deriveLocalBranchNameFromRemoteRef(branch.name) : branch.name; setIsBranchMenuOpen(false); onComposerFocusRequest?.(); runBranchAction(async () => { const previousBranch = resolvedActiveBranch; setOptimisticBranch(selectedBranchName); try { const checkoutResult = await api.git.checkout({ cwd: selectionTarget.checkoutCwd, branch: branch.name, }); const nextBranchName = branch.isRemote ? (checkoutResult.branch ?? selectedBranchName) : selectedBranchName; setOptimisticBranch(nextBranchName); setThreadBranch(nextBranchName, selectionTarget.nextWorktreePath); } catch (error) { setOptimisticBranch(previousBranch); toastManager.add({ type: "error", title: "Failed to checkout branch.", description: toBranchActionErrorMessage(error), }); } }); }; const createBranch = (rawName: string) => { const name = rawName.trim(); const api = readEnvironmentApi(environmentId); if (!api || !branchCwd || !name || isBranchActionPending) return; setIsBranchMenuOpen(false); onComposerFocusRequest?.(); runBranchAction(async () => { const previousBranch = resolvedActiveBranch; setOptimisticBranch(name); try { const createBranchResult = await api.git.createBranch({ cwd: branchCwd, branch: name, checkout: true, }); setOptimisticBranch(createBranchResult.branch); setThreadBranch(createBranchResult.branch, activeWorktreePath); } catch (error) { setOptimisticBranch(previousBranch); toastManager.add({ type: "error", title: "Failed to create and checkout branch.", description: toBranchActionErrorMessage(error), }); } }); }; useEffect(() => { if ( effectiveEnvMode !== "worktree" || activeWorktreePath || activeThreadBranch || !currentGitBranch ) { return; } setThreadBranch(currentGitBranch, null); }, [activeThreadBranch, activeWorktreePath, currentGitBranch, effectiveEnvMode, setThreadBranch]); // --------------------------------------------------------------------------- // Combobox / virtualizer plumbing // --------------------------------------------------------------------------- const handleOpenChange = useCallback( (open: boolean) => { setIsBranchMenuOpen(open); if (!open) { setBranchQuery(""); return; } void queryClient.invalidateQueries({ queryKey: gitQueryKeys.branches(environmentId, branchCwd), }); }, [branchCwd, environmentId, queryClient], ); const branchListScrollElementRef = useRef<HTMLDivElement | null>(null); const maybeFetchNextBranchPage = useCallback(() => { if (!isBranchMenuOpen || !hasNextPage || isFetchingNextPage) { return; } const scrollElement = branchListScrollElementRef.current; if (!scrollElement) { return; } const distanceFromBottom = scrollElement.scrollHeight - scrollElement.scrollTop - scrollElement.clientHeight; if (distanceFromBottom > 96) { return; } void fetchNextPage().catch(() => undefined); }, [fetchNextPage, hasNextPage, isBranchMenuOpen, isFetchingNextPage]); 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, ]); useEffect(() => { if (!isBranchMenuOpen) { return; } branchListScrollElementRef.current?.scrollTo({ top: 0 }); }, [deferredTrimmedBranchQuery, isBranchMenuOpen]); useEffect(() => { const scrollElement = branchListScrollElementRef.current; if (!scrollElement || !isBranchMenuOpen) { return; } const handleScroll = () => { maybeFetchNextBranchPage(); }; scrollElement.addEventListener("scroll", handleScroll, { passive: true }); handleScroll(); return () => { scrollElement.removeEventListener("scroll", handleScroll); }; }, [isBranchMenuOpen, maybeFetchNextBranchPage]); useEffect(() => { maybeFetchNextBranchPage(); }, [branches.length, maybeFetchNextBranchPage]); const triggerLabel = getBranchTriggerLabel({ activeWorktreePath, effectiveEnvMode, resolvedActiveBranch, }); function renderPickerItem(itemValue: string, index: number, style?: CSSProperties) { if (checkoutPullRequestItemValue && itemValue === checkoutPullRequestItemValue) { return ( <ComboboxItemLines 1–500 of 633. Load more
import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime"; import type { EnvironmentId, GitBranch, ThreadId } from "@t3tools/contracts"; import { useInfiniteQuery, 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 { useComposerDraftStore, type DraftId } from "../composerDraftStore"; import { readEnvironmentApi } from "../environmentApi"; import { gitBranchSearchInfiniteQueryOptions, gitQueryKeys } from "../lib/gitReactQuery"; import { useGitStatus } from "../lib/gitStatusState"; import { newCommandId } from "../lib/utils"; import { parsePullRequestReference } from "../pullRequestReference"; import { useStore } from "../store"; import { createProjectSelectorByRef, createThreadSelectorByRef } from "../storeSelectors"; import { deriveLocalBranchNameFromRemoteRef, resolveBranchSelectionTarget, resolveBranchToolbarValue, resolveDraftEnvModeAfterBranchChange, resolveEffectiveEnvMode, shouldIncludeBranchPickerItem, } from "./BranchToolbar.logic"; import { Button } from "./ui/button"; import { Combobox, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, ComboboxPopup, ComboboxStatus, ComboboxTrigger, } from "./ui/combobox"; import { toastManager } from "./ui/toast"; interface BranchToolbarBranchSelectorProps { environmentId: EnvironmentId; threadId: ThreadId; draftId?: DraftId; envLocked: boolean; 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: "local" | "worktree"; 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({ environmentId, threadId, draftId, envLocked, onCheckoutPullRequestRequest, onComposerFocusRequest, }: BranchToolbarBranchSelectorProps) { // --------------------------------------------------------------------------- // Thread / project state (pushed down from parent to colocate with mutation) // --------------------------------------------------------------------------- const threadRef = useMemo( () => scopeThreadRef(environmentId, threadId), [environmentId, threadId], ); const serverThreadSelector = useMemo(() => createThreadSelectorByRef(threadRef), [threadRef]); const serverThread = useStore(serverThreadSelector); const serverSession = serverThread?.session ?? null; const setThreadBranchAction = useStore((store) => store.setThreadBranch); const draftThread = useComposerDraftStore((store) => store.getDraftThreadByRef(threadRef)); const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext); const activeProjectRef = serverThread ? scopeProjectRef(serverThread.environmentId, serverThread.projectId) : draftThread ? scopeProjectRef(draftThread.environmentId, draftThread.projectId) : null; const activeProjectSelector = useMemo( () => createProjectSelectorByRef(activeProjectRef), [activeProjectRef], ); const activeProject = useStore(activeProjectSelector); const activeThreadId = serverThread?.id ?? (draftThread ? threadId : undefined); const activeThreadBranch = serverThread?.branch ?? draftThread?.branch ?? null; const activeWorktreePath = serverThread?.worktreePath ?? draftThread?.worktreePath ?? null; const activeProjectCwd = activeProject?.cwd ?? null; const branchCwd = activeWorktreePath ?? activeProjectCwd; const hasServerThread = serverThread !== undefined; const effectiveEnvMode = resolveEffectiveEnvMode({ activeWorktreePath, hasServerThread, draftThreadEnvMode: draftThread?.envMode, }); // --------------------------------------------------------------------------- // Thread branch mutation (colocated — only this component calls it) // --------------------------------------------------------------------------- const setThreadBranch = useCallback( (branch: string | null, worktreePath: string | null) => { if (!activeThreadId || !activeProject) return; const api = readEnvironmentApi(environmentId); if (serverSession && worktreePath !== activeWorktreePath && api) { void api.orchestration .dispatchCommand({ type: "thread.session.stop", commandId: newCommandId(), threadId: activeThreadId, createdAt: new Date().toISOString(), }) .catch(() => undefined); } if (api && hasServerThread) { void api.orchestration.dispatchCommand({ type: "thread.meta.update", commandId: newCommandId(), threadId: activeThreadId, branch, worktreePath, }); } if (hasServerThread) { setThreadBranchAction(threadRef, branch, worktreePath); return; } const nextDraftEnvMode = resolveDraftEnvModeAfterBranchChange({ nextWorktreePath: worktreePath, currentWorktreePath: activeWorktreePath, effectiveEnvMode, }); setDraftThreadContext(draftId ?? threadRef, { branch, worktreePath, envMode: nextDraftEnvMode, projectRef: scopeProjectRef(environmentId, activeProject.id), }); }, [ activeThreadId, activeProject, serverSession, activeWorktreePath, hasServerThread, setThreadBranchAction, setDraftThreadContext, draftId, threadRef, environmentId, effectiveEnvMode, ], ); // --------------------------------------------------------------------------- // Git branch queries // --------------------------------------------------------------------------- const queryClient = useQueryClient(); const [isBranchMenuOpen, setIsBranchMenuOpen] = useState(false); const [branchQuery, setBranchQuery] = useState(""); const deferredBranchQuery = useDeferredValue(branchQuery); const branchStatusQuery = useGitStatus({ environmentId, cwd: branchCwd }); const trimmedBranchQuery = branchQuery.trim(); const deferredTrimmedBranchQuery = deferredBranchQuery.trim(); useEffect(() => { if (!branchCwd) return; void queryClient.prefetchInfiniteQuery( gitBranchSearchInfiniteQueryOptions({ environmentId, cwd: branchCwd, query: "" }), ); }, [branchCwd, environmentId, queryClient]); const { data: branchesSearchData, fetchNextPage, hasNextPage, isFetchingNextPage, isPending: isBranchesSearchPending, } = useInfiniteQuery( gitBranchSearchInfiniteQueryOptions({ environmentId, cwd: branchCwd, query: deferredTrimmedBranchQuery, }), ); const branches = useMemo( () => branchesSearchData?.pages.flatMap((page) => page.branches) ?? [], [branchesSearchData?.pages], ); 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 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) => shouldIncludeBranchPickerItem({ itemValue, normalizedQuery: normalizedDeferredBranchQuery, createBranchItemValue, checkoutPullRequestItemValue, }), ), [ branchPickerItems, checkoutPullRequestItemValue, createBranchItemValue, normalizedDeferredBranchQuery, ], ); const [resolvedActiveBranch, setOptimisticBranch] = useOptimistic( canonicalActiveBranch, (_currentBranch: string | null, optimisticBranch: string | null) => optimisticBranch, ); const [isBranchActionPending, startBranchActionTransition] = useTransition(); const shouldVirtualizeBranchList = filteredBranchPickerItems.length > 40; const totalBranchCount = branchesSearchData?.pages[0]?.totalCount ?? 0; const branchStatusText = isBranchesSearchPending ? "Loading branches..." : isFetchingNextPage ? "Loading more branches..." : hasNextPage ? `Showing ${branches.length} of ${totalBranchCount} branches` : null; // --------------------------------------------------------------------------- // Branch actions // --------------------------------------------------------------------------- const runBranchAction = (action: () => Promise<void>) => { startBranchActionTransition(async () => { await action().catch(() => undefined); await queryClient .invalidateQueries({ queryKey: gitQueryKeys.branches(environmentId, branchCwd) }) .catch(() => undefined); }); }; const selectBranch = (branch: GitBranch) => { const api = readEnvironmentApi(environmentId); if (!api || !branchCwd || !activeProjectCwd || isBranchActionPending) return; if (isSelectingWorktreeBase) { setThreadBranch(branch.name, null); setIsBranchMenuOpen(false); onComposerFocusRequest?.(); return; } const selectionTarget = resolveBranchSelectionTarget({ activeProjectCwd, activeWorktreePath, branch, }); if (selectionTarget.reuseExistingWorktree) { setThreadBranch(branch.name, selectionTarget.nextWorktreePath); setIsBranchMenuOpen(false); onComposerFocusRequest?.(); return; } const selectedBranchName = branch.isRemote ? deriveLocalBranchNameFromRemoteRef(branch.name) : branch.name; setIsBranchMenuOpen(false); onComposerFocusRequest?.(); runBranchAction(async () => { const previousBranch = resolvedActiveBranch; setOptimisticBranch(selectedBranchName); try { const checkoutResult = await api.git.checkout({ cwd: selectionTarget.checkoutCwd, branch: branch.name, }); const nextBranchName = branch.isRemote ? (checkoutResult.branch ?? selectedBranchName) : selectedBranchName; setOptimisticBranch(nextBranchName); setThreadBranch(nextBranchName, selectionTarget.nextWorktreePath); } catch (error) { setOptimisticBranch(previousBranch); toastManager.add({ type: "error", title: "Failed to checkout branch.", description: toBranchActionErrorMessage(error), }); } }); }; const createBranch = (rawName: string) => { const name = rawName.trim(); const api = readEnvironmentApi(environmentId); if (!api || !branchCwd || !name || isBranchActionPending) return; setIsBranchMenuOpen(false); onComposerFocusRequest?.(); runBranchAction(async () => { const previousBranch = resolvedActiveBranch; setOptimisticBranch(name); try { const createBranchResult = await api.git.createBranch({ cwd: branchCwd, branch: name, checkout: true, }); setOptimisticBranch(createBranchResult.branch); setThreadBranch(createBranchResult.branch, activeWorktreePath); } catch (error) { setOptimisticBranch(previousBranch); toastManager.add({ type: "error", title: "Failed to create and checkout branch.", description: toBranchActionErrorMessage(error), }); } }); }; useEffect(() => { if ( effectiveEnvMode !== "worktree" || activeWorktreePath || activeThreadBranch || !currentGitBranch ) { return; } setThreadBranch(currentGitBranch, null); }, [activeThreadBranch, activeWorktreePath, currentGitBranch, effectiveEnvMode, setThreadBranch]); // --------------------------------------------------------------------------- // Combobox / virtualizer plumbing // --------------------------------------------------------------------------- const handleOpenChange = useCallback( (open: boolean) => { setIsBranchMenuOpen(open); if (!open) { setBranchQuery(""); return; } void queryClient.invalidateQueries({ queryKey: gitQueryKeys.branches(environmentId, branchCwd), }); }, [branchCwd, environmentId, queryClient], ); const branchListScrollElementRef = useRef<HTMLDivElement | null>(null); const maybeFetchNextBranchPage = useCallback(() => { if (!isBranchMenuOpen || !hasNextPage || isFetchingNextPage) { return; } const scrollElement = branchListScrollElementRef.current; if (!scrollElement) { return; } const distanceFromBottom = scrollElement.scrollHeight - scrollElement.scrollTop - scrollElement.clientHeight; if (distanceFromBottom > 96) { return; } void fetchNextPage().catch(() => undefined); }, [fetchNextPage, hasNextPage, isBranchMenuOpen, isFetchingNextPage]); 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, ]); useEffect(() => { if (!isBranchMenuOpen) { return; } branchListScrollElementRef.current?.scrollTo({ top: 0 }); }, [deferredTrimmedBranchQuery, isBranchMenuOpen]); useEffect(() => { const scrollElement = branchListScrollElementRef.current; if (!scrollElement || !isBranchMenuOpen) { return; } const handleScroll = () => { maybeFetchNextBranchPage(); }; scrollElement.addEventListener("scroll", handleScroll, { passive: true }); handleScroll(); return () => { scrollElement.removeEventListener("scroll", handleScroll); }; }, [isBranchMenuOpen, maybeFetchNextBranchPage]); useEffect(() => { maybeFetchNextBranchPage(); }, [branches.length, maybeFetchNextBranchPage]); const triggerLabel = getBranchTriggerLabel({ activeWorktreePath, effectiveEnvMode, resolvedActiveBranch, }); function renderPickerItem(itemValue: string, index: number, style?: CSSProperties) { if (checkoutPullRequestItemValue && itemValue === checkoutPullRequestItemValue) { return ( <ComboboxItem