Onboarding detect provider from api key (#5955)

Co-authored-by: spencrmartin <spencermartin@squareup.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
Zane
2025-12-05 13:06:52 -08:00
committed by GitHub
parent 6fa3bd7e8a
commit 1db40709bb
18 changed files with 729 additions and 290 deletions
+3
View File
@@ -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,
@@ -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<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",
@@ -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<DetectProviderRequest>,
) -> Result<Json<DetectProviderResponse>, 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<Json<String>, StatusCode> {
}
}
}
#[utoipa::path(
post,
path = "/config/custom-providers",
@@ -834,6 +867,7 @@ pub fn routes(state: Arc<AppState>) -> 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))
+51
View File
@@ -0,0 +1,51 @@
use crate::model::ModelConfig;
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"),
)
.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
}
+1
View File
@@ -1,5 +1,6 @@
pub mod anthropic;
pub mod api_client;
pub mod auto_detect;
pub mod azure;
pub mod azureauth;
pub mod base;
+62
View File
@@ -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": [
+11 -6
View File
@@ -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 (
<div className="w-screen h-screen bg-background-default">
<ProviderSettings onClose={onClose} isOnboarding={true} />
<ProviderSettings
onClose={() => {
navigate('/', { replace: true });
}}
isOnboarding={true}
onProviderLaunched={() => {
onSelectProvider();
navigate('/', { replace: true });
}}
/>
</div>
);
};
+10 -1
View File
@@ -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<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
/**
@@ -127,6 +127,15 @@ export const updateCustomProvider = <ThrowOnError extends boolean = false>(optio
}
});
export const detectProvider = <ThrowOnError extends boolean = false>(options: Options<DetectProviderData, ThrowOnError>) => (options.client ?? client).post<DetectProviderResponses, DetectProviderErrors, ThrowOnError>({
url: '/config/detect-provider',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
export const getExtensions = <ThrowOnError extends boolean = false>(options?: Options<GetExtensionsData, ThrowOnError>) => (options?.client ?? client).get<GetExtensionsResponses, GetExtensionsErrors, ThrowOnError>({ url: '/config/extensions', ...options });
export const addExtension = <ThrowOnError extends boolean = false>(options: Options<AddExtensionData, ThrowOnError>) => (options.client ?? client).post<AddExtensionResponses, AddExtensionErrors, ThrowOnError>({
+32
View File
@@ -142,6 +142,15 @@ export type DeleteRecipeRequest = {
id: string;
};
export type DetectProviderRequest = {
api_key: string;
};
export type DetectProviderResponse = {
models: Array<string>;
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;
+183
View File
@@ -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<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
</span>
</div>
<div className="w-full p-4 sm:p-6 bg-background-muted border border-background-hover rounded-xl">
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<Key className="w-4 h-4 mb-3 text-text-standard" />
<h3 className="font-medium text-text-standard text-sm sm:text-base">
Quick Setup with API Key
</h3>
</div>
</div>
<p className="text-text-muted text-sm sm:text-base mb-4">
Enter your API key and we'll automatically detect which provider it works with.
</p>
<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 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();
}
}}
/>
<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-muted rounded text-sm text-text-muted">
<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-muted">Suggestions:</p>
<ul className="text-xs text-text-muted 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>
);
}
@@ -187,7 +187,7 @@ export default function ProgressiveMessageList({
if (hasInlineSystemNotification(message)) {
return (
<div
key={message.id && `${message.id}-${message.content.length}`}
key={message.id ?? `msg-${index}-${message.created}`}
className={`relative ${index === 0 ? 'mt-0' : 'mt-4'} assistant`}
data-testid="message-container"
>
@@ -201,7 +201,7 @@ export default function ProgressiveMessageList({
return (
<div
key={message.id && `${message.id}-${message.content.length}`}
key={message.id ?? `msg-${index}-${message.created}`}
className={`relative ${index === 0 ? 'mt-0' : 'mt-4'} ${isUser ? 'user' : 'assistant'} ${messageIsInChain ? 'in-chain' : ''}`}
data-testid="message-container"
>
+205 -277
View File
@@ -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 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);
setUserInActiveSetup(false);
setShowFirstTimeSetup(false);
setHasProvider(true);
navigate('/', { replace: 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;
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.`,
});
// Close the modal and mark as having provider
setOpenRouterSetupState(null);
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 (
<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>
<div className="h-screen w-full bg-background-default flex items-center justify-center">
<WelcomeGooseLogo />
</div>
);
}
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 (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">
<div className="max-w-md w-full mx-auto p-8">
<div className="mb-8 text-center">
<WelcomeGooseLogo />
</div>
<OllamaSetup
onSuccess={() => {
setShowOllamaSetup(false);
setHasProvider(true);
// Navigate to chat after successful setup
navigate('/', { replace: true });
}}
onCancel={() => {
setShowOllamaSetup(false);
setShowFirstTimeSetup(true);
}}
/>
</div>
</div>
);
return <OllamaSetup onSuccess={handleOllamaComplete} onCancel={handleOllamaCancel} />;
}
if (showFirstTimeSetup) {
if (!hasProvider && showFirstTimeSetup) {
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-lg w-full mx-auto p-8">
{/* Header section - same width as buttons, left aligned */}
<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">
@@ -279,104 +213,29 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
<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 you setup with a provider so we can
make incredible work together. Scroll down to see options.
Since its your first time here, lets get you set up with an AI provider so goose
can work its magic.
</p>
</div>
{/* Setup options - same width container */}
<ApiKeyTester
onSuccess={handleApiKeySuccess}
onStartTesting={() => {
setUserInActiveSetup(true);
}}
/>
<div className="space-y-3 sm:space-y-4">
<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
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>
{/* 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>
{/* Other providers Card - outline style */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
{/* Tetrate Card */}
<div
onClick={() => navigate('/welcome', { replace: true })}
onClick={handleTetrateSetup}
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">
<Tetrate className="w-5 h-5 mb-3 text-text-standard" />
<h3 className="font-medium text-text-standard text-sm sm:text-base">
Other providers
Tetrate Agent Router
</h3>
</div>
<div className="text-text-muted group-hover:text-text-standard transition-colors">
@@ -396,22 +255,91 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG
</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.
Secure access to multiple AI models with automatic setup. Free tier available.
</p>
</div>
{/* OpenRouter Card */}
<div
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 */}
<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 mb-3 text-text-standard" />
<h3 className="font-medium text-text-standard text-sm sm:text-base">
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="text-text-muted text-sm sm:text-base">
Access 200+ models with one API. Pay-per-use pricing.
</p>
</div>
</div>
{/* Other providers section */}
<div className="w-full p-4 sm:p-6 bg-transparent border border-background-hover rounded-xl">
<h3 className="font-medium text-text-standard text-sm sm:text-base mb-3">
Other Providers
</h3>
<p className="text-text-muted 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>
</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}
/>
)}
</div>
);
}
if (!hasProvider) {
// This shouldn't happen, but just in case
return null;
}
return <>{children}</>;
}
@@ -0,0 +1,18 @@
export default function Anthropic({ className = '' }) {
return (
<svg
width="24"
height="24"
viewBox="0 0 1200 1200"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
className={className}
>
<path
d="M 233.959793 800.214905 L 468.644287 668.536987 L 472.590637 657.100647 L 468.644287 650.738403 L 457.208069 650.738403 L 417.986633 648.322144 L 283.892639 644.69812 L 167.597321 639.865845 L 54.926208 633.825623 L 26.577238 627.785339 L 3.3e-05 592.751709 L 2.73832 575.27533 L 26.577238 559.248352 L 60.724873 562.228149 L 136.187973 567.382629 L 249.422867 575.194763 L 331.570496 580.026978 L 453.261841 592.671082 L 472.590637 592.671082 L 475.328857 584.859009 L 468.724915 580.026978 L 463.570557 575.194763 L 346.389313 495.785217 L 219.543671 411.865906 L 153.100723 363.543762 L 117.181267 339.060425 L 99.060455 316.107361 L 91.248367 266.01355 L 123.865784 230.093994 L 167.677887 233.073853 L 178.872513 236.053772 L 223.248367 270.201477 L 318.040283 343.570496 L 441.825592 434.738342 L 459.946411 449.798706 L 467.194672 444.64447 L 468.080597 441.020203 L 459.946411 427.409485 L 392.617493 305.718323 L 320.778564 181.932983 L 288.80542 130.630859 L 280.348999 99.865845 C 277.369171 87.221436 275.194641 76.590698 275.194641 63.624268 L 312.322174 13.20813 L 332.8591 6.604126 L 382.389313 13.20813 L 403.248352 31.328979 L 434.013519 101.71814 L 483.865753 212.537048 L 561.181274 363.221497 L 583.812134 407.919434 L 595.892639 449.315491 L 600.40271 461.959839 L 608.214783 461.959839 L 608.214783 454.711609 L 614.577271 369.825623 L 626.335632 265.61084 L 637.771851 131.516846 L 641.718201 93.745117 L 660.402832 48.483276 L 697.530334 24.000122 L 726.52356 37.852417 L 750.362549 72 L 747.060486 94.067139 L 732.886047 186.201416 L 705.100708 330.52356 L 686.979919 427.167847 L 697.530334 427.167847 L 709.61084 415.087341 L 758.496704 350.174561 L 840.644348 247.490051 L 876.885925 206.738342 L 919.167847 161.71814 L 946.308838 140.29541 L 997.61084 140.29541 L 1035.38269 196.429626 L 1018.469849 254.416199 L 965.637634 321.422852 L 921.825562 378.201538 L 859.006714 462.765259 L 819.785278 530.41626 L 823.409424 535.812073 L 832.75177 534.92627 L 974.657776 504.724915 L 1051.328979 490.872559 L 1142.818848 475.167786 L 1184.214844 494.496582 L 1188.724854 514.147644 L 1172.456421 554.335693 L 1074.604126 578.496765 L 959.838989 601.449829 L 788.939636 641.879272 L 786.845764 643.409485 L 789.261841 646.389343 L 866.255127 653.637634 L 899.194702 655.409424 L 979.812134 655.409424 L 1129.932861 666.604187 L 1169.154419 692.537109 L 1192.671265 724.268677 L 1188.724854 748.429688 L 1128.322144 779.194641 L 1046.818848 759.865845 L 856.590759 714.604126 L 791.355774 698.335754 L 782.335693 698.335754 L 782.335693 703.731567 L 836.69812 756.885986 L 936.322205 846.845581 L 1061.073975 962.81897 L 1067.436279 991.490112 L 1051.409424 1014.120911 L 1034.496704 1011.704712 L 924.885986 929.234924 L 882.604126 892.107544 L 786.845764 811.48999 L 780.483276 811.48999 L 780.483276 819.946289 L 802.550415 852.241699 L 919.087341 1027.409424 L 925.127625 1081.127686 L 916.671204 1098.604126 L 886.469849 1109.154419 L 853.288696 1103.114136 L 785.073914 1007.355835 L 714.684631 899.516785 L 657.906067 802.872498 L 650.979858 806.81897 L 617.476624 1167.704834 L 601.771851 1186.147705 L 565.530212 1200 L 535.328857 1177.046997 L 519.302124 1139.919556 L 535.328857 1066.550537 L 554.657776 970.792053 L 570.362488 894.68457 L 584.536926 800.134277 L 592.993347 768.724976 L 592.429626 766.630859 L 585.503479 767.516968 L 514.22821 865.369263 L 405.825531 1011.865906 L 320.053711 1103.677979 L 299.516815 1111.812256 L 263.919525 1093.369263 L 267.221497 1060.429688 L 287.114136 1031.114136 L 405.825531 880.107361 L 477.422913 786.52356 L 523.651062 732.483276 L 523.328918 724.671265 L 520.590698 724.671265 L 205.288605 929.395935 L 149.154434 936.644409 L 124.993355 914.01355 L 127.973183 876.885986 L 139.409409 864.80542 L 234.201385 799.570435 L 233.879227 799.8927 Z"
fill="currentColor"
/>
</svg>
);
}
@@ -0,0 +1,23 @@
interface ArrowRightProps {
className?: string;
}
export function ArrowRight({ className = '' }: ArrowRightProps) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
d="M3.33678 8.00138L12.6701 8.00555M12.6701 8.00555L8.00553 3.3368M12.6701 8.00555L8.00136 12.6701"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
+30
View File
@@ -0,0 +1,30 @@
interface KeyProps {
className?: string;
}
export function Key({ className = '' }: KeyProps) {
return (
<svg
width="16"
height="17"
viewBox="0 0 16 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<g clipPath="url(#clip0_986_19974)">
<path
d="M10.3382 5.68265L11.8709 7.21667C11.9954 7.33888 12.1629 7.40737 12.3374 7.40745C12.5119 7.40753 12.6795 7.33918 12.8042 7.21709L14.2048 5.81771C14.327 5.69315 14.3955 5.52563 14.3956 5.35113C14.3957 5.17663 14.3273 5.00905 14.2052 4.88438L12.6726 3.35036M14.0064 2.01762L7.60358 8.41476M8.66915 11.0152C8.66825 13.0403 7.02589 14.6812 5.00085 14.6803C2.9758 14.6794 1.33492 13.037 1.33582 11.012C1.33673 8.98692 2.97908 7.34603 5.00413 7.34693C7.02917 7.34784 8.67006 8.9902 8.66915 11.0152Z"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
<defs>
<clipPath id="clip0_986_19974">
<rect width="16" height="16" fill="white" transform="translate(0.00708008 0.677979) rotate(0.0256089)" />
</clipPath>
</defs>
</svg>
);
}
@@ -0,0 +1,17 @@
export default function OpenAI({ className = '' }) {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"
fill="currentColor"
/>
</svg>
);
}
@@ -0,0 +1,22 @@
export default function Tetrate({ className = '' }) {
return (
<svg
width="24"
height="24"
viewBox="0 0 694 701"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
className={className}
>
<path
d="M343.5 226C390.72 226 429 264.28 429 311.5C429 321.676 427.222 331.436 423.961 340.487C409.127 381.654 375 419.742 375 463.499C375 507.257 409.127 545.346 423.961 586.512C427.222 595.564 429 605.324 429 615.5C429 662.72 390.72 701 343.5 701C296.28 701 258 662.72 258 615.5C258 606.44 259.409 597.709 262.021 589.514C275.211 548.119 308 509.828 308 466.382V460.616C308 417.171 275.211 378.88 262.021 337.485C259.409 329.291 258 320.56 258 311.5C258 264.28 296.28 226 343.5 226Z"
fill="currentColor"
/>
<path
d="M272.622 39.9541C297.034 1.458 347.7 -11.255 387.546 11.75C395.392 16.2801 402.249 21.8668 408.04 28.2256C437.293 60.3466 454.06 107.888 491.685 129.61L496.678 132.493C534.303 154.216 583.859 144.966 626.303 154.239C634.705 156.075 642.971 159.22 650.817 163.75C691.711 187.36 705.722 239.652 682.112 280.546C658.502 321.44 606.211 335.451 565.317 311.841C556.505 306.753 548.941 300.333 542.733 292.982C514.499 259.553 498.577 210.955 460.682 189.076C436.139 174.906 406.497 174.321 376.956 173.566C358.811 173.102 340.613 173.735 322.491 174.754C290.52 176.551 257.865 176.779 231.635 192.853C194.326 215.717 179.682 264.716 152.333 298.874C146.319 306.385 138.926 313 130.25 318.317C89.9879 342.99 37.3483 330.353 12.6754 290.091C-11.9974 249.829 0.639271 197.189 40.901 172.517C48.6259 167.783 56.8065 164.422 65.1578 162.367C107.345 151.985 157.126 159.934 194.17 137.233L199.086 134.221C232.774 113.576 248.658 72.037 272.622 39.9541Z"
fill="currentColor"
/>
</svg>
);
}
+12
View File
@@ -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,
};
@@ -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<ProviderDetails[]>([]);
@@ -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 (