diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index 8909234e7f4e..2ff1f1da62ed 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -330,6 +330,7 @@ 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, @@ -398,6 +399,8 @@ 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, diff --git a/crates/goose-server/src/routes/config_management.rs b/crates/goose-server/src/routes/config_management.rs index 5e2001af4b48..3b4848bf5a06 100644 --- a/crates/goose-server/src/routes/config_management.rs +++ b/crates/goose-server/src/routes/config_management.rs @@ -11,6 +11,7 @@ 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::create_with_default_model; use goose::providers::pricing::{ @@ -131,6 +132,16 @@ pub struct SlashCommandsResponse { pub commands: Vec, } +#[derive(Deserialize, ToSchema)] +pub struct DetectProviderRequest { + pub api_key: String, +} + +#[derive(Serialize, ToSchema)] +pub struct DetectProviderResponse { + pub provider_name: String, + pub models: Vec, +} #[utoipa::path( post, path = "/config/upsert", @@ -596,6 +607,29 @@ 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, +) -> Result, StatusCode> { + 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(StatusCode::NOT_FOUND), + } +} + #[utoipa::path( post, path = "/config/backup", @@ -686,7 +720,6 @@ pub async fn validate_config() -> Result, StatusCode> { } } } - #[utoipa::path( post, path = "/config/custom-providers", @@ -834,6 +867,7 @@ pub fn routes(state: Arc) -> Router { .route("/config/extensions/{name}", delete(remove_extension)) .route("/config/providers", get(providers)) .route("/config/providers/{name}/models", get(get_provider_models)) + .route("/config/detect-provider", post(detect_provider)) .route("/config/slash_commands", get(get_slash_commands)) .route("/config/pricing", post(get_pricing)) .route("/config/init", post(init_config)) diff --git a/crates/goose/src/providers/auto_detect.rs b/crates/goose/src/providers/auto_detect.rs new file mode 100644 index 000000000000..71a60f6b151e --- /dev/null +++ b/crates/goose/src/providers/auto_detect.rs @@ -0,0 +1,51 @@ +use crate::model::ModelConfig; + +pub async fn detect_provider_from_api_key(api_key: &str) -> Option<(String, Vec)> { + 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"), + ) + .await + { + Ok(provider) => match provider.fetch_supported_models().await { + Ok(Some(models)) => 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 +} diff --git a/crates/goose/src/providers/mod.rs b/crates/goose/src/providers/mod.rs index dd554f3dc878..2e27b45d6da0 100644 --- a/crates/goose/src/providers/mod.rs +++ b/crates/goose/src/providers/mod.rs @@ -1,5 +1,6 @@ pub mod anthropic; pub mod api_client; +pub mod auto_detect; pub mod azure; pub mod azureauth; pub mod base; diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index f8acee9d6302..303baa7b5b44 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -585,6 +585,39 @@ } } }, + "/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": [ @@ -2774,6 +2807,35 @@ } } }, + "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" + } + } + }, "EditMessageRequest": { "type": "object", "required": [ diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 973b42383810..414b9943c6dc 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { IpcRendererEvent } from 'electron'; import { HashRouter, @@ -255,14 +255,19 @@ interface WelcomeRouteProps { const WelcomeRoute = ({ onSelectProvider }: WelcomeRouteProps) => { const navigate = useNavigate(); - const onClose = useCallback(() => { - onSelectProvider(); - navigate('/'); - }, [navigate, onSelectProvider]); return (
- + { + navigate('/', { replace: true }); + }} + isOnboarding={true} + onProviderLaunched={() => { + onSelectProvider(); + navigate('/', { replace: true }); + }} + />
); }; diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index 2e188b181997..8eea0f9ec742 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CheckProviderData, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EditMessageData, EditMessageErrors, EditMessageResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; +import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CheckProviderData, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EditMessageData, EditMessageErrors, EditMessageResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen'; export type Options = Options2 & { /** @@ -127,6 +127,15 @@ export const updateCustomProvider = (optio } }); +export const detectProvider = (options: Options) => (options.client ?? client).post({ + url: '/config/detect-provider', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + export const getExtensions = (options?: Options) => (options?.client ?? client).get({ url: '/config/extensions', ...options }); export const addExtension = (options: Options) => (options.client ?? client).post({ diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index e082260e9b77..d897d8b94b66 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -142,6 +142,15 @@ export type DeleteRecipeRequest = { id: string; }; +export type DetectProviderRequest = { + api_key: string; +}; + +export type DetectProviderResponse = { + models: Array; + provider_name: string; +}; + export type EditMessageRequest = { editType?: EditType; timestamp: number; @@ -1444,6 +1453,29 @@ 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; diff --git a/ui/desktop/src/components/ApiKeyTester.tsx b/ui/desktop/src/components/ApiKeyTester.tsx new file mode 100644 index 000000000000..136f55544832 --- /dev/null +++ b/ui/desktop/src/components/ApiKeyTester.tsx @@ -0,0 +1,183 @@ +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(null); + const [error, setError] = useState(false); + const inputRef = useRef(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 ( +
+ {/* Recommended pill */} +
+ + Recommended + +
+ +
+
+
+ +

