Add PKCE support for Tetrate Agent Router Service (#4165)

Signed-off-by: John Landa <jonathanlanda@gmail.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
John Landa
2025-08-21 08:04:09 +02:00
committed by GitHub
parent 69ce34a828
commit e86c7ddb19
16 changed files with 1342 additions and 97 deletions
+257 -96
View File
@@ -3,6 +3,7 @@ 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 WelcomeGooseLogo from './WelcomeGooseLogo';
import { initializeSystem } from '../utils/providerUtils';
import { toastService } from '../toasts';
@@ -31,6 +32,80 @@ export default function ProviderGuard({ children }: ProviderGuardProps) {
showRetry: boolean;
autoClose?: number;
} | null>(null);
const [tetrateSetupState, setTetrateSetupState] = useState<{
show: boolean;
title: string;
message: string;
showProgress: boolean;
showRetry: boolean;
autoClose?: number;
} | null>(null);
const handleTetrateSetup = async () => {
setTetrateSetupState({
show: true,
title: 'Setting up Tetrate Agent Router Service',
message: 'A browser window will open for authentication...',
showProgress: true,
showRetry: false,
});
const result = await startTetrateSetup();
if (result.success) {
setTetrateSetupState({
show: true,
title: 'Setup Complete!',
message: 'Tetrate Agent Router has been configured successfully. Initializing Goose...',
showProgress: true,
showRetry: false,
});
// After successful Tetrate 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 Tetrate. You can change the model via the dropdown.`,
});
// Close the modal and mark as having provider
setTetrateSetupState(null);
setShowFirstTimeSetup(false);
setHasProvider(true);
} else {
throw new Error('Provider or model not found after Tetrate setup');
}
} catch (error) {
console.error('Failed to initialize after Tetrate setup:', error);
toastService.configure({ silent: false });
toastService.error({
title: 'Initialization Failed',
msg: `Failed to initialize with Tetrate: ${error instanceof Error ? error.message : String(error)}`,
traceback: error instanceof Error ? error.stack || '' : '',
});
}
} else {
setTetrateSetupState({
show: true,
title: 'Tetrate setup pending',
message: result.message,
showProgress: false,
showRetry: true,
});
}
};
const handleOpenRouterSetup = async () => {
setOpenRouterSetupState({
@@ -68,14 +143,14 @@ export default function ProviderGuard({ children }: ProviderGuardProps) {
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.`,
msg: `Started goose with ${model} by OpenRouter. You can change the model via the dropdown.`,
});
// Close the modal and mark as having provider
setOpenRouterSetupState(null);
setShowFirstTimeSetup(false);
setHasProvider(true);
// Navigate to chat after successful setup
navigate('/', { replace: true });
} else {
@@ -121,7 +196,6 @@ export default function ProviderGuard({ children }: ProviderGuardProps) {
}
const ollamaStatus = await checkOllamaStatus();
setOllamaDetected(ollamaStatus.isRunning);
} catch (error) {
// On error, assume no provider and redirect to welcome
console.error('Error checking provider configuration:', error);
@@ -152,7 +226,13 @@ export default function ProviderGuard({ children }: ProviderGuardProps) {
};
}, [showFirstTimeSetup]);
if (isChecking && !openRouterSetupState?.show && !showFirstTimeSetup && !showOllamaSetup) {
if (
isChecking &&
!openRouterSetupState?.show &&
!tetrateSetupState?.show &&
!showFirstTimeSetup &&
!showOllamaSetup
) {
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>
@@ -174,6 +254,20 @@ export default function ProviderGuard({ children }: ProviderGuardProps) {
);
}
if (tetrateSetupState?.show) {
return (
<SetupModal
title={tetrateSetupState.title}
message={tetrateSetupState.message}
showProgress={tetrateSetupState.showProgress}
showRetry={tetrateSetupState.showRetry}
onRetry={handleTetrateSetup}
autoClose={tetrateSetupState.autoClose}
onClose={() => setTetrateSetupState(null)}
/>
);
}
if (showOllamaSetup) {
return (
<div className="min-h-screen w-full flex flex-col items-center justify-center p-4 bg-background-default">
@@ -210,111 +304,178 @@ export default function ProviderGuard({ children }: ProviderGuardProps) {
<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>
<h1 className="text-2xl sm:text-4xl font-light text-left">Welcome to Goose</h1>
</div>
<p className="text-text-muted text-base sm:text-lg mt-4 sm:mt-6">
Since it's your first time here, let's get your set you with a provider so we can make incredible work together.
Since it's your first time here, let's get you setup with a provider so we can
make incredible work together. Scroll down to see options.
</p>
</div>
{/* Setup options - same width container */}
<div className="space-y-3 sm:space-y-4">
{/* Primary OpenRouter Card with subtle shimmer - wrapped for badge positioning */}
<div className="relative">
{/* Recommended badge - positioned relative to wrapper */}
<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
</span>
</div>
<div
onClick={handleOpenRouterSetup}
className="relative w-full p-4 sm:p-6 bg-background-muted border border-background-hover rounded-xl hover:border-text-muted transition-all duration-200 cursor-pointer group overflow-hidden"
>
{/* 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-1">
<OpenRouter className="w-5 h-5 sm:w-6 sm:h-6 mb-12 text-text-standard" />
<h3 className="font-medium text-text-standard text-sm sm:text-base">
Automatic setup with OpenRouter
</h3>
<div className="relative">
{/* Tetrate Card */}
{/* Recommended badge - positioned relative to wrapper */}
<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
</span>
</div>
<div className="text-text-muted group-hover:text-text-standard 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="relative text-text-muted text-sm sm:text-base">
Get instant access to multiple AI models including GPT-4, Claude, and more. Quick setup with just a few clicks.
</p>
</div>
</div>
{/* Ollama Card - outline style */}
<div className="relative">
{/* Detected badge - similar to recommended but green */}
{ollamaDetected && (
<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">
Detected
</span>
</div>
)}
<div
onClick={() => {
setShowFirstTimeSetup(false);
setShowOllamaSetup(true);
}}
className="w-full p-4 sm:p-6 bg-transparent border border-background-hover rounded-xl hover:border-text-muted transition-all duration-200 cursor-pointer group"
>
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<Ollama className="w-5 h-5 sm:w-6 sm:h-6 mb-12 text-text-standard" />
<h3 className="font-medium text-text-standard text-sm sm:text-base">
Ollama
</h3>
</div>
<div className="text-text-muted group-hover:text-text-standard transition-colors flex-shrink-0">
<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
onClick={handleTetrateSetup}
className="w-full p-4 sm:p-6 bg-background-muted border border-background-hover rounded-xl hover:border-text-muted transition-all duration-200 cursor-pointer group"
>
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<h3 className="font-medium text-text-standard text-sm sm:text-base">
Automatic setup with Tetrate Agent Router
</h3>
</div>
<div className="text-text-muted group-hover:text-text-standard 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">
Get secure access to multiple AI models, start for free. Quick setup with just
a few clicks.
</p>
</div>
</div>
<p className="text-text-muted text-sm sm:text-base">
Run AI models locally on your computer. Completely free and private with no internet required.
</p>
</div>
</div>
{/* Other providers Card - outline style */}
<div
onClick={() => navigate('/welcome', { replace: true })}
className="w-full p-4 sm:p-6 bg-transparent border border-background-hover rounded-xl hover:border-text-muted transition-all duration-200 cursor-pointer group"
>
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<h3 className="font-medium text-text-standard text-sm sm:text-base">
Other providers
</h3>
</div>
<div className="text-text-muted group-hover:text-text-standard 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">
If you've already signed up for providers like Anthropic, OpenAI etc, you can enter your own keys.
</p>
</div>
{/* Primary OpenRouter Card with subtle shimmer - wrapped for badge positioning */}
<div className="relative">
<div
onClick={handleOpenRouterSetup}
className="relative w-full p-4 sm:p-6 bg-background-muted border border-background-hover rounded-xl hover:border-text-muted transition-all duration-200 cursor-pointer group overflow-hidden"
>
{/* 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-1">
<OpenRouter className="w-5 h-5 sm:w-6 sm:h-6 mb-12 text-text-standard" />
<h3 className="font-medium text-text-standard text-sm sm:text-base">
Automatic setup with OpenRouter
</h3>
</div>
<div className="text-text-muted group-hover:text-text-standard 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="relative text-text-muted text-sm sm:text-base">
Get instant access to multiple AI models including GPT-4, Claude, and more.
Quick setup with just a few clicks.
</p>
</div>
</div>
{/* Ollama Card - outline style */}
<div className="relative">
{/* Detected badge - similar to recommended but green */}
{ollamaDetected && (
<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">
Detected
</span>
</div>
)}
<div
onClick={() => {
setShowFirstTimeSetup(false);
setShowOllamaSetup(true);
}}
className="w-full p-4 sm:p-6 bg-transparent border border-background-hover rounded-xl hover:border-text-muted transition-all duration-200 cursor-pointer group"
>
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<Ollama className="w-5 h-5 sm:w-6 sm:h-6 mb-12 text-text-standard" />
<h3 className="font-medium text-text-standard text-sm sm:text-base">
Ollama
</h3>
</div>
<div className="text-text-muted group-hover:text-text-standard transition-colors flex-shrink-0">
<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">
Run AI models locally on your computer. Completely free and private with no
internet required.
</p>
</div>
</div>
{/* Other providers Card - outline style */}
<div
onClick={() => navigate('/welcome', { replace: true })}
className="w-full p-4 sm:p-6 bg-transparent border border-background-hover rounded-xl hover:border-text-muted transition-all duration-200 cursor-pointer group"
>
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<h3 className="font-medium text-text-standard text-sm sm:text-base">
Other providers
</h3>
</div>
<div className="text-text-muted group-hover:text-text-standard 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">
If you've already signed up for providers like Anthropic, OpenAI etc, you can
enter your own keys.
</p>
</div>
</div>
</div>
</div>
@@ -76,7 +76,7 @@ export default function ProviderSettings({ onClose, isOnboarding }: ProviderSett
toastService.configure({ silent: false });
toastService.success({
title: 'Success!',
msg: `Started goose with ${model} by ${provider.metadata.display_name}. You can change the model via the lower right corner.`,
msg: `Started goose with ${model} by ${provider.metadata.display_name}. You can change the model via the dropdown.`,
});
onClose();