Repository navigation
Make onboarding responsive to MCP connection state #810
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<typeof setInterval> | 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) { | |
| <h2 mix={css(cardTitleCss)}>You are connected</h2> | ||
| <p mix={css(descriptionCss)}> | ||
| 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. | ||
| </p> | ||
| </section> | ||
| ) : null} | ||
|
|
||
| <section | ||
| id="discovery" | ||
| mix={css(cardCss)} | ||
| data-testid="onboarding-discovery" | ||
| > | ||
| <h2 mix={css(cardTitleCss)}> | ||
| Not sure what you'd use Kody for? | ||
| </h2> | ||
| <p mix={css(descriptionCss)}> | ||
| 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. | ||
| </p> | ||
| <pre mix={css(codeBlockCss)}>{discoveryPrompt}</pre> | ||
| <CopyTextButton | ||
| value={discoveryPrompt} | ||
| idleLabel="Copy discovery prompt" | ||
| variant="secondary" | ||
| /> | ||
| </section> | ||
|
|
||
| <section mix={css(cardCss)}> | ||
| <h2 mix={css(cardTitleCss)}>1. Add Kody as an MCP server</h2> | ||
| <p mix={css(descriptionCss)}> | ||
| 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. | ||
| </p> | ||
| <OnboardingMcpClientTabs mcpServerUrl={mcpServerUrl} /> | ||
| </section> | ||
| {renderOnboardingStep({ | ||
| title: '1. Not sure what you\u2019d use Kody for?', | ||
| collapsed: hasMcpClient, | ||
| id: 'discovery', | ||
| testId: 'onboarding-discovery', | ||
| children: ( | ||
| <> | ||
| <p mix={css(descriptionCss)}> | ||
| 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. | ||
| </p> | ||
| <pre mix={css(codeBlockCss)}>{discoveryPrompt}</pre> | ||
| <CopyTextButton | ||
| value={discoveryPrompt} | ||
| idleLabel="Copy discovery prompt" | ||
| variant="secondary" | ||
| /> | ||
| </> | ||
| ), | ||
| })} | ||
|
|
||
| {renderByokExplainer({ image: 'handoff' })} | ||
| {renderOnboardingStep({ | ||
| title: '2. Add Kody as an MCP server', | ||
| collapsed: hasMcpClient, | ||
| children: ( | ||
| <> | ||
| <p mix={css(descriptionCss)}> | ||
| 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. | ||
| </p> | ||
| <OnboardingMcpClientTabs mcpServerUrl={mcpServerUrl} /> | ||
| </> | ||
| ), | ||
| })} | ||
|
|
||
| <section | ||
| mix={css(cardCss)} | ||
| data-testid="onboarding-starter-packages" | ||
| > | ||
| <h2 mix={css(cardTitleCss)}>2. Install a starter package</h2> | ||
| <h2 mix={css(cardTitleCss)}>3. Install a starter package</h2> | ||
| <p mix={css(descriptionCss)}> | ||
| {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,40 +314,77 @@ export function OnboardingRoute(handle: Handle) { | |
| </p> | ||
| </section> | ||
|
|
||
| <p mix={css({ margin: 0 })}> | ||
| {loggedIn ? ( | ||
| <> | ||
| <a href="/account" mix={css(mutedLinkCss)}> | ||
| Back to account | ||
| </a> | ||
| {' · '} | ||
| <a href="/account/secrets" mix={css(primaryLinkCss)}> | ||
| Secrets | ||
| </a> | ||
| {' · '} | ||
| <a href="/account/integrations" mix={css(primaryLinkCss)}> | ||
| Integrations | ||
| </a> | ||
| </> | ||
| ) : ( | ||
| <> | ||
| <a href="/signup" mix={css(primaryLinkCss)}> | ||
| Sign up | ||
| </a> | ||
| {' · '} | ||
| <a href="/login" mix={css(mutedLinkCss)}> | ||
| Log in | ||
| </a> | ||
| </> | ||
| )} | ||
| </p> | ||
| {renderByokExplainer({ image: 'handoff' })} | ||
|
|
||
| {loggedIn ? null : ( | ||
| <p mix={css({ margin: 0 })}> | ||
| <a href="/signup" mix={css(primaryLinkCss)}> | ||
| Sign up | ||
| </a> | ||
| {' · '} | ||
| <a href="/login" mix={css(mutedLinkCss)}> | ||
| Log in | ||
| </a> | ||
| </p> | ||
| )} | ||
| </> | ||
| ) : null} | ||
| </AccountManagementShell> | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| type OnboardingStepSlot = any | ||
|
|
||
| /** | ||
| * Renders an onboarding step as an open card, or — once an MCP client is | ||
| * already connected — as a collapsed `<details>` 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 ( | ||
| <section id={input.id} mix={css(cardCss)} data-testid={input.testId}> | ||
| <h2 mix={css(cardTitleCss)}>{input.title}</h2> | ||
| {input.children} | ||
| </section> | ||
| ) | ||
| } | ||
| return ( | ||
| <details id={input.id} mix={css(cardCss)} data-testid={input.testId}> | ||
| <summary mix={css(collapsedStepSummaryCss)}> | ||
| <h2 mix={css(collapsedStepTitleCss)}>{input.title}</h2> | ||
| <span mix={css(collapsedStepHintCss)}>Done — open to revisit</span> | ||
| </summary> | ||
| {input.children} | ||
| </details> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Discovery hash stays collapsedMedium Severity When Reviewed by Cursor Bugbot for commit 6361f32. Configure here. |
||
| ) | ||
| } | ||
|
|
||
| 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, | ||
|
|
||


Uh oh!
There was an error while loading. Please reload this page.