| ... | @@ -1,5 +1,5 @@ | ... | @@ -1,5 +1,5 @@ |
| 1 | import { useAtomValue } from "@effect/atom-react"; | 1 | import { useAtomValue } from "@effect/atom-react"; |
| 2 | import { connectionStatusText } from "@t3tools/client-runtime/connection"; | 2 | import { connectionStatusTitle } from "@t3tools/client-runtime/connection"; |
| 3 | import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors"; | 3 | import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors"; |
| 4 | import { | 4 | import { |
| 5 | isAtomCommandInterrupted, | 5 | isAtomCommandInterrupted, |
| ... | @@ -56,6 +56,14 @@ import { | ... | @@ -56,6 +56,14 @@ import { |
| 56 | type ProviderUpdateCandidate, | 56 | type ProviderUpdateCandidate, |
| 57 | } from "../ProviderUpdateLaunchNotification.logic"; | 57 | } from "../ProviderUpdateLaunchNotification.logic"; |
| 58 | import { Button } from "../ui/button"; | 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 | import { | 67 | import { |
| 60 | NumberField, | 68 | NumberField, |
| 61 | NumberFieldDecrement, | 69 | NumberFieldDecrement, |
| ... | @@ -67,6 +75,7 @@ import { ScrollArea } from "../ui/scroll-area"; | ... | @@ -67,6 +75,7 @@ import { ScrollArea } from "../ui/scroll-area"; |
| 67 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; | 75 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 68 | import { stackedThreadToast, toastManager } from "../ui/toast"; | 76 | import { stackedThreadToast, toastManager } from "../ui/toast"; |
| 69 | import { AddProviderInstanceDialog } from "./AddProviderInstanceDialog"; | 77 | import { AddProviderInstanceDialog } from "./AddProviderInstanceDialog"; |
| | 78 | import { ExpandableText } from "./ExpandableText"; |
| 70 | import { ProviderInstanceCard } from "./ProviderInstanceCard"; | 79 | import { ProviderInstanceCard } from "./ProviderInstanceCard"; |
| 71 | import { UsageProviderSettings } from "./UsageProviderSettings"; | 80 | import { UsageProviderSettings } from "./UsageProviderSettings"; |
| 72 | import { ProviderSetupSection, readAntigravityAuthMethod } from "./ProviderSetupSection"; | 81 | import { ProviderSetupSection, readAntigravityAuthMethod } from "./ProviderSetupSection"; |
| ... | @@ -159,7 +168,52 @@ function providerEnvironmentDetail(environment: EnvironmentPresentation): string | ... | @@ -159,7 +168,52 @@ function providerEnvironmentDetail(environment: EnvironmentPresentation): string |
| 159 | return environment.displayUrl ?? "Remote device"; | 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 | environment, | 217 | environment, |
| 164 | access, | 218 | access, |
| 165 | deviceTabs, | 219 | deviceTabs, |
| ... | @@ -174,18 +228,33 @@ function EnvironmentUnavailableRow({ | ... | @@ -174,18 +228,33 @@ function EnvironmentUnavailableRow({ |
| 174 | : access.kind === "error" | 228 | : access.kind === "error" |
| 175 | ? "Could not connect to this device" | 229 | ? "Could not connect to this device" |
| 176 | : "Provider settings are unavailable"; | 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 | const description = isLoading | 233 | const description = isLoading |
| 178 | ? access.reason === "permissions" | 234 | ? access.reason === "permissions" |
| 179 | ? "Checking what this session is allowed to change." | 235 | ? "Checking what this session is allowed to change." |
| 180 | : `Waiting for ${environment.label}'s configuration.` | 236 | : `Waiting for ${environment.label}'s configuration.` |
| 181 | : connectionStatusText(environment.connection); | 237 | : connectionStatusTitle(environment.connection); |
| | 238 | const error = isLoading ? null : environment.connection.error; |
| 182 | // No spinner: this state can persist indefinitely for a wedged device, and a | 239 | // No spinner: this state can persist indefinitely for a wedged device, and a |
| 183 | // continuously repainting animation would run the whole time. | 240 | // continuously repainting animation would run the whole time. |
| 184 | return ( | 241 | return ( |
| 185 | <SettingsSection {...searchableSetting("providers")}> | 242 | <ProviderSettingsPlaceholder |
| 186 | {deviceTabs} | 243 | deviceTabs={deviceTabs} |
| 187 | <SettingsRow title={title} description={description} /> | 244 | icon={ |
| 188 | </SettingsSection> | 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,7 +329,7 @@ function ProviderSettingsPanelContent(target: ProviderSettingsTarget) { |
| 260 | const machine = resolveEnvironmentMachineKind(environment.serverConfig); | 329 | const machine = resolveEnvironmentMachineKind(environment.serverConfig); |
| 261 | const selected = environment.environmentId === effectiveEnvironmentId; | 330 | const selected = environment.environmentId === effectiveEnvironmentId; |
| 262 | const detail = providerEnvironmentDetail(environment); | 331 | const detail = providerEnvironmentDetail(environment); |
| 263 | const statusText = connectionStatusText(environment.connection); | 332 | const statusText = connectionStatusTitle(environment.connection); |
| 264 | return ( | 333 | return ( |
| 265 | <Tooltip key={environment.environmentId}> | 334 | <Tooltip key={environment.environmentId}> |
| 266 | <TooltipTrigger | 335 | <TooltipTrigger |
| ... | @@ -302,25 +371,23 @@ function ProviderSettingsPanelContent(target: ProviderSettingsTarget) { | ... | @@ -302,25 +371,23 @@ function ProviderSettingsPanelContent(target: ProviderSettingsTarget) { |
| 302 | return ( | 371 | return ( |
| 303 | <> | 372 | <> |
| 304 | {targetEnvironmentMissing ? ( | 373 | {targetEnvironmentMissing ? ( |
| 305 | <SettingsSection {...searchableSetting("providers")}> | 374 | <ProviderSettingsPlaceholder |
| 306 | {deviceTabs} | 375 | deviceTabs={deviceTabs} |
| 307 | <SettingsRow | 376 | icon={<EnvironmentMachineIcon kind={resolveEnvironmentMachineKind(null)} />} |
| 308 | title="Device unavailable" | 377 | title="Device unavailable" |
| 309 | description="Reconnect this device to set up its provider, or select another device." | 378 | description="Reconnect this device to set up its provider, or select another device." |
| 310 | /> | 379 | /> |
| 311 | </SettingsSection> | | |
| 312 | ) : null} | 380 | ) : null} |
| 313 | {options.length === 0 && !targetEnvironmentMissing ? ( | 381 | {options.length === 0 && !targetEnvironmentMissing ? ( |
| 314 | <SettingsSection {...searchableSetting("providers")}> | 382 | <ProviderSettingsPlaceholder |
| 315 | <SettingsRow | 383 | icon={<EnvironmentMachineIcon kind={resolveEnvironmentMachineKind(null)} />} |
| 316 | title={isReady ? "No connected devices" : "Loading devices"} | 384 | title={isReady ? "No connected devices" : "Loading devices"} |
| 317 | description={ | 385 | description={ |
| 318 | isReady | 386 | isReady |
| 319 | ? "Connect an execution environment before configuring providers." | 387 | ? "Connect an execution environment before configuring providers." |
| 320 | : "Reading connected execution environments." | 388 | : "Reading connected execution environments." |
| 321 | } | 389 | } |
| 322 | /> | 390 | /> |
| 323 | </SettingsSection> | | |
| 324 | ) : null} | 391 | ) : null} |
| 325 | | 392 | |
| 326 | {selectedEnvironment ? ( | 393 | {selectedEnvironment ? ( |
| ... | @@ -450,7 +517,7 @@ function AccessGatedProviderSettings({ | ... | @@ -450,7 +517,7 @@ function AccessGatedProviderSettings({ |
| 450 | }); | 517 | }); |
| 451 | if (access.kind !== "editable" && access.kind !== "read-only") { | 518 | if (access.kind !== "editable" && access.kind !== "read-only") { |
| 452 | return ( | 519 | return ( |
| 453 | <EnvironmentUnavailableRow | 520 | <EnvironmentUnavailablePlaceholder |
| 454 | environment={environment} | 521 | environment={environment} |
| 455 | access={access} | 522 | access={access} |
| 456 | deviceTabs={deviceTabs} | 523 | deviceTabs={deviceTabs} |
| ... | @@ -967,14 +1034,20 @@ export function EnvironmentProviderSettings({ | ... | @@ -967,14 +1034,20 @@ export function EnvironmentProviderSettings({ |
| 967 | > | 1034 | > |
| 968 | {deviceTabs} | 1035 | {deviceTabs} |
| 969 | {readOnly ? ( | 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 | <SettingsRow | 1038 | <SettingsRow |
| 972 | title="Limited permissions" | 1039 | title="Limited permissions" |
| 973 | description={`This session can view ${environmentLabel}'s providers but can't change their settings.`} | 1040 | description={`This session can view ${environmentLabel}'s providers but can't change their settings.`} |
| 974 | /> | 1041 | /> |
| 975 | </div> | 1042 | </div> |
| 976 | ) : null} | 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 | <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"> | 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 | <ScrollArea scrollFade chainVerticalScroll className="lg:min-h-0 lg:flex-1"> | 1052 | <ScrollArea scrollFade chainVerticalScroll className="lg:min-h-0 lg:flex-1"> |
| 980 | <div className="divide-y divide-border/50"> | 1053 | <div className="divide-y divide-border/50"> |