import { useAuth } from "@clerk/react";import { useAtomValue } from "@effect/atom-react";import type { AgentSessionProjectCandidate, EnvironmentId, ProjectId, ScopedProjectRef, ServerConfig, ServerProvider,} from "@t3tools/contracts";import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime/environment";import { isAtomCommandInterrupted, squashAtomCommandFailure,} from "@t3tools/client-runtime/state/runtime";import { CommandId, ProviderDriverKind, ThreadId } from "@t3tools/contracts";import * as Schema from "effect/Schema";import { ArrowRightIcon, CheckIcon, ChevronRightIcon, CloudIcon, CopyIcon, LinkIcon, MonitorIcon, TerminalIcon,} from "lucide-react";import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { TYPOGRAPHY_ADVANCED_STORAGE_KEY } from "../../appearanceFonts";import { useLocalStorage } from "../../hooks/useLocalStorage";import { hasCloudPublicConfig } from "../../cloud/publicConfig";import { useT3ConnectAuthPrompt } from "../clerk/useT3ConnectAuthPrompt";import { useCompleteOnboarding } from "../../onboarding/firstRun";import { groupOnboardingProjects, partitionOnboardingProjects, onboardingProjectKey, resolveOnboardingLandingProject, resolveOnboardingProjectId, type OnboardingProjectGroup,} from "../../onboarding/projectImport.logic";import { getOnboardingProviderState, resolveOnboardingProviderInstallCommand, resolveOnboardingProviderLoginCommand, selectOnboardingProvidersByDriver,} from "../../onboarding/providerReadiness.logic";import { useCopyToClipboard } from "../../hooks/useCopyToClipboard";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 { useProjectScans } from "../../onboarding/useProjectScans";import { projectEnvironment } from "../../state/projects";import { serverEnvironment } from "../../state/server";import { terminalEnvironment } from "../../state/terminal";import { useAtomCommand } from "../../state/use-atom-command";import { connectPairing } from "../../connection/onboarding";import { getProviderSummary } from "../settings/providerStatus";import { getDriverOption } from "../settings/providerDriverMeta";import { TerminalViewport } from "../ThreadTerminalDrawer";import { CloudEnvironmentConnectRows } from "../cloud/CloudEnvironmentConnectList";import { ClaudeAI, OpenAI } from "../Icons";import { T3Wordmark } from "../T3Wordmark";import { Button } from "../ui/button";import { Checkbox } from "../ui/checkbox";import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "../ui/collapsible";import { Input } from "../ui/input";import { Tooltip, TooltipTrigger, TooltipPopup } from "../ui/tooltip";import { ScrollArea } from "../ui/scroll-area";import { Spinner } from "../ui/spinner";import { WizardPanel, WizardSteps, WizardPopup, WizardHeader } from "../ui/wizard";import { Dialog } from "../ui/dialog";import { toastManager } from "../ui/toast";import { cn } from "../../lib/utils";import { formatRelativeTime } from "../../timestampFormat"; /** * First-run welcome wizard. Rendered over the workspace at `/welcome` on a * fresh install (no completed-onboarding flag, empty workspace). Flow per the * onboarding overhaul spec: connection choice → sign-in/pair (remote paths) → * agent setup with inline install terminal → project import → main screen. * Every step past the connection gate is skippable; the whole wizard is * re-runnable by clearing the flag. */ type WizardStep = "connection" | "agents" | "import";const NO_ENVIRONMENTS: readonly EnvironmentId[] = []; const AGENT_ONBOARDING_THREAD_ID = ThreadId.make("onboarding-agent-setup");const ONBOARDING_STAGES = ["Connect", "Agents", "Projects"] as const;const SCAN_LIMIT_MESSAGE = "Scan limit reached. Some projects or conversations may be missing."; export function WelcomeWizard({ localAvailable, onDone,}: { /** Whether this client is authenticated to the server serving the app. */ readonly localAvailable: boolean; readonly onDone: (projectRef?: ScopedProjectRef) => void;}) { const completeOnboarding = useCompleteOnboarding(); const [step, setStep] = useState<WizardStep>("connection"); const { environments } = useEnvironments(); const [selection, setSelection] = useState<ReadonlySet<EnvironmentId> | null>(null); const autoSelectedComputers = useRef(new Set<EnvironmentId>()); const [setupIds, setSetupIds] = useState<readonly EnvironmentId[]>([]); const [isImporting, setIsImporting] = useState(false); const finishingPromiseRef = useRef<Promise<boolean> | null>(null); const completionErrorToastIdRef = useRef<ReturnType<typeof toastManager.add> | null>(null); const primaryEnvironment = usePrimaryEnvironment(); useEffect(() => { const newComputers = environments.filter( (environment) => !autoSelectedComputers.current.has(environment.environmentId), ); if (newComputers.length === 0) return; for (const environment of newComputers) { autoSelectedComputers.current.add(environment.environmentId); } setSelection( (current) => new Set([ ...(current ?? []), ...newComputers.map((environment) => environment.environmentId), ]), ); }, [environments]); const selectedIds = selection ?? new Set(primaryEnvironment ? [primaryEnvironment.environmentId] : []); const scans = useProjectScans(step === "import" ? setupIds : NO_ENVIRONMENTS); const isLoadingProjects = step === "import" && scans.every((scan) => scan.data === null) && scans.some((scan) => scan.isPending); const startSetup = (ids: readonly EnvironmentId[]) => { if (ids.length === 0) return; setSetupIds(ids); setStep("agents"); }; const stageIndex = step === "agents" ? 1 : step === "import" ? 2 : 0; const finish = useCallback( (projectRef?: ScopedProjectRef) => { if (finishingPromiseRef.current !== null) return finishingPromiseRef.current; if (completionErrorToastIdRef.current !== null) { toastManager.close(completionErrorToastIdRef.current); completionErrorToastIdRef.current = null; } const completion = completeOnboarding() .then(() => { if (completionErrorToastIdRef.current !== null) { toastManager.close(completionErrorToastIdRef.current); completionErrorToastIdRef.current = null; } onDone(projectRef); return true; }) .catch(() => { const errorToast = { type: "error", title: "Could not finish setup", description: "Your settings could not be saved. Try again.", } as const; if (completionErrorToastIdRef.current === null) { completionErrorToastIdRef.current = toastManager.add(errorToast); } else { toastManager.update(completionErrorToastIdRef.current, errorToast); } return false; }) .finally(() => { if (finishingPromiseRef.current === completion) { finishingPromiseRef.current = null; } }); finishingPromiseRef.current = completion; return completion; }, [completeOnboarding, onDone], ); return ( <Dialog open disablePointerDismissal onOpenChange={(_, event) => event.cancel()}> <WizardPopup bottomStickOnMobile={false} showCloseButton={false} initialFocus={() => document.getElementById("onboarding-pairing-url") ?? true} > <WizardHeader title="Set up T3 Code" identity={ <div className="flex items-baseline gap-1.5" role="img" aria-label="T3 Code"> <T3Wordmark className="h-4 w-auto shrink-0" aria-hidden /> <span className="text-[1.4rem] font-medium tracking-tight text-muted-foreground"> Code </span> </div> } > <WizardSteps steps={ONBOARDING_STAGES} currentStep={stageIndex} isStepDisabled={(index) => isImporting || index >= stageIndex} onStepChange={(index) => { if (isImporting || index > stageIndex) return; setStep(index === 0 ? "connection" : "agents"); }} /> </WizardHeader> <WizardPanel holdHeight={isLoadingProjects}> {step === "connection" ? ( <ConnectionStep expandPairingInitially={!localAvailable && !hasCloudPublicConfig()} selectedIds={selectedIds} autoSelectedComputers={autoSelectedComputers.current} onSelectionChange={setSelection} onToggleEnvironment={(environmentId, checked) => setSelection((current) => { const next = new Set(current ?? selectedIds); if (checked) next.add(environmentId); else next.delete(environmentId); return next; }) } onContinue={() => startSetup( environments .filter((environment) => selectedIds.has(environment.environmentId)) .map((environment) => environment.environmentId), ) } onPaired={(environmentId) => { setSelection(new Set([...selectedIds, environmentId])); }} /> ) : step === "agents" ? ( <AgentsStep environmentIds={setupIds} onContinue={() => setStep("import")} /> ) : ( <ImportStep scans={scans} isImporting={isImporting} setIsImporting={setIsImporting} onDone={finish} /> )} </WizardPanel> </WizardPopup> </Dialog> );} // ── Step 1: connection choice ──────────────────────────────── function ConnectionStep({ autoSelectedComputers, expandPairingInitially, selectedIds, onSelectionChange, onToggleEnvironment, onContinue, onPaired,}: { readonly autoSelectedComputers: Set<EnvironmentId>; readonly expandPairingInitially: boolean; readonly selectedIds: ReadonlySet<EnvironmentId>; readonly onSelectionChange: (ids: ReadonlySet<EnvironmentId>) => void; readonly onToggleEnvironment: (environmentId: EnvironmentId, checked: boolean) => void; readonly onContinue: () => void; readonly onPaired: (environmentId: EnvironmentId) => void;}) { const { environments } = useEnvironments(); const cloudEnabled = hasCloudPublicConfig(); const directEnvironments = environments.filter( (environment) => !cloudEnabled || !isOnboardingRelayEnvironment(environment), ); 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 continueRef = useRef<HTMLButtonElement>(null); useEffect(() => { if ( ready && (document.activeElement === document.body || document.activeElement?.getAttribute("role") === "dialog") ) { continueRef.current?.focus(); } }, [ready]); return ( <> <h1 className="text-2xl font-semibold tracking-tight text-foreground"> Connect your computers </h1> <p className="mt-2.5 text-sm leading-relaxed text-muted-foreground"> Choose one or more computers. We’ll set up agents and projects on each. </p> {directEnvironments.length > 0 ? ( <fieldset className="mt-5 space-y-2"> <legend className="sr-only">Computers to set up</legend> {directEnvironments.map((environment) => ( <label key={environment.environmentId} className="flex cursor-pointer items-center gap-3 rounded-lg border border-border bg-background px-3 py-3" > <Checkbox checked={selectedIds.has(environment.environmentId)} onCheckedChange={(checked) => { const next = new Set(selectedIds); if (checked) next.add(environment.environmentId); else next.delete(environment.environmentId); onSelectionChange(next); }} /> <MonitorIcon className="size-4 shrink-0 text-muted-foreground" /> <span className="min-w-0 flex-1"> <span className="flex items-baseline justify-between gap-3"> <span className="min-w-0 text-sm font-medium break-words"> {environment.label} </span> <span className="shrink-0 text-xs text-muted-foreground"> {environment.connection.phase === "connected" ? "Connected" : "Connecting…"} </span> </span> {environment.displayUrl ? ( <span className="mt-0.5 block text-xs break-all text-muted-foreground"> {environment.displayUrl} </span> ) : null} </span> </label> ))} </fieldset> ) : null} <div className="mt-4 space-y-2"> {cloudEnabled ? ( <ConnectAccountOption autoSelectedComputers={autoSelectedComputers} disabled={isPairing} selectedIds={selectedIds} onToggleEnvironment={onToggleEnvironment} /> ) : null} <Collapsible open={pairingOpen} onOpenChange={setPairingOpen} className="rounded-lg border border-border bg-background" > <CollapsibleTrigger disabled={isPairing} render={ <Button variant="ghost" className="h-auto min-h-14 w-full justify-start gap-3 px-3 py-3 text-left whitespace-normal sm:h-auto" /> } > <LinkIcon className="size-4 text-muted-foreground" /> <span className="flex-1">Add a computer</span> <ChevronRightIcon className={cn("size-4 text-muted-foreground", pairingOpen && "rotate-90")} /> </CollapsibleTrigger> <CollapsiblePanel> <div className="px-3 pb-3"> <PairingForm isPairing={isPairing} setIsPairing={setIsPairing} onPaired={(environmentId) => { setPairingOpen(false); onPaired(environmentId); requestAnimationFrame(() => continueRef.current?.focus()); }} /> </div> </CollapsiblePanel> </Collapsible> </div> <div className="mt-6 flex items-center justify-end gap-3"> <Button ref={continueRef} autoFocus={!expandPairingInitially} disabled={!ready || isPairing} onClick={onContinue} > Continue <ArrowRightIcon className="size-3.5" /> </Button> </div> </> );} function ConnectAccountOption({ autoSelectedComputers, disabled, selectedIds, onToggleEnvironment,}: { readonly autoSelectedComputers: Set<EnvironmentId>; readonly disabled: boolean; readonly selectedIds: ReadonlySet<EnvironmentId>; readonly onToggleEnvironment: (environmentId: EnvironmentId, checked: boolean) => void;}) { const { environments } = useEnvironments(); const { isLoaded, isSignedIn } = useAuth({ treatPendingAsSignedOut: false }); const { openAuthPrompt } = useT3ConnectAuthPrompt(); const [expanded, setExpanded] = useState(true); const [discoveryReady, setDiscoveryReady] = useState(false); const onDiscoveryReady = useCallback(() => setDiscoveryReady(true), []); return ( <Collapsible open={expanded && !!isSignedIn && discoveryReady} onOpenChange={setExpanded} className="rounded-lg border border-border bg-background" > <CollapsibleTrigger disabled={disabled || !isLoaded} onClick={(event) => { if (!isSignedIn) { event.preventDefault(); setExpanded(true); openAuthPrompt(); } }} render={ <Button variant="ghost" className="h-auto min-h-14 w-full justify-start gap-3 px-3 py-3 text-left whitespace-normal sm:h-auto" /> } > <CloudIcon className="size-4 text-muted-foreground" /> <span className="flex-1">T3 Connect</span> <span className="text-xs text-muted-foreground"> {!isLoaded ? "Loading sign-in…" : !isSignedIn ? "Sign in" : !discoveryReady ? "Loading computers…" : null} </span> <ChevronRightIcon className={cn("size-4 text-muted-foreground", expanded && isSignedIn && "rotate-90")} /> </CollapsibleTrigger> <CollapsiblePanel keepMounted> <div className="px-3 pb-3"> <div className="mb-3 space-y-1.5"> {isSignedIn ? ( <CloudEnvironmentConnectRows primaryEnvironmentId={null} savedEnvironments={environments} showSavedEnvironments onDiscoveryReady={onDiscoveryReady} selection={{ selectedIds, onChange: onToggleEnvironment, autoSelectedComputers }} refreshWhileEmpty empty={ <p className="py-3 text-sm text-muted-foreground">No computers linked yet.</p> } /> ) : null} </div> <p className="text-sm text-muted-foreground"> Run this on each computer you want to connect. </p> <CommandBlock command="npx t3 connect" className="mt-3" /> <p className="mt-3 text-xs text-muted-foreground"> Keep T3 Code running. Select the computers you want to set up above. </p> </div> </CollapsiblePanel> </Collapsible> );} // ── Step 2′: Direct pairing ────────────────────────────────── /** * Register a computer in this browser using a server-minted pairing link. */function PairingForm({ isPairing, setIsPairing, onPaired,}: { readonly isPairing: boolean; readonly setIsPairing: (value: boolean) => void; readonly onPaired: (environmentId: EnvironmentId) => void;Lines 1–500 of 1563. Load more
import { useAuth } from "@clerk/react"; import { useAtomValue } from "@effect/atom-react"; import type { AgentSessionProjectCandidate, EnvironmentId, ProjectId, ScopedProjectRef, ServerConfig, ServerProvider, } from "@t3tools/contracts"; import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime/environment"; import { isAtomCommandInterrupted, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; import { CommandId, ProviderDriverKind, ThreadId } from "@t3tools/contracts"; import * as Schema from "effect/Schema"; import { ArrowRightIcon, CheckIcon, ChevronRightIcon, CloudIcon, CopyIcon, LinkIcon, MonitorIcon, TerminalIcon, } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { TYPOGRAPHY_ADVANCED_STORAGE_KEY } from "../../appearanceFonts"; import { useLocalStorage } from "../../hooks/useLocalStorage"; import { hasCloudPublicConfig } from "../../cloud/publicConfig"; import { useT3ConnectAuthPrompt } from "../clerk/useT3ConnectAuthPrompt"; import { useCompleteOnboarding } from "../../onboarding/firstRun"; import { groupOnboardingProjects, partitionOnboardingProjects, onboardingProjectKey, resolveOnboardingLandingProject, resolveOnboardingProjectId, type OnboardingProjectGroup, } from "../../onboarding/projectImport.logic"; import { getOnboardingProviderState, resolveOnboardingProviderInstallCommand, resolveOnboardingProviderLoginCommand, selectOnboardingProvidersByDriver, } from "../../onboarding/providerReadiness.logic"; import { useCopyToClipboard } from "../../hooks/useCopyToClipboard"; 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 { useProjectScans } from "../../onboarding/useProjectScans"; import { projectEnvironment } from "../../state/projects"; import { serverEnvironment } from "../../state/server"; import { terminalEnvironment } from "../../state/terminal"; import { useAtomCommand } from "../../state/use-atom-command"; import { connectPairing } from "../../connection/onboarding"; import { getProviderSummary } from "../settings/providerStatus"; import { getDriverOption } from "../settings/providerDriverMeta"; import { TerminalViewport } from "../ThreadTerminalDrawer"; import { CloudEnvironmentConnectRows } from "../cloud/CloudEnvironmentConnectList"; import { ClaudeAI, OpenAI } from "../Icons"; import { T3Wordmark } from "../T3Wordmark"; import { Button } from "../ui/button"; import { Checkbox } from "../ui/checkbox"; import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "../ui/collapsible"; import { Input } from "../ui/input"; import { Tooltip, TooltipTrigger, TooltipPopup } from "../ui/tooltip"; import { ScrollArea } from "../ui/scroll-area"; import { Spinner } from "../ui/spinner"; import { WizardPanel, WizardSteps, WizardPopup, WizardHeader } from "../ui/wizard"; import { Dialog } from "../ui/dialog"; import { toastManager } from "../ui/toast"; import { cn } from "../../lib/utils"; import { formatRelativeTime } from "../../timestampFormat"; /** * First-run welcome wizard. Rendered over the workspace at `/welcome` on a * fresh install (no completed-onboarding flag, empty workspace). Flow per the * onboarding overhaul spec: connection choice → sign-in/pair (remote paths) → * agent setup with inline install terminal → project import → main screen. * Every step past the connection gate is skippable; the whole wizard is * re-runnable by clearing the flag. */ type WizardStep = "connection" | "agents" | "import"; const NO_ENVIRONMENTS: readonly EnvironmentId[] = []; const AGENT_ONBOARDING_THREAD_ID = ThreadId.make("onboarding-agent-setup"); const ONBOARDING_STAGES = ["Connect", "Agents", "Projects"] as const; const SCAN_LIMIT_MESSAGE = "Scan limit reached. Some projects or conversations may be missing."; export function WelcomeWizard({ localAvailable, onDone, }: { /** Whether this client is authenticated to the server serving the app. */ readonly localAvailable: boolean; readonly onDone: (projectRef?: ScopedProjectRef) => void; }) { const completeOnboarding = useCompleteOnboarding(); const [step, setStep] = useState<WizardStep>("connection"); const { environments } = useEnvironments(); const [selection, setSelection] = useState<ReadonlySet<EnvironmentId> | null>(null); const autoSelectedComputers = useRef(new Set<EnvironmentId>()); const [setupIds, setSetupIds] = useState<readonly EnvironmentId[]>([]); const [isImporting, setIsImporting] = useState(false); const finishingPromiseRef = useRef<Promise<boolean> | null>(null); const completionErrorToastIdRef = useRef<ReturnType<typeof toastManager.add> | null>(null); const primaryEnvironment = usePrimaryEnvironment(); useEffect(() => { const newComputers = environments.filter( (environment) => !autoSelectedComputers.current.has(environment.environmentId), ); if (newComputers.length === 0) return; for (const environment of newComputers) { autoSelectedComputers.current.add(environment.environmentId); } setSelection( (current) => new Set([ ...(current ?? []), ...newComputers.map((environment) => environment.environmentId), ]), ); }, [environments]); const selectedIds = selection ?? new Set(primaryEnvironment ? [primaryEnvironment.environmentId] : []); const scans = useProjectScans(step === "import" ? setupIds : NO_ENVIRONMENTS); const isLoadingProjects = step === "import" && scans.every((scan) => scan.data === null) && scans.some((scan) => scan.isPending); const startSetup = (ids: readonly EnvironmentId[]) => { if (ids.length === 0) return; setSetupIds(ids); setStep("agents"); }; const stageIndex = step === "agents" ? 1 : step === "import" ? 2 : 0; const finish = useCallback( (projectRef?: ScopedProjectRef) => { if (finishingPromiseRef.current !== null) return finishingPromiseRef.current; if (completionErrorToastIdRef.current !== null) { toastManager.close(completionErrorToastIdRef.current); completionErrorToastIdRef.current = null; } const completion = completeOnboarding() .then(() => { if (completionErrorToastIdRef.current !== null) { toastManager.close(completionErrorToastIdRef.current); completionErrorToastIdRef.current = null; } onDone(projectRef); return true; }) .catch(() => { const errorToast = { type: "error", title: "Could not finish setup", description: "Your settings could not be saved. Try again.", } as const; if (completionErrorToastIdRef.current === null) { completionErrorToastIdRef.current = toastManager.add(errorToast); } else { toastManager.update(completionErrorToastIdRef.current, errorToast); } return false; }) .finally(() => { if (finishingPromiseRef.current === completion) { finishingPromiseRef.current = null; } }); finishingPromiseRef.current = completion; return completion; }, [completeOnboarding, onDone], ); return ( <Dialog open disablePointerDismissal onOpenChange={(_, event) => event.cancel()}> <WizardPopup bottomStickOnMobile={false} showCloseButton={false} initialFocus={() => document.getElementById("onboarding-pairing-url") ?? true} > <WizardHeader title="Set up T3 Code" identity={ <div className="flex items-baseline gap-1.5" role="img" aria-label="T3 Code"> <T3Wordmark className="h-4 w-auto shrink-0" aria-hidden /> <span className="text-[1.4rem] font-medium tracking-tight text-muted-foreground"> Code </span> </div> } > <WizardSteps steps={ONBOARDING_STAGES} currentStep={stageIndex} isStepDisabled={(index) => isImporting || index >= stageIndex} onStepChange={(index) => { if (isImporting || index > stageIndex) return; setStep(index === 0 ? "connection" : "agents"); }} /> </WizardHeader> <WizardPanel holdHeight={isLoadingProjects}> {step === "connection" ? ( <ConnectionStep expandPairingInitially={!localAvailable && !hasCloudPublicConfig()} selectedIds={selectedIds} autoSelectedComputers={autoSelectedComputers.current} onSelectionChange={setSelection} onToggleEnvironment={(environmentId, checked) => setSelection((current) => { const next = new Set(current ?? selectedIds); if (checked) next.add(environmentId); else next.delete(environmentId); return next; }) } onContinue={() => startSetup( environments .filter((environment) => selectedIds.has(environment.environmentId)) .map((environment) => environment.environmentId), ) } onPaired={(environmentId) => { setSelection(new Set([...selectedIds, environmentId])); }} /> ) : step === "agents" ? ( <AgentsStep environmentIds={setupIds} onContinue={() => setStep("import")} /> ) : ( <ImportStep scans={scans} isImporting={isImporting} setIsImporting={setIsImporting} onDone={finish} /> )} </WizardPanel> </WizardPopup> </Dialog> ); } // ── Step 1: connection choice ──────────────────────────────── function ConnectionStep({ autoSelectedComputers, expandPairingInitially, selectedIds, onSelectionChange, onToggleEnvironment, onContinue, onPaired, }: { readonly autoSelectedComputers: Set<EnvironmentId>; readonly expandPairingInitially: boolean; readonly selectedIds: ReadonlySet<EnvironmentId>; readonly onSelectionChange: (ids: ReadonlySet<EnvironmentId>) => void; readonly onToggleEnvironment: (environmentId: EnvironmentId, checked: boolean) => void; readonly onContinue: () => void; readonly onPaired: (environmentId: EnvironmentId) => void; }) { const { environments } = useEnvironments(); const cloudEnabled = hasCloudPublicConfig(); const directEnvironments = environments.filter( (environment) => !cloudEnabled || !isOnboardingRelayEnvironment(environment), ); 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 continueRef = useRef<HTMLButtonElement>(null); useEffect(() => { if ( ready && (document.activeElement === document.body || document.activeElement?.getAttribute("role") === "dialog") ) { continueRef.current?.focus(); } }, [ready]); return ( <> <h1 className="text-2xl font-semibold tracking-tight text-foreground"> Connect your computers </h1> <p className="mt-2.5 text-sm leading-relaxed text-muted-foreground"> Choose one or more computers. We’ll set up agents and projects on each. </p> {directEnvironments.length > 0 ? ( <fieldset className="mt-5 space-y-2"> <legend className="sr-only">Computers to set up</legend> {directEnvironments.map((environment) => ( <label key={environment.environmentId} className="flex cursor-pointer items-center gap-3 rounded-lg border border-border bg-background px-3 py-3" > <Checkbox checked={selectedIds.has(environment.environmentId)} onCheckedChange={(checked) => { const next = new Set(selectedIds); if (checked) next.add(environment.environmentId); else next.delete(environment.environmentId); onSelectionChange(next); }} /> <MonitorIcon className="size-4 shrink-0 text-muted-foreground" /> <span className="min-w-0 flex-1"> <span className="flex items-baseline justify-between gap-3"> <span className="min-w-0 text-sm font-medium break-words"> {environment.label} </span> <span className="shrink-0 text-xs text-muted-foreground"> {environment.connection.phase === "connected" ? "Connected" : "Connecting…"} </span> </span> {environment.displayUrl ? ( <span className="mt-0.5 block text-xs break-all text-muted-foreground"> {environment.displayUrl} </span> ) : null} </span> </label> ))} </fieldset> ) : null} <div className="mt-4 space-y-2"> {cloudEnabled ? ( <ConnectAccountOption autoSelectedComputers={autoSelectedComputers} disabled={isPairing} selectedIds={selectedIds} onToggleEnvironment={onToggleEnvironment} /> ) : null} <Collapsible open={pairingOpen} onOpenChange={setPairingOpen} className="rounded-lg border border-border bg-background" > <CollapsibleTrigger disabled={isPairing} render={ <Button variant="ghost" className="h-auto min-h-14 w-full justify-start gap-3 px-3 py-3 text-left whitespace-normal sm:h-auto" /> } > <LinkIcon className="size-4 text-muted-foreground" /> <span className="flex-1">Add a computer</span> <ChevronRightIcon className={cn("size-4 text-muted-foreground", pairingOpen && "rotate-90")} /> </CollapsibleTrigger> <CollapsiblePanel> <div className="px-3 pb-3"> <PairingForm isPairing={isPairing} setIsPairing={setIsPairing} onPaired={(environmentId) => { setPairingOpen(false); onPaired(environmentId); requestAnimationFrame(() => continueRef.current?.focus()); }} /> </div> </CollapsiblePanel> </Collapsible> </div> <div className="mt-6 flex items-center justify-end gap-3"> <Button ref={continueRef} autoFocus={!expandPairingInitially} disabled={!ready || isPairing} onClick={onContinue} > Continue <ArrowRightIcon className="size-3.5" /> </Button> </div> </> ); } function ConnectAccountOption({ autoSelectedComputers, disabled, selectedIds, onToggleEnvironment, }: { readonly autoSelectedComputers: Set<EnvironmentId>; readonly disabled: boolean; readonly selectedIds: ReadonlySet<EnvironmentId>; readonly onToggleEnvironment: (environmentId: EnvironmentId, checked: boolean) => void; }) { const { environments } = useEnvironments(); const { isLoaded, isSignedIn } = useAuth({ treatPendingAsSignedOut: false }); const { openAuthPrompt } = useT3ConnectAuthPrompt(); const [expanded, setExpanded] = useState(true); const [discoveryReady, setDiscoveryReady] = useState(false); const onDiscoveryReady = useCallback(() => setDiscoveryReady(true), []); return ( <Collapsible open={expanded && !!isSignedIn && discoveryReady} onOpenChange={setExpanded} className="rounded-lg border border-border bg-background" > <CollapsibleTrigger disabled={disabled || !isLoaded} onClick={(event) => { if (!isSignedIn) { event.preventDefault(); setExpanded(true); openAuthPrompt(); } }} render={ <Button variant="ghost" className="h-auto min-h-14 w-full justify-start gap-3 px-3 py-3 text-left whitespace-normal sm:h-auto" /> } > <CloudIcon className="size-4 text-muted-foreground" /> <span className="flex-1">T3 Connect</span> <span className="text-xs text-muted-foreground"> {!isLoaded ? "Loading sign-in…" : !isSignedIn ? "Sign in" : !discoveryReady ? "Loading computers…" : null} </span> <ChevronRightIcon className={cn("size-4 text-muted-foreground", expanded && isSignedIn && "rotate-90")} /> </CollapsibleTrigger> <CollapsiblePanel keepMounted> <div className="px-3 pb-3"> <div className="mb-3 space-y-1.5"> {isSignedIn ? ( <CloudEnvironmentConnectRows primaryEnvironmentId={null} savedEnvironments={environments} showSavedEnvironments onDiscoveryReady={onDiscoveryReady} selection={{ selectedIds, onChange: onToggleEnvironment, autoSelectedComputers }} refreshWhileEmpty empty={ <p className="py-3 text-sm text-muted-foreground">No computers linked yet.</p> } /> ) : null} </div> <p className="text-sm text-muted-foreground"> Run this on each computer you want to connect. </p> <CommandBlock command="npx t3 connect" className="mt-3" /> <p className="mt-3 text-xs text-muted-foreground"> Keep T3 Code running. Select the computers you want to set up above. </p> </div> </CollapsiblePanel> </Collapsible> ); } // ── Step 2′: Direct pairing ────────────────────────────────── /** * Register a computer in this browser using a server-minted pairing link. */ function PairingForm({ isPairing, setIsPairing, onPaired, }: { readonly isPairing: boolean; readonly setIsPairing: (value: boolean) => void; readonly onPaired: (environmentId: EnvironmentId) => void;