Open History to see the last commit
1import { Toolbar } from "@base-ui/react/toolbar";
2import { type ProviderInstanceId } from "@t3tools/contracts";
3import { memo, useLayoutEffect, useRef, useState } from "react";
4import { StarIcon } from "lucide-react";
5import { ProviderInstanceIcon } from "./ProviderInstanceIcon";
6import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
7import { cn } from "~/lib/utils";
8import {
9 isProviderInstancePickerReady,
10 shouldShowInstanceBadge,
11 type ProviderInstanceEntry,
12} from "../../providerInstances";
13
14function describeUnavailableInstance(entry: ProviderInstanceEntry): string {
15 const label = entry.displayName;
16 // An unavailable instance (a driver this build lacks or the policy leaves out) says why instead.
17 if ((!entry.enabled && entry.isAvailable) || entry.status === "disabled") {
18 return `${label}: Disabled in settings.`;
19 }
20 if (entry.status === "ready" && entry.isAvailable) {
21 return label;
22 }
23 const kind =
24 entry.status === "error" ? "Unavailable" : entry.status === "warning" ? "Limited" : "Not ready";
25 const msg = entry.snapshot.message?.trim();
26 return msg ? `${label}: ${kind}. ${msg}` : `${label}: ${kind}.`;
27}
28
29const PICKER_TOOLTIP_SIDE = "left" as const;
30const PICKER_TOOLTIP_SIDE_OFFSET = 8;
31
32export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {
33 selectedInstanceId: ProviderInstanceId | "favorites";
34 onSelectInstance: (instanceId: ProviderInstanceId | "favorites") => void;
35 onFocusSearch: () => void;
36 instanceEntries: ReadonlyArray<ProviderInstanceEntry>;
37 showFavorites?: boolean;
38 disabledInstanceIds?: ReadonlySet<ProviderInstanceId>;
39 selectableUnavailableInstanceIds?: ReadonlySet<ProviderInstanceId>;
40 getDisabledInstanceTooltip?: (entry: ProviderInstanceEntry) => string;
41}) {
42 const handleSelect = (instanceId: ProviderInstanceId | "favorites") => {
43 props.onSelectInstance(instanceId);
44 };
45 const showFavorites = props.showFavorites ?? true;
46 const [hoveredInstanceId, setHoveredInstanceId] = useState<ProviderInstanceId | null>(null);
47 const sidebarContentRef = useRef<HTMLDivElement>(null);
48 const [selectedIndicatorTop, setSelectedIndicatorTop] = useState<number | null>(null);
49 // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- reads the selected provider item's DOM offset to position the selection indicator
50 useLayoutEffect(() => {
51 const content = sidebarContentRef.current;
52 if (!content) {
53 return;
54 }
55 const selectedItem = Array.from(
56 content.querySelectorAll<HTMLElement>("[data-model-picker-provider]"),
57 ).find((item) => item.dataset.modelPickerProvider === props.selectedInstanceId);
58 if (!selectedItem) {
59 setSelectedIndicatorTop(null);
60 return;
61 }
62 setSelectedIndicatorTop(selectedItem.offsetTop + selectedItem.offsetHeight / 2 - 10);
63 }, [
64 // oxlint-disable-next-line react/exhaustive-effect-dependencies -- Adding, removing or reordering providers moves the selected item, so the indicator re-measures.
65 props.instanceEntries,
66 props.selectedInstanceId,
67 showFavorites,
68 ]);
69
70 return (
71 <Toolbar.Root
72 className="w-11 shrink-0 overflow-hidden bg-muted/30"
73 data-model-picker-sidebar="true"
74 aria-label="Providers"
75 orientation="vertical"
76 onKeyDown={(event) => {
77 if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
78 if (event.key === "ArrowRight") {
79 event.preventDefault();
80 props.onFocusSearch();
81 return;
82 }
83 }}
84 >
85 <div className="h-full overflow-y-auto overscroll-contain [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
86 <div ref={sidebarContentRef} className="relative flex min-h-full flex-col gap-1 p-1">
87 {selectedIndicatorTop !== null ? (
88 <div
89 data-model-picker-selected-indicator="true"
90 className="pointer-events-none absolute right-1 z-10 h-5 w-0.75 rounded-full bg-primary transition-[top] duration-200 ease-out"
91 style={{ top: selectedIndicatorTop }}
92 />
93 ) : null}
94 {showFavorites ? (
95 <>
96 <div className="relative w-full" data-model-picker-provider="favorites">
97 <Tooltip>
98 <TooltipTrigger
99 render={
100 <Toolbar.Button
101 className={cn(
102 "relative isolate flex w-full cursor-pointer aspect-square items-center justify-center rounded-md transition-colors hover:bg-foreground/10 focus-visible:bg-foreground/10 focus-visible:outline-none",
103 )}
104 onClick={() => handleSelect("favorites")}
105 type="button"
106 aria-label="Favorites"
107 aria-pressed={props.selectedInstanceId === "favorites"}
108 >
109 <StarIcon className="size-5 fill-current shrink-0" aria-hidden />
110 </Toolbar.Button>
111 }
112 />
113 <TooltipPopup
114 side={PICKER_TOOLTIP_SIDE}
115 sideOffset={PICKER_TOOLTIP_SIDE_OFFSET}
116 align="center"
117 >
118 Favorites
119 </TooltipPopup>
120 </Tooltip>
121 </div>
122 <div className="border-b border-border/70" aria-hidden="true" />
123 </>
124 ) : null}
125
126 {props.instanceEntries.map((entry) => {
127 const isUnavailable = !isProviderInstancePickerReady(entry);
128 const isContextDisabled = props.disabledInstanceIds?.has(entry.instanceId) ?? false;
129 const unavailableSelectionIsReachable =
130 props.selectableUnavailableInstanceIds?.has(entry.instanceId) ?? false;
131 const isDisabled =
132 (isUnavailable && !unavailableSelectionIsReachable) || isContextDisabled;
133 const isSelected = props.selectedInstanceId === entry.instanceId;
134 const isHovered = hoveredInstanceId === entry.instanceId;
135 const showInstanceBadge = shouldShowInstanceBadge(entry, props.instanceEntries);
136
137 const tooltip = isUnavailable
138 ? describeUnavailableInstance(entry)
139 : isContextDisabled
140 ? (props.getDisabledInstanceTooltip?.(entry) ?? entry.displayName)
141 : entry.displayName;
142
143 const button = (
144 <Toolbar.Button
145 className={cn(
146 "relative isolate flex w-full cursor-pointer aspect-square items-center justify-center rounded-md transition-colors hover:bg-foreground/10 focus-visible:bg-foreground/10 focus-visible:outline-none",
147 isDisabled && "opacity-50 cursor-not-allowed hover:bg-transparent",
148 )}
149 data-provider-accent-color={entry.accentColor}
150 onClick={() => !isDisabled && handleSelect(entry.instanceId)}
151 onMouseEnter={() => setHoveredInstanceId(entry.instanceId)}
152 onMouseLeave={() =>
153 setHoveredInstanceId((current) => (current === entry.instanceId ? null : current))
154 }
155 onFocus={() => setHoveredInstanceId(entry.instanceId)}
156 onBlur={() =>
157 setHoveredInstanceId((current) => (current === entry.instanceId ? null : current))
158 }
159 disabled={isDisabled}
160 focusableWhenDisabled={!isDisabled}
161 aria-pressed={isSelected}
162 type="button"
163 aria-label={isUnavailable || isContextDisabled ? tooltip : entry.displayName}
164 >
165 <ProviderInstanceIcon
166 driverKind={entry.driverKind}
167 displayName={entry.displayName}
168 accentColor={entry.accentColor}
169 acpRegistryAgentId={entry.acpRegistryAgentId}
170 acpRegistryIconUrl={entry.acpRegistryIconUrl}
171 showBadge={showInstanceBadge}
172 className="size-6 z-30"
173 iconClassName="size-5"
174 indicatorBackground={
175 isHovered && !isDisabled
176 ? "var(--muted)"
177 : isSelected
178 ? "var(--background)"
179 : "color-mix(in oklab, var(--muted) 30%, transparent)"
180 }
181 {...(entry.accentColor ? { badgeClassName: "h-3 min-w-3 px-0.5 text-5xs" } : {})}
182 />
183 </Toolbar.Button>
184 );
185
186 const trigger = isDisabled ? (
187 <span className="relative block w-full">{button}</span>
188 ) : (
189 button
190 );
191
192 return (
193 <div
194 key={entry.instanceId}
195 className="relative w-full"
196 data-model-picker-provider={entry.instanceId}
197 >
198 <Tooltip>
199 <TooltipTrigger render={trigger} />
200 <TooltipPopup
201 side={PICKER_TOOLTIP_SIDE}
202 sideOffset={PICKER_TOOLTIP_SIDE_OFFSET}
203 align="center"
204 >
205 {tooltip}
206 </TooltipPopup>
207 </Tooltip>
208 </div>
209 );
210 })}
211 </div>
212 </div>
213 </Toolbar.Root>
214 );
215});