authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-10-07 10:21:24-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-07 10:21:24-07:00
logdb46c42744874ca123bc170a90c1faa312f20119
tree6396142c4db1337046781a50dd07af9ce0e253bc
parent87c366b1206b13701fbcc38e459d457f0f9da2df
signature Signed by PGP key B5690EEEBB952194

fix(web): onboarding Continue no longer locks on computers that won't connect (#16887)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

6 files changed, 100 insertions(+), 17 deletions(-)

apps/web/src/components/onboarding/WelcomeWizard.import.test.tsx+1-1
......@@ -64,7 +64,7 @@ vi.mock("../../state/environments", () => {
6464 environmentId,
6565 label: environmentId,
6666 connection: { phase: "connected" },
67 entry: { target: { _tag: "DirectConnectionTarget" } },
67 entry: { enabled: true, target: { _tag: "DirectConnectionTarget" } },
6868 });
6969 const primary = environment("primary");
7070 return {
apps/web/src/components/onboarding/WelcomeWizard.terminal.test.tsx+2
......@@ -151,6 +151,7 @@ const primaryEnvironment = {
151151 label: "This computer",
152152 connection: { phase: "connected" },
153153 entry: {
154 enabled: true,
154155 target: new PrimaryConnectionTarget({
155156 environmentId: primaryId,
156157 label: "This computer",
......@@ -164,6 +165,7 @@ const remoteEnvironment = {
164165 label: "Paired computer",
165166 connection: { phase: "connected" },
166167 entry: {
168 enabled: true,
167169 target: new BearerConnectionTarget({
168170 environmentId: remoteId,
169171 label: "Paired computer",
apps/web/src/components/onboarding/WelcomeWizard.test.tsx+1
......@@ -38,6 +38,7 @@ vi.mock("../../state/environments", () => {
3838 environmentId: "test-env",
3939 label: "Computer",
4040 connection: { phase: "connected" },
41 entry: { enabled: true },
4142 };
4243 return {
4344 useEnvironments: () => ({ environments: [environment] }),
apps/web/src/components/onboarding/WelcomeWizard.tsx+21-16
......@@ -58,7 +58,10 @@ import { newProjectId, randomUUID } from "../../lib/utils";
5858import { agentSessionImport } from "../../state/agentSessions";
5959import { readProjects, useProjects } from "../../state/entities";
6060import { useEnvironments, usePrimaryEnvironment } from "../../state/environments";
61import { isOnboardingRelayEnvironment } from "../../onboarding/targetEnvironment.logic";
61import {
62 isOnboardingRelayEnvironment,
63 resolveOnboardingSetup,
64} from "../../onboarding/targetEnvironment.logic";
6265import { useProjectScans } from "../../onboarding/useProjectScans";
6366import { projectEnvironment } from "../../state/projects";
6467import { serverEnvironment } from "../../state/server";
......@@ -78,6 +81,7 @@ import { readCodexSetupMode } from "../settings/CodexSetupSection.logic";
7881import { buildProviderInstanceUpdatePatch } from "../settings/SettingsPanels.logic";
7982import { TerminalViewport } from "../ThreadTerminalDrawer";
8083import { CloudEnvironmentConnectRows } from "../cloud/CloudEnvironmentConnectList";
84import { presentSavedCloudEnvironmentConnection } from "../cloud/cloudEnvironmentConnectionPresentation";
8185import { ProviderInstanceIcon } from "../chat/ProviderInstanceIcon";
8286import { T3Wordmark } from "../T3Wordmark";
8387import { Alert, AlertDescription } from "../ui/alert";
......@@ -141,11 +145,14 @@ export function WelcomeWizard({
141145 for (const environment of newComputers) {
142146 autoSelectedComputers.current.add(environment.environmentId);
143147 }
148 // A computer the user switched off stays unselected until they pick it.
149 const enabledComputers = newComputers.filter((environment) => environment.entry.enabled);
150 if (enabledComputers.length === 0) return;
144151 setSelection(
145152 (current) =>
146153 new Set([
147154 ...(current ?? []),
148 ...newComputers.map((environment) => environment.environmentId),
155 ...enabledComputers.map((environment) => environment.environmentId),
149156 ]),
150157 );
151158 }, [environments]);
......@@ -262,11 +269,7 @@ export function WelcomeWizard({
262269 })
263270 }
264271 onContinue={() =>
265 startSetup(
266 environments
267 .filter((environment) => selectedIds.has(environment.environmentId))
268 .map((environment) => environment.environmentId),
269 )
272 startSetup(resolveOnboardingSetup(environments, selectedIds).environmentIds)
270273 }
271274 onPaired={(environmentId) => {
272275 setSelection(new Set([...selectedIds, environmentId]));
......@@ -315,14 +318,10 @@ function ConnectionStep({
315318 );
316319 const [pairingOpen, setPairingOpen] = useState(expandPairingInitially);
317320 const [isPairing, setIsPairing] = useState(false);
318 const ready =
319 selectedIds.size > 0 &&
320 [...selectedIds].every((id) =>
321 environments.some(
322 (environment) =>
323 environment.environmentId === id && environment.connection.phase === "connected",
324 ),
325 );
321 const { ready, skippedIds } = resolveOnboardingSetup(environments, selectedIds);
322 const skippedLabels = environments
323 .filter((environment) => skippedIds.includes(environment.environmentId))
324 .map((environment) => environment.label);
326325 const continueRef = useRef<HTMLButtonElement>(null);
327326 useEffect(() => {
328327 if (
......@@ -365,7 +364,7 @@ function ConnectionStep({
365364 {environment.label}
366365 </span>
367366 <span className="shrink-0 text-xs text-muted-foreground">
368 {environment.connection.phase === "connected" ? "Connected" : "Connecting…"}
367 {presentSavedCloudEnvironmentConnection(environment.connection).buttonLabel}
369368 </span>
370369 </span>
371370 {environment.displayUrl ? (
......@@ -421,6 +420,12 @@ function ConnectionStep({
421420 </Collapsible>
422421 </div>
423422 </div>
423 {skippedLabels.length > 0 ? (
424 <p className="mt-4 text-xs text-muted-foreground">
425 Not connected, so setup skips {skippedLabels.join(", ")}. You can set{" "}
426 {skippedLabels.length === 1 ? "it" : "them"} up later from Settings.
427 </p>
428 ) : null}
424429 <div className="mt-6 flex flex-col-reverse gap-4 sm:flex-row sm:items-center sm:justify-between sm:gap-6">
425430 <p className="min-w-0 text-xs leading-relaxed text-muted-foreground">
426431 T3 Code collects anonymous usage data to help us improve it. To read more about how your
apps/web/src/onboarding/targetEnvironment.logic.test.ts+47
......@@ -9,6 +9,7 @@ import { describe, expect, it } from "vite-plus/test";
99
1010import {
1111 isOnboardingRelayEnvironment,
12 resolveOnboardingSetup,
1213 resolveOnboardingTargetEnvironment,
1314} from "./targetEnvironment.logic";
1415
......@@ -209,3 +210,49 @@ describe("isOnboardingRelayEnvironment", () => {
209210 ).toEqual([olderRelay]);
210211 });
211212});
213
214describe("resolveOnboardingSetup", () => {
215 const computer = (id: string, phase: string) => ({
216 environmentId: EnvironmentId.make(id),
217 connection: { phase },
218 });
219 const local = computer("local", "connected");
220
221 it("skips selected computers that will not connect on their own", () => {
222 const environments = [
223 local,
224 computer("switched-off", "available"),
225 computer("offline", "offline"),
226 computer("failing", "reconnecting"),
227 computer("old-client", "unsupported"),
228 ];
229 expect(
230 resolveOnboardingSetup(
231 environments,
232 new Set(environments.map((environment) => environment.environmentId)),
233 ),
234 ).toEqual({
235 ready: true,
236 environmentIds: [local.environmentId],
237 skippedIds: environments.slice(1).map((environment) => environment.environmentId),
238 });
239 });
240
241 it("waits for a first connection attempt to settle", () => {
242 const connecting = computer("new", "connecting");
243 expect(
244 resolveOnboardingSetup(
245 [local, connecting],
246 new Set([local.environmentId, connecting.environmentId]),
247 ).ready,
248 ).toBe(false);
249 });
250
251 it("is not ready without a connected selection", () => {
252 expect(resolveOnboardingSetup([local], new Set()).ready).toBe(false);
253 expect(
254 resolveOnboardingSetup([computer("off", "available")], new Set([EnvironmentId.make("off")]))
255 .ready,
256 ).toBe(false);
257 });
258});
apps/web/src/onboarding/targetEnvironment.logic.ts+28
......@@ -47,3 +47,31 @@ export function resolveOnboardingTargetEnvironment<TEnvironment extends Onboardi
4747
4848 return mode === "local" ? null : (connectedRelayEnvironments[0] ?? null);
4949}
50
51/**
52 * The computers the wizard sets up from the user's selection. Continue waits
53 * only on a first connection attempt, which settles on its own. Selected
54 * computers that are switched off, offline, failing, or unsupported are skipped
55 * so they can never lock the user out of onboarding.
56 */
57export function resolveOnboardingSetup(
58 environments: ReadonlyArray<Pick<OnboardingEnvironment, "environmentId" | "connection">>,
59 selectedIds: ReadonlySet<EnvironmentId>,
60): {
61 readonly ready: boolean;
62 readonly environmentIds: ReadonlyArray<EnvironmentId>;
63 readonly skippedIds: ReadonlyArray<EnvironmentId>;
64} {
65 const selected = environments.filter((environment) => selectedIds.has(environment.environmentId));
66 const idsInPhase = (keep: (phase: string) => boolean) =>
67 selected
68 .filter((environment) => keep(environment.connection.phase))
69 .map((environment) => environment.environmentId);
70 const environmentIds = idsInPhase((phase) => phase === "connected");
71 const settling = selected.some((environment) => environment.connection.phase === "connecting");
72 return {
73 ready: environmentIds.length > 0 && !settling,
74 environmentIds,
75 skippedIds: idsInPhase((phase) => phase !== "connected" && phase !== "connecting"),
76 };
77}