1import {
2 resolveEnvironmentMachineKind,
3 type EnvironmentId,
4 type ExecutionEnvironmentCapabilities,
5 type EnvironmentMachineKind,
6 type ServerConfig,
7 type ServerProvider,
8} from "@t3tools/contracts";
9import { Atom } from "effect/reactivity";
10
11export type ThreadListProvider = Pick<
12 ServerProvider,
13 "instanceId" | "driver" | "displayName" | "accentColor" | "iconUrl"
14>;
15
16const capabilityKeys = [
17 "threadSettlement",
18 "threadSnooze",
19 "threadPinning",
20 "threadAutoSettleOptOut",
21 "threadPinReorder",
22 "threadActiveReorder",
23 "threadTitleRegeneration",
24] as const;
25
26const EMPTY_PROVIDERS: ThreadListProvider[] = [];
27
28function selectEnvironment(
29 config: ServerConfig | undefined,
30 capabilities: ExecutionEnvironmentCapabilities,
31) {
32 return {
33 providers:
34 config?.providers.map(({ instanceId, driver, displayName, accentColor, iconUrl }) => ({
35 instanceId,
36 driver,
37 displayName,
38 accentColor,
39 iconUrl,
40 })) ?? EMPTY_PROVIDERS,
41 machineKind: resolveEnvironmentMachineKind(config ?? null),
42 capabilities,
43 };
44}
45
46type ListEnvironment = ReturnType<typeof selectEnvironment>;
47
48function sameProviders(
49 left: ReadonlyArray<ThreadListProvider>,
50 right: ReadonlyArray<ThreadListProvider>,
51) {
52 return (
53 left.length === right.length &&
54 left.every((provider, index) => {
55 const other = right[index]!;
56 return (
57 provider.instanceId === other.instanceId &&
58 provider.driver === other.driver &&
59 provider.displayName === other.displayName &&
60 provider.accentColor === other.accentColor &&
61 provider.iconUrl === other.iconUrl
62 );
63 })
64 );
65}
66
67function collectEnvironments(environments: ReadonlyMap<EnvironmentId, ListEnvironment>) {
68 const providersByEnvironmentId = new Map<EnvironmentId, ReadonlyArray<ThreadListProvider>>();
69 const machineByEnvironmentId = new Map<EnvironmentId, EnvironmentMachineKind>();
70 const settlementEnvironmentIds = new Set<EnvironmentId>();
71 const snoozeEnvironmentIds = new Set<EnvironmentId>();
72 const pinningEnvironmentIds = new Set<EnvironmentId>();
73 const autoSettleOptOutEnvironmentIds = new Set<EnvironmentId>();
74 const pinReorderEnvironmentIds = new Set<EnvironmentId>();
75 const activeReorderEnvironmentIds = new Set<EnvironmentId>();
76 const titleRegenerationEnvironmentIds = new Set<EnvironmentId>();
77 for (const [id, { providers, machineKind, capabilities }] of environments) {
78 providersByEnvironmentId.set(id, providers);
79 machineByEnvironmentId.set(id, machineKind);
80 if (capabilities.threadSettlement === true) settlementEnvironmentIds.add(id);
81 if (capabilities.threadSnooze === true) snoozeEnvironmentIds.add(id);
82 if (capabilities.threadAutoSettleOptOut === true) autoSettleOptOutEnvironmentIds.add(id);
83 if (capabilities.threadPinning === true) pinningEnvironmentIds.add(id);
84 if (capabilities.threadPinReorder === true) pinReorderEnvironmentIds.add(id);
85 if (capabilities.threadActiveReorder === true) activeReorderEnvironmentIds.add(id);
86 if (capabilities.threadTitleRegeneration === true) titleRegenerationEnvironmentIds.add(id);
87 }
88 return {
89 providersByEnvironmentId,
90 machineByEnvironmentId,
91 settlementEnvironmentIds,
92 snoozeEnvironmentIds,
93 pinningEnvironmentIds,
94 autoSettleOptOutEnvironmentIds,
95 pinReorderEnvironmentIds,
96 activeReorderEnvironmentIds,
97 titleRegenerationEnvironmentIds,
98 };
99}
100
101/** Provider freshness and model catalogs do not affect the navigation lists. */
102export function createThreadListEnvironmentsAtom(
103 configsAtom: Atom.Atom<ReadonlyMap<EnvironmentId, ServerConfig>>,
104 capabilitiesAtom: Atom.Atom<ReadonlyMap<EnvironmentId, ExecutionEnvironmentCapabilities>>,
105) {
106 let previous = new Map<EnvironmentId, ListEnvironment>();
107 let result = collectEnvironments(previous);
108 return Atom.make((get) => {
109 const capabilities = get(capabilitiesAtom);
110 const configs = get(configsAtom);
111 const next = new Map<EnvironmentId, ListEnvironment>();
112 let changed = capabilities.size !== previous.size;
113 for (const [id, supported] of capabilities) {
114 const selected = selectEnvironment(configs.get(id), supported);
115 const prior = previous.get(id);
116 if (prior && sameProviders(prior.providers, selected.providers)) {
117 selected.providers = prior.providers;
118 }
119 const unchanged =
120 prior &&
121 prior.providers === selected.providers &&
122 prior.machineKind === selected.machineKind &&
123 capabilityKeys.every(
124 (key) => (prior.capabilities[key] === true) === (selected.capabilities[key] === true),
125 );
126 next.set(id, unchanged ? prior : selected);
127 if (!unchanged) changed = true;
128 }
129 if (changed) {
130 previous = next;
131 result = collectEnvironments(next);
132 }
133 return result;
134 }).pipe(Atom.withLabel("thread-list-environments"));
135}