/** * @license * Copyright 2025 Vybestack LLC * SPDX-License-Identifier: Apache-2.0 */ import { useState, useCallback, useEffect } from 'react'; import { DebugLogger } from '@vybestack/llxprt-code-telemetry'; import { type LoadedSettings } from '../../config/settings.js'; import type { Agent } from '@vybestack/llxprt-code-agents'; 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 | Promise; selectAuthMethod: (method: 'oauth' | 'api_key') => void; onAuthComplete: () => void | Promise; onAuthError: (error: string) => void; skipSetup: () => void; goBack: () => void; saveProfile: (name: string) => Promise; dismiss: () => void; /** Re-trigger the welcome onboarding flow (for /setup command) */ resetAndReopen: () => void; } export interface UseWelcomeOnboardingOptions { settings: LoadedSettings; isFolderTrustComplete: boolean; agent: Agent; suppressStartup?: 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; } function useProviderLoader( runtime: ReturnType, ): [string[], React.Dispatch>] { const [availableProviders, setAvailableProviders] = useState([]); useEffect(() => { const providerManager = runtime.getCliProviderManager(); const providers = providerManager.listProviders(); setAvailableProviders(providers); debug.log(`Loaded ${providers.length} providers: ${providers.join(', ')}`); }, [runtime]); return [availableProviders, setAvailableProviders]; } function useModelLoader( runtime: ReturnType, selectedProvider: string | undefined, setState: React.Dispatch>, ): [ModelInfo[], React.Dispatch>] { const [availableModels, setAvailableModels] = useState([]); useEffect(() => { const loadModels = async () => { if (!selectedProvider) { setAvailableModels([]); setState((prev) => ({ ...prev, modelsLoadStatus: 'idle' })); return; } setState((prev) => ({ ...prev, modelsLoadStatus: 'loading' })); try { const models = await runtime.listAvailableModels(selectedProvider); const modelInfos: ModelInfo[] = models.map((m) => ({ id: m.id, name: m.name, })); setAvailableModels(modelInfos); setState((prev) => ({ ...prev, modelsLoadStatus: 'success' })); debug.log(`Loaded ${modelInfos.length} models for ${selectedProvider}`); } catch (error) { debug.log(`Failed to load models: ${error}`); setAvailableModels([]); setState((prev) => ({ ...prev, modelsLoadStatus: 'error' })); } }; void loadModels(); }, [runtime, selectedProvider, setState]); return [availableModels, setAvailableModels]; } function useSelectModelAction( runtime: ReturnType, setState: React.Dispatch>, ) { return useCallback( async (modelId: string) => { debug.log(`[selectModel] Setting model: ${modelId}`); try { await runtime.setActiveModel(modelId); debug.log(`[selectModel] Model set successfully: ${modelId}`); } catch (error) { debug.log(`[selectModel] Failed to set model: ${error}`); setState((prev) => ({ ...prev, error: `Failed to set model: ${error instanceof Error ? error.message : String(error)}`, })); return; } setState((prev) => ({ ...prev, selectedModel: modelId, step: 'completion', })); }, [runtime, setState], ); } function useOnAuthCompleteAction( runtime: ReturnType, selectedProvider: string | undefined, setState: React.Dispatch>, setAvailableModels: React.Dispatch>, ) { return useCallback(async () => { debug.log( `[onAuthComplete] Auth complete for provider: ${selectedProvider}`, ); try { if (selectedProvider) { setState((prev) => ({ ...prev, modelsLoadStatus: 'loading' })); const models = await runtime.listAvailableModels(selectedProvider); const modelInfos: ModelInfo[] = models.map((m) => ({ id: m.id, name: m.name, })); setAvailableModels(modelInfos); setState((prev) => ({ ...prev, modelsLoadStatus: 'success' })); debug.log( `[onAuthComplete] Loaded ${modelInfos.length} models for ${selectedProvider}`, ); } } catch (error) { debug.log(`[onAuthComplete] Failed to reload models: ${error}`); setAvailableModels([]); setState((prev) => ({ ...prev, modelsLoadStatus: 'error' })); } setState((prev) => ({ ...prev, step: 'model', authInProgress: false, error: undefined, })); }, [runtime, selectedProvider, setState, setAvailableModels]); } function useGoBackAction( setState: React.Dispatch>, ) { return 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 'model': return { ...prev, step: 'auth_method', selectedModel: undefined }; case 'provider': return { ...prev, step: 'welcome' }; default: return prev; } }); }, [setState]); } function useWelcomeStepActions( runtime: ReturnType, setState: React.Dispatch>, selectedProvider: string | undefined, setAvailableModels: React.Dispatch>, ) { const startSetup = useCallback(() => { setState((prev) => ({ ...prev, step: 'provider' })); }, [setState]); const selectProvider = useCallback( (providerId: string) => { setState((prev) => ({ ...prev, selectedProvider: providerId, step: 'auth_method', })); }, [setState], ); const selectModel = useSelectModelAction(runtime, setState); const onAuthComplete = useOnAuthCompleteAction( runtime, selectedProvider, setState, setAvailableModels, ); const selectAuthMethod = useCallback( (method: 'oauth' | 'api_key') => { setState((prev) => ({ ...prev, selectedAuthMethod: method, step: 'authenticating', authInProgress: true, })); }, [setState], ); const onAuthError = useCallback( (error: string) => { setState((prev) => ({ ...prev, authInProgress: false, error, step: 'auth_method', })); }, [setState], ); const skipSetup = useCallback(() => { setState((prev) => ({ ...prev, step: 'skipped' })); }, [setState]); const goBack = useGoBackAction(setState); return { startSetup, selectProvider, selectModel, selectAuthMethod, onAuthComplete, onAuthError, skipSetup, goBack, }; } function useProfileSave(runtime: ReturnType) { return 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...`); 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], ); } function useTriggerAuth( runtime: ReturnType, agent: Agent, ) { return useCallback( async ( provider: string, method: 'oauth' | 'api_key', apiKey?: string, ): Promise => { debug.log(`[triggerAuth] START provider=${provider} method=${method}`); const providerManager = runtime.getCliProviderManager(); debug.log( `[triggerAuth] Before auth - current active: ${providerManager.getActiveProviderName()}`, ); // Authenticate FIRST before switching provider (prevents double OAuth) if (method === 'oauth') { debug.log(`[triggerAuth] Starting OAuth for ${provider}`); await runtime.getCliOAuthManager().authenticate(provider, undefined, { signalAuthCompletion: true, }); debug.log(`[triggerAuth] OAuth complete for ${provider}`); } // Now switch to the provider AFTER auth is complete // IMPORTANT: Pass autoOAuth: false to prevent the agent provider switch // from triggering a second OAuth flow - we already authenticated above const switchResult = await agent.setProvider(provider, undefined, { autoOAuth: false, }); debug.log( `[triggerAuth] After provider switch - changed: ${switchResult.changed}, now active: ${providerManager.getActiveProviderName()}`, ); // For API key method, set the key after switching provider if (method === 'api_key') { if (!apiKey) { throw new Error('API key is required for API key authentication'); } debug.log( `[triggerAuth] Calling updateActiveProviderApiKey for ${provider}`, ); const result = await runtime.updateActiveProviderApiKey(apiKey); debug.log( `[triggerAuth] API key result: ${result.message}, providerName=${result.providerName}`, ); } debug.log( `[triggerAuth] END - active provider: ${providerManager.getActiveProviderName()}`, ); }, [runtime, agent], ); } export const useWelcomeOnboarding = ( options: UseWelcomeOnboardingOptions, ): UseWelcomeOnboardingReturn => { const { settings: _settings, isFolderTrustComplete, agent } = options; const suppressStartup = options.suppressStartup === true; const runtime = useRuntimeApi(); const [welcomeCompleted, setWelcomeCompleted] = useState(() => suppressStartup ? true : 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] = useProviderLoader(runtime); const [availableModels, setAvailableModels] = useModelLoader( runtime, state.selectedProvider, setState, ); const { startSetup, selectProvider, selectModel, selectAuthMethod, onAuthComplete, onAuthError, skipSetup, goBack, } = useWelcomeStepActions( runtime, setState, state.selectedProvider, setAvailableModels, ); const saveProfile = useProfileSave(runtime); const triggerAuth = useTriggerAuth(runtime, agent); const dismiss = useCallback(() => { const skipped = state.step === 'skipped'; markWelcomeCompleted(skipped); setWelcomeCompleted(true); debug.log(`Welcome flow completed (skipped: ${skipped})`); }, [state.step]); const resetAndReopen = useCallback(() => { debug.log('[resetAndReopen] Re-triggering welcome onboarding'); setWelcomeCompleted(false); setState({ step: 'welcome', authInProgress: false, modelsLoadStatus: 'idle', }); setAvailableModels([]); }, [setState, setAvailableModels]); return { showWelcome, state, actions: { startSetup, selectProvider, selectModel, selectAuthMethod, onAuthComplete, onAuthError, skipSetup, goBack, saveProfile, dismiss, resetAndReopen, }, availableProviders, availableModels, triggerAuth, }; };