Two flashes visible in the onboarding flow: 1. create-org flickered 'Create your organization' → 'Loading' → back. The header was gated on the pending-invitations atom being initial OR waiting, and that atom is keyed on auth — so the create-org submit (which invalidates the auth keys) put it into 'waiting' and flipped the whole page to 'Loading'. Almost no new user has a pending invitation, so we now show NO loading placeholder at all: the create form renders immediately and stays put, and invitations (when they exist) just appear above it once the fetch resolves. Removed the InvitationsSkeleton entirely. 2. The connect-MCP step flashed the app sidebar. 'Continue to app' / 'Skip' navigated to the BARE /{-$orgSlug}, which mounts the shell at / and then OrgSlugGate fires a SECOND navigation to canonicalize / → /<slug>. That double hop is the window where the shell paints over the still-mounted onboarding page. Now it navigates straight to /<slug> (the page already has the org slug), so the shell mounts once. Both pre-existing; the new auth-routing flow test (#1006) made them visible.
Unified · Split
| @@ -6,7 +6,6 @@ import * as Exit from "effect/Exit"; · 5 unmodified lines | ||
| 6 | 6 | import { authWriteKeys } from "@executor-js/react/api/reactivity-keys"; |
| 7 | 7 | import { trackEvent } from "@executor-js/react/api/analytics"; |
| 8 | 8 | import { Button } from "@executor-js/react/components/button"; |
| 9 | −import { Skeleton } from "@executor-js/react/components/skeleton"; | |
| 10 | 9 | |
| 11 | 10 | import { AUTH_PATHS } from "../../auth/api"; |
| 12 | 11 | import { acceptInvitation, pendingInvitationsAtom, useAuth } from "../auth"; |
| @@ -84,8 +83,11 @@ export const CreateOrgPage = () => { · 71 unmodified lines | ||
| 84 | 83 | }, |
| 85 | 84 | }); |
| 86 | 85 | |
| 87 | − const isLoading = | |
| 88 | − AsyncResult.isInitial(invitationsResult) || AsyncResult.isWaiting(invitationsResult); | |
| 86 | + // Almost no new user has a pending invitation, so we never show a loading | |
| 87 | + // placeholder for them — the create form renders immediately and stays put. | |
| 88 | + // Invitations (when they exist) just appear above it once the fetch resolves; | |
| 89 | + // a refetch (the create-org submit invalidates the auth-keyed atom) keeps the | |
| 90 | + // last value, so nothing flickers. | |
| 89 | 91 | const invitations = AsyncResult.match(invitationsResult, { |
| 90 | 92 | onInitial: () => [] as readonly PendingInvitation[], |
| 91 | 93 | onFailure: () => [] as readonly PendingInvitation[], |
| @@ -103,13 +105,9 @@ export const CreateOrgPage = () => { · 11 unmodified lines | ||
| 103 | 105 | Step 1 of 2 |
| 104 | 106 | </p> |
| 105 | 107 | <h1 className="font-serif text-3xl"> |
| 106 | − {isLoading | |
| 107 | − ? "Loading" | |
| 108 | − : count === 0 | |
| 109 | − ? "Create your organization" | |
| 110 | − : "You've been invited"} | |
| 108 | + {count > 0 ? "You've been invited" : "Create your organization"} | |
| 111 | 109 | </h1> |
| 112 | − {!isLoading && count === 0 && ( | |
| 110 | + {count === 0 && ( | |
| 113 | 111 | <p className="text-sm text-muted-foreground"> |
| 114 | 112 | Organizations group your sources, secrets, and teammates. You can invite others once |
| 115 | 113 | it's set up. |
| @@ -117,9 +115,7 @@ export const CreateOrgPage = () => { · 1 unmodified line | ||
| 117 | 115 | )} |
| 118 | 116 | </header> |
| 119 | 117 | |
| 120 | − {isLoading && <InvitationsSkeleton />} | |
| 121 | − | |
| 122 | − {!isLoading && sole && ( | |
| 118 | + {sole && ( | |
| 123 | 119 | <SingleInvitationView |
| 124 | 120 | invitation={sole} |
| 125 | 121 | accepting={acceptingId === sole.id} |
| @@ -128,7 +124,7 @@ export const CreateOrgPage = () => { · 2 unmodified lines | ||
| 128 | 124 | /> |
| 129 | 125 | )} |
| 130 | 126 | |
| 131 | − {!isLoading && count > 1 && ( | |
| 127 | + {count > 1 && ( | |
| 132 | 128 | <MultiInvitationsView |
| 133 | 129 | invitations={invitations} |
| 134 | 130 | acceptingId={acceptingId} |
| @@ -137,9 +133,7 @@ export const CreateOrgPage = () => { · 2 unmodified lines | ||
| 137 | 133 | /> |
| 138 | 134 | )} |
| 139 | 135 | |
| 140 | − {!isLoading && (count === 0 || sole || count > 1) && ( | |
| 141 | − <CreateOrgSection isPrimary={count === 0} form={form} /> | |
| 142 | − )} | |
| 136 | + <CreateOrgSection isPrimary={count === 0} form={form} /> | |
| 143 | 137 | |
| 144 | 138 | <footer className="flex items-center justify-center"> |
| 145 | 139 | {/* oxlint-disable-next-line react/forbid-elements */} |
| @@ -161,25 +155,6 @@ export const CreateOrgPage = () => { · 15 unmodified lines | ||
| 161 | 155 | |
| 162 | 156 | // --------------------------------------------------------------------------- |
| 163 | 157 | |
| 164 | −const InvitationsSkeleton = () => ( | |
| 165 | − <div className="flex flex-col gap-2.5"> | |
| 166 | − <InvitationRowSkeleton /> | |
| 167 | − <InvitationRowSkeleton /> | |
| 168 | − </div> | |
| 169 | −); | |
| 170 | − | |
| 171 | −const InvitationRowSkeleton = () => ( | |
| 172 | − <div className="flex items-center gap-3 rounded-md border border-border px-3 py-2.5"> | |
| 173 | − <div className="flex flex-1 flex-col gap-1.5"> | |
| 174 | − <Skeleton className="h-3.5 w-2/5" /> | |
| 175 | − <Skeleton className="h-3 w-3/5" /> | |
| 176 | − </div> | |
| 177 | − <Skeleton className="h-8 w-16 rounded-md" /> | |
| 178 | − </div> | |
| 179 | −); | |
| 180 | − | |
| 181 | −// --------------------------------------------------------------------------- | |
| 182 | − | |
| 183 | 158 | const InviterAttribution = ({ invitation }: { invitation: PendingInvitation }) => { |
| 184 | 159 | const inviterLabel = invitation.inviter |
| 185 | 160 | ? invitation.inviter.name && invitation.inviter.name.length > 0 |
| @@ -23,6 +23,12 @@ export const SetupMcpPage = () => { · 22 unmodified lines | ||
| 23 | 23 | const auth = useAuth(); |
| 24 | 24 | const organizationSlug = |
| 25 | 25 | auth.status === "authenticated" ? (auth.organization?.slug ?? null) : null; |
| 26 | + // Land DIRECTLY on the org's canonical URL. Navigating to the bare | |
| 27 | + // `/{-$orgSlug}` would mount the shell at `/`, then OrgSlugGate would fire a | |
| 28 | + // SECOND navigation to canonicalize `/` → `/<slug>` — that double hop is the | |
| 29 | + // window where the shell paints over this still-mounted onboarding page. | |
| 30 | + const goToApp = () => | |
| 31 | + navigate({ to: "/{-$orgSlug}", params: { orgSlug: organizationSlug ?? undefined } }); | |
| 26 | 32 | const [origin, setOrigin] = useState<string | null>(null); |
| 27 | 33 | const [advancedOpen, setAdvancedOpen] = useState(false); |
| 28 | 34 | const [elicitationMode, setElicitationMode] = useState<McpElicitationMode>("model"); |
| @@ -150,7 +156,7 @@ export const SetupMcpPage = () => { · 121 unmodified lines | ||
| 150 | 156 | type="button" |
| 151 | 157 | onClick={() => { |
| 152 | 158 | trackEvent("setup_mcp_skipped"); |
| 153 | − void navigate({ to: "/{-$orgSlug}" }); | |
| 159 | + void goToApp(); | |
| 154 | 160 | }} |
| 155 | 161 | className="text-xs text-muted-foreground transition-colors hover:text-foreground" |
| 156 | 162 | > |
| @@ -160,7 +166,7 @@ export const SetupMcpPage = () => { · 3 unmodified lines | ||
| 160 | 166 | size="sm" |
| 161 | 167 | onClick={() => { |
| 162 | 168 | trackEvent("setup_mcp_completed"); |
| 163 | − void navigate({ to: "/{-$orgSlug}" }); | |
| 169 | + void goToApp(); | |
| 164 | 170 | }} |
| 165 | 171 | > |
| 166 | 172 | Continue to app |