diff --git a/packages/worker/client/routes/onboarding.tsx b/packages/worker/client/routes/onboarding.tsx index 037429f641..18b263c187 100644 --- a/packages/worker/client/routes/onboarding.tsx +++ b/packages/worker/client/routes/onboarding.tsx @@ -152,6 +152,38 @@ export function OnboardingRoute(handle: Handle) { } } + // Users typically keep this page open while their MCP client runs the + // OAuth flow, so poll the same JSON endpoint until a grant shows up and + // collapse the completed steps without requiring a manual refresh. + let pollIntervalId: ReturnType | undefined + let pollInFlight = false + + async function pollForMcpConnection() { + if (hasMcpClient) { + clearInterval(pollIntervalId) + return + } + if (pollInFlight || status !== 'ready' || !loggedIn) return + if (document.hidden) return + if (!isOnboardingPath(readCurrentRouterHref(handle))) return + pollInFlight = true + try { + const payload = await fetchOnboardingPayload(handle.signal) + if (handle.signal.aborted || !payload?.hasMcpClient) return + applyPayload(payload) + handle.update() + } catch { + // Transient poll failures are fine; the next tick retries. + } finally { + pollInFlight = false + } + } + + if (typeof document !== 'undefined') { + pollIntervalId = setInterval(pollForMcpConnection, 5000) + handle.signal.addEventListener('abort', () => clearInterval(pollIntervalId)) + } + function applyRouteLoaderData(href: string) { if (!isOnboardingPath(href)) return false const routeData = tryConsumeRouteLoaderData(handle, 'onboarding', href) @@ -209,52 +241,57 @@ export function OnboardingRoute(handle: Handle) {

You are connected

At least one AI agent has already authorized access to this - account. You can still use the steps below to connect another - host or re-run setup with your agent. + account, so the first steps below are collapsed. Open them any + time to connect another host or re-run setup with your agent.

) : null} -
-

- Not sure what you'd use Kody for? -

-

- Paste this into any AI agent that can fetch a URL or search the - web — ChatGPT, Claude, or whatever you already use. It has the - agent read Kody's docs, interview you, and suggest concrete - automations. No account or setup needed yet. -

-
{discoveryPrompt}
- -
- -
-

1. Add Kody as an MCP server

-

- Pick your MCP client below for host-specific setup. When the - agent connects, it starts an OAuth flow — sign in to Kody if - needed, approve the request, and the agent receives a token - scoped to your account. -

- -
+ {renderOnboardingStep({ + title: '1. Not sure what you\u2019d use Kody for?', + collapsed: hasMcpClient, + id: 'discovery', + testId: 'onboarding-discovery', + children: ( + <> +

+ Paste this into any AI agent that can fetch a URL or search + the web — ChatGPT, Claude, or whatever you already use. It + has the agent read Kody's docs, interview you, and + suggest concrete automations. No account or setup needed + yet. +

+
{discoveryPrompt}
+ + + ), + })} - {renderByokExplainer({ image: 'handoff' })} + {renderOnboardingStep({ + title: '2. Add Kody as an MCP server', + collapsed: hasMcpClient, + children: ( + <> +

+ Pick your MCP client below for host-specific setup. When the + agent connects, it starts an OAuth flow — sign in to Kody if + needed, approve the request, and the agent receives a token + scoped to your account. +

+ + + ), + })}
-

2. Install a starter package

+

3. Install a starter package

{featuredListings.length > 0 ? 'These packages were reviewed by an admin and support one-click install here. After install, use Copy prompt so your agent can finish any remaining setup — or pick Choose your own adventure to explore with your agent instead.' @@ -277,33 +314,19 @@ export function OnboardingRoute(handle: Handle) {

-

- {loggedIn ? ( - <> - - Back to account - - {' · '} - - Secrets - - {' · '} - - Integrations - - - ) : ( - <> - - Sign up - - {' · '} - - Log in - - - )} -

+ {renderByokExplainer({ image: 'handoff' })} + + {loggedIn ? null : ( +

+ + Sign up + + {' · '} + + Log in + +

+ )} ) : null} @@ -311,6 +334,57 @@ export function OnboardingRoute(handle: Handle) { } } +type OnboardingStepSlot = any + +/** + * Renders an onboarding step as an open card, or — once an MCP client is + * already connected — as a collapsed `
` card the user can reopen to + * redo the step (e.g. connect another host). + */ +function renderOnboardingStep(input: { + title: string + collapsed: boolean + id?: string + testId?: string + children: OnboardingStepSlot +}) { + if (!input.collapsed) { + return ( +
+

{input.title}

+ {input.children} +
+ ) + } + return ( +
+ +

{input.title}

+ Done — open to revisit +
+ {input.children} +
+ ) +} + +const collapsedStepSummaryCss = { + cursor: 'pointer', + '&::marker': { + color: colors.textMuted, + }, +} + +const collapsedStepTitleCss = { + ...cardTitleCss, + display: 'inline' as const, +} + +const collapsedStepHintCss = { + marginLeft: spacing.sm, + color: colors.textMuted, + fontSize: typography.fontSize.sm, +} + const codeBlockCss = { ...insetCardCss, margin: 0,