1import { ComposerSelectControl } from "./chat/ComposerControl";
2import { ComposerContextLabel } from "./ComposerContextLabel";
3import { Tooltip, TooltipTrigger, TooltipPopup } from "./ui/tooltip";
4import type { EnvironmentId } from "@t3tools/contracts";
5import { ScaleIcon } from "lucide-react";
6import { memo, useMemo } from "react";
7
8import type { EnvironmentOption } from "./BranchToolbar.logic";
9import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon";
10import { useComposerMenuProps } from "./chat/composerEventScope";
11import {
12 Select,
13 SelectGroup,
14 SelectGroupLabel,
15 SelectItem,
16 SelectPopup,
17 SelectValue,
18} from "./ui/select";
19
20interface BranchToolbarEnvironmentSelectorProps {
21 autoEnvironmentLabel?: string | undefined;
22 onAutoEnvironment?: (() => void) | undefined;
23 envLocked: boolean;
24 environmentId: EnvironmentId;
25 availableEnvironments: readonly EnvironmentOption[];
26 onEnvironmentChange?: (environmentId: EnvironmentId) => void;
27}
28
29export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvironmentSelector({
30 autoEnvironmentLabel,
31 onAutoEnvironment,
32 envLocked,
33 environmentId,
34 availableEnvironments,
35 onEnvironmentChange,
36}: BranchToolbarEnvironmentSelectorProps) {
37 const composerFloatingLayerProps = useComposerMenuProps();
38 const activeEnvironment = useMemo(() => {
39 return availableEnvironments.find((env) => env.environmentId === environmentId) ?? null;
40 }, [availableEnvironments, environmentId]);
41
42 const environmentItems = useMemo(
43 () => [
44 ...(onAutoEnvironment
45 ? [{ value: "auto", label: autoEnvironmentLabel ?? "Auto balance" }]
46 : []),
47 ...availableEnvironments.map((env) => ({
48 value: env.environmentId,
49 label: env.label,
50 })),
51 ],
52 [availableEnvironments, autoEnvironmentLabel, onAutoEnvironment],
53 );
54
55 // The static label carries the xs control's height (h-7 sm:h-6) as well as
56 // its padding: the composer context strip has no min-height of its own, and
57 // the glass seam joining it to the composer assumes a fixed strip height, so
58 // a shorter label would drag the seam out of line whenever this label is the
59 // only thing in the strip.
60 if (envLocked || onEnvironmentChange === undefined) {
61 const lockedRow = (
62 <span
63 className="inline-flex h-7 min-w-0 max-w-full items-center gap-1 border border-transparent px-1.75 font-normal text-muted-foreground/70 text-xs sm:h-6"
64 data-composer-context-control
65 >
66 <EnvironmentMachineIcon
67 kind={activeEnvironment?.machine ?? "server"}
68 className="size-3 shrink-0"
69 />
70 <ComposerContextLabel>{activeEnvironment?.label ?? "Run on"}</ComposerContextLabel>
71 </span>
72 );
73 return (
74 <Tooltip>
75 <TooltipTrigger render={lockedRow} />
76 <TooltipPopup>{activeEnvironment?.label ?? "Run on"}</TooltipPopup>
77 </Tooltip>
78 );
79 }
80
81 return (
82 <Select
83 modal={false}
84 value={autoEnvironmentLabel ? "auto" : environmentId}
85 onValueChange={(value) =>
86 value === "auto" ? onAutoEnvironment?.() : onEnvironmentChange(value as EnvironmentId)
87 }
88 items={environmentItems}
89 >
90 <Tooltip>
91 <TooltipTrigger
92 render={
93 <ComposerSelectControl
94 size="xs"
95 className="min-w-0 max-w-full"
96 aria-label="Run on"
97 data-composer-shortcut="composer.host"
98 data-composer-context-control
99 />
100 }
101 >
102 {autoEnvironmentLabel ? (
103 <ScaleIcon className="size-3 shrink-0" aria-hidden="true" />
104 ) : (
105 <EnvironmentMachineIcon
106 kind={activeEnvironment?.machine ?? "server"}
107 className="size-3 shrink-0"
108 />
109 )}
110 <ComposerContextLabel>
111 <SelectValue />
112 </ComposerContextLabel>
113 </TooltipTrigger>
114 <TooltipPopup>{autoEnvironmentLabel ?? activeEnvironment?.label ?? "Run on"}</TooltipPopup>
115 </Tooltip>
116 <SelectPopup alignItemWithTrigger={false} {...composerFloatingLayerProps}>
117 <SelectGroup>
118 <SelectGroupLabel>Run on</SelectGroupLabel>
119 {onAutoEnvironment && (
120 <SelectItem
121 value="auto"
122 onClick={() => {
123 if (autoEnvironmentLabel) onAutoEnvironment?.();
124 }}
125 >
126 <span className="inline-flex items-center gap-1.5">
127 <ScaleIcon className="size-3" aria-hidden="true" />
128 {autoEnvironmentLabel ?? "Auto balance"}
129 </span>
130 </SelectItem>
131 )}
132 {availableEnvironments.map((env) => (
133 <SelectItem key={env.environmentId} value={env.environmentId}>
134 <span className="inline-flex items-center gap-1.5">
135 <EnvironmentMachineIcon kind={env.machine} className="size-3" />
136 {env.label}
137 </span>
138 </SelectItem>
139 ))}
140 </SelectGroup>
141 </SelectPopup>
142 </Select>
143 );
144});