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("connection"); const { environments } = useEnvironments(); const [selection, setSelection] = useState | null>(null); const autoSelectedComputers = useRef(new Set()); const [setupIds, setSetupIds] = useState([]); const [isImporting, setIsImporting] = useState(false); const finishingPromiseRef = useRef | null>(null); const completionErrorToastIdRef = useRef | 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 ( event.cancel()}> document.getElementById("onboarding-pairing-url") ?? true} > Code } > isImporting || index >= stageIndex} onStepChange={(index) => { if (isImporting || index > stageIndex) return; setStep(index === 0 ? "connection" : "agents"); }} /> {step === "connection" ? ( 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" ? ( setStep("import")} /> ) : ( )} ); } // ── Step 1: connection choice ──────────────────────────────── function ConnectionStep({ autoSelectedComputers, expandPairingInitially, selectedIds, onSelectionChange, onToggleEnvironment, onContinue, onPaired, }: { readonly autoSelectedComputers: Set; readonly expandPairingInitially: boolean; readonly selectedIds: ReadonlySet; readonly onSelectionChange: (ids: ReadonlySet) => 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(null); useEffect(() => { if ( ready && (document.activeElement === document.body || document.activeElement?.getAttribute("role") === "dialog") ) { continueRef.current?.focus(); } }, [ready]); return ( <>

Connect your computers

Choose one or more computers. We’ll set up agents and projects on each.

{directEnvironments.length > 0 ? (
Computers to set up {directEnvironments.map((environment) => ( ))}
) : null}
{cloudEnabled ? ( ) : null} } > Add a computer
{ setPairingOpen(false); onPaired(environmentId); requestAnimationFrame(() => continueRef.current?.focus()); }} />
); } function ConnectAccountOption({ autoSelectedComputers, disabled, selectedIds, onToggleEnvironment, }: { readonly autoSelectedComputers: Set; readonly disabled: boolean; readonly selectedIds: ReadonlySet; 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 ( { if (!isSignedIn) { event.preventDefault(); setExpanded(true); openAuthPrompt(); } }} render={

Run this on the computer with your code.

Start T3 Code first, or run npx t3 serve. Add{" "} --tailscale to use your tailnet.

); } // ── Step 3: agents ─────────────────────────────────────────── const PRIMARY_AGENT_DRIVERS = ["claudeAgent", "codex"] as const; type OnboardingAgentDriver = (typeof PRIMARY_AGENT_DRIVERS)[number]; /** Setup values stay fixed while provider probes refresh the surrounding cards. */ interface AgentTerminalSession { readonly environmentId: EnvironmentId; readonly driver: OnboardingAgentDriver; readonly providerInstanceId: ServerProvider["instanceId"]; readonly cwd: string; readonly command: string; readonly keybindings: ServerConfig["keybindings"]; } /** * Claude Code and Codex use live probe status. Install opens the built-in * terminal inline with the vendor's standalone installer pre-typed. The update * RPC can't install a binary that isn't there yet (it infers the installer from * the installed binary's path), and the terminal also handles the interactive * login that follows. */ function AgentsStep({ environmentIds, onContinue, }: { readonly environmentIds: readonly EnvironmentId[]; readonly onContinue: () => void; }) { const { environments } = useEnvironments(); return (
{environmentIds.map((environmentId) => ( environment.environmentId === environmentId) ?.label ?? "Computer" } /> ))}
); } function ConnectedAgentsStep({ environmentId, machineLabel, }: { readonly environmentId: EnvironmentId; readonly machineLabel: string; }) { const providers = useAtomValue(serverEnvironment.providersValueAtom(environmentId)); const refreshProviders = useAtomCommand(serverEnvironment.refreshProviders, { reportFailure: false, }); const serverConfig = useAtomValue(serverEnvironment.configValueAtom(environmentId)); const [terminalSession, setTerminalSession] = useState(null); // Re-probe on entry so freshly installed CLIs show up without a manual // refresh; harmless when nothing changed (single-flighted per environment). useEffect(() => { void refreshProviders({ environmentId, input: {} }); }, [environmentId, refreshProviders]); const byDriver = useMemo(() => selectOnboardingProvidersByDriver(providers), [providers]); const primaryAgents = PRIMARY_AGENT_DRIVERS.map((driver) => ({ driver, provider: byDriver.get(driver), })); return (

{machineLabel}

{primaryAgents.map(({ driver, provider }) => ( { if (provider === undefined || serverConfig === null) return; setTerminalSession({ environmentId, driver, providerInstanceId: provider.instanceId, cwd: serverConfig.cwd, command: provider.installed ? resolveOnboardingProviderLoginCommand( provider, serverConfig.settings, serverConfig.environment.platform.os, ) : resolveOnboardingProviderInstallCommand( driver, serverConfig.environment.platform.os, ), keybindings: serverConfig.keybindings, }); }} /> ))}
{terminalSession !== null ? ( { setTerminalSession(null); void refreshProviders({ environmentId, input: {} }); }} /> ) : null}
); } function AgentCard({ driver, provider, terminalOpen, terminalAvailable, onOpenTerminal, }: { readonly driver: OnboardingAgentDriver; readonly provider: ServerProvider | undefined; readonly terminalOpen: boolean; readonly terminalAvailable: boolean; readonly onOpenTerminal: () => void; }) { const meta = getDriverOption(ProviderDriverKind.make(driver)); const Icon = meta?.icon; const displayName = driver === "claudeAgent" ? "Claude Code" : (meta?.label ?? driver); const summary = getProviderSummary(provider); const providerState = getOnboardingProviderState(provider); return (
{Icon ? ( ) : null}
{displayName}

{summary.headline} {summary.detail ? ` · ${summary.detail}` : ""}

{providerState === "ready" ? ( Ready ) : providerState === "checking" ? ( Checking... ) : providerState === "disabled" ? ( Disabled ) : providerState === "attention" ? ( {summary.headline} ) : ( )}
); } /** * Inline install terminal. Opens a PTY on the connected environment under a * synthetic onboarding thread id (terminals are keyed by free-form thread id; * the server validates only the cwd) and pre-types the install or login * command without submitting, so the user reviews and presses Enter. */ function AgentInstallTerminal({ session, onClose, }: { readonly session: AgentTerminalSession; readonly onClose: () => void; }) { const { command, cwd, driver, environmentId, keybindings, providerInstanceId } = session; // Same terminal typography preference the thread drawer honors. const [advancedTypography] = useLocalStorage( TYPOGRAPHY_ADVANCED_STORAGE_KEY, false, Schema.Boolean, ); const openTerminal = useAtomCommand(terminalEnvironment.open, { reportFailure: false }); const writeTerminal = useAtomCommand(terminalEnvironment.write, { reportFailure: false }); const closeTerminal = useAtomCommand(terminalEnvironment.close, { reportFailure: false }); const setupQueueRef = useRef(Promise.resolve()); const setupGenerationRef = useRef(0); const activeSetupGenerationRef = useRef(null); const [terminalId] = useState(() => `onboarding-${driver}-${randomUUID()}`); const threadRef = useMemo( () => scopeThreadRef(environmentId, AGENT_ONBOARDING_THREAD_ID), [environmentId], ); const [setupAttempt, setSetupAttempt] = useState(0); const [setupState, setSetupState] = useState< "preparing" | "ready" | "openFailed" | "writeFailed" >("preparing"); const terminalReady = setupState === "ready" || setupState === "writeFailed"; // Keep each setup generation distinct. In Strict Mode, a canceled open can // finish after the replacement setup starts; it must not close or pre-type // into the replacement session that shares this terminal id. useEffect(() => { const generation = setupGenerationRef.current + 1; setupGenerationRef.current = generation; activeSetupGenerationRef.current = generation; setSetupState("preparing"); setupQueueRef.current = setupQueueRef.current.then(async () => { if (activeSetupGenerationRef.current !== generation) return; const opened = await openTerminal({ environmentId, input: { threadId: AGENT_ONBOARDING_THREAD_ID, terminalId, cwd, providerInstanceId, }, }); if (opened._tag !== "Success") { if (activeSetupGenerationRef.current === generation) setSetupState("openFailed"); return; } if (activeSetupGenerationRef.current !== generation) return; const wrote = await writeTerminal({ environmentId, input: { threadId: AGENT_ONBOARDING_THREAD_ID, terminalId, data: command }, }); if (activeSetupGenerationRef.current !== generation) return; setSetupState(wrote._tag === "Success" ? "ready" : "writeFailed"); }); // Every exit path unmounts the drawer (Done, Continue/Skip, card switch, // session exit), so this cleanup is the single place the PTY dies — // nothing is left running behind the wizard. An interrupted install is // re-runnable from the card. return () => { if (activeSetupGenerationRef.current === generation) { activeSetupGenerationRef.current = null; } setupQueueRef.current = setupQueueRef.current.then(async () => { await closeTerminal({ environmentId, input: { threadId: AGENT_ONBOARDING_THREAD_ID, terminalId, deleteHistory: true }, }); }); }; }, [ closeTerminal, command, cwd, environmentId, openTerminal, providerInstanceId, setupAttempt, terminalId, writeTerminal, ]); return (
{setupState === "writeFailed" ? ( <> Run {command} in this terminal. ) : setupState === "ready" ? ( "Review the command, then press Enter to run it." ) : setupState === "openFailed" ? ( "Could not open the setup terminal." ) : ( "Preparing command..." )}
{setupState === "openFailed" ? ( ) : null}
{terminalReady ? ( ) : null}
); } // ── Step 4: import ─────────────────────────────────────────── function ImportStep({ scans, isImporting, setIsImporting, onDone, }: { readonly scans: ReturnType; readonly isImporting: boolean; readonly setIsImporting: (value: boolean) => void; readonly onDone: (projectRef?: ScopedProjectRef) => Promise; }) { const { environments } = useEnvironments(); const createProject = useAtomCommand(projectEnvironment.create, { reportFailure: false }); const importThreads = useAtomCommand(agentSessionImport, { reportFailure: false }); const projects = useProjects(); const [selectedPaths, setSelectedPaths] = useState | null>(null); const [importError, setImportError] = useState(""); const [landingProject, setLandingProject] = useState(null); // Keep project creation attempts separate from completed history imports so both can retry. const importedProjectsRef = useRef(new Map()); const projectsWithImportedHistoryRef = useRef(new Map()); const lastImportSelectionRef = useRef>([]); const projectAttemptsRef = useRef( new Map(), ); const importGenerationRef = useRef(0); // Ignore command completions after leaving the import step. useEffect(() => { importGenerationRef.current += 1; return () => { importGenerationRef.current += 1; }; }, []); useEffect(() => { if ( landingProject !== null && projects.some( (project) => project.id === landingProject.projectId && project.environmentId === landingProject.environmentId, ) ) { setLandingProject(null); void onDone(landingProject).then((completed) => { if (!completed) setIsImporting(false); }); } }, [landingProject, onDone, projects, setIsImporting]); const { available: candidates, recent } = useMemo( () => partitionOnboardingProjects( scans.flatMap((scan) => (scan.data?.candidates ?? []).map((candidate) => ({ ...candidate, environmentId: scan.environmentId, key: onboardingProjectKey(scan.environmentId, candidate.path), })), ), ), [scans], ); const selectedKeys = useMemo( () => selectedPaths ?? new Set(recent.map((candidate) => candidate.key)), [selectedPaths, recent], ); const selected = candidates.filter((candidate) => selectedKeys.has(candidate.key)); const finishAfterImport = () => { const projectRef = resolveOnboardingLandingProject( lastImportSelectionRef.current, projectsWithImportedHistoryRef.current, importedProjectsRef.current, ); if (projectRef === undefined) { void onDone(); return; } setIsImporting(true); setLandingProject(projectRef); }; const runImport = async (selection: typeof candidates) => { if (isImporting) return; if (selection.length === 0) { void onDone(); return; } setIsImporting(true); setImportError(""); lastImportSelectionRef.current = selection.map((candidate) => candidate.key); const importGeneration = importGenerationRef.current; const importedProjects = importedProjectsRef.current; const projectAttempts = projectAttemptsRef.current; // Interrupted imports are neither failures nor successes — the command was // superseded or the environment dropped — but they still didn't land, so // they must not read as "imported everything". Retries skip paths that // already landed this session (re-creating them would only trip the // duplicate-root invariant and read as a failure). let importedProjectsCount = importedProjects.size > 0 ? selection.filter((candidate) => importedProjects.has(candidate.key)).length : 0; let importedThreadCount = 0; let skippedThreadCount = 0; const refreshEnvironments = new Set(); for (const candidate of selection) { const { environmentId } = candidate; if ( importGeneration !== importGenerationRef.current || importedProjects !== importedProjectsRef.current ) { return; } if (importedProjects.has(candidate.key)) continue; let projectId = resolveOnboardingProjectId(readProjects(), environmentId, candidate); if (projectId === null) { let attempt = projectAttempts.get(candidate.key); if (attempt === undefined) { const nextProjectId = newProjectId(); attempt = { projectId: nextProjectId, commandId: CommandId.make(`onboarding:project:create:${nextProjectId}`), }; projectAttempts.set(candidate.key, attempt); } projectId = attempt.projectId; const result = await createProject({ environmentId, input: { projectId, commandId: attempt.commandId, title: candidate.title, workspaceRoot: candidate.path, createWorkspaceRootIfMissing: false, defaultModelSelection: null, }, }); if ( importGeneration !== importGenerationRef.current || importedProjects !== importedProjectsRef.current ) { return; } if (result._tag !== "Success") { if (!isAtomCommandInterrupted(result)) { projectAttempts.delete(candidate.key); refreshEnvironments.add(environmentId); } continue; } } const threadImportResult = await importThreads({ environmentId, input: { projectId, expectedWorkspaceRoot: candidate.path }, }); if ( importGeneration !== importGenerationRef.current || importedProjects !== importedProjectsRef.current ) { return; } if (threadImportResult._tag === "Success") { importedThreadCount += threadImportResult.value.importedCount; skippedThreadCount += threadImportResult.value.skippedCount; if (threadImportResult.value.importedCount > 0) { projectsWithImportedHistoryRef.current.set( candidate.key, scopeProjectRef(environmentId, projectId), ); } if (threadImportResult.value.skippedCount === 0) { importedProjectsCount += 1; importedProjects.set(candidate.key, scopeProjectRef(environmentId, projectId)); } } else if (!isAtomCommandInterrupted(threadImportResult)) { projectAttempts.delete(candidate.key); refreshEnvironments.add(environmentId); } } for (const scan of scans) { if (refreshEnvironments.has(scan.environmentId)) scan.refresh(); } setIsImporting(false); if (importedProjectsCount < selection.length) { if (importedThreadCount > 0 && skippedThreadCount > 0) { setImportError( `Imported ${importedThreadCount} ${importedThreadCount === 1 ? "thread" : "threads"}. ${skippedThreadCount} ${skippedThreadCount === 1 ? "thread" : "threads"} could not be imported.`, ); } else if (skippedThreadCount > 0) { setImportError( `${skippedThreadCount} ${skippedThreadCount === 1 ? "thread could" : "threads could"} not be imported.`, ); } else if (importedThreadCount > 0) { setImportError( `Imported ${importedThreadCount} ${importedThreadCount === 1 ? "thread" : "threads"}. Some thread history could not be imported.`, ); } else { setImportError("Could not import thread history."); } return; } finishAfterImport(); }; if (scans.every((scan) => scan.data === null) && scans.some((scan) => scan.isPending)) { return (

