Skip to content
Source
Sign inSign up

welcomePage.logic.ts

180 lines6.9 KB

1import {
2 isProviderAvailable,
3 managedCapabilityDenial,
4 type ManagedPolicySummary,
5 type ProviderInstanceConfig,
6 ProviderDriverKind,
7 type ProviderInstanceId,
8 resolveProjectProviderInstanceEnabled,
9 type ServerProvider,
10 type ServerSettings,
11} from "@t3tools/contracts";
12​
13import { getOnboardingProviderState } from "./providerReadiness.logic";
14​
15type ComputerSettings = Pick<ServerSettings, "providerInstances" | "projectSettingsOverrides">;
16​
17// Ready agents first, then ones that need a sign-in or a look. Never ranked by vendor.
18const FOUND_ORDER = { ready: 0, signIn: 1, attention: 2 } as const;
19type FoundState = keyof typeof FOUND_ORDER;
20​
21const isFoundState = (state: string): state is FoundState => state in FOUND_ORDER;
22​
23const ACP_REGISTRY_DRIVER = ProviderDriverKind.make("acpRegistry");
24​
25/**
26 * An agent's state for new threads on this computer. The server also lists agents that only some
27 * projects turn on, lists an agent it could not load as off, which Turn on cannot fix, and can pass
28 * a CLI whose version it knows is broken.
29 */
30export function welcomeAgentState(
31 provider: ServerProvider,
32 settings: ComputerSettings | null | undefined,
33) {
34 if (!isProviderAvailable(provider)) return "attention";
35 if (settings && !resolveProjectProviderInstanceEnabled(settings, null, provider.instanceId)) {
36 return "disabled";
37 }
38 const state = getOnboardingProviderState(provider);
39 if (state === "ready" && provider.compatibilityAdvisory?.status === "broken") return "attention";
40 // An ACP agent that is not prepared reports the same fields as one still being checked.
41 return state === "checking" && provider.driver === ACP_REGISTRY_DRIVER ? "attention" : state;
43​
44/**
45 * Splits a computer's allowed providers into agents already on it and agents the user can add.
46 * Order inside each group is by state, then by the name the user sees. Agents still being
47 * checked are listed with the ones to add, so a slow check never drops an agent.
48 */
49export function groupWelcomeAgents(
50 providers: ReadonlyArray<ServerProvider> | null | undefined,
51 settings: ComputerSettings | null | undefined,
52 nameOf: (provider: ServerProvider) => string,
53 isAllowed: (provider: ServerProvider) => boolean,
54) {
55 const found: Array<{ readonly provider: ServerProvider; readonly state: FoundState }> = [];
56 const available: ServerProvider[] = [];
57 let checking = providers == null;
58 let readyCount = 0;
59 for (const provider of providers ?? []) {
60 if (!isAllowed(provider)) continue;
61 const state = welcomeAgentState(provider, settings);
62 if (state === "ready") readyCount += 1;
63 if (state === "checking") checking = true;
64 if (isFoundState(state)) found.push({ provider, state });
65 else available.push(provider);
66 }
67 const byName = (a: ServerProvider, b: ServerProvider) => nameOf(a).localeCompare(nameOf(b));
68 found.sort(
69 (a, b) => FOUND_ORDER[a.state] - FOUND_ORDER[b.state] || byName(a.provider, b.provider),
70 );
71 available.sort(byName);
72 return { checking, readyCount, found: found.map((entry) => entry.provider), available };
74​
75/** Older settings can also keep `enabled: false` inside `config`, which still turns the agent off. */
76export function turnedOnInstance(instance: ProviderInstanceConfig): ProviderInstanceConfig {
77 const { config } = instance;
78 const configOff =
79 typeof config === "object" &&
80 config !== null &&
81 "enabled" in config &&
82 config.enabled === false;
83 return {
84 ...instance,
85 enabled: true,
86 ...(configOff ? { config: { ...config, enabled: true } } : {}),
87 };
89​
90/** A CLI the server knows is too old or otherwise broken for T3 Code; Settings updates it. */
91export function isBrokenVersion(provider: ServerProvider) {
92 return provider.installed && provider.compatibilityAdvisory?.status === "broken";
94​
95/**
96 * What "Use existing CLI" does next for Codex: wait for the CLI's own check, open its terminal,
97 * or stop. Only the ChatGPT sign-in runtime reports `setup.canAuthenticate`.
98 */
99export function cliTerminalStep(
100 provider: ServerProvider,
101 settings: ComputerSettings | null | undefined,
102) {
103 if (provider.setup?.canAuthenticate === true) return "wait";
104 const state = welcomeAgentState(provider, settings);
105 if (state === "checking") return "wait";
106 // A broken version is updated from its row, and an installed, signed-in CLI needs no terminal.
107 if (isBrokenVersion(provider)) return "done";
108 if (provider.installed && provider.auth.status === "authenticated") return "done";
109 // The terminal shows the command first, so it also helps a CLI whose check failed.
110 const fixable =
111 state === "install" ||
112 state === "signIn" ||
113 (state === "attention" && isProviderAvailable(provider));
114 return fixable ? "open" : "done";
116​
117/**
118 * Whether Codex's row opens its setup card, which offers ChatGPT sign-in or the CLI. A signed-in
119 * or broken Codex gets its plain row action instead, since the card has nothing left for it.
120 */
121export function codexNeedsSetup(
122 provider: ServerProvider,
123 settings: ComputerSettings | null | undefined,
124) {
125 if (
126 provider.driver !== "codex" ||
127 provider.auth.status === "authenticated" ||
128 isBrokenVersion(provider)
129 ) {
130 return false;
131 }
132 const state = welcomeAgentState(provider, settings);
133 return (
134 state === "install" ||
135 state === "signIn" ||
136 (state === "attention" && isProviderAvailable(provider))
137 );
139​
140/**
141 * The phone joins by signing in to T3 Connect, which shares the primary computer. Another
142 * computer, a build without T3 Connect, or a policy that turns it off gets no Phone section.
143 */
144export function welcomeOffersPhone(input: {
145 readonly isPrimary: boolean;
146 readonly cloud: boolean;
147 // Relay read and write access, which sharing this computer through T3 Connect needs.
148 readonly canShare: boolean;
149 readonly policy: ManagedPolicySummary | undefined;
150}) {
151 return (
152 input.isPrimary &&
153 input.cloud &&
154 input.canShare &&
155 managedCapabilityDenial(input.policy, "remoteClients") === undefined &&
156 managedCapabilityDenial(input.policy, "connect") === undefined
157 );
159​
160/**
161 * The Codex instance the setup card shows, or null when the card is closed. A rebuild briefly
162 * drops an instance from the list, so the card keeps its last status meanwhile instead of
163 * unmounting and losing a sign-in in progress.
164 */
165export function codexCardProvider(input: {
166 readonly providers: ReadonlyArray<ServerProvider> | null | undefined;
167 readonly instanceId: ProviderInstanceId | null;
168 readonly last: ServerProvider | null;
169 readonly settings: ComputerSettings | null | undefined;
170}) {
171 if (input.instanceId === null) return null;
172 const provider =
173 input.providers?.find((candidate) => candidate.instanceId === input.instanceId) ??
174 (input.last?.instanceId === input.instanceId ? input.last : undefined);
175 if (provider === undefined) return null;
176 const open =
177 codexNeedsSetup(provider, input.settings) ||
178 welcomeAgentState(provider, input.settings) === "checking";
179 return open ? provider : null;