| ... | ... | @@ -1,5 +1,5 @@ |
| 1 | 1 | import { useAtomValue } from "@effect/atom-react"; |
| 2 | | import { connectionStatusText } from "@t3tools/client-runtime/connection"; |
| 2 | import { connectionStatusTitle } from "@t3tools/client-runtime/connection"; |
| 3 | 3 | import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors"; |
| 4 | 4 | import { |
| 5 | 5 | isAtomCommandInterrupted, |
| ... | ... | @@ -56,6 +56,14 @@ import { |
| 56 | 56 | type ProviderUpdateCandidate, |
| 57 | 57 | } from "../ProviderUpdateLaunchNotification.logic"; |
| 58 | 58 | import { Button } from "../ui/button"; |
| 59 | import { |
| 60 | Empty, |
| 61 | EmptyContent, |
| 62 | EmptyDescription, |
| 63 | EmptyHeader, |
| 64 | EmptyMedia, |
| 65 | EmptyTitle, |
| 66 | } from "../ui/empty"; |
| 59 | 67 | import { |
| 60 | 68 | NumberField, |
| 61 | 69 | NumberFieldDecrement, |
| ... | ... | @@ -67,6 +75,7 @@ import { ScrollArea } from "../ui/scroll-area"; |
| 67 | 75 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 68 | 76 | import { stackedThreadToast, toastManager } from "../ui/toast"; |
| 69 | 77 | import { AddProviderInstanceDialog } from "./AddProviderInstanceDialog"; |
| 78 | import { ExpandableText } from "./ExpandableText"; |
| 70 | 79 | import { ProviderInstanceCard } from "./ProviderInstanceCard"; |
| 71 | 80 | import { UsageProviderSettings } from "./UsageProviderSettings"; |
| 72 | 81 | import { ProviderSetupSection, readAntigravityAuthMethod } from "./ProviderSetupSection"; |
| ... | ... | @@ -159,7 +168,52 @@ function providerEnvironmentDetail(environment: EnvironmentPresentation): string |
| 159 | 168 | return environment.displayUrl ?? "Remote device"; |
| 160 | 169 | } |
| 161 | 170 | |
| 162 | | function EnvironmentUnavailableRow({ |
| 171 | const providerCardClassName = "rounded-xl border border-border/60 bg-card/40 shadow-xs/5"; |
| 172 | // Shared by the editor grid and the placeholder states so switching devices |
| 173 | // never changes the card's footprint. |
| 174 | const providerCardHeightClassName = "lg:h-[min(44rem,calc(100dvh-11rem))] lg:min-h-[32rem]"; |
| 175 | |
| 176 | /** |
| 177 | * Same chrome as the provider editor (section heading, floating device tabs, |
| 178 | * tall card) for states that cannot render provider settings yet. |
| 179 | */ |
| 180 | function ProviderSettingsPlaceholder({ |
| 181 | deviceTabs, |
| 182 | icon, |
| 183 | title, |
| 184 | description, |
| 185 | children, |
| 186 | }: { |
| 187 | readonly deviceTabs?: ReactNode; |
| 188 | readonly icon: ReactNode; |
| 189 | readonly title: string; |
| 190 | readonly description: string; |
| 191 | readonly children?: ReactNode; |
| 192 | }) { |
| 193 | return ( |
| 194 | <SettingsSection {...searchableSetting("providers")} variant="plain"> |
| 195 | {deviceTabs} |
| 196 | <div |
| 197 | className={cn( |
| 198 | providerCardClassName, |
| 199 | providerCardHeightClassName, |
| 200 | "flex overflow-x-hidden overflow-y-auto", |
| 201 | )} |
| 202 | > |
| 203 | <Empty className="min-h-88"> |
| 204 | <EmptyMedia variant="icon">{icon}</EmptyMedia> |
| 205 | <EmptyHeader> |
| 206 | <EmptyTitle>{title}</EmptyTitle> |
| 207 | <EmptyDescription>{description}</EmptyDescription> |
| 208 | </EmptyHeader> |
| 209 | {children ? <EmptyContent className="max-w-xl">{children}</EmptyContent> : null} |
| 210 | </Empty> |
| 211 | </div> |
| 212 | </SettingsSection> |
| 213 | ); |
| 214 | } |
| 215 | |
| 216 | function EnvironmentUnavailablePlaceholder({ |
| 163 | 217 | environment, |
| 164 | 218 | access, |
| 165 | 219 | deviceTabs, |
| ... | ... | @@ -174,18 +228,33 @@ function EnvironmentUnavailableRow({ |
| 174 | 228 | : access.kind === "error" |
| 175 | 229 | ? "Could not connect to this device" |
| 176 | 230 | : "Provider settings are unavailable"; |
| 231 | // Keep the description to a short status; the raw failure can be a |
| 232 | // multi-paragraph CLI dump, so it goes below, clamped and expandable. |
| 177 | 233 | const description = isLoading |
| 178 | 234 | ? access.reason === "permissions" |
| 179 | 235 | ? "Checking what this session is allowed to change." |
| 180 | 236 | : `Waiting for ${environment.label}'s configuration.` |
| 181 | | : connectionStatusText(environment.connection); |
| 237 | : connectionStatusTitle(environment.connection); |
| 238 | const error = isLoading ? null : environment.connection.error; |
| 182 | 239 | // No spinner: this state can persist indefinitely for a wedged device, and a |
| 183 | 240 | // continuously repainting animation would run the whole time. |
| 184 | 241 | return ( |
| 185 | | <SettingsSection {...searchableSetting("providers")}> |
| 186 | | {deviceTabs} |
| 187 | | <SettingsRow title={title} description={description} /> |
| 188 | | </SettingsSection> |
| 242 | <ProviderSettingsPlaceholder |
| 243 | deviceTabs={deviceTabs} |
| 244 | icon={ |
| 245 | <EnvironmentMachineIcon kind={resolveEnvironmentMachineKind(environment.serverConfig)} /> |
| 246 | } |
| 247 | title={title} |
| 248 | description={description} |
| 249 | > |
| 250 | {error ? ( |
| 251 | <ExpandableText |
| 252 | key={environment.environmentId} |
| 253 | text={error} |
| 254 | className="w-full text-left font-mono text-xs leading-relaxed text-muted-foreground" |
| 255 | /> |
| 256 | ) : null} |
| 257 | </ProviderSettingsPlaceholder> |
| 189 | 258 | ); |
| 190 | 259 | } |
| 191 | 260 | |
| ... | ... | @@ -260,7 +329,7 @@ function ProviderSettingsPanelContent(target: ProviderSettingsTarget) { |
| 260 | 329 | const machine = resolveEnvironmentMachineKind(environment.serverConfig); |
| 261 | 330 | const selected = environment.environmentId === effectiveEnvironmentId; |
| 262 | 331 | const detail = providerEnvironmentDetail(environment); |
| 263 | | const statusText = connectionStatusText(environment.connection); |
| 332 | const statusText = connectionStatusTitle(environment.connection); |
| 264 | 333 | return ( |
| 265 | 334 | <Tooltip key={environment.environmentId}> |
| 266 | 335 | <TooltipTrigger |
| ... | ... | @@ -302,25 +371,23 @@ function ProviderSettingsPanelContent(target: ProviderSettingsTarget) { |
| 302 | 371 | return ( |
| 303 | 372 | <> |
| 304 | 373 | {targetEnvironmentMissing ? ( |
| 305 | | <SettingsSection {...searchableSetting("providers")}> |
| 306 | | {deviceTabs} |
| 307 | | <SettingsRow |
| 308 | | title="Device unavailable" |
| 309 | | description="Reconnect this device to set up its provider, or select another device." |
| 310 | | /> |
| 311 | | </SettingsSection> |
| 374 | <ProviderSettingsPlaceholder |
| 375 | deviceTabs={deviceTabs} |
| 376 | icon={<EnvironmentMachineIcon kind={resolveEnvironmentMachineKind(null)} />} |
| 377 | title="Device unavailable" |
| 378 | description="Reconnect this device to set up its provider, or select another device." |
| 379 | /> |
| 312 | 380 | ) : null} |
| 313 | 381 | {options.length === 0 && !targetEnvironmentMissing ? ( |
| 314 | | <SettingsSection {...searchableSetting("providers")}> |
| 315 | | <SettingsRow |
| 316 | | title={isReady ? "No connected devices" : "Loading devices"} |
| 317 | | description={ |
| 318 | | isReady |
| 319 | | ? "Connect an execution environment before configuring providers." |
| 320 | | : "Reading connected execution environments." |
| 321 | | } |
| 322 | | /> |
| 323 | | </SettingsSection> |
| 382 | <ProviderSettingsPlaceholder |
| 383 | icon={<EnvironmentMachineIcon kind={resolveEnvironmentMachineKind(null)} />} |
| 384 | title={isReady ? "No connected devices" : "Loading devices"} |
| 385 | description={ |
| 386 | isReady |
| 387 | ? "Connect an execution environment before configuring providers." |
| 388 | : "Reading connected execution environments." |
| 389 | } |
| 390 | /> |
| 324 | 391 | ) : null} |
| 325 | 392 | |
| 326 | 393 | {selectedEnvironment ? ( |
| ... | ... | @@ -450,7 +517,7 @@ function AccessGatedProviderSettings({ |
| 450 | 517 | }); |
| 451 | 518 | if (access.kind !== "editable" && access.kind !== "read-only") { |
| 452 | 519 | return ( |
| 453 | | <EnvironmentUnavailableRow |
| 520 | <EnvironmentUnavailablePlaceholder |
| 454 | 521 | environment={environment} |
| 455 | 522 | access={access} |
| 456 | 523 | deviceTabs={deviceTabs} |
| ... | ... | @@ -967,14 +1034,20 @@ export function EnvironmentProviderSettings({ |
| 967 | 1034 | > |
| 968 | 1035 | {deviceTabs} |
| 969 | 1036 | {readOnly ? ( |
| 970 | | <div className="overflow-hidden rounded-xl border border-border/60 bg-card/40 shadow-xs/5"> |
| 1037 | <div className={cn(providerCardClassName, "overflow-hidden")}> |
| 971 | 1038 | <SettingsRow |
| 972 | 1039 | title="Limited permissions" |
| 973 | 1040 | description={`This session can view ${environmentLabel}'s providers but can't change their settings.`} |
| 974 | 1041 | /> |
| 975 | 1042 | </div> |
| 976 | 1043 | ) : null} |
| 977 | | <div className="overflow-hidden rounded-xl border border-border/60 bg-card/40 shadow-xs/5 lg:grid lg:h-[min(44rem,calc(100dvh-11rem))] lg:min-h-[32rem] lg:grid-cols-[17rem_minmax(0,1fr)]"> |
| 1044 | <div |
| 1045 | className={cn( |
| 1046 | providerCardClassName, |
| 1047 | providerCardHeightClassName, |
| 1048 | "overflow-hidden lg:grid lg:grid-cols-[17rem_minmax(0,1fr)]", |
| 1049 | )} |
| 1050 | > |
| 978 | 1051 | <div className="border-b border-border/60 bg-muted/10 lg:flex lg:min-h-0 lg:flex-col lg:border-r lg:border-b-0"> |
| 979 | 1052 | <ScrollArea scrollFade chainVerticalScroll className="lg:min-h-0 lg:flex-1"> |
| 980 | 1053 | <div className="divide-y divide-border/50"> |