Your projects

Looking for projects from Claude Code and Codex…

); } return ( {candidates.length > 0 ? (
{selected.length} of {candidates.length} selected
) : null}
{scans.map((scan) => { const scanCandidates = candidates.filter( (candidate) => candidate.environmentId === scan.environmentId, ); const label = environments.find((environment) => environment.environmentId === scan.environmentId) ?.label ?? "Computer"; return (
{scans.length > 1 ? ( {label} ) : null} {scan.isPending && scan.data === null ? (
Looking for projects…
) : scan.error !== null ? (
Could not check projects. {scan.error}
) : scanCandidates.length === 0 ? (

No existing Claude Code or Codex projects found.

) : null} {scan.data?.truncated ? (

{SCAN_LIMIT_MESSAGE}

) : null}
); })}
{importError ?

{importError}

: null}
); } type ImportCandidate = AgentSessionProjectCandidate & { readonly environmentId: EnvironmentId; readonly key: string; }; /** * Repositories first, newest activity on top. Clones of one repository share * a group with a tri-state checkbox. Folders that are not git repositories * sit collapsed at the bottom so they stay reachable without adding noise. * Source icons appear only on repository rows so the columns stay still. */ function ImportCandidateList({ candidates, selectedKeys, onSelectionChange, }: { readonly candidates: ReadonlyArray; readonly selectedKeys: ReadonlySet; readonly onSelectionChange: (next: ReadonlySet) => void; }) { const { repositories, other } = useMemo(() => groupOnboardingProjects(candidates), [candidates]); const setKeys = (keys: ReadonlyArray, checked: boolean) => { const next = new Set(selectedKeys); for (const key of keys) { if (checked) next.add(key); else next.delete(key); } onSelectionChange(next); }; const otherSelected = other.filter((candidate) => selectedKeys.has(candidate.key)).length; return ( <> {repositories.map((group) => ( ))} {other.length > 0 ? (
0 && otherSelected < other.length} onCheckedChange={(checked) => setKeys( other.map((candidate) => candidate.key), checked === true, ) } /> Other folders {other.length} {other.length === 1 ? "folder" : "folders"}
{other.map((candidate) => ( setKeys([candidate.key], checked)} /> ))}
) : null} ); } function ImportRepositoryGroup({ group, selectedKeys, onToggle, }: { readonly group: OnboardingProjectGroup; readonly selectedKeys: ReadonlySet; readonly onToggle: (keys: ReadonlyArray, checked: boolean) => void; }) { const keys = group.candidates.map((candidate) => candidate.key); const selectedCount = keys.filter((key) => selectedKeys.has(key)).length; const single = group.candidates.length === 1; const only = group.candidates[0]; if (single && only !== undefined) { return ( onToggle([only.key], checked)} /> ); } return (
0 && selectedCount < keys.length} onCheckedChange={(checked) => onToggle(keys, checked === true)} /> {group.label} c.sources))]} threadCount={group.threadCount} lastActiveAt={group.lastActiveAt} />
{group.candidates.map((candidate) => ( onToggle([candidate.key], checked)} /> ))}
); } function ImportCandidateRow({ candidate, label, secondary, nested = false, checked, onCheckedChange, }: { readonly candidate: ImportCandidate; readonly label: string; readonly secondary?: string; readonly nested?: boolean; readonly checked: boolean; readonly onCheckedChange: (checked: boolean) => void; }) { return ( ); } /** * Trailing columns shared by every import row: source icons, thread count, * last activity. Each column has a fixed width and each icon has its own slot * so nothing shifts between rows that differ in sources or digit count. */ function ImportRowMeta({ sources, threadCount, lastActiveAt, }: { readonly sources: ReadonlyArray<"claudeAgent" | "codex"> | null; readonly threadCount: number; readonly lastActiveAt: string | null; }) { const relative = lastActiveAt === null ? null : formatRelativeTime(lastActiveAt); // "just now" does not fit the fixed column, so collapse it. const age = relative === null ? "" : relative.suffix === null ? "now" : relative.value; return ( {sources?.includes("claudeAgent") ? ( ) : null} {sources?.includes("codex") ? : null} {threadCount} {age} ); } // ── Shared bits ────────────────────────────────────────────── function StepShell({ title, description, children, }: { readonly title: string; readonly description?: string; readonly children?: React.ReactNode; }) { return ( <>

{title}

{description ? (

{description}

) : null} {children} ); } function CommandBlock({ command, className, prominent = false, }: { readonly command: string; readonly className?: string; readonly prominent?: boolean; }) { const { copyToClipboard, isCopied } = useCopyToClipboard({ timeout: 1500, target: "command", }); return (
$ {command}
); }