From 2fbdfe8ca8b2ea5fdd7a5ca481eed68970ced624 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:57:29 -0700 Subject: [PATCH 1/2] fix(web): onboarding Continue no longer locks on computers that won't connect Continue required every selected computer to be connected, but the wizard auto-selects every saved computer, including switched-off, offline, failing, and unsupported ones, and hides saved T3 Connect entries the relay no longer reports. A single such computer disabled Continue with no way to deselect it. Continue now enables once a selected computer is connected and no first connection attempt is still settling; computers that will not connect on their own are skipped. Switched-off computers are no longer auto-selected. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../onboarding/WelcomeWizard.import.test.tsx | 2 +- .../WelcomeWizard.terminal.test.tsx | 2 + .../onboarding/WelcomeWizard.test.tsx | 1 + .../components/onboarding/WelcomeWizard.tsx | 25 +++++------ .../targetEnvironment.logic.test.ts | 43 +++++++++++++++++++ .../src/onboarding/targetEnvironment.logic.ts | 18 ++++++++ 6 files changed, 75 insertions(+), 16 deletions(-) diff --git a/apps/web/src/components/onboarding/WelcomeWizard.import.test.tsx b/apps/web/src/components/onboarding/WelcomeWizard.import.test.tsx index a9403887d9e8..8b052d76168a 100644 --- a/apps/web/src/components/onboarding/WelcomeWizard.import.test.tsx +++ b/apps/web/src/components/onboarding/WelcomeWizard.import.test.tsx @@ -64,7 +64,7 @@ vi.mock("../../state/environments", () => { environmentId, label: environmentId, connection: { phase: "connected" }, - entry: { target: { _tag: "DirectConnectionTarget" } }, + entry: { enabled: true, target: { _tag: "DirectConnectionTarget" } }, }); const primary = environment("primary"); return { diff --git a/apps/web/src/components/onboarding/WelcomeWizard.terminal.test.tsx b/apps/web/src/components/onboarding/WelcomeWizard.terminal.test.tsx index e59a837d9adf..cf7c38ec5875 100644 --- a/apps/web/src/components/onboarding/WelcomeWizard.terminal.test.tsx +++ b/apps/web/src/components/onboarding/WelcomeWizard.terminal.test.tsx @@ -151,6 +151,7 @@ const primaryEnvironment = { label: "This computer", connection: { phase: "connected" }, entry: { + enabled: true, target: new PrimaryConnectionTarget({ environmentId: primaryId, label: "This computer", @@ -164,6 +165,7 @@ const remoteEnvironment = { label: "Paired computer", connection: { phase: "connected" }, entry: { + enabled: true, target: new BearerConnectionTarget({ environmentId: remoteId, label: "Paired computer", diff --git a/apps/web/src/components/onboarding/WelcomeWizard.test.tsx b/apps/web/src/components/onboarding/WelcomeWizard.test.tsx index 9c357213c369..439a2cc579de 100644 --- a/apps/web/src/components/onboarding/WelcomeWizard.test.tsx +++ b/apps/web/src/components/onboarding/WelcomeWizard.test.tsx @@ -38,6 +38,7 @@ vi.mock("../../state/environments", () => { environmentId: "test-env", label: "Computer", connection: { phase: "connected" }, + entry: { enabled: true }, }; return { useEnvironments: () => ({ environments: [environment] }), diff --git a/apps/web/src/components/onboarding/WelcomeWizard.tsx b/apps/web/src/components/onboarding/WelcomeWizard.tsx index 9d1c96e19eed..ec541c4811c6 100644 --- a/apps/web/src/components/onboarding/WelcomeWizard.tsx +++ b/apps/web/src/components/onboarding/WelcomeWizard.tsx @@ -58,7 +58,10 @@ import { newProjectId, randomUUID } from "../../lib/utils"; import { agentSessionImport } from "../../state/agentSessions"; import { readProjects, useProjects } from "../../state/entities"; import { useEnvironments, usePrimaryEnvironment } from "../../state/environments"; -import { isOnboardingRelayEnvironment } from "../../onboarding/targetEnvironment.logic"; +import { + isOnboardingRelayEnvironment, + resolveOnboardingSetup, +} from "../../onboarding/targetEnvironment.logic"; import { useProjectScans } from "../../onboarding/useProjectScans"; import { projectEnvironment } from "../../state/projects"; import { serverEnvironment } from "../../state/server"; @@ -141,11 +144,14 @@ export function WelcomeWizard({ for (const environment of newComputers) { autoSelectedComputers.current.add(environment.environmentId); } + // A computer the user switched off stays unselected until they pick it. + const enabledComputers = newComputers.filter((environment) => environment.entry.enabled); + if (enabledComputers.length === 0) return; setSelection( (current) => new Set([ ...(current ?? []), - ...newComputers.map((environment) => environment.environmentId), + ...enabledComputers.map((environment) => environment.environmentId), ]), ); }, [environments]); @@ -262,11 +268,7 @@ export function WelcomeWizard({ }) } onContinue={() => - startSetup( - environments - .filter((environment) => selectedIds.has(environment.environmentId)) - .map((environment) => environment.environmentId), - ) + startSetup(resolveOnboardingSetup(environments, selectedIds).environmentIds) } onPaired={(environmentId) => { setSelection(new Set([...selectedIds, environmentId])); @@ -315,14 +317,7 @@ function ConnectionStep({ ); const [pairingOpen, setPairingOpen] = useState(expandPairingInitially); const [isPairing, setIsPairing] = useState(false); - const ready = - selectedIds.size > 0 && - [...selectedIds].every((id) => - environments.some( - (environment) => - environment.environmentId === id && environment.connection.phase === "connected", - ), - ); + const { ready } = resolveOnboardingSetup(environments, selectedIds); const continueRef = useRef(null); useEffect(() => { if ( diff --git a/apps/web/src/onboarding/targetEnvironment.logic.test.ts b/apps/web/src/onboarding/targetEnvironment.logic.test.ts index 9928f83b27db..0c62b70f29c0 100644 --- a/apps/web/src/onboarding/targetEnvironment.logic.test.ts +++ b/apps/web/src/onboarding/targetEnvironment.logic.test.ts @@ -9,6 +9,7 @@ import { describe, expect, it } from "vite-plus/test"; import { isOnboardingRelayEnvironment, + resolveOnboardingSetup, resolveOnboardingTargetEnvironment, } from "./targetEnvironment.logic"; @@ -209,3 +210,45 @@ describe("isOnboardingRelayEnvironment", () => { ).toEqual([olderRelay]); }); }); + +describe("resolveOnboardingSetup", () => { + const computer = (id: string, phase: string) => ({ + environmentId: EnvironmentId.make(id), + connection: { phase }, + }); + const local = computer("local", "connected"); + + it("skips selected computers that will not connect on their own", () => { + const environments = [ + local, + computer("switched-off", "available"), + computer("offline", "offline"), + computer("failing", "reconnecting"), + computer("old-client", "unsupported"), + ]; + expect( + resolveOnboardingSetup( + environments, + new Set(environments.map((environment) => environment.environmentId)), + ), + ).toEqual({ ready: true, environmentIds: [local.environmentId] }); + }); + + it("waits for a first connection attempt to settle", () => { + const connecting = computer("new", "connecting"); + expect( + resolveOnboardingSetup( + [local, connecting], + new Set([local.environmentId, connecting.environmentId]), + ).ready, + ).toBe(false); + }); + + it("is not ready without a connected selection", () => { + expect(resolveOnboardingSetup([local], new Set()).ready).toBe(false); + expect( + resolveOnboardingSetup([computer("off", "available")], new Set([EnvironmentId.make("off")])) + .ready, + ).toBe(false); + }); +}); diff --git a/apps/web/src/onboarding/targetEnvironment.logic.ts b/apps/web/src/onboarding/targetEnvironment.logic.ts index 665590ec990d..45c627a4b6a1 100644 --- a/apps/web/src/onboarding/targetEnvironment.logic.ts +++ b/apps/web/src/onboarding/targetEnvironment.logic.ts @@ -47,3 +47,21 @@ export function resolveOnboardingTargetEnvironment>, + selectedIds: ReadonlySet, +): { readonly ready: boolean; readonly environmentIds: ReadonlyArray } { + const selected = environments.filter((environment) => selectedIds.has(environment.environmentId)); + const environmentIds = selected + .filter((environment) => environment.connection.phase === "connected") + .map((environment) => environment.environmentId); + const settling = selected.some((environment) => environment.connection.phase === "connecting"); + return { ready: environmentIds.length > 0 && !settling, environmentIds }; +} From bc7fe986670e881cf85fcd65044e93cdab2fc32e Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:11:55 -0700 Subject: [PATCH 2/2] fix(web): onboarding names the selected computers setup will skip Co-Authored-By: Claude Opus 5.5 (1M context) --- .../components/onboarding/WelcomeWizard.tsx | 14 +++++++++++-- .../targetEnvironment.logic.test.ts | 6 +++++- .../src/onboarding/targetEnvironment.logic.ts | 20 ++++++++++++++----- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/apps/web/src/components/onboarding/WelcomeWizard.tsx b/apps/web/src/components/onboarding/WelcomeWizard.tsx index ec541c4811c6..e94078271799 100644 --- a/apps/web/src/components/onboarding/WelcomeWizard.tsx +++ b/apps/web/src/components/onboarding/WelcomeWizard.tsx @@ -81,6 +81,7 @@ import { readCodexSetupMode } from "../settings/CodexSetupSection.logic"; import { buildProviderInstanceUpdatePatch } from "../settings/SettingsPanels.logic"; import { TerminalViewport } from "../ThreadTerminalDrawer"; import { CloudEnvironmentConnectRows } from "../cloud/CloudEnvironmentConnectList"; +import { presentSavedCloudEnvironmentConnection } from "../cloud/cloudEnvironmentConnectionPresentation"; import { ProviderInstanceIcon } from "../chat/ProviderInstanceIcon"; import { T3Wordmark } from "../T3Wordmark"; import { Alert, AlertDescription } from "../ui/alert"; @@ -317,7 +318,10 @@ function ConnectionStep({ ); const [pairingOpen, setPairingOpen] = useState(expandPairingInitially); const [isPairing, setIsPairing] = useState(false); - const { ready } = resolveOnboardingSetup(environments, selectedIds); + const { ready, skippedIds } = resolveOnboardingSetup(environments, selectedIds); + const skippedLabels = environments + .filter((environment) => skippedIds.includes(environment.environmentId)) + .map((environment) => environment.label); const continueRef = useRef(null); useEffect(() => { if ( @@ -360,7 +364,7 @@ function ConnectionStep({ {environment.label} - {environment.connection.phase === "connected" ? "Connected" : "Connecting…"} + {presentSavedCloudEnvironmentConnection(environment.connection).buttonLabel} {environment.displayUrl ? ( @@ -416,6 +420,12 @@ function ConnectionStep({ + {skippedLabels.length > 0 ? ( +

