diff --git a/docs/plans/2026-01-03-welcome-onboarding.md b/docs/plans/2026-01-03-welcome-onboarding.md new file mode 100644 index 000000000..23a87bcb0 --- /dev/null +++ b/docs/plans/2026-01-03-welcome-onboarding.md @@ -0,0 +1,1464 @@ +# Welcome Onboarding Flow Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Create an interactive welcome wizard for first-run users that guides them through provider selection, authentication, and optional profile saving. + +**Architecture:** Multi-step dialog component with state machine, leveraging existing RadioButtonSelect, auth infrastructure, and ProfileManager. Triggers after folder trust dialog, persists completion flag to `~/.llxprt/welcomeConfig.json`. + +**Tech Stack:** React + Ink, TypeScript, existing ProviderManager/ProfileManager APIs + +--- + +## Task 1: Create Welcome Config Service + +**Files:** + +- Create: `packages/cli/src/config/welcomeConfig.ts` + +**Step 1: Create the welcome config module** + +```typescript +/** + * @license + * Copyright 2025 Vybestack LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { USER_SETTINGS_DIR } from './paths.js'; + +export const WELCOME_CONFIG_FILENAME = 'welcomeConfig.json'; + +export function getWelcomeConfigPath(): string { + if (process.env['LLXPRT_CODE_WELCOME_CONFIG_PATH']) { + return process.env['LLXPRT_CODE_WELCOME_CONFIG_PATH']; + } + return path.join(USER_SETTINGS_DIR, WELCOME_CONFIG_FILENAME); +} + +export interface WelcomeConfig { + welcomeCompleted: boolean; + completedAt?: string; + skipped?: boolean; +} + +let cachedConfig: WelcomeConfig | undefined; + +export function resetWelcomeConfigForTesting(): void { + cachedConfig = undefined; +} + +export function loadWelcomeConfig(): WelcomeConfig { + if (cachedConfig) { + return cachedConfig; + } + + const configPath = getWelcomeConfigPath(); + + try { + if (fs.existsSync(configPath)) { + const content = fs.readFileSync(configPath, 'utf-8'); + cachedConfig = JSON.parse(content) as WelcomeConfig; + return cachedConfig; + } + } catch (_error) { + // If parsing fails, return default + } + + cachedConfig = { welcomeCompleted: false }; + return cachedConfig; +} + +export function saveWelcomeConfig(config: WelcomeConfig): void { + const configPath = getWelcomeConfigPath(); + + try { + const dirPath = path.dirname(configPath); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + + fs.writeFileSync(configPath, JSON.stringify(config, null, 2), { + encoding: 'utf-8', + mode: 0o600, + }); + cachedConfig = config; + } catch (error) { + console.error('Error saving welcome config:', error); + } +} + +export function markWelcomeCompleted(skipped: boolean = false): void { + saveWelcomeConfig({ + welcomeCompleted: true, + completedAt: new Date().toISOString(), + skipped, + }); +} + +export function isWelcomeCompleted(): boolean { + return loadWelcomeConfig().welcomeCompleted; +} +``` + +**Step 2: Commit** + +```bash +git add packages/cli/src/config/welcomeConfig.ts +git commit -m "feat(welcome): add welcome config service for first-run detection" +``` + +--- + +## Task 2: Create Welcome State Types and Hook + +**Files:** + +- Create: `packages/cli/src/ui/hooks/useWelcomeOnboarding.ts` + +**Step 1: Create the hook with state management** + +```typescript +/** + * @license + * Copyright 2025 Vybestack LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useState, useCallback, useEffect } from 'react'; +import { type Config, DebugLogger } from '@vybestack/llxprt-code-core'; +import { LoadedSettings } from '../../config/settings.js'; +import { + isWelcomeCompleted, + markWelcomeCompleted, +} from '../../config/welcomeConfig.js'; +import { useRuntimeApi } from '../contexts/RuntimeContext.js'; + +const debug = new DebugLogger('llxprt:ui:useWelcomeOnboarding'); + +export type WelcomeStep = + | 'welcome' + | 'provider' + | 'auth_method' + | 'authenticating' + | 'completion' + | 'skipped'; + +export interface WelcomeState { + step: WelcomeStep; + selectedProvider?: string; + selectedAuthMethod?: 'oauth' | 'api_key'; + authInProgress: boolean; + error?: string; +} + +export interface WelcomeActions { + startSetup: () => void; + selectProvider: (providerId: string) => void; + selectAuthMethod: (method: 'oauth' | 'api_key') => void; + onAuthComplete: () => void; + onAuthError: (error: string) => void; + skipSetup: () => void; + goBack: () => void; + saveProfile: (name: string) => Promise; + dismiss: () => void; +} + +export interface UseWelcomeOnboardingReturn { + showWelcome: boolean; + welcomeState: WelcomeState; + welcomeActions: WelcomeActions; + availableProviders: string[]; +} + +export const useWelcomeOnboarding = ( + config: Config, + settings: LoadedSettings, +): UseWelcomeOnboardingReturn => { + const runtime = useRuntimeApi(); + const [showWelcome, setShowWelcome] = useState(() => !isWelcomeCompleted()); + + const [state, setState] = useState({ + step: 'welcome', + authInProgress: false, + }); + + const [availableProviders, setAvailableProviders] = useState([]); + + // Load available providers on mount + useEffect(() => { + const providerManager = runtime.getCliProviderManager(); + if (providerManager) { + const providers = providerManager.listProviders(); + setAvailableProviders(providers); + debug.log( + `Loaded ${providers.length} providers: ${providers.join(', ')}`, + ); + } + }, [runtime]); + + const startSetup = useCallback(() => { + setState((prev) => ({ ...prev, step: 'provider' })); + }, []); + + const selectProvider = useCallback((providerId: string) => { + setState((prev) => ({ + ...prev, + selectedProvider: providerId, + step: 'auth_method', + })); + }, []); + + const selectAuthMethod = useCallback((method: 'oauth' | 'api_key') => { + setState((prev) => ({ + ...prev, + selectedAuthMethod: method, + step: 'authenticating', + authInProgress: true, + })); + }, []); + + const onAuthComplete = useCallback(() => { + const providerManager = runtime.getCliProviderManager(); + if (providerManager && state.selectedProvider) { + try { + providerManager.setActiveProvider(state.selectedProvider); + debug.log(`Set active provider to: ${state.selectedProvider}`); + } catch (error) { + debug.log(`Failed to set active provider: ${error}`); + } + } + + setState((prev) => ({ + ...prev, + step: 'completion', + authInProgress: false, + error: undefined, + })); + }, [runtime, state.selectedProvider]); + + const onAuthError = useCallback((error: string) => { + setState((prev) => ({ + ...prev, + authInProgress: false, + error, + step: 'auth_method', + })); + }, []); + + const skipSetup = useCallback(() => { + setState((prev) => ({ ...prev, step: 'skipped' })); + }, []); + + const goBack = useCallback(() => { + setState((prev) => { + switch (prev.step) { + case 'auth_method': + return { ...prev, step: 'provider', selectedProvider: undefined }; + case 'authenticating': + return { + ...prev, + step: 'auth_method', + selectedAuthMethod: undefined, + authInProgress: false, + }; + case 'provider': + return { ...prev, step: 'welcome' }; + default: + return prev; + } + }); + }, []); + + const saveProfile = useCallback( + async (name: string) => { + try { + const profileManager = runtime.getProfileManager(); + const settingsService = runtime.getSettingsService(); + if (profileManager && settingsService) { + await profileManager.save(name, settingsService); + debug.log(`Saved profile: ${name}`); + } + } catch (error) { + debug.log(`Failed to save profile: ${error}`); + throw error; + } + }, + [runtime], + ); + + const dismiss = useCallback(() => { + const skipped = state.step === 'skipped'; + markWelcomeCompleted(skipped); + setShowWelcome(false); + debug.log(`Welcome flow completed (skipped: ${skipped})`); + }, [state.step]); + + return { + showWelcome, + welcomeState: state, + welcomeActions: { + startSetup, + selectProvider, + selectAuthMethod, + onAuthComplete, + onAuthError, + skipSetup, + goBack, + saveProfile, + dismiss, + }, + availableProviders, + }; +}; +``` + +**Step 2: Commit** + +```bash +git add packages/cli/src/ui/hooks/useWelcomeOnboarding.ts +git commit -m "feat(welcome): add useWelcomeOnboarding hook for state management" +``` + +--- + +## Task 3: Create WelcomeStep Component + +**Files:** + +- Create: `packages/cli/src/ui/components/WelcomeOnboarding/WelcomeStep.tsx` + +**Step 1: Create the welcome step component** + +```typescript +/** + * @license + * Copyright 2025 Vybestack LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { Box, Text } from 'ink'; +import { Colors } from '../../colors.js'; +import { + RadioButtonSelect, + type RadioSelectItem, +} from '../shared/RadioButtonSelect.js'; + +export type WelcomeChoice = 'setup' | 'skip'; + +interface WelcomeStepProps { + onSelect: (choice: WelcomeChoice) => void; + isFocused?: boolean; +} + +export const WelcomeStep: React.FC = ({ + onSelect, + isFocused = true, +}) => { + const options: Array> = [ + { + label: 'Set up now (recommended)', + value: 'setup', + key: 'setup', + }, + { + label: 'Skip setup (I know what I\'m doing)', + value: 'skip', + key: 'skip', + }, + ]; + + return ( + + + + Welcome to llxprt! + + + Let's get you set up in just a few steps. + + You'll choose an AI provider and configure authentication + + so llxprt can work its magic. + + + + What would you like to do? + + + + + + + Use ↑↓ to navigate, Enter to select, Esc to skip + + + + ); +}; +``` + +**Step 2: Commit** + +```bash +git add packages/cli/src/ui/components/WelcomeOnboarding/WelcomeStep.tsx +git commit -m "feat(welcome): add WelcomeStep component" +``` + +--- + +## Task 4: Create ProviderSelectStep Component + +**Files:** + +- Create: `packages/cli/src/ui/components/WelcomeOnboarding/ProviderSelectStep.tsx` + +**Step 1: Create the provider selection component** + +```typescript +/** + * @license + * Copyright 2025 Vybestack LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useMemo } from 'react'; +import { Box, Text } from 'ink'; +import { Colors } from '../../colors.js'; +import { + RadioButtonSelect, + type RadioSelectItem, +} from '../shared/RadioButtonSelect.js'; + +const PROVIDER_DISPLAY_NAMES: Record = { + anthropic: 'Anthropic (Claude)', + openai: 'OpenAI (GPT-4, etc.)', + gemini: 'Google Gemini', + deepseek: 'DeepSeek', + qwen: 'Qwen', + 'openai-responses': 'OpenAI Responses API', + openaivercel: 'OpenAI (Vercel AI SDK)', +}; + +interface ProviderSelectStepProps { + providers: string[]; + onSelect: (providerId: string) => void; + onSkip: () => void; + isFocused?: boolean; +} + +export const ProviderSelectStep: React.FC = ({ + providers, + onSelect, + onSkip, + isFocused = true, +}) => { + const options: Array> = useMemo(() => { + const providerOptions = providers.map((provider) => ({ + label: PROVIDER_DISPLAY_NAMES[provider] || provider, + value: provider, + key: provider, + })); + + // Add "configure manually" option + providerOptions.push({ + label: 'Configure manually later', + value: '__skip__', + key: '__skip__', + }); + + return providerOptions; + }, [providers]); + + const handleSelect = (value: string) => { + if (value === '__skip__') { + onSkip(); + } else { + onSelect(value); + } + }; + + return ( + + + + Step 1 of 3: Choose Your AI Provider + + + Select which AI provider you'd like to use: + + + + + + + Use ↑↓ to navigate, Enter to select, Esc to skip + + + + ); +}; +``` + +**Step 2: Commit** + +```bash +git add packages/cli/src/ui/components/WelcomeOnboarding/ProviderSelectStep.tsx +git commit -m "feat(welcome): add ProviderSelectStep component" +``` + +--- + +## Task 5: Create AuthMethodStep Component + +**Files:** + +- Create: `packages/cli/src/ui/components/WelcomeOnboarding/AuthMethodStep.tsx` + +**Step 1: Create the auth method selection component** + +```typescript +/** + * @license + * Copyright 2025 Vybestack LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useMemo } from 'react'; +import { Box, Text } from 'ink'; +import { Colors } from '../../colors.js'; +import { + RadioButtonSelect, + type RadioSelectItem, +} from '../shared/RadioButtonSelect.js'; + +// Providers that support OAuth +const OAUTH_PROVIDERS = new Set(['anthropic', 'openai', 'gemini', 'qwen']); + +const API_KEY_URLS: Record = { + anthropic: 'console.anthropic.com/settings/keys', + openai: 'platform.openai.com/api-keys', + gemini: 'aistudio.google.com/app/apikey', + deepseek: 'platform.deepseek.com/api_keys', + qwen: 'dashscope.console.aliyun.com/apiKey', +}; + +type AuthMethod = 'oauth' | 'api_key' | 'back'; + +interface AuthMethodStepProps { + provider: string; + onSelect: (method: 'oauth' | 'api_key') => void; + onBack: () => void; + error?: string; + isFocused?: boolean; +} + +export const AuthMethodStep: React.FC = ({ + provider, + onSelect, + onBack, + error, + isFocused = true, +}) => { + const supportsOAuth = OAUTH_PROVIDERS.has(provider); + const apiKeyUrl = API_KEY_URLS[provider]; + + const options: Array> = useMemo(() => { + const opts: Array> = []; + + if (supportsOAuth) { + opts.push({ + label: 'OAuth (Recommended - secure & easy)', + value: 'oauth', + key: 'oauth', + }); + } + + opts.push({ + label: 'API Key', + value: 'api_key', + key: 'api_key', + }); + + opts.push({ + label: '← Back to provider selection', + value: 'back', + key: 'back', + }); + + return opts; + }, [supportsOAuth]); + + const handleSelect = (value: AuthMethod) => { + if (value === 'back') { + onBack(); + } else { + onSelect(value); + } + }; + + const providerDisplay = + provider.charAt(0).toUpperCase() + provider.slice(1); + + return ( + + + + Step 2 of 3: Choose Authentication Method + + + How would you like to authenticate with {providerDisplay}? + + + {error && ( + + {error} + + )} + + + + {apiKeyUrl && ( + + Get API key at: {apiKeyUrl} + + )} + + + + Use ↑↓ to navigate, Enter to select, Esc to skip + + + + ); +}; +``` + +**Step 2: Commit** + +```bash +git add packages/cli/src/ui/components/WelcomeOnboarding/AuthMethodStep.tsx +git commit -m "feat(welcome): add AuthMethodStep component" +``` + +--- + +## Task 6: Create AuthenticationStep Component + +**Files:** + +- Create: `packages/cli/src/ui/components/WelcomeOnboarding/AuthenticationStep.tsx` + +**Step 1: Create the authentication step component** + +```typescript +/** + * @license + * Copyright 2025 Vybestack LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useEffect, useCallback } from 'react'; +import { Box, Text } from 'ink'; +import TextInput from 'ink-text-input'; +import { Colors } from '../../colors.js'; +import { useKeypress } from '../../hooks/useKeypress.js'; +import Spinner from 'ink-spinner'; + +interface AuthenticationStepProps { + provider: string; + method: 'oauth' | 'api_key'; + onComplete: () => void; + onError: (error: string) => void; + onBack: () => void; + triggerAuth: (provider: string, method: 'oauth' | 'api_key', apiKey?: string) => Promise; + isFocused?: boolean; +} + +export const AuthenticationStep: React.FC = ({ + provider, + method, + onComplete, + onError, + onBack, + triggerAuth, + isFocused = true, +}) => { + const [apiKey, setApiKey] = useState(''); + const [isAuthenticating, setIsAuthenticating] = useState(false); + const [showApiKeyInput, setShowApiKeyInput] = useState(method === 'api_key'); + + const providerDisplay = + provider.charAt(0).toUpperCase() + provider.slice(1); + + // Handle escape to go back + useKeypress( + (key) => { + if (key.name === 'escape' && !isAuthenticating) { + onBack(); + } + }, + { isActive: isFocused && !isAuthenticating }, + ); + + // Start OAuth flow automatically + useEffect(() => { + if (method === 'oauth' && !isAuthenticating) { + setIsAuthenticating(true); + triggerAuth(provider, 'oauth') + .then(() => { + onComplete(); + }) + .catch((error) => { + setIsAuthenticating(false); + onError(error instanceof Error ? error.message : String(error)); + }); + } + }, [method, provider, triggerAuth, onComplete, onError, isAuthenticating]); + + const handleApiKeySubmit = useCallback(async () => { + if (!apiKey.trim()) { + return; + } + + setIsAuthenticating(true); + try { + await triggerAuth(provider, 'api_key', apiKey.trim()); + onComplete(); + } catch (error) { + setIsAuthenticating(false); + onError(error instanceof Error ? error.message : String(error)); + } + }, [apiKey, provider, triggerAuth, onComplete, onError]); + + if (method === 'oauth') { + return ( + + + + Step 3 of 3: Authenticating with {providerDisplay} + + + + + + + + + {' '}Opening browser for OAuth authentication... + + + + Please complete the authentication in your browser. + This window will update when done. + + + Press Esc to cancel and go back + + + ); + } + + return ( + + + + Step 3 of 3: Enter Your API Key + + + + {isAuthenticating ? ( + + + + + + {' '}Validating API key... + + + ) : ( + <> + + Enter your {providerDisplay} API key: + + + + API Key: + + + + )} + + + + Press Enter when done, Esc to go back + + + + ); +}; +``` + +**Step 2: Commit** + +```bash +git add packages/cli/src/ui/components/WelcomeOnboarding/AuthenticationStep.tsx +git commit -m "feat(welcome): add AuthenticationStep component" +``` + +--- + +## Task 7: Create CompletionStep Component + +**Files:** + +- Create: `packages/cli/src/ui/components/WelcomeOnboarding/CompletionStep.tsx` + +**Step 1: Create the completion step component** + +```typescript +/** + * @license + * Copyright 2025 Vybestack LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useCallback } from 'react'; +import { Box, Text } from 'ink'; +import TextInput from 'ink-text-input'; +import { Colors } from '../../colors.js'; +import { useKeypress } from '../../hooks/useKeypress.js'; + +interface CompletionStepProps { + provider: string; + authMethod: 'oauth' | 'api_key'; + onSaveProfile: (name: string) => Promise; + onDismiss: () => void; + isFocused?: boolean; +} + +export const CompletionStep: React.FC = ({ + provider, + authMethod, + onSaveProfile, + onDismiss, + isFocused = true, +}) => { + const [showProfilePrompt, setShowProfilePrompt] = useState(true); + const [profileName, setProfileName] = useState(''); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(); + + const providerDisplay = + provider.charAt(0).toUpperCase() + provider.slice(1); + const authDisplay = authMethod === 'oauth' ? 'OAuth' : 'API Key'; + + // Handle escape to skip profile save + useKeypress( + (key) => { + if (key.name === 'escape' && showProfilePrompt && !saving) { + setShowProfilePrompt(false); + } + }, + { isActive: isFocused && showProfilePrompt && !saving }, + ); + + // Handle enter to dismiss after profile step + useKeypress( + (key) => { + if (key.name === 'return' && !showProfilePrompt) { + onDismiss(); + } + }, + { isActive: isFocused && !showProfilePrompt }, + ); + + const handleProfileSubmit = useCallback(async () => { + if (!profileName.trim()) { + setShowProfilePrompt(false); + return; + } + + setSaving(true); + setError(undefined); + + try { + await onSaveProfile(profileName.trim()); + setShowProfilePrompt(false); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to save profile'); + setSaving(false); + } + }, [profileName, onSaveProfile]); + + return ( + + + + ✓ You're all set! + + + + + Provider: {providerDisplay} + Authentication: {authDisplay} + + + {showProfilePrompt ? ( + + + Save this setup as a profile? (optional) + + + + Profiles let you quickly switch between configurations. + + + Use /profile load <name> to restore this setup later. + + + + {error && ( + + {error} + + )} + + {saving ? ( + Saving profile... + ) : ( + + Profile name: + + + )} + + + + + Enter a name and press Enter to save, or Esc to skip + + + + ) : ( + + + Try asking me something like: + + "Explain how async/await works in JavaScript" + + + + + Press Enter to continue... + + + )} + + ); +}; +``` + +**Step 2: Commit** + +```bash +git add packages/cli/src/ui/components/WelcomeOnboarding/CompletionStep.tsx +git commit -m "feat(welcome): add CompletionStep component with profile save" +``` + +--- + +## Task 8: Create SkipExitStep Component + +**Files:** + +- Create: `packages/cli/src/ui/components/WelcomeOnboarding/SkipExitStep.tsx` + +**Step 1: Create the skip exit step component** + +```typescript +/** + * @license + * Copyright 2025 Vybestack LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { Box, Text } from 'ink'; +import { Colors } from '../../colors.js'; +import { useKeypress } from '../../hooks/useKeypress.js'; + +interface SkipExitStepProps { + onDismiss: () => void; + isFocused?: boolean; +} + +export const SkipExitStep: React.FC = ({ + onDismiss, + isFocused = true, +}) => { + useKeypress( + (key) => { + if (key.name === 'return') { + onDismiss(); + } + }, + { isActive: isFocused }, + ); + + return ( + + + Setup skipped + + + + To configure llxprt manually: + + + • Use /auth <provider> to + set up authentication + + + • Use /provider to select your + AI provider + + + • Type /help for more commands + + + + + Press Enter to continue... + + + ); +}; +``` + +**Step 2: Commit** + +```bash +git add packages/cli/src/ui/components/WelcomeOnboarding/SkipExitStep.tsx +git commit -m "feat(welcome): add SkipExitStep component" +``` + +--- + +## Task 9: Create Main WelcomeDialog Component + +**Files:** + +- Create: `packages/cli/src/ui/components/WelcomeOnboarding/WelcomeDialog.tsx` +- Create: `packages/cli/src/ui/components/WelcomeOnboarding/index.ts` + +**Step 1: Create the main dialog component** + +```typescript +/** + * @license + * Copyright 2025 Vybestack LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback } from 'react'; +import { Box } from 'ink'; +import { Colors } from '../../colors.js'; +import { useKeypress } from '../../hooks/useKeypress.js'; +import { WelcomeStep, type WelcomeChoice } from './WelcomeStep.js'; +import { ProviderSelectStep } from './ProviderSelectStep.js'; +import { AuthMethodStep } from './AuthMethodStep.js'; +import { AuthenticationStep } from './AuthenticationStep.js'; +import { CompletionStep } from './CompletionStep.js'; +import { SkipExitStep } from './SkipExitStep.js'; +import type { WelcomeState, WelcomeActions } from '../../hooks/useWelcomeOnboarding.js'; + +interface WelcomeDialogProps { + state: WelcomeState; + actions: WelcomeActions; + availableProviders: string[]; + triggerAuth: ( + provider: string, + method: 'oauth' | 'api_key', + apiKey?: string, + ) => Promise; +} + +export const WelcomeDialog: React.FC = ({ + state, + actions, + availableProviders, + triggerAuth, +}) => { + // Handle global escape to skip (except during auth) + useKeypress( + (key) => { + if (key.name === 'escape' && !state.authInProgress) { + actions.skipSetup(); + } + }, + { isActive: state.step !== 'completion' && state.step !== 'skipped' }, + ); + + const handleWelcomeSelect = useCallback( + (choice: WelcomeChoice) => { + if (choice === 'setup') { + actions.startSetup(); + } else { + actions.skipSetup(); + } + }, + [actions], + ); + + const renderStep = () => { + switch (state.step) { + case 'welcome': + return ; + + case 'provider': + return ( + + ); + + case 'auth_method': + return ( + + ); + + case 'authenticating': + return ( + + ); + + case 'completion': + return ( + + ); + + case 'skipped': + return ; + + default: + return null; + } + }; + + return ( + + {renderStep()} + + ); +}; +``` + +**Step 2: Create the index file** + +```typescript +/** + * @license + * Copyright 2025 Vybestack LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export { WelcomeDialog } from './WelcomeDialog.js'; +export { WelcomeStep } from './WelcomeStep.js'; +export { ProviderSelectStep } from './ProviderSelectStep.js'; +export { AuthMethodStep } from './AuthMethodStep.js'; +export { AuthenticationStep } from './AuthenticationStep.js'; +export { CompletionStep } from './CompletionStep.js'; +export { SkipExitStep } from './SkipExitStep.js'; +``` + +**Step 3: Commit** + +```bash +git add packages/cli/src/ui/components/WelcomeOnboarding/ +git commit -m "feat(welcome): add WelcomeDialog main component and exports" +``` + +--- + +## Task 10: Integrate into AppContainer + +**Files:** + +- Modify: `packages/cli/src/ui/AppContainer.tsx` + +**Step 1: Add imports at top of file** + +Add after existing imports (around line 100): + +```typescript +import { useWelcomeOnboarding } from './hooks/useWelcomeOnboarding.js'; +import { WelcomeDialog } from './components/WelcomeOnboarding/index.js'; +``` + +**Step 2: Add hook usage** + +Add inside AppContainer function, after the useFolderTrust hook (around line 625): + +```typescript +const { showWelcome, welcomeState, welcomeActions, availableProviders } = + useWelcomeOnboarding(config, settings); +``` + +**Step 3: Add triggerAuth function** + +Add the auth trigger function (after the welcome hook): + +```typescript +const triggerWelcomeAuth = useCallback( + async (provider: string, method: 'oauth' | 'api_key', apiKey?: string) => { + if (method === 'api_key' && apiKey) { + // Set API key in settings + const settingsService = runtime.getSettingsService(); + if (settingsService) { + settingsService.setProviderSetting(provider, 'apiKey', apiKey); + } + } else if (method === 'oauth') { + // Trigger OAuth flow + await config.refreshAuth(`oauth_${provider}`); + } + }, + [config, runtime], +); +``` + +**Step 4: Add welcome dialog to the render** + +In the `uiState` object, add to UIState interface check (around line 1475): + +```typescript + // Welcome flow + showWelcome, +``` + +Update the condition for initial prompt submission (around line 1474): + +```typescript +useEffect(() => { + if ( + initialPrompt && + !initialPromptSubmitted.current && + !isAuthenticating && + !isAuthDialogOpen && + !isThemeDialogOpen && + !isEditorDialogOpen && + !isProviderDialogOpen && + !isProviderModelDialogOpen && + !isToolsDialogOpen && + !showPrivacyNotice && + !showWelcome && // Add this + geminiClient + ) { + submitQuery(initialPrompt); + initialPromptSubmitted.current = true; + } +}, [ + // ... existing deps + showWelcome, // Add this +]); +``` + +**Step 5: Render welcome dialog in DefaultAppLayout** + +The WelcomeDialog should be rendered in the layout. Modify `packages/cli/src/ui/layouts/DefaultAppLayout.tsx` to include welcome dialog before main content when `showWelcome` is true. + +Add to UIState interface in `packages/cli/src/ui/contexts/UIStateContext.ts`: + +```typescript +showWelcome: boolean; +``` + +**Step 6: Commit** + +```bash +git add packages/cli/src/ui/AppContainer.tsx +git add packages/cli/src/ui/contexts/UIStateContext.ts +git commit -m "feat(welcome): integrate welcome onboarding into AppContainer" +``` + +--- + +## Task 11: Update DefaultAppLayout + +**Files:** + +- Modify: `packages/cli/src/ui/layouts/DefaultAppLayout.tsx` + +**Step 1: Add welcome dialog rendering** + +Import and render the WelcomeDialog when `showWelcome` is true, after folder trust dialog check but before main content. + +```typescript +// At the top of the render, after isFolderTrustDialogOpen check: +if (!isFolderTrustDialogOpen && showWelcome) { + return ( + + ); +} +``` + +**Step 2: Commit** + +```bash +git add packages/cli/src/ui/layouts/DefaultAppLayout.tsx +git commit -m "feat(welcome): render welcome dialog in DefaultAppLayout" +``` + +--- + +## Task 12: Run Build and Fix Errors + +**Step 1: Run typecheck** + +```bash +npm run typecheck +``` + +Expected: May have errors to fix + +**Step 2: Fix any type errors** + +Address any TypeScript errors that arise. + +**Step 3: Run lint** + +```bash +npm run lint +``` + +**Step 4: Fix lint errors** + +Address any linting issues. + +**Step 5: Run format** + +```bash +npm run format +``` + +**Step 6: Run build** + +```bash +npm run build +``` + +**Step 7: Commit fixes** + +```bash +git add -A +git commit -m "fix(welcome): address build and lint errors" +``` + +--- + +## Task 13: Manual Testing + +**Step 1: Reset welcome config for testing** + +```bash +rm -f ~/.llxprt/welcomeConfig.json +``` + +**Step 2: Run llxprt** + +```bash +npm run bundle && node scripts/start.js +``` + +**Step 3: Test welcome flow** + +- Verify welcome screen appears +- Test "Set up now" path +- Test provider selection +- Test auth method selection +- Test skip via ESC +- Test "Skip setup" option +- Verify profile save prompt + +**Step 4: Verify persistence** + +Exit and restart llxprt - welcome should NOT appear again. + +--- + +## Summary + +This plan creates: + +- `welcomeConfig.ts` - First-run detection service +- `useWelcomeOnboarding.ts` - State management hook +- `WelcomeOnboarding/` component folder with 6 step components +- Integration into AppContainer and DefaultAppLayout + +The flow: + +1. After folder trust → check `welcomeCompleted` +2. If false → show welcome dialog +3. User navigates: Welcome → Provider → Auth Method → Auth → Completion +4. ESC from any step → Skip exit +5. On completion/skip → mark `welcomeCompleted: true` +6. Never show again diff --git a/packages/cli/src/config/welcomeConfig.ts b/packages/cli/src/config/welcomeConfig.ts new file mode 100644 index 000000000..c42672bb1 --- /dev/null +++ b/packages/cli/src/config/welcomeConfig.ts @@ -0,0 +1,82 @@ +/** + * @license + * Copyright 2025 Vybestack LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { USER_SETTINGS_DIR } from './paths.js'; + +export const WELCOME_CONFIG_FILENAME = 'welcomeConfig.json'; + +export function getWelcomeConfigPath(): string { + if (process.env['LLXPRT_CODE_WELCOME_CONFIG_PATH']) { + return process.env['LLXPRT_CODE_WELCOME_CONFIG_PATH']; + } + return path.join(USER_SETTINGS_DIR, WELCOME_CONFIG_FILENAME); +} + +export interface WelcomeConfig { + welcomeCompleted: boolean; + completedAt?: string; + skipped?: boolean; +} + +let cachedConfig: WelcomeConfig | undefined; + +export function resetWelcomeConfigForTesting(): void { + cachedConfig = undefined; +} + +export function loadWelcomeConfig(): WelcomeConfig { + if (cachedConfig) { + return cachedConfig; + } + + const configPath = getWelcomeConfigPath(); + + try { + if (fs.existsSync(configPath)) { + const content = fs.readFileSync(configPath, 'utf-8'); + cachedConfig = JSON.parse(content) as WelcomeConfig; + return cachedConfig; + } + } catch (_error) { + // If parsing fails, return default + } + + cachedConfig = { welcomeCompleted: false }; + return cachedConfig; +} + +export function saveWelcomeConfig(config: WelcomeConfig): void { + const configPath = getWelcomeConfigPath(); + + try { + const dirPath = path.dirname(configPath); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + + fs.writeFileSync(configPath, JSON.stringify(config, null, 2), { + encoding: 'utf-8', + mode: 0o600, + }); + cachedConfig = config; + } catch (error) { + console.error('Error saving welcome config:', error); + } +} + +export function markWelcomeCompleted(skipped: boolean = false): void { + saveWelcomeConfig({ + welcomeCompleted: true, + completedAt: new Date().toISOString(), + skipped, + }); +} + +export function isWelcomeCompleted(): boolean { + return loadWelcomeConfig().welcomeCompleted; +} diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 1f99d8815..60160d41d 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -27,6 +27,7 @@ import { useLoadingIndicator } from './hooks/useLoadingIndicator.js'; import { useThemeCommand } from './hooks/useThemeCommand.js'; import { useAuthCommand } from './hooks/useAuthCommand.js'; import { useFolderTrust } from './hooks/useFolderTrust.js'; +import { useWelcomeOnboarding } from './hooks/useWelcomeOnboarding.js'; import { useIdeTrustListener } from './hooks/useIdeTrustListener.js'; import { useEditorSettings } from './hooks/useEditorSettings.js'; import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js'; @@ -625,6 +626,19 @@ export const AppContainer = (props: AppContainerProps) => { const { isFolderTrustDialogOpen, handleFolderTrustSelect, isRestarting } = useFolderTrust(settings, config, addItem); + // Welcome onboarding - shown after folder trust, before other dialogs + const { + showWelcome: isWelcomeDialogOpen, + state: welcomeState, + actions: welcomeActions, + availableProviders: welcomeAvailableProviders, + availableModels: welcomeAvailableModels, + triggerAuth: triggerWelcomeAuth, + } = useWelcomeOnboarding({ + settings, + isFolderTrustComplete: !isFolderTrustDialogOpen && !isRestarting, + }); + const { needsRestart: ideNeedsRestart } = useIdeTrustListener(config); useEffect(() => { if (ideNeedsRestart) { @@ -1483,6 +1497,7 @@ export const AppContainer = (props: AppContainerProps) => { !isProviderModelDialogOpen && !isToolsDialogOpen && !showPrivacyNotice && + !isWelcomeDialogOpen && geminiClient ) { submitQuery(initialPrompt); @@ -1499,6 +1514,7 @@ export const AppContainer = (props: AppContainerProps) => { isProviderModelDialogOpen, isToolsDialogOpen, showPrivacyNotice, + isWelcomeDialogOpen, geminiClient, ]); @@ -1632,6 +1648,12 @@ export const AppContainer = (props: AppContainerProps) => { isRestarting, isTrustedFolder: config.isTrustedFolder(), + // Welcome onboarding + isWelcomeDialogOpen, + welcomeState, + welcomeAvailableProviders, + welcomeAvailableModels, + // Input history inputHistory: inputHistoryStore.inputHistory, @@ -1709,6 +1731,10 @@ export const AppContainer = (props: AppContainerProps) => { // Folder trust dialog handleFolderTrustSelect, + // Welcome onboarding + welcomeActions, + triggerWelcomeAuth, + // Permissions dialog openPermissionsDialog, closePermissionsDialog, @@ -1792,6 +1818,8 @@ export const AppContainer = (props: AppContainerProps) => { handleToolsSelect, exitToolsDialog, handleFolderTrustSelect, + welcomeActions, + triggerWelcomeAuth, openPermissionsDialog, closePermissionsDialog, openLoggingDialog, diff --git a/packages/cli/src/ui/components/DialogManager.tsx b/packages/cli/src/ui/components/DialogManager.tsx index e1fe1473f..c4be717f7 100644 --- a/packages/cli/src/ui/components/DialogManager.tsx +++ b/packages/cli/src/ui/components/DialogManager.tsx @@ -9,6 +9,7 @@ import { useCallback } from 'react'; import { IdeIntegrationNudge } from '../IdeIntegrationNudge.js'; // import { LoopDetectionConfirmation } from './LoopDetectionConfirmation.js'; // TODO: Not yet ported from upstream import { FolderTrustDialog } from './FolderTrustDialog.js'; +import { WelcomeDialog } from './WelcomeOnboarding/WelcomeDialog.js'; import { ShellConfirmationDialog } from './ShellConfirmationDialog.js'; import { ConsentPrompt } from './ConsentPrompt.js'; import { ThemeDialog } from './ThemeDialog.js'; @@ -98,6 +99,17 @@ export const DialogManager = ({ /> ); } + if (uiState.isWelcomeDialogOpen) { + return ( + + ); + } if (uiState.shellConfirmationRequest) { return ( diff --git a/packages/cli/src/ui/components/WelcomeOnboarding/AuthMethodStep.tsx b/packages/cli/src/ui/components/WelcomeOnboarding/AuthMethodStep.tsx new file mode 100644 index 000000000..6271d844e --- /dev/null +++ b/packages/cli/src/ui/components/WelcomeOnboarding/AuthMethodStep.tsx @@ -0,0 +1,120 @@ +/** + * @license + * Copyright 2025 Vybestack LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useMemo, useCallback } from 'react'; +import { Box, Text } from 'ink'; +import { Colors } from '../../colors.js'; +import { + RadioButtonSelect, + type RadioSelectItem, +} from '../shared/RadioButtonSelect.js'; + +// Providers that support OAuth +const OAUTH_PROVIDERS = new Set(['anthropic', 'openai', 'gemini', 'qwen']); + +const API_KEY_URLS: Record = { + anthropic: 'console.anthropic.com/settings/keys', + openai: 'platform.openai.com/api-keys', + gemini: 'aistudio.google.com/app/apikey', + deepseek: 'platform.deepseek.com/api_keys', + qwen: 'dashscope.console.aliyun.com/apiKey', +}; + +type AuthMethod = 'oauth' | 'api_key' | 'back'; + +interface AuthMethodStepProps { + provider: string; + onSelect: (method: 'oauth' | 'api_key') => void; + onBack: () => void; + error?: string; + isFocused?: boolean; +} + +export const AuthMethodStep: React.FC = ({ + provider, + onSelect, + onBack, + error, + isFocused = true, +}) => { + const supportsOAuth = OAUTH_PROVIDERS.has(provider); + const apiKeyUrl = API_KEY_URLS[provider]; + + const options: Array> = useMemo(() => { + const opts: Array> = []; + + if (supportsOAuth) { + opts.push({ + label: 'OAuth (Recommended - secure & easy)', + value: 'oauth', + key: 'oauth', + }); + } + + opts.push({ + label: 'API Key', + value: 'api_key', + key: 'api_key', + }); + + opts.push({ + label: '← Back to provider selection', + value: 'back', + key: 'back', + }); + + return opts; + }, [supportsOAuth]); + + const handleSelect = useCallback( + (value: AuthMethod) => { + if (value === 'back') { + onBack(); + } else { + onSelect(value); + } + }, + [onBack, onSelect], + ); + + const providerDisplay = provider.charAt(0).toUpperCase() + provider.slice(1); + + return ( + + + + Step 3 of 5: Choose Authentication Method + + + How would you like to authenticate with {providerDisplay}? + + + {error && ( + + {error} + + )} + + + + {apiKeyUrl && ( + + Get API key at: {apiKeyUrl} + + )} + + + + Use ↑↓ to navigate, Enter to select, Esc to skip + + + + ); +}; diff --git a/packages/cli/src/ui/components/WelcomeOnboarding/AuthenticationStep.tsx b/packages/cli/src/ui/components/WelcomeOnboarding/AuthenticationStep.tsx new file mode 100644 index 000000000..bc75f05c6 --- /dev/null +++ b/packages/cli/src/ui/components/WelcomeOnboarding/AuthenticationStep.tsx @@ -0,0 +1,174 @@ +/** + * @license + * Copyright 2025 Vybestack LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useEffect, useCallback } from 'react'; +import { Box, Text } from 'ink'; +import Spinner from 'ink-spinner'; +import { Colors } from '../../colors.js'; +import { useKeypress } from '../../hooks/useKeypress.js'; + +interface AuthenticationStepProps { + provider: string; + method: 'oauth' | 'api_key'; + onComplete: () => void; + onError: (error: string) => void; + onBack: () => void; + triggerAuth: ( + provider: string, + method: 'oauth' | 'api_key', + apiKey?: string, + ) => Promise; + isFocused?: boolean; +} + +export const AuthenticationStep: React.FC = ({ + provider, + method, + onComplete, + onError, + onBack, + triggerAuth, + isFocused = true, +}) => { + const [apiKey, setApiKey] = useState(''); + const [isAuthenticating, setIsAuthenticating] = useState(false); + const [authStarted, setAuthStarted] = useState(false); + + const providerDisplay = provider.charAt(0).toUpperCase() + provider.slice(1); + + const handleApiKeySubmit = useCallback(async () => { + if (!apiKey.trim()) { + return; + } + + setIsAuthenticating(true); + try { + await triggerAuth(provider, 'api_key', apiKey.trim()); + onComplete(); + } catch (error: unknown) { + setIsAuthenticating(false); + onError(error instanceof Error ? error.message : String(error)); + } + }, [apiKey, provider, triggerAuth, onComplete, onError]); + + // Handle keyboard input for API key mode + useKeypress( + (key) => { + if (key.name === 'escape' && !isAuthenticating) { + onBack(); + return; + } + + if (method !== 'api_key' || isAuthenticating) { + return; + } + + if (key.name === 'return') { + handleApiKeySubmit(); + return; + } + + if (key.name === 'backspace' || key.name === 'delete') { + setApiKey((prev) => prev.slice(0, -1)); + return; + } + + // Accept printable characters (including paste - multi-char sequences) + const char = key.sequence; + if (char && !key.ctrl && !key.meta) { + // Filter to only printable ASCII characters (handles both typing and paste) + const printable = char.replace(/[^\x20-\x7E]/g, ''); + if (printable) { + setApiKey((prev) => prev + printable); + } + } + }, + { isActive: isFocused }, + ); + + // Start OAuth flow automatically + useEffect(() => { + if (method === 'oauth' && !authStarted) { + setAuthStarted(true); + setIsAuthenticating(true); + triggerAuth(provider, 'oauth') + .then(() => { + onComplete(); + }) + .catch((error: unknown) => { + setIsAuthenticating(false); + onError(error instanceof Error ? error.message : String(error)); + }); + } + }, [method, provider, triggerAuth, onComplete, onError, authStarted]); + + if (method === 'oauth') { + return ( + + + + Step 4 of 5: Authenticating with {providerDisplay} + + + + + + + + {' '} + Opening browser for OAuth authentication... + + + + Please complete the authentication in your browser. + This window will update when done. + + + Press Esc to cancel and go back + + + ); + } + + const maskedValue = '•'.repeat(apiKey.length); + + return ( + + + + Step 4 of 5: Enter Your API Key + + + + {isAuthenticating ? ( + + + + + {' '} + Validating API key... + + + ) : ( + <> + + Enter your {providerDisplay} API key: + + + + API Key: + {maskedValue} + + + + )} + + + Press Enter when done, Esc to go back + + + ); +}; diff --git a/packages/cli/src/ui/components/WelcomeOnboarding/CompletionStep.tsx b/packages/cli/src/ui/components/WelcomeOnboarding/CompletionStep.tsx new file mode 100644 index 000000000..7fe060fb1 --- /dev/null +++ b/packages/cli/src/ui/components/WelcomeOnboarding/CompletionStep.tsx @@ -0,0 +1,162 @@ +/** + * @license + * Copyright 2025 Vybestack LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useCallback } from 'react'; +import { Box, Text } from 'ink'; +import { Colors } from '../../colors.js'; +import { useKeypress } from '../../hooks/useKeypress.js'; + +interface CompletionStepProps { + provider: string; + model?: string; + authMethod: 'oauth' | 'api_key'; + onSaveProfile: (name: string) => Promise; + onDismiss: () => void; + isFocused?: boolean; +} + +export const CompletionStep: React.FC = ({ + provider, + model, + authMethod, + onSaveProfile, + onDismiss, + isFocused = true, +}) => { + const [showProfilePrompt, setShowProfilePrompt] = useState(true); + const [profileName, setProfileName] = useState(''); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(); + + const providerDisplay = provider.charAt(0).toUpperCase() + provider.slice(1); + const authDisplay = authMethod === 'oauth' ? 'OAuth' : 'API Key'; + + const handleProfileSubmit = useCallback(async () => { + const trimmedName = profileName.trim(); + if (!trimmedName) { + setError('Profile name is required'); + return; + } + + setSaving(true); + setError(undefined); + + try { + await onSaveProfile(trimmedName); + setShowProfilePrompt(false); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : 'Failed to save profile'); + setSaving(false); + } + }, [profileName, onSaveProfile]); + + // Handle keyboard input + useKeypress( + (key) => { + if (key.name === 'return') { + if (showProfilePrompt && !saving) { + handleProfileSubmit(); + } else if (!showProfilePrompt) { + onDismiss(); + } + return; + } + + // Only accept input in profile prompt mode + if (!showProfilePrompt || saving) { + return; + } + + if (key.name === 'backspace' || key.name === 'delete') { + setProfileName((prev) => prev.slice(0, -1)); + return; + } + + // Accept printable characters (including paste - multi-char sequences) + const char = key.sequence; + if (char && !key.ctrl && !key.meta) { + // Filter to only printable ASCII characters + const printable = char.replace(/[^\x20-\x7E]/g, ''); + if (printable) { + setProfileName((prev) => prev + printable); + } + } + }, + { isActive: isFocused }, + ); + + return ( + + + + Step 5 of 5: Save Your Profile + + + + {'✓ Authentication complete!'} + + + + + Provider: {providerDisplay} + {model && Model: {model}} + Authentication: {authDisplay} + + + {showProfilePrompt ? ( + + + Save this setup as a profile + + + + This profile will be loaded automatically on startup. + + + Use /profile load <name> to switch profiles later. + + + + {error && ( + + {error} + + )} + + {saving ? ( + Saving profile... + ) : ( + + Profile name: + {profileName} + + + )} + + + + + Enter a name and press Enter to save + + + + ) : ( + + + Try asking me something like: + + {'"Explain how async/await works in JavaScript"'} + + + + + Press Enter to continue... + + + )} + + ); +}; diff --git a/packages/cli/src/ui/components/WelcomeOnboarding/ModelSelectStep.tsx b/packages/cli/src/ui/components/WelcomeOnboarding/ModelSelectStep.tsx new file mode 100644 index 000000000..bfda951d2 --- /dev/null +++ b/packages/cli/src/ui/components/WelcomeOnboarding/ModelSelectStep.tsx @@ -0,0 +1,129 @@ +/** + * @license + * Copyright 2025 Vybestack LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useMemo, useCallback } from 'react'; +import { Box, Text } from 'ink'; +import { Colors } from '../../colors.js'; +import { useKeypress } from '../../hooks/useKeypress.js'; +import { + RadioButtonSelect, + type RadioSelectItem, +} from '../shared/RadioButtonSelect.js'; +import type { + ModelInfo, + ModelsLoadStatus, +} from '../../hooks/useWelcomeOnboarding.js'; + +interface ModelSelectStepProps { + provider: string; + models: ModelInfo[]; + modelsLoadStatus: ModelsLoadStatus; + onSelect: (modelId: string) => void; + onBack: () => void; + isFocused?: boolean; +} + +export const ModelSelectStep: React.FC = ({ + provider, + models, + modelsLoadStatus, + onSelect, + onBack, + isFocused = true, +}) => { + const providerDisplay = provider.charAt(0).toUpperCase() + provider.slice(1); + + const options: Array> = useMemo(() => { + const modelOptions = models.map((model) => ({ + label: model.name, + value: model.id, + key: model.id, + })); + + // Add back option + modelOptions.push({ + label: '← Back to provider selection', + value: '__back__', + key: '__back__', + }); + + return modelOptions; + }, [models]); + + const handleSelect = useCallback( + (value: string) => { + if (value === '__back__') { + onBack(); + } else { + onSelect(value); + } + }, + [onBack, onSelect], + ); + + // Handle Escape to go back (especially useful in error/empty states) + useKeypress( + (key) => { + if (key.name === 'escape') { + onBack(); + } + }, + { isActive: isFocused }, + ); + + return ( + + + + Step 2 of 5: Choose Your Model + + + Select a model for {providerDisplay}: + + + {modelsLoadStatus === 'loading' && ( + + Loading models... + + )} + + {modelsLoadStatus === 'error' && ( + + Failed to load models. + Press Esc to go back and try again. + + )} + + {modelsLoadStatus === 'success' && models.length > 0 && ( + + )} + + {modelsLoadStatus === 'success' && models.length === 0 && ( + + + No models available for this provider. + + + Press Esc to go back and select a different provider. + + + )} + + + + {modelsLoadStatus === 'success' && models.length > 0 + ? 'Use ↑↓ to navigate, Enter to select' + : 'Press Esc to go back'} + + + + ); +}; diff --git a/packages/cli/src/ui/components/WelcomeOnboarding/ProviderSelectStep.tsx b/packages/cli/src/ui/components/WelcomeOnboarding/ProviderSelectStep.tsx new file mode 100644 index 000000000..72f4dde69 --- /dev/null +++ b/packages/cli/src/ui/components/WelcomeOnboarding/ProviderSelectStep.tsx @@ -0,0 +1,90 @@ +/** + * @license + * Copyright 2025 Vybestack LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useMemo, useCallback } from 'react'; +import { Box, Text } from 'ink'; +import { Colors } from '../../colors.js'; +import { + RadioButtonSelect, + type RadioSelectItem, +} from '../shared/RadioButtonSelect.js'; + +const PROVIDER_DISPLAY_NAMES: Record = { + anthropic: 'Anthropic (Claude)', + openai: 'OpenAI (GPT-4, etc.)', + gemini: 'Google Gemini', + deepseek: 'DeepSeek', + qwen: 'Qwen', + 'openai-responses': 'OpenAI Responses API', + openaivercel: 'OpenAI (Vercel AI SDK)', +}; + +interface ProviderSelectStepProps { + providers: string[]; + onSelect: (providerId: string) => void; + onSkip: () => void; + isFocused?: boolean; +} + +export const ProviderSelectStep: React.FC = ({ + providers, + onSelect, + onSkip, + isFocused = true, +}) => { + const options: Array> = useMemo(() => { + const providerOptions = providers.map((provider) => ({ + label: PROVIDER_DISPLAY_NAMES[provider] || provider, + value: provider, + key: provider, + })); + + // Add "configure manually" option + providerOptions.push({ + label: 'Configure manually later', + value: '__skip__', + key: '__skip__', + }); + + return providerOptions; + }, [providers]); + + const handleSelect = useCallback( + (value: string) => { + if (value === '__skip__') { + onSkip(); + } else { + onSelect(value); + } + }, + [onSkip, onSelect], + ); + + return ( + + + + Step 1 of 5: Choose Your AI Provider + + + {"Select which AI provider you'd like to use:"} + + + + + + + Use ↑↓ to navigate, Enter to select, Esc to skip + + + + ); +}; diff --git a/packages/cli/src/ui/components/WelcomeOnboarding/SkipExitStep.tsx b/packages/cli/src/ui/components/WelcomeOnboarding/SkipExitStep.tsx new file mode 100644 index 000000000..b2e12b1e2 --- /dev/null +++ b/packages/cli/src/ui/components/WelcomeOnboarding/SkipExitStep.tsx @@ -0,0 +1,57 @@ +/** + * @license + * Copyright 2025 Vybestack LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { Box, Text } from 'ink'; +import { Colors } from '../../colors.js'; +import { useKeypress } from '../../hooks/useKeypress.js'; + +interface SkipExitStepProps { + onDismiss: () => void; + isFocused?: boolean; +} + +export const SkipExitStep: React.FC = ({ + onDismiss, + isFocused = true, +}) => { + useKeypress( + (key) => { + if (key.name === 'return') { + onDismiss(); + } + }, + { isActive: isFocused }, + ); + + return ( + + + Setup skipped + + + + To configure llxprt manually: + + + • Use /auth <provider> to + set up authentication + + + • Use /provider to select your + AI provider + + + • Type /help for more commands + + + + + Press Enter to continue... + + + ); +}; diff --git a/packages/cli/src/ui/components/WelcomeOnboarding/WelcomeDialog.tsx b/packages/cli/src/ui/components/WelcomeOnboarding/WelcomeDialog.tsx new file mode 100644 index 000000000..0b58eeade --- /dev/null +++ b/packages/cli/src/ui/components/WelcomeOnboarding/WelcomeDialog.tsx @@ -0,0 +1,146 @@ +/** + * @license + * Copyright 2025 Vybestack LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback } from 'react'; +import { Box } from 'ink'; +import { Colors } from '../../colors.js'; +import { useKeypress } from '../../hooks/useKeypress.js'; +import { WelcomeStep, type WelcomeChoice } from './WelcomeStep.js'; +import { ProviderSelectStep } from './ProviderSelectStep.js'; +import { ModelSelectStep } from './ModelSelectStep.js'; +import { AuthMethodStep } from './AuthMethodStep.js'; +import { AuthenticationStep } from './AuthenticationStep.js'; +import { CompletionStep } from './CompletionStep.js'; +import { SkipExitStep } from './SkipExitStep.js'; +import type { + WelcomeState, + WelcomeActions, + ModelInfo, +} from '../../hooks/useWelcomeOnboarding.js'; + +interface WelcomeDialogProps { + state: WelcomeState; + actions: WelcomeActions; + availableProviders: string[]; + availableModels: ModelInfo[]; + triggerAuth: ( + provider: string, + method: 'oauth' | 'api_key', + apiKey?: string, + ) => Promise; +} + +export const WelcomeDialog: React.FC = ({ + state, + actions, + availableProviders, + availableModels, + triggerAuth, +}) => { + // Handle global escape to skip (except during auth) + useKeypress( + (key) => { + if (key.name === 'escape' && !state.authInProgress) { + actions.skipSetup(); + } + }, + { isActive: state.step !== 'completion' && state.step !== 'skipped' }, + ); + + const handleWelcomeSelect = useCallback( + (choice: WelcomeChoice) => { + if (choice === 'setup') { + actions.startSetup(); + } else { + actions.skipSetup(); + } + }, + [actions], + ); + + const renderStep = () => { + switch (state.step) { + case 'welcome': + return ; + + case 'provider': + return ( + + ); + + case 'model': + if (!state.selectedProvider) return null; + return ( + + ); + + case 'auth_method': + if (!state.selectedProvider) return null; + return ( + + ); + + case 'authenticating': + if (!state.selectedProvider || !state.selectedAuthMethod) return null; + return ( + + ); + + case 'completion': + if (!state.selectedProvider || !state.selectedAuthMethod) return null; + return ( + + ); + + case 'skipped': + return ; + + default: + return null; + } + }; + + return ( + + {renderStep()} + + ); +}; diff --git a/packages/cli/src/ui/components/WelcomeOnboarding/WelcomeStep.tsx b/packages/cli/src/ui/components/WelcomeOnboarding/WelcomeStep.tsx new file mode 100644 index 000000000..faee3db25 --- /dev/null +++ b/packages/cli/src/ui/components/WelcomeOnboarding/WelcomeStep.tsx @@ -0,0 +1,70 @@ +/** + * @license + * Copyright 2025 Vybestack LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { Box, Text } from 'ink'; +import { Colors } from '../../colors.js'; +import { + RadioButtonSelect, + type RadioSelectItem, +} from '../shared/RadioButtonSelect.js'; + +export type WelcomeChoice = 'setup' | 'skip'; + +interface WelcomeStepProps { + onSelect: (choice: WelcomeChoice) => void; + isFocused?: boolean; +} + +export const WelcomeStep: React.FC = ({ + onSelect, + isFocused = true, +}) => { + const options: Array> = [ + { + label: 'Set up now (recommended)', + value: 'setup', + key: 'setup', + }, + { + label: "Skip setup (I know what I'm doing)", + value: 'skip', + key: 'skip', + }, + ]; + + return ( + + + + Welcome to llxprt! + + + {"Let's get you set up in just a few steps."} + + {"You'll choose an AI provider and configure authentication"} + + so llxprt can work its magic. + + + + What would you like to do? + + + + + + + Use ↑↓ to navigate, Enter to select, Esc to skip + + + + ); +}; diff --git a/packages/cli/src/ui/components/WelcomeOnboarding/index.ts b/packages/cli/src/ui/components/WelcomeOnboarding/index.ts new file mode 100644 index 000000000..044c3626a --- /dev/null +++ b/packages/cli/src/ui/components/WelcomeOnboarding/index.ts @@ -0,0 +1,13 @@ +/** + * @license + * Copyright 2025 Vybestack LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export { WelcomeDialog } from './WelcomeDialog.js'; +export { WelcomeStep } from './WelcomeStep.js'; +export { ProviderSelectStep } from './ProviderSelectStep.js'; +export { AuthMethodStep } from './AuthMethodStep.js'; +export { AuthenticationStep } from './AuthenticationStep.js'; +export { CompletionStep } from './CompletionStep.js'; +export { SkipExitStep } from './SkipExitStep.js'; diff --git a/packages/cli/src/ui/contexts/UIActionsContext.tsx b/packages/cli/src/ui/contexts/UIActionsContext.tsx index 606b51752..473f23e2d 100644 --- a/packages/cli/src/ui/contexts/UIActionsContext.tsx +++ b/packages/cli/src/ui/contexts/UIActionsContext.tsx @@ -81,6 +81,25 @@ export interface UIActions { // Folder trust dialog handleFolderTrustSelect: (choice: FolderTrustChoice) => void; + // Welcome onboarding + welcomeActions: { + startSetup: () => void; + selectProvider: (providerId: string) => void; + selectModel: (modelId: string) => void; + selectAuthMethod: (method: 'oauth' | 'api_key') => void; + onAuthComplete: () => void; + onAuthError: (error: string) => void; + skipSetup: () => void; + goBack: () => void; + saveProfile: (name: string) => Promise; + dismiss: () => void; + }; + triggerWelcomeAuth: ( + provider: string, + method: 'oauth' | 'api_key', + apiKey?: string, + ) => Promise; + // Permissions dialog openPermissionsDialog: () => void; closePermissionsDialog: () => void; diff --git a/packages/cli/src/ui/contexts/UIStateContext.tsx b/packages/cli/src/ui/contexts/UIStateContext.tsx index 5106d9e59..290ba9527 100644 --- a/packages/cli/src/ui/contexts/UIStateContext.tsx +++ b/packages/cli/src/ui/contexts/UIStateContext.tsx @@ -27,6 +27,7 @@ import type { import type { SlashCommand, CommandContext } from '../commands/types.js'; import type { ShellConfirmationRequest } from '../components/ShellConfirmationDialog.js'; import type { LoadedSettings } from '../../config/settings.js'; +import type { WelcomeState, ModelInfo } from '../hooks/useWelcomeOnboarding.js'; /** * UI State shape for the AppContainer architecture. @@ -156,6 +157,12 @@ export interface UIState { isRestarting: boolean; isTrustedFolder: boolean; + // Welcome onboarding + isWelcomeDialogOpen: boolean; + welcomeState: WelcomeState; + welcomeAvailableProviders: string[]; + welcomeAvailableModels: ModelInfo[]; + // Input history inputHistory: string[]; diff --git a/packages/cli/src/ui/hooks/useWelcomeOnboarding.ts b/packages/cli/src/ui/hooks/useWelcomeOnboarding.ts new file mode 100644 index 000000000..f193ca26a --- /dev/null +++ b/packages/cli/src/ui/hooks/useWelcomeOnboarding.ts @@ -0,0 +1,348 @@ +/** + * @license + * Copyright 2025 Vybestack LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useState, useCallback, useEffect } from 'react'; +import { DebugLogger } from '@vybestack/llxprt-code-core'; +import { type LoadedSettings } from '../../config/settings.js'; +import { + isWelcomeCompleted, + markWelcomeCompleted, +} from '../../config/welcomeConfig.js'; +import { useRuntimeApi } from '../contexts/RuntimeContext.js'; + +const debug = new DebugLogger('llxprt:ui:useWelcomeOnboarding'); + +export type WelcomeStep = + | 'welcome' + | 'provider' + | 'model' + | 'auth_method' + | 'authenticating' + | 'completion' + | 'skipped'; + +export type ModelsLoadStatus = 'idle' | 'loading' | 'success' | 'error'; + +export interface WelcomeState { + step: WelcomeStep; + selectedProvider?: string; + selectedModel?: string; + selectedAuthMethod?: 'oauth' | 'api_key'; + authInProgress: boolean; + modelsLoadStatus: ModelsLoadStatus; + error?: string; +} + +export interface WelcomeActions { + startSetup: () => void; + selectProvider: (providerId: string) => void; + selectModel: (modelId: string) => void; + selectAuthMethod: (method: 'oauth' | 'api_key') => void; + onAuthComplete: () => void; + onAuthError: (error: string) => void; + skipSetup: () => void; + goBack: () => void; + saveProfile: (name: string) => Promise; + dismiss: () => void; +} + +export interface UseWelcomeOnboardingOptions { + settings: LoadedSettings; + isFolderTrustComplete: boolean; +} + +export interface ModelInfo { + id: string; + name: string; +} + +export interface UseWelcomeOnboardingReturn { + showWelcome: boolean; + state: WelcomeState; + actions: WelcomeActions; + availableProviders: string[]; + availableModels: ModelInfo[]; + triggerAuth: ( + provider: string, + method: 'oauth' | 'api_key', + apiKey?: string, + ) => Promise; +} + +export const useWelcomeOnboarding = ( + options: UseWelcomeOnboardingOptions, +): UseWelcomeOnboardingReturn => { + const { settings: _settings, isFolderTrustComplete } = options; + const runtime = useRuntimeApi(); + const [welcomeCompleted, setWelcomeCompleted] = useState(() => + isWelcomeCompleted(), + ); + + // Only show welcome after folder trust is complete + const showWelcome = !welcomeCompleted && isFolderTrustComplete; + + const [state, setState] = useState({ + step: 'welcome', + authInProgress: false, + modelsLoadStatus: 'idle', + }); + + const [availableProviders, setAvailableProviders] = useState([]); + const [availableModels, setAvailableModels] = useState([]); + + // Load available providers on mount + useEffect(() => { + const providerManager = runtime.getCliProviderManager(); + if (providerManager) { + const providers = providerManager.listProviders(); + setAvailableProviders(providers); + debug.log( + `Loaded ${providers.length} providers: ${providers.join(', ')}`, + ); + } + }, [runtime]); + + // Load available models when provider is selected + useEffect(() => { + const loadModels = async () => { + if (!state.selectedProvider) { + setAvailableModels([]); + setState((prev) => ({ ...prev, modelsLoadStatus: 'idle' })); + return; + } + + setState((prev) => ({ ...prev, modelsLoadStatus: 'loading' })); + + try { + const models = await runtime.listAvailableModels( + state.selectedProvider, + ); + const modelInfos: ModelInfo[] = models.map((m) => ({ + id: m.name, + name: m.name, + })); + setAvailableModels(modelInfos); + setState((prev) => ({ ...prev, modelsLoadStatus: 'success' })); + debug.log( + `Loaded ${modelInfos.length} models for ${state.selectedProvider}`, + ); + } catch (error) { + debug.log(`Failed to load models: ${error}`); + setAvailableModels([]); + setState((prev) => ({ ...prev, modelsLoadStatus: 'error' })); + } + }; + + loadModels(); + }, [runtime, state.selectedProvider]); + + const startSetup = useCallback(() => { + setState((prev) => ({ ...prev, step: 'provider' })); + }, []); + + const selectProvider = useCallback((providerId: string) => { + setState((prev) => ({ + ...prev, + selectedProvider: providerId, + step: 'model', + })); + }, []); + + const selectModel = useCallback((modelId: string) => { + setState((prev) => ({ + ...prev, + selectedModel: modelId, + step: 'auth_method', + })); + }, []); + + const selectAuthMethod = useCallback((method: 'oauth' | 'api_key') => { + setState((prev) => ({ + ...prev, + selectedAuthMethod: method, + step: 'authenticating', + authInProgress: true, + })); + }, []); + + const onAuthComplete = useCallback(() => { + // Provider switch already happened in triggerAuth, just update UI state + debug.log( + `[onAuthComplete] Auth complete for provider: ${state.selectedProvider}`, + ); + + setState((prev) => ({ + ...prev, + step: 'completion', + authInProgress: false, + error: undefined, + })); + }, [state.selectedProvider]); + + const onAuthError = useCallback((error: string) => { + setState((prev) => ({ + ...prev, + authInProgress: false, + error, + step: 'auth_method', + })); + }, []); + + const skipSetup = useCallback(() => { + setState((prev) => ({ ...prev, step: 'skipped' })); + }, []); + + const goBack = useCallback(() => { + setState((prev) => { + switch (prev.step) { + case 'model': + return { ...prev, step: 'provider', selectedProvider: undefined }; + case 'auth_method': + return { ...prev, step: 'model', selectedModel: undefined }; + case 'authenticating': + return { + ...prev, + step: 'auth_method', + selectedAuthMethod: undefined, + authInProgress: false, + }; + case 'provider': + return { ...prev, step: 'welcome' }; + default: + return prev; + } + }); + }, []); + + const saveProfile = useCallback( + async (name: string) => { + try { + const providerManager = runtime.getCliProviderManager(); + debug.log( + `[saveProfile] START name=${name}, active provider: ${providerManager?.getActiveProviderName()}`, + ); + + // Check if profile already exists + const existingProfiles = await runtime.listSavedProfiles(); + debug.log( + `[saveProfile] Existing profiles: ${existingProfiles.join(', ')}`, + ); + if (existingProfiles.includes(name)) { + throw new Error( + `Profile "${name}" already exists. Please choose a different name.`, + ); + } + + // Save the profile snapshot + debug.log(`[saveProfile] Calling saveProfileSnapshot...`); + await runtime.saveProfileSnapshot(name); + debug.log(`[saveProfile] Saved profile: ${name}`); + + // Set as default profile so it loads on startup + debug.log(`[saveProfile] Setting as default profile...`); + await runtime.setDefaultProfileName(name); + debug.log(`[saveProfile] Set default profile: ${name}`); + + // Load the profile immediately in current session + debug.log(`[saveProfile] Loading profile...`); + const loadResult = await runtime.loadProfileByName(name); + debug.log( + `[saveProfile] Load result: ${JSON.stringify(loadResult, null, 2)}`, + ); + debug.log( + `[saveProfile] After load - active provider: ${providerManager?.getActiveProviderName()}`, + ); + } catch (error) { + debug.log(`[saveProfile] Failed: ${error}`); + throw error; + } + }, + [runtime], + ); + + const dismiss = useCallback(() => { + const skipped = state.step === 'skipped'; + markWelcomeCompleted(skipped); + setWelcomeCompleted(true); + debug.log(`Welcome flow completed (skipped: ${skipped})`); + }, [state.step]); + + // Trigger authentication for the selected provider + const triggerAuth = useCallback( + async ( + provider: string, + method: 'oauth' | 'api_key', + apiKey?: string, + ): Promise => { + debug.log(`[triggerAuth] START provider=${provider} method=${method}`); + const oauthManager = runtime.getCliOAuthManager(); + const providerManager = runtime.getCliProviderManager(); + + debug.log( + `[triggerAuth] Before switch - current active: ${providerManager?.getActiveProviderName()}`, + ); + + // Use switchActiveProvider (not setActiveProvider) - it does full provider switch + // including config updates, ephemeral settings, and settingsService updates + const switchResult = await runtime.switchActiveProvider(provider); + debug.log( + `[triggerAuth] After switchActiveProvider - changed: ${switchResult.changed}, now active: ${providerManager?.getActiveProviderName()}`, + ); + + // Set the selected model + if (state.selectedModel) { + debug.log(`[triggerAuth] Setting model to: ${state.selectedModel}`); + await runtime.setActiveModel(state.selectedModel); + debug.log(`[triggerAuth] Model set to: ${state.selectedModel}`); + } + + if (method === 'oauth') { + // Trigger OAuth flow + if (!oauthManager) { + throw new Error('OAuth manager not available'); + } + await oauthManager.authenticate(provider); + debug.log(`[triggerAuth] OAuth complete for ${provider}`); + } else if (apiKey) { + // API key path: set the key for the now-active provider + debug.log( + `[triggerAuth] Calling updateActiveProviderApiKey for ${provider}`, + ); + const result = await runtime.updateActiveProviderApiKey(apiKey); + debug.log( + `[triggerAuth] API key result: ${result.message}, providerName=${result.providerName}`, + ); + } else { + throw new Error('API key is required for API key authentication'); + } + + debug.log( + `[triggerAuth] END - active provider: ${providerManager?.getActiveProviderName()}`, + ); + }, + [runtime, state.selectedModel], + ); + + return { + showWelcome, + state, + actions: { + startSetup, + selectProvider, + selectModel, + selectAuthMethod, + onAuthComplete, + onAuthError, + skipSetup, + goBack, + saveProfile, + dismiss, + }, + availableProviders, + availableModels, + triggerAuth, + }; +}; diff --git a/packages/cli/src/ui/layouts/DefaultAppLayout.tsx b/packages/cli/src/ui/layouts/DefaultAppLayout.tsx index 3b5596d3c..2ea699673 100644 --- a/packages/cli/src/ui/layouts/DefaultAppLayout.tsx +++ b/packages/cli/src/ui/layouts/DefaultAppLayout.tsx @@ -148,6 +148,7 @@ export const DefaultAppLayout = ({ uiState.shouldShowIdePrompt || uiState.showIdeRestartPrompt || uiState.isFolderTrustDialogOpen || + uiState.isWelcomeDialogOpen || uiState.isPermissionsDialogOpen || uiState.shellConfirmationRequest || uiState.confirmationRequest || diff --git a/project-plans/welcome-onboarding/DESIGN.md b/project-plans/welcome-onboarding/DESIGN.md new file mode 100644 index 000000000..9675496c2 --- /dev/null +++ b/project-plans/welcome-onboarding/DESIGN.md @@ -0,0 +1,165 @@ +# Welcome Onboarding Flow - Design Document + +## Overview + +Interactive welcome wizard for first-run users. Appears after folder trust dialog, guides through provider selection and authentication, optionally saves as profile. + +## Design Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| Flow trigger | After folder trust | Clean separation of concerns | +| Profile save | Optional prompt | Educate without forcing | +| Provider list | Dynamic from ProviderManager | Future-proof, no hardcoding | +| OAuth handling | Use existing flow | Reuse infrastructure, set provider after | +| First-run flag | Config file | Consistent with folder trust pattern | + +## Flow + +``` +WELCOME → PROVIDER_SELECT → AUTH_METHOD → AUTHENTICATION → COMPLETION + ↓ ↓ ↓ ↓ + └───────────┴───────────────┴──────────────┴──→ SKIP_EXIT +``` + +**Steps:** +1. **Welcome** - Setup/skip choice +2. **Provider Selection** - Dynamic list from ProviderManager +3. **Auth Method** - OAuth vs API key (provider-dependent) +4. **Authentication** - Delegates to existing `/auth` flow +5. **Completion** - Success + optional profile save + +**Exit Paths:** +- Complete auth → set active provider → optional profile save → mark complete +- Skip/ESC → show manual setup hints → mark complete + +## Component Architecture + +``` +packages/cli/src/ui/components/ + └── WelcomeOnboarding/ + ├── WelcomeDialog.tsx # Main orchestrator, state machine + ├── WelcomeStep.tsx # Step 1: Welcome + setup/skip + ├── ProviderSelectStep.tsx # Step 2: Provider list + ├── AuthMethodStep.tsx # Step 3: OAuth vs API key + ├── AuthenticationStep.tsx # Step 4: Auth in progress + ├── CompletionStep.tsx # Step 5: Success + profile save + └── SkipExitStep.tsx # Shown when user skips + +packages/cli/src/ui/hooks/ + └── useWelcomeOnboarding.ts # State management, first-run detection +``` + +## State Management + +```typescript +interface WelcomeState { + step: 'welcome' | 'provider' | 'auth_method' | 'authenticating' | 'completion' | 'skipped'; + selectedProvider?: string; + selectedAuthMethod?: 'oauth' | 'api_key'; + authInProgress: boolean; + error?: string; +} + +interface WelcomeActions { + startSetup: () => void; + selectProvider: (providerId: string) => void; + selectAuthMethod: (method: 'oauth' | 'api_key') => void; + onAuthComplete: () => void; + onAuthError: (error: string) => void; + skipSetup: () => void; + goBack: () => void; + saveProfile: (name: string) => Promise; + dismiss: () => void; +} +``` + +## Integration + +**AppContainer.tsx:** +```tsx +const { showWelcome, welcomeState, welcomeActions } = useWelcomeOnboarding(config, settings); + +{!isFolderTrustDialogOpen && showWelcome && ( + +)} +``` + +**First-Run Detection:** +```tsx +const shouldShow = !config.get('welcomeCompleted'); +``` + +**On Completion:** +```tsx +providerManager.setActiveProvider(state.selectedProvider); +config.set('welcomeCompleted', true); +await config.save(); +``` + +## UI Details + +### WelcomeStep +- Header: "Welcome to llxprt!" +- Options: "Set up now (recommended)" / "Skip setup (I know what I'm doing)" +- Footer: Navigation hints + +### ProviderSelectStep +- Header: "Step 1 of 3: Choose Your AI Provider" +- Dynamic list from `providerManager.getProviders()` +- Includes "Configure manually later" option + +### AuthMethodStep +- Header: "Step 2 of 3: Choose Authentication" +- Conditional: OAuth + API key (if supported) or API key only +- Back navigation option + +### AuthenticationStep +- Header: "Step 3 of 3: Authenticating..." +- Spinner for OAuth, text input for API key +- Delegates to existing auth infrastructure + +### CompletionStep +- Header: "You're all set!" +- Summary of provider + auth method +- Optional profile save: text input for name +- "Press Enter to continue..." + +### SkipExitStep +- Header: "Setup skipped" +- Manual setup hints: `/auth`, `/provider` +- "Press Enter to continue..." + +## Keyboard Shortcuts + +| Key | Action | +|-----|--------| +| Esc | Skip to exit (except during OAuth) | +| Enter | Select/confirm | +| ↑↓ | Navigate options | +| Backspace | Go back | + +## Edge Cases + +| Scenario | Handling | +|----------|----------| +| Auth fails | Show error, offer retry or back | +| OAuth cancelled | Return to auth method step | +| API key invalid | Show error, allow re-entry | +| Provider has no auth | Skip auth steps → completion | +| Profile save fails | Show error, allow skip | + +## Config Changes + +`~/.llxprt/config.json`: +```json +{ + "welcomeCompleted": true +} +``` + +## Blocking Behavior + +- Welcome flow blocks main input (like folder trust) +- Other dialogs wait until welcome completes +- Initial prompt auto-submit waits for `!showWelcome` diff --git a/project-plans/welcome-onboarding/PRD.md b/project-plans/welcome-onboarding/PRD.md new file mode 100644 index 000000000..a1183502e --- /dev/null +++ b/project-plans/welcome-onboarding/PRD.md @@ -0,0 +1,879 @@ +# Welcome Onboarding Flow: Product Requirements Document + +## Purpose + +Provide a guided first-run experience that helps new users configure authentication and select their first provider, enabling them to start using llxprt immediately without confusion or errors. + +## Problem Statement + +Currently, when users launch llxprt for the first time: + +1. No provider is configured (Gemini is default but has no credentials) +2. Users immediately encounter errors about missing API keys/authentication +3. Users don't know what to do - should they provide an API key? Use OAuth? Which provider should they choose? +4. Users must discover `/auth` and `/provider` commands through trial and error +5. The experience is confusing and creates friction for new users + +**Current Flow (Poor UX):** +``` +1. Launch llxprt +2. Try to type something +3. Get error: "Gemini API key not configured" +4. User doesn't know what to do +5. User exits in frustration OR searches docs +``` + +**Desired Flow (Good UX):** +``` +1. Launch llxprt +2. Welcome screen: "Let's get you set up!" +3. Guided provider selection +4. Guided authentication setup (OAuth or API key) +5. Confirmation: "All set! Try asking me something." +6. User starts using llxprt successfully +``` + +## Proposed Solution + +Create an interactive welcome dialog that appears on first run, guiding users through: +1. Provider selection from available options +2. Authentication method choice (OAuth vs API key) +3. Completion of authentication flow +4. Automatic provider activation +5. Optional: Quick tutorial or example prompt + +This leverages existing UI components (RadioButtonSelect, dialogs) and integrates with existing auth/provider systems. + +## Architectural Context + +### Existing Components to Leverage + +**UI Components:** +- `RadioButtonSelect` - For provider and option selection +- `FolderTrustDialog` - Template for blocking dialogs +- `DialogManager` - Dialog orchestration +- `useKeypress` - Keyboard navigation + +**Backend Services:** +- `ProviderManager` - Provider registration and switching +- `OAuthManager` - OAuth flow orchestration +- `SettingsService` - Persistence of first-run flag +- Auth commands and infrastructure + +**Entry Points:** +- `AppContainer.tsx` - Main app initialization +- `useFolderTrust` hook - Example of startup dialog + +### Integration Points + +1. **First-Run Detection**: Check settings for `firstRunCompleted` flag +2. **Welcome Dialog**: New component similar to `FolderTrustDialog` +3. **Provider Selection**: Use `RadioButtonSelect` with provider list +4. **Auth Flow**: Delegate to existing `/auth` command infrastructure +5. **Provider Activation**: Call `ProviderManager.setActiveProvider()` +6. **Persistence**: Save `firstRunCompleted: true` to settings + +## Technical Environment + +- **Type**: CLI Tool Enhancement +- **Runtime**: Node.js 20.x +- **Language**: TypeScript +- **UI Framework**: React + Ink +- **Affected Packages**: + - `packages/cli/src/ui/components/` - New welcome dialog + - `packages/cli/src/ui/hooks/` - New useWelcomeOnboarding hook + - `packages/core/src/settings/` - First-run flag + - `packages/core/src/providers/` - Provider listing + +## Functional Requirements + +### Core Flow Requirements + +**[REQ-001]** The system shall detect first-run on application startup. + - **[REQ-001.1]** Check for `firstRunCompleted` setting in SettingsService + - **[REQ-001.2]** If false/undefined, trigger welcome flow + - **[REQ-001.3]** Skip welcome flow if setting is true + +**[REQ-002]** The system shall display a welcome screen before any other interactions. + - **[REQ-002.1]** Block other input until welcome flow completes + - **[REQ-002.2]** Show friendly, informative welcome message + - **[REQ-002.3]** Set clear expectations about setup process + +**[REQ-003]** The system shall present all available providers for selection. + - **[REQ-003.1]** List: Anthropic, OpenAI, Gemini, DeepSeek, Qwen, and others + - **[REQ-003.2]** Show brief description of each provider + - **[REQ-003.3]** Use RadioButtonSelect for keyboard navigation + - **[REQ-003.4]** Highlight recommended/popular options + +**[REQ-004]** The system shall offer authentication method choice for selected provider. + - **[REQ-004.1]** If OAuth available: present "OAuth (recommended)" option + - **[REQ-004.2]** Always present "API Key" option + - **[REQ-004.3]** Show pros/cons or brief explanation of each method + - **[REQ-004.4]** Use RadioButtonSelect for method selection + +**[REQ-005]** The system shall guide users through OAuth setup if selected. + - **[REQ-005.1]** Delegate to existing `/auth enable` logic + - **[REQ-005.2]** Display in-progress indicators during auth flow + - **[REQ-005.3]** Handle OAuth completion or cancellation + - **[REQ-005.4]** Show success/failure messages + +**[REQ-006]** The system shall guide users through API key setup if selected. + - **[REQ-006.1]** Display instructions on where to get API key + - **[REQ-006.2]** Provide secure input field for API key entry + - **[REQ-006.3]** Validate API key format (basic validation) + - **[REQ-006.4]** Save API key to settings/profile + +**[REQ-007]** The system shall automatically set the configured provider as active. + - **[REQ-007.1]** Call `ProviderManager.setActiveProvider()` after successful auth + - **[REQ-007.2]** Persist active provider to settings + - **[REQ-007.3]** Show confirmation message + +**[REQ-008]** The system shall mark first-run as completed after successful setup. + - **[REQ-008.1]** Set `firstRunCompleted: true` in settings + - **[REQ-008.2]** Persist to disk immediately + - **[REQ-008.3]** Never show welcome flow again for this installation + +**[REQ-009]** The system shall allow users to skip or exit the welcome flow at any time. + - **[REQ-009.1]** Provide clear "Skip setup (I know what I'm doing)" option on welcome screen + - **[REQ-009.2]** ESC key exits welcome flow immediately from any step + - **[REQ-009.3]** Mark first-run complete even if skipped + - **[REQ-009.4]** Show brief acknowledgment: "Setup skipped. Use /auth and /provider to configure manually." + - **[REQ-009.5]** Never force users through the flow - respect power users' time + - **[REQ-009.6]** Allow navigation back and skip from any intermediate step + +### User Experience Requirements + +**[REQ-010]** The welcome flow shall be clear, concise, and respectful of user expertise. + - **[REQ-010.1]** Use plain language without jargon + - **[REQ-010.2]** Progressive disclosure - show only what's needed at each step + - **[REQ-010.3]** Maximum 4 steps from start to completion + - **[REQ-010.4]** Skip option prominent and accessible at all times + - **[REQ-010.5]** No negative language or guilt about skipping ("I know what I'm doing" not "No, I don't want help") + +**[REQ-011]** The system shall provide helpful context at each step. + - **[REQ-011.1]** Explain what providers are and why to choose one + - **[REQ-011.2]** Explain OAuth vs API key trade-offs + - **[REQ-011.3]** Show links to provider signup pages if needed + - **[REQ-011.4]** Provide escape hatches and help text + +**[REQ-012]** The system shall show progress through the welcome flow. + - **[REQ-012.1]** Display step indicators (e.g., "Step 1 of 3") + - **[REQ-012.2]** Show what was completed and what's next + - **[REQ-012.3]** Allow backward navigation where appropriate + +**[REQ-013]** The system shall celebrate successful completion. + - **[REQ-013.1]** Show success message: "You're all set!" + - **[REQ-013.2]** Optionally show a sample prompt to try + - **[REQ-013.3]** Transition smoothly to normal operation + +### Advanced Requirements (Optional Phase 2) + +**[REQ-014]** The system should support manual trigger of welcome flow. + - **[REQ-014.1]** Add `/onboard` or `/setup` command + - **[REQ-014.2]** Allow users to re-run setup if needed + - **[REQ-014.3]** Warn about overwriting existing configuration + +**[REQ-015]** The system should remember partial progress if interrupted. + - **[REQ-015.1]** Save intermediate state during flow + - **[REQ-015.2]** Resume from last completed step on restart + - **[REQ-015.3]** Clear partial state after completion + +**[REQ-016]** The system should offer provider recommendations. + - **[REQ-016.1]** Suggest providers based on use case (coding, chat, etc.) + - **[REQ-016.2]** Show which providers have free tiers + - **[REQ-016.3]** Indicate which are fastest to set up + +## UI Wireframes (Text-Based) + +### Step 1: Welcome Screen +``` +┌──────────────────────────────────────────────────────┐ +│ │ +│ Welcome to llxprt! │ +│ │ +│ Let's get you set up in just a few steps. │ +│ You'll need to choose an AI provider and configure │ +│ authentication so llxprt can work its magic. │ +│ │ +│ What would you like to do? │ +│ │ +│ ● 1. Set up now (recommended for new users) │ +│ 2. Skip setup (I know what I'm doing) │ +│ │ +│ Use ↑↓ arrows to navigate, Enter to select │ +│ Press Esc anytime to skip setup │ +│ │ +└──────────────────────────────────────────────────────┘ +``` + +### Step 2: Provider Selection +``` +┌──────────────────────────────────────────────────────┐ +│ Step 1 of 3: Choose Your AI Provider │ +│ │ +│ Select which AI provider you'd like to use: │ +│ │ +│ ● 1. Anthropic (Claude) │ +│ 2. OpenAI (GPT-4, GPT-3.5) │ +│ 3. Google Gemini │ +│ 4. DeepSeek │ +│ 5. Qwen │ +│ 6. I'll configure this manually later │ +│ │ +│ Use ↑↓ arrows to navigate, Enter to select │ +│ Esc to skip setup │ +└──────────────────────────────────────────────────────┘ +``` + +### Step 3: Auth Method Selection +``` +┌──────────────────────────────────────────────────────┐ +│ Step 2 of 3: Choose Authentication Method │ +│ │ +│ How would you like to authenticate with Anthropic? │ +│ │ +│ ● 1. OAuth (Recommended - secure & easy) │ +│ → Browser-based authentication │ +│ → No API key needed │ +│ │ +│ 2. API Key (Traditional) │ +│ → You'll need to provide your API key │ +│ → Get one at: console.anthropic.com │ +│ │ +│ 3. Back to provider selection │ +│ │ +│ Use ↑↓ arrows to navigate, Enter to select │ +└──────────────────────────────────────────────────────┘ +``` + +### Step 4a: OAuth Flow (if OAuth selected) +``` +┌──────────────────────────────────────────────────────┐ +│ Step 3 of 3: Authenticating with Anthropic │ +│ │ +│ Opening browser for OAuth authentication... │ +│ │ +│ Please complete the authentication in your browser. │ +│ This window will update when done. │ +│ │ +│ [Spinner animation] │ +│ │ +└──────────────────────────────────────────────────────┘ +``` + +### Step 4b: API Key Flow (if API Key selected) +``` +┌──────────────────────────────────────────────────────┐ +│ Step 3 of 3: Enter Your API Key │ +│ │ +│ Get your Anthropic API key at: │ +│ https://console.anthropic.com/settings/keys │ +│ │ +│ Enter API key: ******************************** │ +│ │ +│ Press Enter when done, or Esc to go back │ +│ │ +└──────────────────────────────────────────────────────┘ +``` + +### Step 5a: Success (Setup Completed) +``` +┌──────────────────────────────────────────────────────┐ +│ │ +│ [OK] You're all set up! │ +│ │ +│ Provider: Anthropic (Claude) │ +│ Authentication: OAuth │ +│ │ +│ Try asking me something like: │ +│ "Explain how async/await works in JavaScript" │ +│ │ +│ Press Enter to continue... │ +│ │ +└──────────────────────────────────────────────────────┘ +``` + +### Step 5b: Skipped (User Opted Out) +``` +┌──────────────────────────────────────────────────────┐ +│ │ +│ Setup skipped │ +│ │ +│ To configure llxprt manually: │ +│ • Use /auth to set up authentication │ +│ • Use /provider to select your AI provider │ +│ • Type /help for more commands │ +│ │ +│ Press Enter to continue... │ +│ │ +└──────────────────────────────────────────────────────┘ +``` + +## Data Structures + +```typescript +// Settings extension +interface WelcomeSettings { + firstRunCompleted: boolean; + welcomeFlowVersion?: string; // For future migrations + skippedWelcome?: boolean; + completedSteps?: WelcomeStep[]; +} + +// Welcome flow state +enum WelcomeStep { + WELCOME = 'welcome', + PROVIDER_SELECTION = 'provider_selection', + AUTH_METHOD = 'auth_method', + AUTHENTICATION = 'authentication', + COMPLETION = 'completion', + SKIP_EXIT = 'skip_exit', +} + +interface WelcomeFlowState { + currentStep: WelcomeStep; + selectedProvider?: string; + selectedAuthMethod?: 'oauth' | 'api_key'; + inProgress: boolean; + skipped: boolean; + error?: string; +} + +// Provider info for display +interface ProviderInfo { + id: string; + displayName: string; + description: string; + supportsOAuth: boolean; + signupUrl?: string; + apiKeyUrl?: string; + recommended?: boolean; +} + +// Available providers +const PROVIDERS: ProviderInfo[] = [ + { + id: 'anthropic', + displayName: 'Anthropic (Claude)', + description: 'Advanced reasoning and code assistance', + supportsOAuth: true, + signupUrl: 'https://console.anthropic.com/signup', + apiKeyUrl: 'https://console.anthropic.com/settings/keys', + recommended: true, + }, + { + id: 'openai', + displayName: 'OpenAI (GPT-4, GPT-3.5)', + description: 'Versatile models for chat and code', + supportsOAuth: true, + signupUrl: 'https://platform.openai.com/signup', + apiKeyUrl: 'https://platform.openai.com/api-keys', + }, + { + id: 'gemini', + displayName: 'Google Gemini', + description: 'Multimodal AI from Google', + supportsOAuth: true, + signupUrl: 'https://makersuite.google.com/app/apikey', + apiKeyUrl: 'https://makersuite.google.com/app/apikey', + }, + // ... other providers +]; +``` + +## Component Architecture + +### New Components + +**`WelcomeDialog.tsx`** +- Main dialog component +- State machine for multi-step flow +- Orchestrates child components +- Handles completion and persistence + +**`ProviderSelectionStep.tsx`** +- Displays provider list with RadioButtonSelect +- Shows provider descriptions +- Handles provider selection + +**`AuthMethodStep.tsx`** +- Shows OAuth vs API key options +- Provider-specific messaging +- Links to documentation + +**`AuthenticationStep.tsx`** +- OAuth: Progress indicator during auth flow +- API Key: Secure text input field +- Error handling and retry logic + +**`CompletionStep.tsx`** +- Success message +- Configuration summary +- Sample prompt suggestions + +### New Hooks + +**`useWelcomeFlow.ts`** +```typescript +interface UseWelcomeFlowReturn { + shouldShowWelcome: boolean; + welcomeState: WelcomeFlowState; + actions: { + startSetup: () => void; + selectProvider: (provider: string) => void; + selectAuthMethod: (method: 'oauth' | 'api_key') => void; + completeAuth: () => void; + skipWelcome: () => void; // User chooses to skip + cancelWelcome: () => void; // ESC key pressed + goBack: () => void; + }; +} + +function useWelcomeFlow(): UseWelcomeFlowReturn; +``` + +**`useFirstRunDetection.ts`** +```typescript +interface UseFirstRunDetectionReturn { + isFirstRun: boolean; + markCompleted: () => void; +} + +function useFirstRunDetection(): UseFirstRunDetectionReturn; +``` + +## Flow State Machine + +``` + ┌──────────────┐ + │ ESC/SKIP │ (Anytime) + └──────┬───────┘ + │ + ▼ + ┌──────────────┐ + ┌─────┤ SKIP_EXIT ├─────┐ + │ └──────────────┘ │ + │ │ +┌─────────┐ │ │ +│ WELCOME │───┤ │ +└────┬────┘ │ │ + │ Setup │ │ + ▼ │ │ +┌──────────────────┐ │ +│ PROVIDER_SELECT │◄────┐ │ +└────┬─────────────┘ │ │ + │ Select provider │ Back │ + ▼ │ │ +┌──────────────────┐ │ │ +│ AUTH_METHOD │─────┤ │ +└────┬─────────────┘ │ │ + │ Select method │ │ + ▼ │ │ +┌──────────────────┐ │ │ +│ AUTHENTICATION │─────┘ │ +└────┬─────────────┘ │ + │ Success │ + ▼ │ +┌──────────────────┐ │ +│ COMPLETION │ │ +└────┬─────────────┘ │ + │ Enter │ + ▼ │ +┌──────────────────┐◄────────────────────┘ +│ Normal Operation │ +└──────────────────┘ +``` + +## Integration Sequence + +### 1. First-Run Detection +```typescript +// In AppContainer.tsx or similar +const { isFirstRun, markCompleted } = useFirstRunDetection(); +const welcomeFlow = useWelcomeFlow(); + +useEffect(() => { + if (isFirstRun && !welcomeFlow.shouldShowWelcome) { + welcomeFlow.actions.start(); + } +}, [isFirstRun]); +``` + +### 2. Provider Selection +```typescript +// In WelcomeDialog.tsx +const handleProviderSelect = (providerId: string) => { + welcomeFlow.actions.selectProvider(providerId); + // Auto-advance to auth method step +}; +``` + +### 3. Authentication Execution +```typescript +// For OAuth +const handleOAuthAuth = async () => { + const authCommand = `/auth ${selectedProvider} enable`; + // Delegate to existing auth command logic + await executeAuthCommand(authCommand); +}; + +// For API Key +const handleApiKeyAuth = async (apiKey: string) => { + settingsService.set(`${selectedProvider}ApiKey`, apiKey); + await settingsService.save(); +}; +``` + +### 4. Provider Activation +```typescript +// After successful auth +const activateProvider = () => { + providerManager.setActiveProvider(selectedProvider); + markCompleted(); + welcomeFlow.actions.complete(); +}; +``` + +## Non-Functional Requirements + +### Performance + +**[REQ-017]** Welcome flow shall start within 500ms of app launch. + - **[REQ-017.1]** First-run check must be synchronous and fast + - **[REQ-017.2]** Provider list loaded from static configuration + +**[REQ-018]** Step transitions shall be instant (<100ms). + - **[REQ-018.1]** No network calls during navigation + - **[REQ-018.2]** Pre-load next step content + +### Reliability + +**[REQ-019]** Welcome flow shall never prevent app from starting. + - **[REQ-019.1]** Errors in welcome flow fall back to normal start + - **[REQ-019.2]** Corrupted state resets to initial step + - **[REQ-019.3]** Always provide skip/exit option + +**[REQ-020]** Authentication failures shall allow retry. + - **[REQ-020.1]** Show error message with retry option + - **[REQ-020.2]** Allow switching auth methods + - **[REQ-020.3]** Preserve provider selection on retry + +### Usability + +**[REQ-021]** All interactions shall be keyboard-driven. + - **[REQ-021.1]** No mouse required + - **[REQ-021.2]** Clear keyboard shortcuts at each step + - **[REQ-021.3]** Consistent navigation patterns + +**[REQ-022]** Help shall be available at every step. + - **[REQ-022.1]** `?` key shows context help + - **[REQ-022.2]** Links to documentation displayed + - **[REQ-022.3]** Tooltips for complex options + +## Testing Requirements + +### Unit Tests + +**[TEST-001]** Test first-run detection logic + - New installation → shows welcome + - Existing installation → skips welcome + - Completed flag persists correctly + +**[TEST-002]** Test state machine transitions + - All valid state transitions work + - Invalid transitions are prevented + - Back navigation works correctly + +**[TEST-003]** Test provider selection + - All providers selectable + - Provider info displays correctly + - Selection persists during flow + +**[TEST-004]** Test auth method selection + - OAuth option shown when supported + - API key always available + - Method selection persists + +**[TEST-005]** Test authentication integration + - OAuth flow triggers correctly + - API key validation works + - Errors handled gracefully + +### Integration Tests + +**[TEST-006]** Test complete welcome flow + - Start to finish with OAuth + - Start to finish with API key + - Skip at various points + +**[TEST-007]** Test provider activation + - Selected provider becomes active + - Settings persisted correctly + - App ready to use after completion + +**[TEST-008]** Test interruption handling + - Exit during flow + - Crash recovery + - Resume partial flow (if implemented) + +### User Acceptance Tests + +**[TEST-009]** New user onboarding + 1. Fresh install of llxprt + 2. Launch application + 3. Complete welcome flow + 4. Verify can immediately start using + 5. No errors or confusion + +**[TEST-010]** Skip flow validation + 1. Launch fresh install + 2. Skip welcome flow (via "Skip setup" option) + 3. Verify app starts normally + 4. Verify welcome never shows again + 5. Verify skip acknowledgment message shown + 6. Manual setup still possible via /auth and /provider + +**[TEST-011]** ESC key skip validation + 1. Launch fresh install + 2. Press ESC during various steps + 3. Verify immediate exit to skip screen + 4. Verify first-run marked complete + 5. App continues to normal operation + +**[TEST-012]** Power user experience + 1. User with existing knowledge + 2. Can skip in <2 seconds from launch + 3. No friction or annoyance + 4. Clear path to manual configuration + +## Documentation Requirements + +**[DOC-001]** User documentation + - Add "Getting Started" guide with screenshots + - Document welcome flow steps + - Explain skip vs complete + - Manual setup alternative + +**[DOC-002]** Developer documentation + - Architecture diagram + - State machine documentation + - Extension points for new providers + - Testing strategy + +**[DOC-003]** Troubleshooting guide + - Common welcome flow issues + - How to reset first-run flag + - Manual trigger of welcome flow + - Recovery from errors + +**[DOC-004]** Changelog entry + - Announce new welcome flow + - Explain benefits for new users + - Note no impact on existing users + +## Success Metrics + +### User Experience Metrics +- **Time to First Successful Interaction**: + - New users with setup: Target <2 minutes from launch + - Power users who skip: Target <5 seconds from launch to normal operation +- **Setup Completion Rate**: Target >80% complete welcome flow (vs skip) +- **Skip Experience**: <2 seconds from launch to skip confirmation +- **Authentication Success Rate**: Target >95% successful auth on first attempt +- **User Confusion Reports**: Reduce by >70% compared to current onboarding +- **Power User Friction**: Zero complaints about forced onboarding + +### Technical Metrics +- **Welcome Flow Start Time**: <500ms from app launch +- **Step Transition Time**: <100ms between steps +- **Error Rate**: <5% encounter errors during flow +- **Skip Rate**: <20% skip welcome flow + +## Future Enhancements + +### Phase 2 (Post-Launch) + +**Multi-Provider Setup** +- Allow configuring multiple providers in welcome flow +- Show comparison matrix during selection +- Support fallback provider configuration + +**Smart Recommendations** +- Detect use case from command line flags +- Recommend provider based on task type +- Show which providers have free tiers + +**Tutorial Mode** +- Interactive tutorial after setup +- Sample prompts with explanations +- Feature discovery tour + +**Profile Templates** +- Pre-configured profiles for common use cases +- "Developer", "Writer", "Data Analyst" templates +- One-click profile selection + +**Provider Health Checks** +- Validate provider API availability during setup +- Show real-time status of providers +- Suggest alternatives if provider down + +## Open Questions + +1. **Should we show pricing information during provider selection?** + - **Decision Needed**: May help users make informed choice, but adds complexity + +2. **How should we handle provider-specific requirements (e.g., Gemini project ID)?** + - **Decision Needed**: Show additional step for providers with extra config, or handle in post-setup? + +3. **Should welcome flow support multiple profiles/workspaces?** + - **Decision Needed**: Keep simple for v1, add in v2 + +4. **What happens if user has env vars set but no provider configured?** + - **Decision Needed**: Skip welcome or show "detected config" message + +5. **Should we allow changing provider/auth during welcome flow?** + - **Decision Needed**: Support back navigation or force restart? + +## Dependencies + +- RadioButtonSelect component (existing) +- Dialog system infrastructure (existing) +- ProviderManager API (existing) +- OAuth/Auth command infrastructure (existing) +- SettingsService API (existing) +- Keyboard input handling (existing) + +## Risks and Mitigations + +| Risk | Impact | Probability | Mitigation | +|------|--------|-------------|------------| +| Welcome flow too complex/long | High | Medium | Keep to 3-4 steps max, progressive disclosure | +| Auth failures during setup | High | Medium | Clear error messages, retry, allow skip | +| Users skip and still confused | Medium | Medium | Make manual setup easy, show hints in normal mode | +| Provider API changes break flow | Medium | Low | Validate providers at runtime, graceful degradation | +| State corruption prevents startup | High | Low | Always allow escape hatch, reset mechanism | +| Power users annoyed by onboarding | Medium | High | **Prominent skip option, ESC works everywhere, <2s to skip** | +| Skip too easy, new users miss help | Low | Medium | Make setup option default, but skip clearly visible | + +## Acceptance Criteria + +[OK] New users see welcome flow on first launch +[OK] Welcome flow completes in <3 minutes for typical user +[OK] Power users can skip in <2 seconds without friction +[OK] ESC key works at any step to skip immediately +[OK] Skip option prominent with positive, respectful language +[OK] All providers can be configured through welcome flow +[OK] OAuth and API key authentication both work +[OK] Successfully configured provider is immediately usable +[OK] Welcome flow never shows again after completion (skip or complete) +[OK] Existing users never see welcome flow +[OK] Clear, friendly messaging throughout +[OK] All keyboard-driven, no mouse required +[OK] Skip acknowledgment shows manual setup commands (/auth, /provider) + +## Timeline Estimate + +- **Design & UX Refinement**: 4-6 hours +- **Component Implementation**: 16-20 hours + - WelcomeDialog and step components: 8-10 hours + - Hooks and state management: 4-6 hours + - Integration with existing systems: 4-6 hours +- **Testing**: 8-12 hours + - Unit tests: 4-6 hours + - Integration tests: 2-4 hours + - User acceptance testing: 2-4 hours +- **Documentation**: 4-6 hours +- **Polish & Bug Fixes**: 4-8 hours +- **Total**: 36-52 hours (5-7 days) + +## Version History + +| Version | Date | Author | Changes | + +## Design Philosophy: Respecting User Expertise + +### Core Principles + +1. **Never Force**: Users should never feel trapped or forced through onboarding +2. **Respect Time**: Power users value their time - make skip instantaneous +3. **Positive Language**: "I know what I'm doing" not "No thanks" or "Skip" +4. **Clear Exit**: ESC key should work from any screen, anytime +5. **No Shame**: Skipping is a valid, respected choice, not a failure +6. **Helpful Fallback**: If skipped, provide clear pointers to manual setup + +### User Personas + +**Persona 1: Complete Beginner** +- Never used CLI tools before +- Needs hand-holding through setup +- Benefits from: Full guided flow with explanations +- Estimated time: 2-3 minutes + +**Persona 2: Experienced Developer (New to llxprt)** +- Knows CLI tools, first time with llxprt +- Wants to see options but can configure themselves +- Benefits from: Quick overview, optional skip after seeing providers +- Estimated time: 30-60 seconds (may complete or skip) + +**Persona 3: Power User / Advanced** +- Already read docs, knows exactly what to do +- Just wants to get started immediately +- Benefits from: Instant skip option, no friction +- Estimated time: <2 seconds (immediate skip) + +**Persona 4: Returning User (Reinstall/New Machine)** +- Used llxprt before, reinstalling +- Already knows the system +- Benefits from: Instant skip, muscle memory works (ESC) +- Estimated time: <1 second (ESC reflex) + +### Skip UX Best Practices + +**DO:** +- Make skip option visible on first screen +- Use clear, positive language: "I know what I'm doing" +- Allow ESC key from anywhere +- Show brief, helpful message after skip +- Mark first-run complete even when skipped +- Provide command references (/auth, /provider) + +**DON'T:** +- Hide skip option in deep menus +- Use negative language: "No", "Cancel", "Not now" +- Require confirmation to skip ("Are you sure?") +- Make user feel bad for skipping +- Block ESC key or other exit methods +- Leave user confused after skipping + +### Skip Flow Examples + +**Good Skip Experience:** +``` +User: [Launches llxprt] +System: [Shows welcome with "Setup now" and "Skip setup" options] +User: [Presses ESC] +System: "Setup skipped. Use /auth and /provider to configure. Press Enter..." +User: [Continues immediately] +Time: <2 seconds +Feeling: Respected, in control +``` + +**Bad Skip Experience (What NOT to do):** +``` +User: [Launches llxprt] +System: [Shows welcome, no skip visible] +User: [Presses ESC] +System: [ESC doesn't work] +User: [Looks for skip, not obvious] +System: [Must navigate through screens] +User: [Finally finds "No thanks"] +System: "Are you sure you want to skip? Setup is recommended." +User: [Confirms] +Time: >30 seconds +Feeling: Frustrated, annoyed +``` + + +|---------|------|--------|---------| +| 1.0 | 2026-01-03 | Initial | Initial PRD creation |