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 | import { authWriteKeys } from "@executor-js/react/api/reactivity-keys"; | 6 | import { authWriteKeys } from "@executor-js/react/api/reactivity-keys"; |
| 7 | import { trackEvent } from "@executor-js/react/api/analytics"; | 7 | import { trackEvent } from "@executor-js/react/api/analytics"; |
| 8 | import { Button } from "@executor-js/react/components/button"; | 8 | import { Button } from "@executor-js/react/components/button"; |
| 9 | import { Skeleton } from "@executor-js/react/components/skeleton"; | ||
| 10 | 9 | ||
| 11 | import { AUTH_PATHS } from "../../auth/api"; | 10 | import { AUTH_PATHS } from "../../auth/api"; |
| 12 | import { acceptInvitation, pendingInvitationsAtom, useAuth } from "../auth"; | 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 = | 86 | // Almost no new user has a pending invitation, so we never show a loading |
| 88 | AsyncResult.isInitial(invitationsResult) || AsyncResult.isWaiting(invitationsResult); | 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 | const invitations = AsyncResult.match(invitationsResult, { | 91 | const invitations = AsyncResult.match(invitationsResult, { |
| 90 | onInitial: () => [] as readonly PendingInvitation[], | 92 | onInitial: () => [] as readonly PendingInvitation[], |
| 91 | onFailure: () => [] as readonly PendingInvitation[], | 93 | onFailure: () => [] as readonly PendingInvitation[], |
| @@ -103,13 +105,9 @@ export const CreateOrgPage = () => { · 11 unmodified lines | |||
| 103 | Step 1 of 2 | 105 | Step 1 of 2 |
| 104 | </p> | 106 | </p> |
| 105 | <h1 className="font-serif text-3xl"> | 107 | <h1 className="font-serif text-3xl"> |
| 106 | {isLoading | 108 | {count > 0 ? "You've been invited" : "Create your organization"} |
| 107 | ? "Loading" | ||
| 108 | : count === 0 | ||
| 109 | ? "Create your organization" | ||
| 110 | : "You've been invited"} | ||
| 111 | </h1> | 109 | </h1> |
| 112 | {!isLoading && count === 0 && ( | 110 | {count === 0 && ( |
| 113 | <p className="text-sm text-muted-foreground"> | 111 | <p className="text-sm text-muted-foreground"> |
| 114 | Organizations group your sources, secrets, and teammates. You can invite others once | 112 | Organizations group your sources, secrets, and teammates. You can invite others once |
| 115 | it's set up. | 113 | it's set up. |
| @@ -117,9 +115,7 @@ export const CreateOrgPage = () => { · 1 unmodified line | |||
| 117 | )} | 115 | )} |
| 118 | </header> | 116 | </header> |
| 119 | 117 | ||
| 120 | {isLoading && <InvitationsSkeleton />} | 118 | {sole && ( |
| 121 | |||
| 122 | {!isLoading && sole && ( | ||
| 123 | <SingleInvitationView | 119 | <SingleInvitationView |
| 124 | invitation={sole} | 120 | invitation={sole} |
| 125 | accepting={acceptingId === sole.id} | 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 | <MultiInvitationsView | 128 | <MultiInvitationsView |
| 133 | invitations={invitations} | 129 | invitations={invitations} |
| 134 | acceptingId={acceptingId} | 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) && ( | 136 | <CreateOrgSection isPrimary={count === 0} form={form} /> |
| 141 | <CreateOrgSection isPrimary={count === 0} form={form} /> | ||
| 142 | )} | ||
| 143 | 137 | ||
| 144 | <footer className="flex items-center justify-center"> | 138 | <footer className="flex items-center justify-center"> |
| 145 | {/* oxlint-disable-next-line react/forbid-elements */} | 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 | const InviterAttribution = ({ invitation }: { invitation: PendingInvitation }) => { | 158 | const InviterAttribution = ({ invitation }: { invitation: PendingInvitation }) => { |
| 184 | const inviterLabel = invitation.inviter | 159 | const inviterLabel = invitation.inviter |
| 185 | ? invitation.inviter.name && invitation.inviter.name.length > 0 | 160 | ? invitation.inviter.name && invitation.inviter.name.length > 0 |
| @@ -23,6 +23,12 @@ export const SetupMcpPage = () => { · 22 unmodified lines | |||
| 23 | const auth = useAuth(); | 23 | const auth = useAuth(); |
| 24 | const organizationSlug = | 24 | const organizationSlug = |
| 25 | auth.status === "authenticated" ? (auth.organization?.slug ?? null) : null; | 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 | const [origin, setOrigin] = useState<string | null>(null); | 32 | const [origin, setOrigin] = useState<string | null>(null); |
| 27 | const [advancedOpen, setAdvancedOpen] = useState(false); | 33 | const [advancedOpen, setAdvancedOpen] = useState(false); |
| 28 | const [elicitationMode, setElicitationMode] = useState<McpElicitationMode>("model"); | 34 | const [elicitationMode, setElicitationMode] = useState<McpElicitationMode>("model"); |
| @@ -150,7 +156,7 @@ export const SetupMcpPage = () => { · 121 unmodified lines | |||
| 150 | type="button" | 156 | type="button" |
| 151 | onClick={() => { | 157 | onClick={() => { |
| 152 | trackEvent("setup_mcp_skipped"); | 158 | trackEvent("setup_mcp_skipped"); |
| 153 | void navigate({ to: "/{-$orgSlug}" }); | 159 | void goToApp(); |
| 154 | }} | 160 | }} |
| 155 | className="text-xs text-muted-foreground transition-colors hover:text-foreground" | 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 | size="sm" | 166 | size="sm" |
| 161 | onClick={() => { | 167 | onClick={() => { |
| 162 | trackEvent("setup_mcp_completed"); | 168 | trackEvent("setup_mcp_completed"); |
| 163 | void navigate({ to: "/{-$orgSlug}" }); | 169 | void goToApp(); |
| 164 | }} | 170 | }} |
| 165 | > | 171 | > |
| 166 | Continue to app | 172 | Continue to app |