+ Not connected, so setup skips {skippedLabels.join(", ")}. You can set{" "} + {skippedLabels.length === 1 ? "it" : "them"} up later from Settings. +

+ ) : null}

T3 Code collects anonymous usage data to help us improve it. To read more about how your diff --git a/apps/web/src/onboarding/targetEnvironment.logic.test.ts b/apps/web/src/onboarding/targetEnvironment.logic.test.ts index 0c62b70f29c0..c92f4cf4441c 100644 --- a/apps/web/src/onboarding/targetEnvironment.logic.test.ts +++ b/apps/web/src/onboarding/targetEnvironment.logic.test.ts @@ -231,7 +231,11 @@ describe("resolveOnboardingSetup", () => { environments, new Set(environments.map((environment) => environment.environmentId)), ), - ).toEqual({ ready: true, environmentIds: [local.environmentId] }); + ).toEqual({ + ready: true, + environmentIds: [local.environmentId], + skippedIds: environments.slice(1).map((environment) => environment.environmentId), + }); }); it("waits for a first connection attempt to settle", () => { diff --git a/apps/web/src/onboarding/targetEnvironment.logic.ts b/apps/web/src/onboarding/targetEnvironment.logic.ts index 45c627a4b6a1..d382ffe32f39 100644 --- a/apps/web/src/onboarding/targetEnvironment.logic.ts +++ b/apps/web/src/onboarding/targetEnvironment.logic.ts @@ -57,11 +57,21 @@ export function resolveOnboardingTargetEnvironment>, selectedIds: ReadonlySet, -): { readonly ready: boolean; readonly environmentIds: ReadonlyArray } { +): { + readonly ready: boolean; + readonly environmentIds: ReadonlyArray; + readonly skippedIds: ReadonlyArray; +} { const selected = environments.filter((environment) => selectedIds.has(environment.environmentId)); - const environmentIds = selected - .filter((environment) => environment.connection.phase === "connected") - .map((environment) => environment.environmentId); + const idsInPhase = (keep: (phase: string) => boolean) => + selected + .filter((environment) => keep(environment.connection.phase)) + .map((environment) => environment.environmentId); + const environmentIds = idsInPhase((phase) => phase === "connected"); const settling = selected.some((environment) => environment.connection.phase === "connecting"); - return { ready: environmentIds.length > 0 && !settling, environmentIds }; + return { + ready: environmentIds.length > 0 && !settling, + environmentIds, + skippedIds: idsInPhase((phase) => phase !== "connected" && phase !== "connecting"), + }; }