+ Quick Setup with API Key +

+
+
+ +

+ Enter your API key and we'll automatically detect which provider it works with. +

+ +
+
+ setApiKey(e.target.value)} + placeholder="Enter your API key (OpenAI, Anthropic, Google, etc.)" + className="flex-1 px-3 py-2 border border-background-hover rounded-lg bg-background-default text-text-standard placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + disabled={isLoading} + onKeyDown={(e) => { + if (e.key === 'Enter' && canSubmit) { + testApiKey(); + } + }} + /> + +
+ + {/* Loading state */} + {isLoading && ( +
+
+ Detecting provider and validating key... +
+ )} + + {/* Success result */} + {result && ( +
+ +
+
Detected {result.provider}
+
+ Model: {result.model} ({result.totalModels} models available) +
+
+
+ )} + + {/* Error result */} + {error && ( +
+
+ +
+
Provider Detection Failed
+
+ Could not detect provider from API key +
+
+
+
+

Suggestions:

+
    +
  • + + + Make sure you are using a valid API key from OpenAI, Anthropic, Google, Groq, + or xAI + +
  • +
  • + + Check that the key is complete and not truncated +
  • +
  • + + Verify your API key is active and has sufficient credits +
  • +
  • + + For local Ollama setup, use the "Other Providers" section below +
  • +
+
+
+ )} +
+
+
+ ); +} diff --git a/ui/desktop/src/components/ProgressiveMessageList.tsx b/ui/desktop/src/components/ProgressiveMessageList.tsx index ab2381feba3d..3d7f8596e876 100644 --- a/ui/desktop/src/components/ProgressiveMessageList.tsx +++ b/ui/desktop/src/components/ProgressiveMessageList.tsx @@ -187,7 +187,7 @@ export default function ProgressiveMessageList({ if (hasInlineSystemNotification(message)) { return (
@@ -201,7 +201,7 @@ export default function ProgressiveMessageList({ return (
diff --git a/ui/desktop/src/components/ProviderGuard.tsx b/ui/desktop/src/components/ProviderGuard.tsx index 1eae8e2cd02f..76e789d6e167 100644 --- a/ui/desktop/src/components/ProviderGuard.tsx +++ b/ui/desktop/src/components/ProviderGuard.tsx @@ -7,9 +7,9 @@ import { startTetrateSetup } from '../utils/tetrateSetup'; import WelcomeGooseLogo from './WelcomeGooseLogo'; import { toastService } from '../toasts'; import { OllamaSetup } from './OllamaSetup'; +import ApiKeyTester from './ApiKeyTester'; -import { Goose } from './icons/Goose'; -import { OpenRouter } from './icons'; +import { Goose, OpenRouter, Tetrate } from './icons'; interface ProviderGuardProps { didSelectProvider: boolean; @@ -17,260 +17,194 @@ interface ProviderGuardProps { } export default function ProviderGuard({ didSelectProvider, children }: ProviderGuardProps) { - const { read } = useConfig(); + const { read, upsert } = useConfig(); const navigate = useNavigate(); const [isChecking, setIsChecking] = useState(true); const [hasProvider, setHasProvider] = useState(false); const [showFirstTimeSetup, setShowFirstTimeSetup] = useState(false); const [showOllamaSetup, setShowOllamaSetup] = useState(false); + const [userInActiveSetup, setUserInActiveSetup] = useState(false); const [openRouterSetupState, setOpenRouterSetupState] = useState<{ show: boolean; title: string; message: string; - showProgress: boolean; 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) { - 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); + try { + const result = await startTetrateSetup(); + if (result.success) { + setTetrateSetupState({ + show: true, + title: 'Setup Complete!', + message: result.message, + showRetry: false, + autoClose: 3000, + }); + setTimeout(() => { 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 || '' : '', + navigate('/', { replace: true }); + }, 3000); + } else { + setTetrateSetupState({ + show: true, + title: 'Setup Failed', + message: result.message, + showRetry: true, }); } - } else { + } catch (error) { + console.error('Tetrate setup error:', error); setTetrateSetupState({ show: true, - title: 'Tetrate setup pending', - message: result.message, - showProgress: false, + title: 'Setup Error', + message: 'An unexpected error occurred during setup.', showRetry: true, }); } }; - 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; + const handleApiKeySuccess = async (provider: string, model: string, apiKey: string) => { + const keyName = `${provider.toUpperCase()}_API_KEY`; + await upsert(keyName, apiKey, true); + await upsert('GOOSE_PROVIDER', provider, false); + await upsert('GOOSE_MODEL', model, false); - if (provider && model) { - toastService.configure({ silent: false }); - toastService.success({ - title: 'Success!', - msg: `Started goose with ${model} by OpenRouter. You can change the model via the dropdown.`, - }); + setUserInActiveSetup(false); + setShowFirstTimeSetup(false); + setHasProvider(true); + navigate('/', { replace: true }); + }; - // Close the modal and mark as having provider - setOpenRouterSetupState(null); + const handleOpenRouterSetup = async () => { + try { + const result = await startOpenRouterSetup(); + if (result.success) { + setOpenRouterSetupState({ + show: true, + title: 'Setup Complete!', + message: result.message, + showRetry: false, + autoClose: 3000, + }); + setTimeout(() => { setShowFirstTimeSetup(false); setHasProvider(true); - - // Navigate to chat after successful setup navigate('/', { replace: true }); - } else { - 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 || '' : '', + }, 3000); + } else { + setOpenRouterSetupState({ + show: true, + title: 'Setup Failed', + message: result.message, + showRetry: true, }); } - } else { + } catch (error) { + console.error('OpenRouter setup error:', error); setOpenRouterSetupState({ show: true, - title: 'Openrouter setup pending', - message: result.message, - showProgress: false, + title: 'Setup Error', + message: 'An unexpected error occurred during setup.', showRetry: true, }); } }; + const handleOllamaComplete = () => { + setShowOllamaSetup(false); + setShowFirstTimeSetup(false); + setHasProvider(true); + navigate('/', { replace: true }); + }; + + const handleOllamaCancel = () => { + setShowOllamaSetup(false); + }; + + const handleRetrySetup = (setupType: 'openrouter' | 'tetrate') => { + if (setupType === 'openrouter') { + setOpenRouterSetupState(null); + handleOpenRouterSetup(); + } else { + setTetrateSetupState(null); + handleTetrateSetup(); + } + }; + + const closeSetupModal = (setupType: 'openrouter' | 'tetrate') => { + if (setupType === 'openrouter') { + setOpenRouterSetupState(null); + } else { + setTetrateSetupState(null); + } + }; + useEffect(() => { const checkProvider = async () => { try { - const config = window.electron.getConfig(); - console.log('ProviderGuard - Full config:', config); + const provider = ((await read('GOOSE_PROVIDER', false)) as string) || ''; + const hasConfiguredProvider = provider.trim() !== ''; - const provider = (await read('GOOSE_PROVIDER', false)) ?? config.GOOSE_DEFAULT_PROVIDER; - const model = (await read('GOOSE_MODEL', false)) ?? config.GOOSE_DEFAULT_MODEL; - - // Always check for Ollama regardless of provider status - - if (provider && model) { - console.log('ProviderGuard - Provider and model found, continuing normally'); + // If user is actively testing keys, don't redirect + if (userInActiveSetup) { + setHasProvider(false); + setShowFirstTimeSetup(true); + } else if (hasConfiguredProvider || didSelectProvider) { setHasProvider(true); + setShowFirstTimeSetup(false); } else { - console.log('ProviderGuard - No provider/model configured'); + setHasProvider(false); setShowFirstTimeSetup(true); } } catch (error) { - // On error, assume no provider and redirect to welcome - console.error('Error checking provider configuration:', error); - navigate('/welcome', { replace: true }); + 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(); - }, [ - navigate, - read, - didSelectProvider, // When the user makes a selection, re-trigger this check - ]); + }, [read, didSelectProvider, userInActiveSetup]); - if ( - isChecking && - !openRouterSetupState?.show && - !tetrateSetupState?.show && - !showFirstTimeSetup && - !showOllamaSetup - ) { + if (isChecking) { return ( -
-
+
+
); } - if (openRouterSetupState?.show) { - return ( - setOpenRouterSetupState(null)} - /> - ); - } - - if (tetrateSetupState?.show) { - return ( - setTetrateSetupState(null)} - /> - ); - } - if (showOllamaSetup) { - return ( -
-
-
- -
- { - setShowOllamaSetup(false); - setHasProvider(true); - // Navigate to chat after successful setup - navigate('/', { replace: true }); - }} - onCancel={() => { - setShowOllamaSetup(false); - setShowFirstTimeSetup(true); - }} - /> -
-
- ); + return ; } - if (showFirstTimeSetup) { + if (!hasProvider && showFirstTimeSetup) { return (
-
- {/* Header section - same width as buttons, left aligned */} +
+ {/* Header section */}
@@ -279,104 +213,65 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG

Welcome to Goose

- 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. + Since it’s your first time here, let’s get you set up with an AI provider so goose + can work its magic.

- {/* Setup options - same width container */} - -
-
- {/* Tetrate Card */} - {/* Recommended badge - positioned relative to wrapper */} -
- - Recommended - -
+ { + setUserInActiveSetup(true); + }} + /> -
-
-
-

- Automatic setup with Tetrate Agent Router -

-
-
- - - -
+
+ {/* Tetrate Card */} +
+
+
+ +

+ Tetrate Agent Router +

-

- Get secure access to multiple AI models, start for free. Quick setup with just - a few clicks. -

-
-
- - {/* Primary OpenRouter Card with subtle shimmer - wrapped for badge positioning */} -
-
- {/* Subtle shimmer effect */} -
- -
-
- -

- Automatic setup with OpenRouter -

-
-
- - - -
+
+ + +
-

- Get instant access to multiple AI models including GPT-4, Claude, and more. - Quick setup with just a few clicks. -

+

+ Secure access to multiple AI models with automatic setup. Free tier available. +

- {/* Other providers Card - outline style */} + {/* OpenRouter Card */}
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" + onClick={handleOpenRouterSetup} + className="relative 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 overflow-hidden" > -
+ {/* Subtle shimmer effect */} +
+ +
+

- Other providers + OpenRouter

@@ -396,22 +291,55 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG

- If you've already signed up for providers like Anthropic, OpenAI etc, you can - enter your own keys. + Access 200+ models with one API. Pay-per-use pricing.

+ + {/* Other providers section */} +
+

+ Other Providers +

+

+ Set up additional providers manually through settings. +

+ +
+ + {/* Setup Modals */} + {openRouterSetupState?.show && ( + handleRetrySetup('openrouter')} + onClose={() => closeSetupModal('openrouter')} + autoClose={openRouterSetupState.autoClose} + /> + )} + + {tetrateSetupState?.show && ( + handleRetrySetup('tetrate')} + onClose={() => closeSetupModal('tetrate')} + autoClose={tetrateSetupState.autoClose} + /> + )}
); } - if (!hasProvider) { - // This shouldn't happen, but just in case - return null; - } - return <>{children}; } diff --git a/ui/desktop/src/components/icons/Anthropic.tsx b/ui/desktop/src/components/icons/Anthropic.tsx new file mode 100644 index 000000000000..7af92a4172e6 --- /dev/null +++ b/ui/desktop/src/components/icons/Anthropic.tsx @@ -0,0 +1,18 @@ +export default function Anthropic({ className = '' }) { + return ( + + ); +} diff --git a/ui/desktop/src/components/icons/ArrowRight.tsx b/ui/desktop/src/components/icons/ArrowRight.tsx new file mode 100644 index 000000000000..5f4fe317746a --- /dev/null +++ b/ui/desktop/src/components/icons/ArrowRight.tsx @@ -0,0 +1,23 @@ +interface ArrowRightProps { + className?: string; +} + +export function ArrowRight({ className = '' }: ArrowRightProps) { + return ( + + + + ); +} diff --git a/ui/desktop/src/components/icons/Key.tsx b/ui/desktop/src/components/icons/Key.tsx new file mode 100644 index 000000000000..5b7f559f6705 --- /dev/null +++ b/ui/desktop/src/components/icons/Key.tsx @@ -0,0 +1,30 @@ +interface KeyProps { + className?: string; +} + +export function Key({ className = '' }: KeyProps) { + return ( + + + + + + + + + + + ); +} diff --git a/ui/desktop/src/components/icons/OpenAI.tsx b/ui/desktop/src/components/icons/OpenAI.tsx new file mode 100644 index 000000000000..b00bcfca7e5f --- /dev/null +++ b/ui/desktop/src/components/icons/OpenAI.tsx @@ -0,0 +1,17 @@ +export default function OpenAI({ className = '' }) { + return ( + + + + ); +} diff --git a/ui/desktop/src/components/icons/Tetrate.tsx b/ui/desktop/src/components/icons/Tetrate.tsx new file mode 100644 index 000000000000..011253c93796 --- /dev/null +++ b/ui/desktop/src/components/icons/Tetrate.tsx @@ -0,0 +1,22 @@ +export default function Tetrate({ className = '' }) { + return ( + + ); +} diff --git a/ui/desktop/src/components/icons/index.tsx b/ui/desktop/src/components/icons/index.tsx index 556f7dc4bc5d..bbc21ee091d8 100644 --- a/ui/desktop/src/components/icons/index.tsx +++ b/ui/desktop/src/components/icons/index.tsx @@ -39,6 +39,12 @@ import Settings from './Settings'; import Time from './Time'; import { Gear } from './Gear'; import Youtube from './Youtube'; +import { Goose } from './Goose'; +import Anthropic from './Anthropic'; +import { ArrowRight } from './ArrowRight'; +import { Key } from './Key'; +import OpenAI from './OpenAI'; +import Tetrate from './Tetrate'; import { Microphone } from './Microphone'; import { Watch0 } from './Watch0'; import { Watch1 } from './Watch1'; @@ -98,4 +104,10 @@ export { Watch5, Watch6, Youtube, + Goose, + Anthropic, + ArrowRight, + Key, + OpenAI, + Tetrate, }; diff --git a/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx b/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx index 5488b90b3ce5..2110b6f60ab5 100644 --- a/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx +++ b/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx @@ -9,9 +9,14 @@ import { toastService } from '../../../toasts'; interface ProviderSettingsProps { onClose: () => void; isOnboarding: boolean; + onProviderLaunched?: () => void; } -export default function ProviderSettings({ onClose, isOnboarding }: ProviderSettingsProps) { +export default function ProviderSettings({ + onClose, + isOnboarding, + onProviderLaunched, +}: ProviderSettingsProps) { const { getProviders } = useConfig(); const [loading, setLoading] = useState(true); const [providers, setProviders] = useState([]); @@ -70,7 +75,11 @@ export default function ProviderSettings({ onClose, isOnboarding }: ProviderSett msg: `Started goose with ${model} by ${provider.metadata.display_name}. You can change the model via the dropdown.`, }); - onClose(); + if (onProviderLaunched) { + onProviderLaunched(); + } else { + onClose(); + } } catch (error) { console.error(`Failed to initialize with provider ${provider_name}:`, error); @@ -83,7 +92,7 @@ export default function ProviderSettings({ onClose, isOnboarding }: ProviderSett }); } }, - [onClose] + [onClose, onProviderLaunched] ); return (