1563 lines · 56.5 KB · TypeScript
RawDownload
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.