Lifei/clean up old onboarding flow (#8099)

This commit is contained in:
Lifei Zhou
2026-03-26 11:34:22 +11:00
committed by GitHub
parent 8615853a89
commit d2553978c4
20 changed files with 10 additions and 1178 deletions
-3
View File
@@ -387,7 +387,6 @@ derive_utoipa!(Icon as IconSchema);
super::routes::status::diagnostics,
super::routes::mcp_ui_proxy::mcp_ui_proxy,
super::routes::config_management::backup_config,
super::routes::config_management::detect_provider,
super::routes::config_management::recover_config,
super::routes::config_management::validate_config,
super::routes::config_management::init_config,
@@ -484,8 +483,6 @@ derive_utoipa!(Icon as IconSchema);
components(schemas(
super::routes::config_management::UpsertConfigQuery,
super::routes::config_management::ConfigKeyQuery,
super::routes::config_management::DetectProviderRequest,
super::routes::config_management::DetectProviderResponse,
super::routes::config_management::ConfigResponse,
super::routes::config_management::ProvidersResponse,
super::routes::config_management::ProviderDetails,
@@ -12,7 +12,6 @@ use goose::config::paths::Paths;
use goose::config::ExtensionEntry;
use goose::config::{Config, ConfigError};
use goose::model::ModelConfig;
use goose::providers::auto_detect::detect_provider_from_api_key;
use goose::providers::base::{ProviderMetadata, ProviderType};
use goose::providers::canonical::maybe_get_canonical_model;
use goose::providers::catalog::{
@@ -149,16 +148,6 @@ pub struct SlashCommandsResponse {
pub commands: Vec<SlashCommand>,
}
#[derive(Deserialize, ToSchema)]
pub struct DetectProviderRequest {
pub api_key: String,
}
#[derive(Serialize, ToSchema)]
pub struct DetectProviderResponse {
pub provider_name: String,
pub models: Vec<String>,
}
#[utoipa::path(
post,
path = "/config/upsert",
@@ -534,31 +523,6 @@ pub async fn upsert_permissions(
Ok(Json("Permissions updated successfully".to_string()))
}
#[utoipa::path(
post,
path = "/config/detect-provider",
request_body = DetectProviderRequest,
responses(
(status = 200, description = "Provider detected successfully", body = DetectProviderResponse),
(status = 404, description = "No matching provider found"),
)
)]
pub async fn detect_provider(
Json(detect_request): Json<DetectProviderRequest>,
) -> Result<Json<DetectProviderResponse>, ErrorResponse> {
let api_key = detect_request.api_key.trim();
match detect_provider_from_api_key(api_key).await {
Some((provider_name, models)) => Ok(Json(DetectProviderResponse {
provider_name,
models,
})),
None => Err(ErrorResponse::not_found(
"Could not detect provider from the provided API key",
)),
}
}
#[utoipa::path(
post,
path = "/config/backup",
@@ -930,7 +894,6 @@ pub fn routes(state: Arc<AppState>) -> Router {
"/config/providers/{name}/cleanup",
post(cleanup_provider_cache),
)
.route("/config/detect-provider", post(detect_provider))
.route("/config/slash_commands", get(get_slash_commands))
.route(
"/config/canonical-model-info",
-61
View File
@@ -1,61 +0,0 @@
use crate::model::ModelConfig;
use crate::providers::retry::{retry_operation, RetryConfig};
pub async fn detect_provider_from_api_key(api_key: &str) -> Option<(String, Vec<String>)> {
let provider_tests = vec![
("anthropic", "ANTHROPIC_API_KEY"),
("openai", "OPENAI_API_KEY"),
("google", "GOOGLE_API_KEY"),
("groq", "GROQ_API_KEY"),
("xai", "XAI_API_KEY"),
// Ollama and OpenRouter don't validate keys, so they would match any input
];
let tasks: Vec<_> = provider_tests
.into_iter()
.map(|(provider_name, env_key)| {
let api_key = api_key.to_string();
tokio::spawn(async move {
let original_value = std::env::var(env_key).ok();
std::env::set_var(env_key, &api_key);
let result = match crate::providers::create(
provider_name,
ModelConfig::new_or_fail("default").with_canonical_limits(provider_name),
Vec::new(),
)
.await
{
Ok(provider) => {
match retry_operation(&RetryConfig::default(), || async {
provider.fetch_supported_models().await
})
.await
{
Ok(models) if !models.is_empty() => {
Some((provider_name.to_string(), models))
}
_ => None,
}
}
Err(_) => None,
};
match original_value {
Some(val) => std::env::set_var(env_key, val),
None => std::env::remove_var(env_key),
}
result
})
})
.collect();
for task in tasks {
if let Ok(Some(result)) = task.await {
return Some(result);
}
}
None
}
-1
View File
@@ -1,6 +1,5 @@
pub mod anthropic;
pub mod api_client;
pub mod auto_detect;
pub mod avian;
pub mod azure;
pub mod azureauth;
-62
View File
@@ -969,39 +969,6 @@
}
}
},
"/config/detect-provider": {
"post": {
"tags": [
"super::routes::config_management"
],
"operationId": "detect_provider",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DetectProviderRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Provider detected successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DetectProviderResponse"
}
}
}
},
"404": {
"description": "No matching provider found"
}
}
}
},
"/config/extensions": {
"get": {
"tags": [
@@ -4701,35 +4668,6 @@
}
}
},
"DetectProviderRequest": {
"type": "object",
"required": [
"api_key"
],
"properties": {
"api_key": {
"type": "string"
}
}
},
"DetectProviderResponse": {
"type": "object",
"required": [
"provider_name",
"models"
],
"properties": {
"models": {
"type": "array",
"items": {
"type": "string"
}
},
"provider_name": {
"type": "string"
}
}
},
"DictationProvider": {
"type": "string",
"enum": [
-17
View File
@@ -61,10 +61,6 @@ vi.mock('./sessions', () => ({
generateSessionId: vi.fn(),
}));
vi.mock('./utils/openRouterSetup', () => ({
startOpenRouterSetup: vi.fn().mockResolvedValue({ success: false, message: 'Test' }),
}));
// Mock the ConfigContext module
vi.mock('./components/ConfigContext', () => ({
useConfig: () => ({
@@ -83,19 +79,6 @@ vi.mock('./components/ErrorBoundary', () => ({
ErrorUI: ({ error }: { error: Error }) => <div>Error: {error.message}</div>,
}));
// Mock ProviderGuard to show the welcome screen when no provider is configured
vi.mock('./components/ProviderGuard', () => ({
default: ({ children }: { children: React.ReactNode }) => {
// In a real app, ProviderGuard would check for provider and show welcome screen
// For this test, we'll simulate that behavior
const hasProvider = window.electron?.getConfig()?.GOOSE_DEFAULT_PROVIDER;
if (!hasProvider) {
return <div>Welcome to Goose!</div>;
}
return <>{children}</>;
},
}));
vi.mock('./components/ModelAndProviderContext', () => ({
ModelAndProviderProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
useModelAndProvider: () => ({
+7 -26
View File
@@ -15,9 +15,7 @@ import { ExtensionInstallModal } from './components/ExtensionInstallModal';
import { ToastContainer } from 'react-toastify';
import AnnouncementModal from './components/AnnouncementModal';
import TelemetryOptOutModal from './components/TelemetryOptOutModal';
import ProviderGuard from './components/ProviderGuard';
import OnboardingGuard from './components/onboarding/OnboardingGuard';
import { USE_NEW_ONBOARDING } from './featureFlags';
import { createSession } from './sessions';
import { ChatType } from './types/chat';
@@ -249,11 +247,7 @@ const ConfigureProvidersRoute = () => {
);
};
interface WelcomeRouteProps {
onSelectProvider: () => void;
}
const WelcomeRoute = ({ onSelectProvider }: WelcomeRouteProps) => {
const WelcomeRoute = () => {
const navigate = useNavigate();
return (
@@ -265,7 +259,6 @@ const WelcomeRoute = ({ onSelectProvider }: WelcomeRouteProps) => {
isOnboarding={true}
onProviderLaunched={(model?: string) => {
trackOnboardingCompleted('other', model);
onSelectProvider();
navigate('/', { replace: true });
}}
/>
@@ -351,7 +344,6 @@ export function AppInner() {
const [fatalError, setFatalError] = useState<string | null>(null);
const [isLoadingSharedSession, setIsLoadingSharedSession] = useState(false);
const [sharedSessionError, setSharedSessionError] = useState<string | null>(null);
const [didSelectProvider, setDidSelectProvider] = useState<boolean>(false);
const navigate = useNavigate();
const setView = useNavigation();
@@ -650,28 +642,17 @@ export function AppInner() {
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
<Routes>
<Route path="launcher" element={<LauncherView />} />
<Route
path="welcome"
element={<WelcomeRoute onSelectProvider={() => setDidSelectProvider(true)} />}
/>
<Route path="welcome" element={<WelcomeRoute />} />
<Route path="configure-providers" element={<ConfigureProvidersRoute />} />
<Route path="standalone-app" element={<StandaloneAppView />} />
<Route
path="/"
element={
USE_NEW_ONBOARDING ? (
<OnboardingGuard>
<ChatProvider chat={chat} setChat={setChat} contextKey="hub">
<AppLayout activeSessions={activeSessions} />
</ChatProvider>
</OnboardingGuard>
) : (
<ProviderGuard didSelectProvider={didSelectProvider}>
<ChatProvider chat={chat} setChat={setChat} contextKey="hub">
<AppLayout activeSessions={activeSessions} />
</ChatProvider>
</ProviderGuard>
)
<OnboardingGuard>
<ChatProvider chat={chat} setChat={setChat} contextKey="hub">
<AppLayout activeSessions={activeSessions} />
</ChatProvider>
</OnboardingGuard>
}
>
<Route index element={<HubRouteWrapper />} />
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-32
View File
@@ -234,15 +234,6 @@ export type DeleteRecipeRequest = {
id: string;
};
export type DetectProviderRequest = {
api_key: string;
};
export type DetectProviderResponse = {
models: Array<string>;
provider_name: string;
};
export type DictationProvider = 'openai' | 'elevenlabs' | 'groq' | 'local';
export type DictationProviderStatus = {
@@ -2356,29 +2347,6 @@ export type UpdateCustomProviderResponses = {
export type UpdateCustomProviderResponse = UpdateCustomProviderResponses[keyof UpdateCustomProviderResponses];
export type DetectProviderData = {
body: DetectProviderRequest;
path?: never;
query?: never;
url: '/config/detect-provider';
};
export type DetectProviderErrors = {
/**
* No matching provider found
*/
404: unknown;
};
export type DetectProviderResponses = {
/**
* Provider detected successfully
*/
200: DetectProviderResponse;
};
export type DetectProviderResponse2 = DetectProviderResponses[keyof DetectProviderResponses];
export type GetExtensionsData = {
body?: never;
path?: never;
-182
View File
@@ -1,182 +0,0 @@
import { useState, useRef } from 'react';
import { detectProvider } from '../api';
import { Key } from './icons/Key';
import { ArrowRight } from './icons/ArrowRight';
import { Button } from './ui/button';
interface ApiKeyTesterProps {
onSuccess: (provider: string, model: string, apiKey: string) => void;
onStartTesting?: () => void;
}
interface DetectionResult {
provider: string;
model: string;
totalModels: number;
}
export default function ApiKeyTester({ onSuccess, onStartTesting }: ApiKeyTesterProps) {
const [apiKey, setApiKey] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [result, setResult] = useState<DetectionResult | null>(null);
const [error, setError] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const testApiKey = async () => {
const actualValue = inputRef.current?.value || apiKey;
if (!actualValue.trim()) {
return;
}
onStartTesting?.();
setIsLoading(true);
setResult(null);
setError(false);
try {
const response = await detectProvider({
body: { api_key: actualValue },
throwOnError: true,
});
if (response.data) {
const { provider_name, models } = response.data;
setResult({
provider: provider_name,
model: models[0],
totalModels: models.length,
});
setTimeout(() => {
onSuccess(provider_name, models[0], actualValue);
}, 1500);
}
} catch {
setError(true);
} finally {
setIsLoading(false);
}
};
const hasInput = apiKey.trim().length > 0;
const canSubmit = hasInput && !isLoading;
return (
<div className="relative w-full mb-6">
{/* Recommended pill */}
<div className="absolute -top-2 -right-2 sm:-top-3 sm:-right-3 z-20">
<span className="inline-block px-2 py-1 text-xs font-medium bg-blue-600 text-white rounded-full">
Recommended if you have API access already
</span>
</div>
<div className="w-full p-3 sm:p-4 bg-background-secondary border rounded-xl">
<div className="flex items-center gap-3 mb-3">
<Key className="w-4 h-4 text-text-primary flex-shrink-0" />
<div className="flex flex-col sm:flex-row sm:items-center sm:gap-2">
<h3 className="font-medium text-text-primary text-sm sm:text-base">
Quick Setup with API Key
</h3>
<span className="text-text-secondary text-xs sm:text-sm">
Auto-detect your provider
</span>
</div>
</div>
<div className="space-y-4">
<div className="flex gap-2 items-stretch">
<input
ref={inputRef}
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="Enter your API key (OpenAI, Anthropic, Google, etc.)"
className="flex-1 px-3 py-2 border rounded-lg bg-background-primary text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
disabled={isLoading}
onKeyDown={(e) => {
if (e.key === 'Enter' && canSubmit) {
testApiKey();
}
}}
/>
<Button
onClick={testApiKey}
disabled={!canSubmit}
variant={canSubmit ? 'default' : 'secondary'}
className="h-auto py-2 px-4"
>
{isLoading ? (
<div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin"></div>
) : (
<ArrowRight className="w-4 h-4" />
)}
</Button>
</div>
{/* Loading state */}
{isLoading && (
<div className="flex items-center gap-2 px-3 py-2 bg-background-secondary rounded text-sm text-text-secondary">
<div className="w-3 h-3 border-2 border-current border-t-transparent rounded-full animate-spin"></div>
<span>Detecting provider and validating key...</span>
</div>
)}
{/* Success result */}
{result && (
<div className="flex items-center gap-2 text-sm p-3 rounded-lg bg-green-50 text-green-800 border border-green-200 dark:bg-green-900/20 dark:text-green-200 dark:border-green-800">
<span></span>
<div className="flex-1">
<div className="font-medium">Detected {result.provider}</div>
<div className="text-green-600 dark:text-green-400 text-xs mt-1">
Model: {result.model} ({result.totalModels} models available)
</div>
</div>
</div>
)}
{/* Error result */}
{error && (
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm p-3 rounded-lg bg-red-50 text-red-800 border border-red-200 dark:bg-red-900/20 dark:text-red-200 dark:border-red-800">
<span></span>
<div className="flex-1">
<div className="font-medium">Provider Detection Failed</div>
<div className="text-red-600 dark:text-red-400 text-xs mt-1">
Could not detect provider from API key
</div>
</div>
</div>
<div className="ml-6 space-y-1">
<p className="text-xs font-medium text-text-secondary">Suggestions:</p>
<ul className="text-xs text-text-secondary space-y-1">
<li className="flex items-start gap-1">
<span className="text-blue-500 mt-0.5"></span>
<span>
Make sure you are using a valid API key from OpenAI, Anthropic, Google, Groq,
or xAI
</span>
</li>
<li className="flex items-start gap-1">
<span className="text-blue-500 mt-0.5"></span>
<span>Check that the key is complete and not truncated</span>
</li>
<li className="flex items-start gap-1">
<span className="text-blue-500 mt-0.5"></span>
<span>Verify your API key is active and has sufficient credits</span>
</li>
<li className="flex items-start gap-1">
<span className="text-blue-500 mt-0.5"></span>
<span>For local Ollama setup, use the "Other Providers" section below</span>
</li>
</ul>
</div>
</div>
)}
</div>
</div>
</div>
);
}
@@ -1,40 +0,0 @@
import { useConfig } from './ConfigContext';
import { toastService } from '../toasts';
import { Goose } from './icons';
import LocalModelPicker from './onboarding/LocalModelPicker';
interface LocalModelSetupProps {
onSuccess: () => void;
onCancel: () => void;
}
export function LocalModelSetup({ onSuccess, onCancel }: LocalModelSetupProps) {
const { upsert } = useConfig();
const handleConfigured = async (_providerName: string, modelId: string) => {
await upsert('GOOSE_PROVIDER', 'local', false);
await upsert('GOOSE_MODEL', modelId, false);
toastService.success({
title: 'Local Model Ready',
msg: `Running entirely on your machine with ${modelId}.`,
});
onSuccess();
};
return (
<div className="space-y-6">
<div className="text-left space-y-3">
<div className="origin-bottom-left goose-icon-animation">
<Goose className="size-6 sm:size-8" />
</div>
<h1 className="text-2xl sm:text-4xl font-light">Run Locally</h1>
<p className="text-text-muted text-base sm:text-lg">
Download a model to run goose entirely on your machine no API keys, no accounts,
completely free and private.
</p>
</div>
<LocalModelPicker onConfigured={handleConfigured} onBack={onCancel} />
</div>
);
}
-582
View File
@@ -1,582 +0,0 @@
import { useEffect, useState, useMemo, useRef, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useConfig } from './ConfigContext';
import { SetupModal } from './SetupModal';
import { startOpenRouterSetup } from '../utils/openRouterSetup';
import { startTetrateSetup } from '../utils/tetrateSetup';
import { startChatGptCodexSetup } from '../utils/chatgptCodexSetup';
import WelcomeGooseLogo from './WelcomeGooseLogo';
import { toastService } from '../toasts';
import { LocalModelSetup } from './LocalModelSetup';
import ApiKeyTester from './ApiKeyTester';
import { SwitchModelModal } from './settings/models/subcomponents/SwitchModelModal';
import { createNavigationHandler } from '../utils/navigationUtils';
import TelemetrySettings from './settings/app/TelemetrySettings';
import {
trackOnboardingStarted,
trackOnboardingProviderSelected,
trackOnboardingCompleted,
trackOnboardingAbandoned,
trackOnboardingSetupFailed,
} from '../utils/analytics';
import { Goose, OpenRouter, Tetrate, ChatGPT } from './icons';
interface ProviderGuardProps {
didSelectProvider: boolean;
children: React.ReactNode;
}
export default function ProviderGuard({ didSelectProvider, children }: ProviderGuardProps) {
const { read, upsert, getProviders } = useConfig();
const navigate = useNavigate();
const [isChecking, setIsChecking] = useState(true);
const [hasProvider, setHasProvider] = useState(false);
const [showFirstTimeSetup, setShowFirstTimeSetup] = useState(false);
const [showLocalModelSetup, setShowLocalModelSetup] = useState(false);
const [userInActiveSetup, setUserInActiveSetup] = useState(false);
const [showSwitchModelModal, setShowSwitchModelModal] = useState(false);
const [switchModelProvider, setSwitchModelProvider] = useState<string | null>(null);
const onboardingTracked = useRef(false);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [showScrollIndicator, setShowScrollIndicator] = useState(true);
const checkScrollPosition = useCallback(() => {
const container = scrollContainerRef.current;
if (!container) return;
const { scrollTop, scrollHeight, clientHeight } = container;
const isNearBottom = scrollTop + clientHeight >= scrollHeight - 50;
const canScroll = scrollHeight > clientHeight;
setShowScrollIndicator(canScroll && !isNearBottom);
}, []);
const setView = useMemo(() => createNavigationHandler(navigate), [navigate]);
const [openRouterSetupState, setOpenRouterSetupState] = useState<{
show: boolean;
title: string;
message: string;
showRetry: boolean;
autoClose?: number;
} | null>(null);
const [tetrateSetupState, setTetrateSetupState] = useState<{
show: boolean;
title: string;
message: string;
showRetry: boolean;
autoClose?: number;
} | null>(null);
const [chatgptCodexSetupState, setChatgptCodexSetupState] = useState<{
show: boolean;
title: string;
message: string;
showRetry: boolean;
autoClose?: number;
} | null>(null);
const handleTetrateSetup = async () => {
trackOnboardingProviderSelected({ method: 'tetrate' });
try {
const result = await startTetrateSetup();
if (result.success) {
setSwitchModelProvider('tetrate');
setShowSwitchModelModal(true);
} else {
trackOnboardingSetupFailed('tetrate', result.message);
setTetrateSetupState({
show: true,
title: 'Setup Failed',
message: result.message,
showRetry: true,
});
}
} catch (error) {
console.error('Tetrate setup error:', error);
trackOnboardingSetupFailed('tetrate', 'unexpected_error');
setTetrateSetupState({
show: true,
title: 'Setup Error',
message: 'An unexpected error occurred during setup.',
showRetry: true,
});
}
};
const handleChatGptCodexSetup = async () => {
trackOnboardingProviderSelected({ method: 'chatgpt_codex' });
try {
const result = await startChatGptCodexSetup();
if (result.success) {
await getProviders(true);
setSwitchModelProvider('chatgpt_codex');
setShowSwitchModelModal(true);
} else {
trackOnboardingSetupFailed('chatgpt_codex', result.message);
setChatgptCodexSetupState({
show: true,
title: 'Setup Failed',
message: result.message,
showRetry: true,
});
}
} catch (error) {
console.error('ChatGPT Codex setup error:', error);
trackOnboardingSetupFailed('chatgpt_codex', 'unexpected_error');
setChatgptCodexSetupState({
show: true,
title: 'Setup Error',
message: 'An unexpected error occurred during setup.',
showRetry: true,
});
}
};
const handleApiKeySuccess = async (provider: string, _model: string, apiKey: string) => {
trackOnboardingProviderSelected({ method: 'api_key' });
const keyName = `${provider.toUpperCase()}_API_KEY`;
await upsert(keyName, apiKey, true);
await upsert('GOOSE_PROVIDER', provider, false);
setSwitchModelProvider(provider);
setShowSwitchModelModal(true);
};
const handleModelSelected = (model: string) => {
if (switchModelProvider) {
trackOnboardingCompleted(switchModelProvider, model);
}
setShowSwitchModelModal(false);
setUserInActiveSetup(false);
setShowFirstTimeSetup(false);
setHasProvider(true);
navigate('/', { replace: true });
};
const handleSwitchModelClose = () => {
setShowSwitchModelModal(false);
};
const handleOpenRouterSetup = async () => {
trackOnboardingProviderSelected({ method: 'openrouter' });
try {
const result = await startOpenRouterSetup();
if (result.success) {
setSwitchModelProvider('openrouter');
setShowSwitchModelModal(true);
} else {
trackOnboardingSetupFailed('openrouter', result.message);
setOpenRouterSetupState({
show: true,
title: 'Setup Failed',
message: result.message,
showRetry: true,
});
}
} catch (error) {
console.error('OpenRouter setup error:', error);
trackOnboardingSetupFailed('openrouter', 'unexpected_error');
setOpenRouterSetupState({
show: true,
title: 'Setup Error',
message: 'An unexpected error occurred during setup.',
showRetry: true,
});
}
};
const handleLocalModelComplete = () => {
trackOnboardingCompleted('local');
setShowLocalModelSetup(false);
setShowFirstTimeSetup(false);
setHasProvider(true);
navigate('/', { replace: true });
};
const handleLocalModelCancel = () => {
trackOnboardingAbandoned('local_model_setup');
setShowLocalModelSetup(false);
};
const handleRetrySetup = (setupType: 'openrouter' | 'tetrate' | 'chatgpt_codex') => {
if (setupType === 'openrouter') {
setOpenRouterSetupState(null);
handleOpenRouterSetup();
} else if (setupType === 'tetrate') {
setTetrateSetupState(null);
handleTetrateSetup();
} else {
setChatgptCodexSetupState(null);
handleChatGptCodexSetup();
}
};
const closeSetupModal = (setupType: 'openrouter' | 'tetrate' | 'chatgpt_codex') => {
if (setupType === 'openrouter') {
setOpenRouterSetupState(null);
} else if (setupType === 'tetrate') {
setTetrateSetupState(null);
} else {
setChatgptCodexSetupState(null);
}
};
useEffect(() => {
const checkProvider = async () => {
try {
const provider = ((await read('GOOSE_PROVIDER', false)) as string) || '';
const hasConfiguredProvider = provider.trim() !== '';
// If user is actively testing keys, don't redirect
if (userInActiveSetup) {
setHasProvider(false);
setShowFirstTimeSetup(true);
} else if (hasConfiguredProvider || didSelectProvider) {
setHasProvider(true);
setShowFirstTimeSetup(false);
} else {
setHasProvider(false);
setShowFirstTimeSetup(true);
}
} catch (error) {
console.error('Error checking provider:', error);
toastService.error({
title: 'Configuration Error',
msg: 'Failed to check provider configuration.',
traceback: error instanceof Error ? error.stack || '' : '',
});
setHasProvider(false);
setShowFirstTimeSetup(true);
} finally {
setIsChecking(false);
}
};
checkProvider();
}, [read, didSelectProvider, userInActiveSetup]);
useEffect(() => {
if (!isChecking && !hasProvider && showFirstTimeSetup && !onboardingTracked.current) {
trackOnboardingStarted();
onboardingTracked.current = true;
}
}, [isChecking, hasProvider, showFirstTimeSetup]);
useEffect(() => {
if (!isChecking && !hasProvider && showFirstTimeSetup) {
// Check scroll position after content renders
const timer = setTimeout(checkScrollPosition, 100);
return () => clearTimeout(timer);
}
return undefined;
}, [isChecking, hasProvider, showFirstTimeSetup, checkScrollPosition]);
if (isChecking) {
return (
<div className="h-screen w-full bg-background-primary flex items-center justify-center">
<WelcomeGooseLogo />
</div>
);
}
if (showLocalModelSetup) {
return (
<div className="h-screen w-full bg-background-default overflow-hidden">
<div className="h-full overflow-y-auto">
<div className="min-h-full flex flex-col items-center justify-center p-4 py-8">
<div className="max-w-2xl w-full mx-auto p-8">
<LocalModelSetup
onSuccess={handleLocalModelComplete}
onCancel={handleLocalModelCancel}
/>
</div>
</div>
</div>
</div>
);
}
if (!hasProvider && showFirstTimeSetup) {
return (
<div className="h-screen w-full bg-background-primary overflow-hidden relative">
<div
ref={scrollContainerRef}
onScroll={checkScrollPosition}
className="h-full overflow-y-auto"
>
<div className="min-h-full flex flex-col items-center justify-center p-4 py-8">
<div className="max-w-2xl w-full mx-auto p-8">
{/* Header section */}
<div className="text-left mb-8 sm:mb-12">
<div className="space-y-3 sm:space-y-4">
<div className="origin-bottom-left goose-icon-animation">
<Goose className="size-6 sm:size-8" />
</div>
<h1 className="text-2xl sm:text-4xl font-light text-left">Welcome to Goose</h1>
</div>
<p className="text-text-secondary text-base sm:text-lg mt-4 sm:mt-6">
Since its your first time here, lets get you set up with an AI provider so goose
can work its magic.
</p>
</div>
<ApiKeyTester
onSuccess={handleApiKeySuccess}
onStartTesting={() => {
setUserInActiveSetup(true);
}}
/>
{/* Run Locally Card */}
<div className="relative w-full mb-4">
<div className="absolute -top-2 -right-2 sm:-top-3 sm:-right-3 z-20">
<span className="inline-block px-2 py-1 text-xs font-medium bg-green-600 text-white rounded-full">
Free &amp; Private
</span>
</div>
<div
onClick={() => {
trackOnboardingProviderSelected({ method: 'local' });
setShowLocalModelSetup(true);
}}
className="w-full p-4 sm:p-6 bg-transparent border rounded-xl transition-all duration-200 cursor-pointer group"
>
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-2">
<span className="font-medium text-text-default text-sm sm:text-base">
Run Locally
</span>
</div>
<div className="text-text-muted group-hover:text-text-default transition-colors">
<svg
className="w-4 h-4 sm:w-5 sm:h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</div>
</div>
<p className="text-text-muted text-sm sm:text-base">
Download a model and run entirely on your machine. No API keys, no accounts.
</p>
</div>
</div>
{/* ChatGPT Subscription Card - Full Width */}
<div className="relative w-full mb-4">
<div className="absolute -top-2 -right-2 sm:-top-3 sm:-right-3 z-20">
<span className="inline-block px-2 py-1 text-xs font-medium bg-blue-600 text-white rounded-full">
Recommended if you have ChatGPT Plus/Pro
</span>
</div>
<div
onClick={handleChatGptCodexSetup}
className="w-full p-4 sm:p-6 bg-transparent border rounded-xl transition-all duration-200 cursor-pointer group"
>
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-2">
<ChatGPT className="w-5 h-5 text-text-primary" />
<span className="font-medium text-text-primary text-sm sm:text-base">
ChatGPT Subscription
</span>
</div>
<div className="text-text-secondary group-hover:text-text-primary transition-colors">
<svg
className="w-4 h-4 sm:w-5 sm:h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</div>
</div>
<p className="text-text-secondary text-sm sm:text-base">
Use your ChatGPT Plus/Pro subscription for GPT-5 Codex models.
</p>
</div>
</div>
{/* Tetrate Card - Full Width */}
<div className="relative w-full mb-4">
<div className="absolute -top-2 -right-2 sm:-top-3 sm:-right-3 z-20">
<span className="inline-block px-2 py-1 text-xs font-medium bg-blue-600 text-white rounded-full">
Recommended for new users
</span>
</div>
<div
onClick={handleTetrateSetup}
className="w-full p-4 sm:p-6 bg-transparent border rounded-xl transition-all duration-200 cursor-pointer group"
>
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-2">
<Tetrate className="w-5 h-5 text-text-primary" />
<span className="text-sm sm:text-base">
<span className="font-medium text-text-primary">Agent Router</span>
<span className="text-text-secondary text-xs"> by Tetrate</span>
</span>
</div>
<div className="text-text-secondary group-hover:text-text-primary transition-colors">
<svg
className="w-4 h-4 sm:w-5 sm:h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</div>
</div>
<p className="text-text-secondary text-sm sm:text-base">
Access multiple AI models with automatic setup. Sign up to receive $10 credit.
</p>
</div>
</div>
{/* OpenRouter Card - Full Width */}
<div
onClick={handleOpenRouterSetup}
className="relative w-full p-4 sm:p-6 bg-transparent border rounded-xl transition-all duration-200 cursor-pointer group overflow-hidden mb-6"
>
{/* Subtle shimmer effect */}
<div className="absolute inset-0 -translate-x-full animate-shimmer bg-gradient-to-r from-transparent via-white/8 to-transparent"></div>
<div className="relative flex items-start justify-between mb-3">
<div className="flex items-center gap-2">
<OpenRouter className="w-5 h-5 text-text-primary" />
<span className="font-medium text-text-primary text-sm sm:text-base">
OpenRouter
</span>
</div>
<div className="text-text-secondary group-hover:text-text-primary transition-colors">
<svg
className="w-4 h-4 sm:w-5 sm:h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</div>
</div>
<p className="text-text-secondary text-sm sm:text-base">
Access 200+ models with one API. Pay-per-use pricing.
</p>
</div>
{/* Other providers section */}
<div className="w-full p-4 sm:p-6 bg-transparent border rounded-xl">
<h3 className="font-medium text-text-primary text-sm sm:text-base mb-3">
Other Providers
</h3>
<p className="text-text-secondary text-sm sm:text-base mb-4">
Set up additional providers manually through settings.
</p>
<button
onClick={() => navigate('/welcome', { replace: true })}
className="text-blue-600 hover:text-blue-500 text-sm font-medium transition-colors"
>
Go to Provider Settings
</button>
</div>
<div className="mt-6">
<TelemetrySettings isWelcome />
</div>
</div>
</div>
</div>
{/* Scroll indicator - fixed at bottom, hides when scrolled to bottom */}
{showScrollIndicator && (
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 pointer-events-none transition-opacity duration-300 opacity-60 animate-bounce">
<div className="flex flex-col items-center gap-1 text-text-secondary">
<span className="text-xs">More options below</span>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</div>
</div>
)}
{/* Setup Modals */}
{openRouterSetupState?.show && (
<SetupModal
title={openRouterSetupState.title}
message={openRouterSetupState.message}
showRetry={openRouterSetupState.showRetry}
onRetry={() => handleRetrySetup('openrouter')}
onClose={() => closeSetupModal('openrouter')}
autoClose={openRouterSetupState.autoClose}
/>
)}
{tetrateSetupState?.show && (
<SetupModal
title={tetrateSetupState.title}
message={tetrateSetupState.message}
showRetry={tetrateSetupState.showRetry}
onRetry={() => handleRetrySetup('tetrate')}
onClose={() => closeSetupModal('tetrate')}
autoClose={tetrateSetupState.autoClose}
/>
)}
{chatgptCodexSetupState?.show && (
<SetupModal
title={chatgptCodexSetupState.title}
message={chatgptCodexSetupState.message}
showRetry={chatgptCodexSetupState.showRetry}
onRetry={() => handleRetrySetup('chatgpt_codex')}
onClose={() => closeSetupModal('chatgpt_codex')}
autoClose={chatgptCodexSetupState.autoClose}
/>
)}
{showSwitchModelModal && (
<SwitchModelModal
sessionId={null}
onClose={handleSwitchModelClose}
setView={setView}
onModelSelected={handleModelSelected}
initialProvider={switchModelProvider}
titleOverride="Choose Model"
/>
)}
</div>
);
}
return <>{children}</>;
}
-62
View File
@@ -1,62 +0,0 @@
import { useEffect } from 'react';
import { Button } from './ui/button';
interface SetupModalProps {
title: string;
message: string;
showProgress?: boolean;
showRetry?: boolean;
onRetry?: () => void;
autoClose?: number;
onClose?: () => void;
}
export function SetupModal({
title,
message,
showProgress,
showRetry,
onRetry,
autoClose,
onClose,
}: SetupModalProps) {
useEffect(() => {
if (autoClose && onClose) {
const timer = window.setTimeout(() => {
onClose();
}, autoClose);
return () => window.clearTimeout(timer);
}
return undefined;
}, [autoClose, onClose]);
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full">
<h2 className="text-xl font-bold mb-4 text-gray-900 dark:text-gray-100">{title}</h2>
<p className="mb-6 text-gray-700 dark:text-gray-300">{message}</p>
{showProgress && (
<div className="flex justify-center mb-4">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
</div>
)}
{onClose && (
<div className="mb-4">
<Button onClick={onClose} className="w-full">
Close
</Button>
<br />
</div>
)}
{showRetry && onRetry && (
<Button onClick={onRetry} className="w-full">
Retry Setup
</Button>
)}
</div>
</div>
);
}
@@ -1,14 +0,0 @@
import { Goose, Rain } from './icons/Goose';
export default function WelcomeGooseLogo({ className = '' }) {
return (
<div className={`${className} relative overflow-hidden`}>
<div className="absolute inset-0 flex items-center justify-center">
<Rain className="w-full h-full scale-[2.5] opacity-0 group-hover/logo:opacity-100 transition-all duration-300 z-1" />
</div>
<div className="absolute inset-0 flex items-center justify-center">
<Goose className="w-full h-full z-2" />
</div>
</div>
);
}
@@ -15,7 +15,6 @@ export default function ResetProviderSection(_props: ResetProviderSectionProps)
await remove('GOOSE_PROVIDER', false);
await remove('GOOSE_MODEL', false);
// Refresh the page to trigger the ProviderGuard check
window.location.reload();
} catch (error) {
console.error('Failed to reset provider and model:', error);
-1
View File
@@ -1 +0,0 @@
export const USE_NEW_ONBOARDING = true;
-12
View File
@@ -303,18 +303,6 @@ export function trackOnboardingCompleted(provider: string, model?: string): void
onboardingStartTime = null;
}
export function trackOnboardingAbandoned(step: string): void {
const durationSeconds = onboardingStartTime
? Math.round((Date.now() - onboardingStartTime) / 1000)
: undefined;
trackEvent({
name: 'onboarding_abandoned',
properties: { step, duration_seconds: durationSeconds },
});
onboardingStartTime = null;
}
export function trackOnboardingSetupFailed(
provider: 'openrouter' | 'tetrate' | 'chatgpt_codex' | 'local',
errorMessage?: string
-16
View File
@@ -1,16 +0,0 @@
import { configureProviderOauth } from '../api';
export async function startChatGptCodexSetup(): Promise<{ success: boolean; message: string }> {
try {
await configureProviderOauth({
path: { name: 'chatgpt_codex' },
throwOnError: true,
});
return { success: true, message: 'ChatGPT Codex setup completed' };
} catch (e) {
return {
success: false,
message: `Failed to start ChatGPT Codex setup: ${e}`,
};
}
}
-17
View File
@@ -1,17 +0,0 @@
import { startOpenrouterSetup } from '../api';
export interface OpenRouterSetupStatus {
isRunning: boolean;
error: string | null;
}
export async function startOpenRouterSetup(): Promise<{ success: boolean; message: string }> {
try {
return (await startOpenrouterSetup({ throwOnError: true })).data;
} catch (e) {
return {
success: false,
message: `Failed to start OpenRouter setup ['${e}]`,
};
}
}