From 66b2898f2160e110b1d164a0494428f6283f9357 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Mon, 20 Jul 2026 13:45:49 -0600 Subject: [PATCH 1/3] Make onboarding responsive to connected state Reorder onboarding into three steps with discovery first, collapse the first two steps into details cards once an MCP client is connected, move the BYOK explainer below the starter packages, and drop the account footer links. Co-authored-by: Cursor --- packages/worker/client/routes/onboarding.tsx | 168 ++++++++++++------- 1 file changed, 105 insertions(+), 63 deletions(-) diff --git a/packages/worker/client/routes/onboarding.tsx b/packages/worker/client/routes/onboarding.tsx index 037429f641..7dd22e78b6 100644 --- a/packages/worker/client/routes/onboarding.tsx +++ b/packages/worker/client/routes/onboarding.tsx @@ -209,52 +209,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 +282,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 +302,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, From 467b9db3f16669f9ce9954137848802c87d4f51b Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Mon, 20 Jul 2026 13:49:09 -0600 Subject: [PATCH 2/3] Poll for MCP connection while onboarding is open The onboarding page only learned about a new MCP OAuth grant on load or navigation, so users watching the page while their client connected never saw the UI react. Poll the onboarding JSON endpoint every few seconds (visible tab, logged in, not yet connected) and apply the payload once a grant appears. Co-authored-by: Cursor --- packages/worker/client/routes/onboarding.tsx | 22 ++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/worker/client/routes/onboarding.tsx b/packages/worker/client/routes/onboarding.tsx index 7dd22e78b6..0cb573c60f 100644 --- a/packages/worker/client/routes/onboarding.tsx +++ b/packages/worker/client/routes/onboarding.tsx @@ -152,6 +152,28 @@ 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. + async function pollForMcpConnection() { + if (status !== 'ready' || !loggedIn || hasMcpClient) return + if (document.hidden) return + if (!isOnboardingPath(readCurrentRouterHref(handle))) return + 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. + } + } + + if (typeof document !== 'undefined') { + const 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) From 6361f3281a9cc7513c0bf3a669cee0dac765f65e Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Mon, 20 Jul 2026 14:10:30 -0600 Subject: [PATCH 3/3] Guard onboarding poll against overlap and stop once connected Review feedback: track an in-flight flag so slow responses cannot overlap, and clear the interval after a grant is detected instead of ticking forever. Co-authored-by: Cursor --- packages/worker/client/routes/onboarding.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/worker/client/routes/onboarding.tsx b/packages/worker/client/routes/onboarding.tsx index 0cb573c60f..18b263c187 100644 --- a/packages/worker/client/routes/onboarding.tsx +++ b/packages/worker/client/routes/onboarding.tsx @@ -155,10 +155,18 @@ 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 (status !== 'ready' || !loggedIn || hasMcpClient) return + 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 @@ -166,11 +174,13 @@ export function OnboardingRoute(handle: Handle) { handle.update() } catch { // Transient poll failures are fine; the next tick retries. + } finally { + pollInFlight = false } } if (typeof document !== 'undefined') { - const pollIntervalId = setInterval(pollForMcpConnection, 5000) + pollIntervalId = setInterval(pollForMcpConnection, 5000) handle.signal.addEventListener('abort', () => clearInterval(pollIntervalId)) }