feat: openrouter out of the box experience for goose installations (#3507)

This commit is contained in:
Michael Neale
2025-07-30 17:17:43 +10:00
committed by GitHub
parent faf252a5a7
commit 601c678722
15 changed files with 977 additions and 100 deletions
+142 -9
View File
@@ -1,33 +1,116 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useConfig } from './ConfigContext';
import { SetupModal } from './SetupModal';
import { startOpenRouterSetup } from '../utils/openRouterSetup';
import WelcomeGooseLogo from './WelcomeGooseLogo';
import { initializeSystem } from '../utils/providerUtils';
import { toastService } from '../toasts';
interface ProviderGuardProps {
children: React.ReactNode;
}
export default function ProviderGuard({ children }: ProviderGuardProps) {
const { read } = useConfig();
const { read, getExtensions, addExtension } = useConfig();
const navigate = useNavigate();
const [isChecking, setIsChecking] = useState(true);
const [hasProvider, setHasProvider] = useState(false);
const [showFirstTimeSetup, setShowFirstTimeSetup] = useState(false);
const [openRouterSetupState, setOpenRouterSetupState] = useState<{
show: boolean;
title: string;
message: string;
showProgress: boolean;
showRetry: boolean;
autoClose?: number;
} | null>(null);
useEffect(() => {
const checkProvider = async () => {
const handleOpenRouterSetup = async () => {
setOpenRouterSetupState({
show: true,
title: 'Setting up OpenRouter',
message: 'A browser window will open for authentication...',
showProgress: true,
showRetry: false,
});
const result = await startOpenRouterSetup();
if (result.success) {
setOpenRouterSetupState({
show: true,
title: 'Setup Complete!',
message: 'OpenRouter has been configured successfully. Initializing Goose...',
showProgress: true,
showRetry: false,
});
// After successful OpenRouter setup, force reload config and initialize system
try {
// Get the latest config from disk
const config = window.electron.getConfig();
const provider = (await read('GOOSE_PROVIDER', false)) ?? config.GOOSE_DEFAULT_PROVIDER;
const model = (await read('GOOSE_MODEL', false)) ?? config.GOOSE_DEFAULT_MODEL;
if (provider && model) {
// Initialize the system with the new provider/model
await initializeSystem(provider as string, model as string, {
getExtensions,
addExtension,
});
toastService.configure({ silent: false });
toastService.success({
title: 'Success!',
msg: `Started goose with ${model} by OpenRouter. You can change the model via the lower right corner.`,
});
// Close the modal and mark as having provider
setOpenRouterSetupState(null);
setShowFirstTimeSetup(false);
setHasProvider(true);
} else {
console.log('No provider/model configured, redirecting to welcome');
navigate('/welcome', { replace: true });
throw new Error('Provider or model not found after OpenRouter setup');
}
} catch (error) {
console.error('Failed to initialize after OpenRouter setup:', error);
toastService.configure({ silent: false });
toastService.error({
title: 'Initialization Failed',
msg: `Failed to initialize with OpenRouter: ${error instanceof Error ? error.message : String(error)}`,
traceback: error instanceof Error ? error.stack || '' : '',
});
}
} else {
setOpenRouterSetupState({
show: true,
title: 'Openrouter setup pending',
message: result.message,
showProgress: false,
showRetry: true,
});
}
};
useEffect(() => {
const checkProvider = async () => {
try {
const config = window.electron.getConfig();
console.log('ProviderGuard - Full config:', config);
const provider = (await read('GOOSE_PROVIDER', false)) ?? config.GOOSE_DEFAULT_PROVIDER;
const model = (await read('GOOSE_MODEL', false)) ?? config.GOOSE_DEFAULT_MODEL;
if (provider && model) {
console.log('ProviderGuard - Provider and model found, continuing normally');
setHasProvider(true);
} else {
console.log('ProviderGuard - No provider/model configured, showing first time setup');
setShowFirstTimeSetup(true);
}
} catch (error) {
console.error('Error checking provider configuration:', error);
// On error, assume no provider and redirect to welcome
console.error('Error checking provider configuration:', error);
navigate('/welcome', { replace: true });
} finally {
setIsChecking(false);
@@ -35,9 +118,10 @@ export default function ProviderGuard({ children }: ProviderGuardProps) {
};
checkProvider();
}, [read, navigate]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [read]);
if (isChecking) {
if (isChecking && !openRouterSetupState?.show && !showFirstTimeSetup) {
return (
<div className="flex justify-center items-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-textStandard"></div>
@@ -45,8 +129,57 @@ export default function ProviderGuard({ children }: ProviderGuardProps) {
);
}
if (openRouterSetupState?.show) {
return (
<SetupModal
title={openRouterSetupState.title}
message={openRouterSetupState.message}
showProgress={openRouterSetupState.showProgress}
showRetry={openRouterSetupState.showRetry}
onRetry={handleOpenRouterSetup}
autoClose={openRouterSetupState.autoClose}
onClose={() => setOpenRouterSetupState(null)}
/>
);
}
if (showFirstTimeSetup) {
return (
<div className="h-screen w-full flex flex-col items-center justify-center bg-background-default">
<div className="max-w-md w-full mx-auto p-8 text-center">
<WelcomeGooseLogo />
<h1 className="text-2xl font-bold text-text-standard mt-8 mb-4">Welcome to Goose!</h1>
<p className="text-text-muted mb-8">
Let's get you set up with an AI provider to start using Goose.
</p>
<div className="space-y-4">
<button
onClick={handleOpenRouterSetup}
className="w-full px-6 py-3 bg-background-muted text-text-standard rounded-lg hover:bg-background-hover transition-colors font-medium"
>
Automatic setup with OpenRouter (recommended)
</button>
<button
onClick={() => navigate('/welcome', { replace: true })}
className="w-full px-6 py-3 bg-background-muted text-text-standard rounded-lg hover:bg-background-hover transition-colors font-medium"
>
Configure Other Providers (advanced)
</button>
</div>
<p className="text-sm text-text-muted mt-6">
OpenRouter provides access to multiple AI models. To use this it will need to create an
account with OpenRouter.
</p>
</div>
</div>
);
}
if (!hasProvider) {
// This will be handled by the navigation above, but we return null to be safe
// This shouldn't happen, but just in case
return null;
}
+62
View File
@@ -0,0 +1,62 @@
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>
);
}
+25
View File
@@ -0,0 +1,25 @@
export interface OpenRouterSetupStatus {
isRunning: boolean;
error: string | null;
}
export async function startOpenRouterSetup(): Promise<{ success: boolean; message: string }> {
const baseUrl = `${window.appConfig.get('GOOSE_API_HOST')}:${window.appConfig.get('GOOSE_PORT')}`;
const response = await fetch(`${baseUrl}/handle_openrouter`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
console.error('Failed to start Openrouter setup:', response.statusText);
return {
success: false,
message: `Failed to start Openrouter setup ['${response.status}]`,
};
}
const result = await response.json();
return result;
}