1import { createFileRoute, redirect, useLocation, useNavigate } from "@tanstack/react-router";
2import { useState } from "react";
3import { EnvironmentId } from "@t3tools/contracts";
4import * as Schema from "effect/Schema";
5import * as Option from "effect/Option";
6
7import { NoProjectsHero } from "../components/NoProjectsHero";
8import { WelcomeWizard } from "../components/onboarding/WelcomeWizard";
9import { useNewThreadHandler } from "../hooks/useHandleNewThread";
10
11const decodeEnvironmentId = Schema.decodeOption(EnvironmentId);
12
13/** Onboarding overlays the workspace. Visiting /welcome reopens setup. */
14export const Route = createFileRoute("/welcome")({
15 beforeLoad: ({ context }) => {
16 const { authGateState } = context;
17 if (authGateState.status !== "authenticated" && authGateState.status !== "hosted-static") {
18 throw redirect({ to: "/pair", replace: true });
19 }
20 },
21 component: WelcomeRouteView,
22});
23
24function WelcomeRouteView() {
25 const { authGateState } = Route.useRouteContext();
26 const navigate = useNavigate();
27 const hash = useLocation({ select: (location) => location.hash });
28 const resumeEnvironmentId = hash.startsWith("agents:")
29 ? Option.getOrUndefined(decodeEnvironmentId(hash.slice("agents:".length)))
30 : undefined;
31 // The root shell can remount this pending outlet after the location changes.
32 // Never reopen setup while the destination route is still loading.
33 const isWelcomeRoute = useLocation({ select: (location) => location.pathname === "/welcome" });
34 const [dismissed, setDismissed] = useState(false);
35 const openNewThread = useNewThreadHandler();
36 // An authenticated gate means a primary server is serving this app —
37 // desktop, `npx t3`, or a dev server — and that server is "this machine"
38 // no matter what hostname the browser used. Only hosted-static has no
39 // local server to offer.
40 const localAvailable = authGateState.status === "authenticated";
41 return (
42 <>
43 <NoProjectsHero />
44 {isWelcomeRoute && !dismissed ? (
45 <WelcomeWizard
46 localAvailable={localAvailable}
47 resumeEnvironmentId={resumeEnvironmentId}
48 onDone={async (projectRef) => {
49 setDismissed(true);
50 if (projectRef !== undefined) {
51 await openNewThread(projectRef, { replace: true }).catch(() =>
52 navigate({ to: "/", replace: true }),
53 );
54 return;
55 }
56 await navigate({ to: "/", replace: true });
57 }}
58 />
59 ) : null}
60 </>
61 );
62}