From 049ce9577c444c6fd1ab238f1e99803061e7d86a Mon Sep 17 00:00:00 2001 From: Juan Francisco Lebrero Date: Fri, 13 Mar 2026 18:49:05 -0300 Subject: [PATCH] feat: simplify onboarding into two-step auth flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the flat 9-option provider list with a two-step flow: 1. How to sign in? (Browser login / API key / Skip) 2. Which provider? (filtered by auth method) This reduces cognitive load on first launch — users pick their auth method first, then see only the relevant providers. --- src/onboarding.ts | 89 ++++++++++++++++++----------------------------- 1 file changed, 34 insertions(+), 55 deletions(-) diff --git a/src/onboarding.ts b/src/onboarding.ts index 93391fc37..7460df931 100644 --- a/src/onboarding.ts +++ b/src/onboarding.ts @@ -251,49 +251,48 @@ async function runLlmStep(p: ClackModule, pc: PicoModule, authStorage: AuthStora const oauthProviders = authStorage.getOAuthProviders() const oauthMap = new Map(oauthProviders.map(op => [op.id, op])) - const choice = await p.select({ - message: 'Choose your LLM provider', + // ── Step 1: How do you want to authenticate? ───────────────────────────── + const method = await p.select({ + message: 'How do you want to sign in?', options: [ - { value: 'anthropic-oauth', label: 'Anthropic — Claude (OAuth login)', hint: 'recommended' }, - { value: 'anthropic-api-key', label: 'Anthropic — Claude (API key)' }, - { value: 'openai-api-key', label: 'OpenAI (API key)' }, - { value: 'github-copilot-oauth', label: 'GitHub Copilot (OAuth login)' }, - { value: 'openai-codex-oauth', label: 'ChatGPT Plus/Pro — Codex (OAuth login)' }, - { value: 'google-gemini-cli-oauth', label: 'Google Gemini CLI (OAuth login)' }, - { value: 'google-antigravity-oauth', label: 'Antigravity — Gemini 3, Claude, GPT-OSS (OAuth login)' }, - { value: 'other-api-key', label: 'Other provider (API key)' }, + { value: 'browser', label: 'Browser login (OAuth)', hint: 'recommended — opens your browser' }, + { value: 'api-key', label: 'API key', hint: 'paste a key from your provider' }, { value: 'skip', label: 'Skip for now', hint: 'use /login inside GSD later' }, ], }) - if (p.isCancel(choice) || choice === 'skip') return false + if (p.isCancel(method) || method === 'skip') return false - // ── OAuth flows ─────────────────────────────────────────────────────────── - if (choice === 'anthropic-oauth') { - return await runOAuthFlow(p, pc, authStorage, 'anthropic', oauthMap) - } - if (choice === 'github-copilot-oauth') { - return await runOAuthFlow(p, pc, authStorage, 'github-copilot', oauthMap) - } - if (choice === 'openai-codex-oauth') { - return await runOAuthFlow(p, pc, authStorage, 'openai-codex', oauthMap) - } - if (choice === 'google-gemini-cli-oauth') { - return await runOAuthFlow(p, pc, authStorage, 'google-gemini-cli', oauthMap) - } - if (choice === 'google-antigravity-oauth') { - return await runOAuthFlow(p, pc, authStorage, 'google-antigravity', oauthMap) + // ── Step 2: Which provider? ────────────────────────────────────────────── + if (method === 'browser') { + const provider = await p.select({ + message: 'Choose provider', + options: [ + { value: 'anthropic', label: 'Anthropic (Claude)', hint: 'recommended' }, + { value: 'github-copilot', label: 'GitHub Copilot' }, + { value: 'openai-codex', label: 'ChatGPT Plus/Pro (Codex)' }, + { value: 'google-gemini-cli', label: 'Google Gemini CLI' }, + { value: 'google-antigravity', label: 'Antigravity (Gemini 3, Claude, GPT-OSS)' }, + ], + }) + if (p.isCancel(provider)) return false + return await runOAuthFlow(p, pc, authStorage, provider as string, oauthMap) } - // ── API key flows ───────────────────────────────────────────────────────── - if (choice === 'anthropic-api-key') { - return await runApiKeyFlow(p, pc, authStorage, 'anthropic', 'Anthropic') - } - if (choice === 'openai-api-key') { - return await runApiKeyFlow(p, pc, authStorage, 'openai', 'OpenAI') - } - if (choice === 'other-api-key') { - return await runOtherProviderFlow(p, pc, authStorage) + if (method === 'api-key') { + const provider = await p.select({ + message: 'Choose provider', + options: [ + { value: 'anthropic', label: 'Anthropic (Claude)' }, + { value: 'openai', label: 'OpenAI' }, + ...OTHER_PROVIDERS.map(op => ({ value: op.value, label: op.label })), + ], + }) + if (p.isCancel(provider)) return false + const label = provider === 'anthropic' ? 'Anthropic' + : provider === 'openai' ? 'OpenAI' + : OTHER_PROVIDERS.find(op => op.value === provider)?.label ?? String(provider) + return await runApiKeyFlow(p, pc, authStorage, provider as string, label) } return false @@ -399,26 +398,6 @@ async function runApiKeyFlow( return true } -// ─── "Other Provider" Sub-Flow ──────────────────────────────────────────────── - -async function runOtherProviderFlow( - p: ClackModule, - pc: PicoModule, - authStorage: AuthStorage, -): Promise { - const provider = await p.select({ - message: 'Select provider', - options: OTHER_PROVIDERS.map(op => ({ - value: op.value, - label: op.label, - })), - }) - - if (p.isCancel(provider)) return false - const label = OTHER_PROVIDERS.find(op => op.value === provider)?.label ?? String(provider) - return runApiKeyFlow(p, pc, authStorage, provider as string, label) -} - // ─── Tool API Keys Step ─────────────────────────────────────────────────────── async function runToolKeysStep(