1import type { ConnectionCatalogEntry } from "@t3tools/client-runtime/connection";
2import type { ServerConfig } from "@t3tools/contracts";
3import { useMemo } from "react";
4
5import { useEnvironments, usePrimaryEnvironmentId } from "~/state/environments";
6import { isDesktopLocalConnectionTarget } from "~/connection/desktopLocal";
7import {
8 buildLocalEnvironmentUpdateGroups,
9 deriveEnvironmentDisplayLabel,
10 type EnvironmentUpdateConnectionState,
11 type LocalEnvironmentProvidersInput,
12 type LocalEnvironmentUpdateGroup,
13} from "./ProviderUpdateLaunchNotification.logic";
14
15/**
16 * A local environment is either the same-origin primary backend or a
17 * desktop-local secondary (the parallel WSL backend), which connects over
18 * loopback with a bearer token and carries a `local:<backendInstanceId>`
19 * connection id. SSH, relay, and other remote targets are excluded.
20 */
21function isLocalConnectionTarget(target: ConnectionCatalogEntry["target"]): boolean {
22 return target._tag === "PrimaryConnectionTarget" || isDesktopLocalConnectionTarget(target);
23}
24
25function normalizeConnectionState(phase: string | undefined): EnvironmentUpdateConnectionState {
26 switch (phase) {
27 case "connected":
28 return "ready";
29 case "connecting":
30 case "reconnecting":
31 return "connecting";
32 case "unsupported":
33 case "error":
34 return "error";
35 case "offline":
36 return "disconnected";
37 default:
38 // "available" (or anything not yet observed) — the backend has not
39 // confirmed it is serving yet, so treat it as still settling so the
40 // popover waits for it.
41 return "connecting";
42 }
43}
44
45/**
46 * Reactively enumerate the enabled local environments (the primary plus any
47 * desktop-local secondary such as WSL) with each one's full provider list and a
48 * flag for whether any is still connecting. Drives the launch popover's gating
49 * and its per-environment update triggers.
50 */
51export function useLocalEnvironmentUpdateGroups(): {
52 readonly groups: LocalEnvironmentUpdateGroup[];
53 readonly isAnySettling: boolean;
54} {
55 const { environments } = useEnvironments();
56 const primaryEnvironmentId = usePrimaryEnvironmentId();
57
58 return useMemo(() => {
59 const inputs: LocalEnvironmentProvidersInput[] = [];
60
61 for (const environment of environments) {
62 if (!isLocalConnectionTarget(environment.entry.target)) {
63 continue;
64 }
65
66 const isPrimary = environment.environmentId === primaryEnvironmentId;
67 const serverConfig: ServerConfig | null = environment.serverConfig;
68
69 inputs.push({
70 environmentId: environment.environmentId,
71 // Secondaries carry a meaningful label straight from the platform source
72 // (e.g. "WSL (Ubuntu)"). The primary's catalog label can be the account
73 // name, so fall back to its platform OS so the row reads "Windows"/"Linux".
74 label: isPrimary
75 ? deriveEnvironmentDisplayLabel({
76 isWsl: false,
77 wslDistro: null,
78 platformOs: serverConfig?.environment.platform.os,
79 fallbackLabel: environment.label,
80 })
81 : environment.label,
82 isPrimary,
83 // The primary is the backend serving this renderer, so it is ready
84 // whenever its providers are available; secondaries report their live
85 // connection phase.
86 connectionState: isPrimary
87 ? "ready"
88 : normalizeConnectionState(environment.connection.phase),
89 providers: serverConfig?.providers ?? [],
90 });
91 }
92
93 // Primary first, then the rest in catalog order.
94 inputs.sort((left, right) => Number(right.isPrimary) - Number(left.isPrimary));
95
96 return buildLocalEnvironmentUpdateGroups(inputs);
97 }, [environments, primaryEnvironmentId]);
98}