1import type { EnvironmentId } from "@t3tools/contracts";
2import { ChevronDownIcon, GitBranchIcon } from "lucide-react";
3import { useDeferredValue, useMemo, useState } from "react";
4
5import { usePaginatedBranches } from "../state/queries";
6import { useEnvironmentQuery } from "../state/query";
7import { vcsEnvironment } from "../state/vcs";
8import { BranchPicker, BranchPickerRefItem } from "./BranchPicker";
9import { resolveBranchTriggerLabel, sanitizeNewRefName } from "./BranchToolbar.logic";
10import { MiddleTruncate } from "./ui/middle-truncate";
11import { Button } from "./ui/button";
12import { ComboboxTrigger } from "./ui/combobox";
13
14/** Select a future worktree's base without changing the project's current checkout. */
15export function WorktreeBaseBranchPicker({
16 environmentId,
17 cwd,
18 value,
19 onValueChange,
20 startFromOrigin,
21 onStartFromOriginChange,
22 disabled = false,
23 id,
24}: {
25 environmentId: EnvironmentId;
26 cwd: string | null;
27 value: string;
28 onValueChange: (branch: string) => void;
29 startFromOrigin: boolean;
30 onStartFromOriginChange: (checked: boolean) => void;
31 disabled?: boolean;
32 id?: string;
33}) {
34 const [open, setOpen] = useState(false);
35 const [query, setQuery] = useState("");
36 const deferredQuery = useDeferredValue(query.trim());
37 const branches = usePaginatedBranches({
38 environmentId,
39 cwd,
40 query: sanitizeNewRefName(deferredQuery),
41 });
42 const selectedRefQuery = useEnvironmentQuery(
43 cwd && value
44 ? vcsEnvironment.listRefs({
45 environmentId,
46 input: { cwd, query: value, limit: 10 },
47 })
48 : null,
49 );
50 const selectedRef =
51 branches.refs.find((branch) => branch.name === value) ??
52 selectedRefQuery.data?.refs.find((branch) => branch.name === value);
53 const label = resolveBranchTriggerLabel({
54 activeWorktreePath: null,
55 effectiveEnvMode: "worktree",
56 resolvedActiveBranch: value || null,
57 resolvedActiveBranchIsRemote: selectedRef ? selectedRef.isRemote === true : null,
58 startFromOrigin,
59 });
60 const branchByName = useMemo(
61 () => new Map(branches.refs.map((branch) => [branch.name, branch])),
62 [branches.refs],
63 );
64 const items = [...branchByName.keys()];
65 const hasNextPage = branches.data?.nextCursor != null;
66 const statusText =
67 branches.error ??
68 (branches.isPending && branches.data === null
69 ? "Loading refs..."
70 : branches.isFetchingNextPage
71 ? "Loading more refs..."
72 : hasNextPage
73 ? `Showing ${branches.refs.length} of ${branches.data?.totalCount} refs`
74 : null);
75 const handleOpenChange = (next: boolean) => {
76 setOpen(next);
77 if (!next) setQuery("");
78 };
79 return (
80 <BranchPicker
81 items={items}
82 filteredItems={items}
83 value={value || null}
84 query={query}
85 resultsQuery={deferredQuery}
86 onQueryChange={setQuery}
87 open={open && !disabled}
88 onOpenChange={handleOpenChange}
89 onSelectItem={(name) => {
90 onValueChange(name);
91 handleOpenChange(false);
92 }}
93 hasNextPage={hasNextPage}
94 isFetchingNextPage={branches.isFetchingNextPage}
95 onLoadNext={branches.loadNext}
96 statusText={statusText}
97 originControl={{ checked: startFromOrigin, onCheckedChange: onStartFromOriginChange }}
98 popupProps={{ align: "start", side: "bottom", className: "flex w-80 flex-col" }}
99 renderItem={(name, index) => {
100 const branch = branchByName.get(name);
101 return branch ? (
102 <BranchPickerRefItem
103 branch={branch}
104 projectCwd={cwd}
105 index={index}
106 onClick={() => {
107 onValueChange(branch.name);
108 handleOpenChange(false);
109 }}
110 />
111 ) : null;
112 }}
113 >
114 <ComboboxTrigger
115 id={id}
116 disabled={disabled || !cwd}
117 render={<Button variant="outline" size="sm" />}
118 className="w-full justify-between "
119 >
120 <GitBranchIcon className="size-3.5 shrink-0 text-muted-foreground" />
121 <MiddleTruncate value={label} className="flex-1 text-left" />
122 <ChevronDownIcon className="size-3.5 shrink-0 text-muted-foreground" />
123 </ComboboxTrigger>
124 </BranchPicker>
125 );
126}