1import { ComposerSelectControl } from "./chat/ComposerControl";
2import { ComposerContextLabel } from "./ComposerContextLabel";
3import { FolderGit2Icon, FolderGitIcon, FolderIcon } from "lucide-react";
4import { memo, type MouseEvent as ReactMouseEvent } from "react";
5
6import {
7 resolveCurrentWorkspaceLabel,
8 resolveEnvModeLabel,
9 resolveLockedWorkspaceLabel,
10 type EnvMode,
11} from "./BranchToolbar.logic";
12import { useComposerMenuProps } from "./chat/composerEventScope";
13import { PreviousWorktreeItemContent } from "./PreviousWorktreeItemContent";
14import {
15 Select,
16 SelectGroup,
17 SelectGroupLabel,
18 SelectItem,
19 SelectPopup,
20 SelectValue,
21} from "./ui/select";
22import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
23import { useAtomValue } from "@effect/atom-react";
24import { shortcutLabelForCommand } from "../keybindings";
25import { primaryServerKeybindingsAtom } from "../state/server";
26
27const PREVIOUS_WORKTREE_SELECT_VALUE = "previous-worktree";
28
29interface BranchToolbarEnvModeSelectorProps {
30 forceNewWorktree?: boolean;
31 envLocked: boolean;
32 effectiveEnvMode: EnvMode;
33 activeWorktreePath: string | null;
34 onEnvModeChange: (mode: EnvMode) => void;
35 previousWorktreeLabel?: string | null;
36 previousWorktreeBranch?: string | null;
37 onUsePreviousWorktree?: () => void;
38}
39
40export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSelector({
41 forceNewWorktree = false,
42 envLocked,
43 effectiveEnvMode,
44 activeWorktreePath,
45 onEnvModeChange,
46 previousWorktreeLabel,
47 previousWorktreeBranch = null,
48 onUsePreviousWorktree,
49}: BranchToolbarEnvModeSelectorProps) {
50 const composerFloatingLayerProps = useComposerMenuProps();
51 const shortcutLabel = shortcutLabelForCommand(
52 useAtomValue(primaryServerKeybindingsAtom),
53 "composer.workspace",
54 );
55 const showPreviousWorktree = Boolean(previousWorktreeLabel && onUsePreviousWorktree);
56 const envModeItems = [
57 {
58 value: "local",
59 label: resolveCurrentWorkspaceLabel(activeWorktreePath),
60 },
61 { value: "worktree", label: resolveEnvModeLabel("worktree") },
62 ...(showPreviousWorktree && previousWorktreeLabel
63 ? [{ value: PREVIOUS_WORKTREE_SELECT_VALUE, label: previousWorktreeLabel }]
64 : []),
65 ];
66
67 const stopContextMenuMouseDown = (event: ReactMouseEvent) => {
68 if (event.button !== 0 || event.ctrlKey) {
69 event.stopPropagation();
70 }
71 };
72
73 if (envLocked || forceNewWorktree) {
74 const lockedRow = (
75 <span
76 className="inline-flex h-7 min-w-0 items-center gap-1.5 border border-transparent px-1.75 font-normal text-muted-foreground/70 text-sm sm:h-6 sm:text-xs"
77 data-composer-context-control
78 >
79 {activeWorktreePath ? (
80 <FolderGitIcon className="size-3" />
81 ) : effectiveEnvMode === "worktree" ? (
82 <FolderGit2Icon className="size-3" />
83 ) : (
84 <FolderIcon className="size-3" />
85 )}
86 <ComposerContextLabel>
87 {forceNewWorktree
88 ? resolveEnvModeLabel("worktree")
89 : resolveLockedWorkspaceLabel(activeWorktreePath, effectiveEnvMode)}
90 </ComposerContextLabel>
91 </span>
92 );
93
94 return (
95 <Tooltip>
96 <TooltipTrigger render={lockedRow} />
97 <TooltipPopup>
98 {forceNewWorktree
99 ? "Each model starts in its own worktree."
100 : (activeWorktreePath ??
101 resolveLockedWorkspaceLabel(activeWorktreePath, effectiveEnvMode))}
102 </TooltipPopup>
103 </Tooltip>
104 );
105 }
106
107 return (
108 <Select
109 modal={false}
110 value={effectiveEnvMode}
111 onValueChange={(value: string | null) => {
112 if (value === PREVIOUS_WORKTREE_SELECT_VALUE) {
113 onUsePreviousWorktree?.();
114 return;
115 }
116 onEnvModeChange(value as EnvMode);
117 }}
118 items={envModeItems}
119 >
120 <Tooltip>
121 <TooltipTrigger
122 render={
123 <ComposerSelectControl
124 size="xs"
125 className="min-w-0 shrink"
126 aria-label="Workspace"
127 data-composer-shortcut="composer.workspace"
128 data-composer-context-control
129 onMouseDownCapture={stopContextMenuMouseDown}
130 />
131 }
132 >
133 {effectiveEnvMode === "worktree" ? (
134 <FolderGit2Icon className="size-3" />
135 ) : activeWorktreePath ? (
136 <FolderGitIcon className="size-3" />
137 ) : (
138 <FolderIcon className="size-3" />
139 )}
140 <ComposerContextLabel>
141 <SelectValue />
142 </ComposerContextLabel>
143 </TooltipTrigger>
144 <TooltipPopup>
145 {effectiveEnvMode === "worktree"
146 ? resolveEnvModeLabel("worktree")
147 : resolveCurrentWorkspaceLabel(activeWorktreePath)}
148 {shortcutLabel ? ` · ${shortcutLabel}` : null}
149 </TooltipPopup>
150 </Tooltip>
151 <SelectPopup
152 alignItemWithTrigger={false}
153 {...composerFloatingLayerProps}
154 className={showPreviousWorktree ? "w-[min(21rem,calc(100vw-2rem))]" : undefined}
155 >
156 <SelectGroup>
157 <SelectGroupLabel>Workspace</SelectGroupLabel>
158 <SelectItem value="local">
159 <span className="inline-flex items-center gap-1.5">
160 {activeWorktreePath ? (
161 <FolderGitIcon className="size-3" />
162 ) : (
163 <FolderIcon className="size-3" />
164 )}
165 {resolveCurrentWorkspaceLabel(activeWorktreePath)}
166 </span>
167 </SelectItem>
168 <SelectItem value="worktree">
169 <span className="inline-flex items-center gap-1.5">
170 <FolderGit2Icon className="size-3" />
171 {resolveEnvModeLabel("worktree")}
172 </span>
173 </SelectItem>
174 {showPreviousWorktree && previousWorktreeLabel ? (
175 <SelectItem value={PREVIOUS_WORKTREE_SELECT_VALUE}>
176 <PreviousWorktreeItemContent branch={previousWorktreeBranch} />
177 </SelectItem>
178 ) : null}
179 </SelectGroup>
180 </SelectPopup>
181 </Select>
182 );
183});