diff --git a/crates/goose-server/src/routes/config_management.rs b/crates/goose-server/src/routes/config_management.rs index 14a5b08ea7..7774cc4da8 100644 --- a/crates/goose-server/src/routes/config_management.rs +++ b/crates/goose-server/src/routes/config_management.rs @@ -10,6 +10,7 @@ use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs}; use goose::config::Config; use goose::config::{extensions::name_to_key, PermissionManager}; use goose::config::{ExtensionConfigManager, ExtensionEntry}; +use goose::model::ModelConfig; use goose::providers::base::ProviderMetadata; use goose::providers::providers as get_providers; use goose::{agents::ExtensionConfig, config::permission::PermissionLevel}; @@ -154,6 +155,14 @@ pub async fn read_config( ) -> Result, StatusCode> { verify_secret_key(&headers, &state)?; + // Special handling for model-limits + if query.key == "model-limits" { + let limits = ModelConfig::get_all_model_limits(); + return Ok(Json( + serde_json::to_value(limits).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?, + )); + } + let config = Config::global(); match config.get(&query.key, query.is_secret) { @@ -481,3 +490,45 @@ pub fn routes(state: Arc) -> Router { .route("/config/permissions", post(upsert_permissions)) .with_state(state) } + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_read_model_limits() { + // Create test state and headers + let test_state = AppState::new( + Arc::new(goose::agents::Agent::default()), + "test".to_string(), + ) + .await; + let mut headers = HeaderMap::new(); + headers.insert("X-Secret-Key", "test".parse().unwrap()); + + // Execute + let result = read_config( + State(test_state), + headers, + Json(ConfigKeyQuery { + key: "model-limits".to_string(), + is_secret: false, + }), + ) + .await; + + // Assert + assert!(result.is_ok()); + let response = result.unwrap(); + + // Parse the response and check the contents + let limits: Vec = + serde_json::from_value(response.0).unwrap(); + assert!(!limits.is_empty()); + + // Check for some expected patterns + let gpt4_limit = limits.iter().find(|l| l.pattern == "gpt-4o"); + assert!(gpt4_limit.is_some()); + assert_eq!(gpt4_limit.unwrap().context_limit, 128_000); + } +} diff --git a/crates/goose-server/src/routes/configs.rs b/crates/goose-server/src/routes/configs.rs deleted file mode 100644 index b2513b2e5a..0000000000 --- a/crates/goose-server/src/routes/configs.rs +++ /dev/null @@ -1,229 +0,0 @@ -use super::utils::verify_secret_key; -use crate::state::AppState; -use axum::{ - extract::{Query, State}, - routing::{delete, get, post}, - Json, Router, -}; -use goose::config::Config; -use http::{HeaderMap, StatusCode}; -use once_cell::sync::Lazy; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::{collections::HashMap, sync::Arc}; - -#[derive(Serialize)] -struct ConfigResponse { - error: bool, -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct ConfigRequest { - key: String, - value: String, - is_secret: bool, -} - -async fn store_config( - State(state): State>, - headers: HeaderMap, - Json(request): Json, -) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - - let config = Config::global(); - let result = if request.is_secret { - config.set_secret(&request.key, Value::String(request.value)) - } else { - config.set_param(&request.key, Value::String(request.value)) - }; - match result { - Ok(_) => Ok(Json(ConfigResponse { error: false })), - Err(_) => Ok(Json(ConfigResponse { error: true })), - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ProviderConfigRequest { - pub providers: Vec, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ConfigStatus { - pub is_set: bool, - pub location: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ProviderResponse { - pub supported: bool, - pub name: Option, - pub description: Option, - pub models: Option>, - pub config_status: HashMap, -} - -#[derive(Debug, Serialize, Deserialize)] -struct ProviderConfig { - name: String, - description: String, - models: Vec, - required_keys: Vec, -} - -static PROVIDER_ENV_REQUIREMENTS: Lazy> = Lazy::new(|| { - let contents = include_str!("providers_and_keys.json"); - serde_json::from_str(contents).expect("Failed to parse providers_and_keys.json") -}); - -fn check_key_status(config: &Config, key: &str) -> (bool, Option) { - if let Ok(_value) = std::env::var(key) { - (true, Some("env".to_string())) - } else if config.get_param::(key).is_ok() { - (true, Some("yaml".to_string())) - } else if config.get_secret::(key).is_ok() { - (true, Some("keyring".to_string())) - } else { - (false, None) - } -} - -async fn check_provider_configs( - Json(request): Json, -) -> Result>, StatusCode> { - let mut response = HashMap::new(); - let config = Config::global(); - - for provider_name in request.providers { - if let Some(provider_config) = PROVIDER_ENV_REQUIREMENTS.get(&provider_name) { - let mut config_status = HashMap::new(); - - for key in &provider_config.required_keys { - let (key_set, key_location) = check_key_status(config, key); - config_status.insert( - key.to_string(), - ConfigStatus { - is_set: key_set, - location: key_location, - }, - ); - } - - response.insert( - provider_name, - ProviderResponse { - supported: true, - name: Some(provider_config.name.clone()), - description: Some(provider_config.description.clone()), - models: Some(provider_config.models.clone()), - config_status, - }, - ); - } else { - response.insert( - provider_name, - ProviderResponse { - supported: false, - name: None, - description: None, - models: None, - config_status: HashMap::new(), - }, - ); - } - } - - Ok(Json(response)) -} - -#[derive(Deserialize)] -pub struct GetConfigQuery { - key: String, -} - -#[derive(Serialize)] -pub struct GetConfigResponse { - value: Option, -} - -pub async fn get_config( - State(state): State>, - headers: HeaderMap, - Query(query): Query, -) -> Result, StatusCode> { - verify_secret_key(&headers, &state)?; - - // Fetch the configuration value. Right now we don't allow get a secret. - let config = Config::global(); - let value = if let Ok(config_value) = config.get_param::(&query.key) { - Some(config_value) - } else { - std::env::var(&query.key).ok() - }; - - // Return the value - Ok(Json(GetConfigResponse { value })) -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct DeleteConfigRequest { - key: String, - is_secret: bool, -} - -async fn delete_config( - State(state): State>, - headers: HeaderMap, - Json(request): Json, -) -> Result { - verify_secret_key(&headers, &state)?; - - // Attempt to delete the key - let config = Config::global(); - let result = if request.is_secret { - config.delete_secret(&request.key) - } else { - config.delete(&request.key) - }; - match result { - Ok(_) => Ok(StatusCode::NO_CONTENT), - Err(_) => Err(StatusCode::NOT_FOUND), - } -} - -pub fn routes(state: Arc) -> Router { - Router::new() - .route("/configs/providers", post(check_provider_configs)) - .route("/configs/get", get(get_config)) - .route("/configs/store", post(store_config)) - .route("/configs/delete", delete(delete_config)) - .with_state(state) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_unsupported_provider() { - // Setup - let request = ProviderConfigRequest { - providers: vec!["unsupported_provider".to_string()], - }; - - // Execute - let result = check_provider_configs(Json(request)).await; - - // Assert - assert!(result.is_ok()); - let Json(response) = result.unwrap(); - - let provider_status = response - .get("unsupported_provider") - .expect("Provider should exist"); - assert!(!provider_status.supported); - assert!(provider_status.config_status.is_empty()); - } -} diff --git a/crates/goose-server/src/routes/mod.rs b/crates/goose-server/src/routes/mod.rs index b561bccd1e..dcdb577e37 100644 --- a/crates/goose-server/src/routes/mod.rs +++ b/crates/goose-server/src/routes/mod.rs @@ -1,7 +1,6 @@ // Export route modules pub mod agent; pub mod config_management; -pub mod configs; pub mod context; pub mod extension; pub mod health; @@ -21,7 +20,6 @@ pub fn configure(state: Arc) -> Router { .merge(agent::routes(state.clone())) .merge(context::routes(state.clone())) .merge(extension::routes(state.clone())) - .merge(configs::routes(state.clone())) .merge(config_management::routes(state.clone())) .merge(recipe::routes(state.clone())) .merge(session::routes(state.clone())) diff --git a/crates/goose/src/model.rs b/crates/goose/src/model.rs index 6e97606622..75829d4b3f 100644 --- a/crates/goose/src/model.rs +++ b/crates/goose/src/model.rs @@ -1,4 +1,6 @@ +use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; const DEFAULT_CONTEXT_LIMIT: usize = 128_000; @@ -6,6 +8,32 @@ const DEFAULT_CONTEXT_LIMIT: usize = 128_000; pub const GPT_4O_TOKENIZER: &str = "Xenova--gpt-4o"; pub const CLAUDE_TOKENIZER: &str = "Xenova--claude-tokenizer"; +// Define the model limits as a static HashMap for reuse +static MODEL_SPECIFIC_LIMITS: Lazy> = Lazy::new(|| { + let mut map = HashMap::new(); + // OpenAI models, https://platform.openai.com/docs/models#models-overview + map.insert("gpt-4o", 128_000); + map.insert("gpt-4-turbo", 128_000); + map.insert("o1-mini", 128_000); + map.insert("o1-preview", 128_000); + map.insert("o1", 200_000); + map.insert("o3-mini", 200_000); + map.insert("gpt-4.1", 1_000_000); + map.insert("gpt-4-1", 1_000_000); + + // Anthropic models, https://docs.anthropic.com/en/docs/about-claude/models + map.insert("claude-3", 200_000); + + // Google models, https://ai.google/get-started/our-models/ + map.insert("gemini-2.5", 1_000_000); + map.insert("gemini-2-5", 1_000_000); + + // Meta Llama models, https://github.com/meta-llama/llama-models/tree/main?tab=readme-ov-file#llama-models-1 + map.insert("llama3.2", 128_000); + map.insert("llama3.3", 128_000); + map +}); + /// Configuration for model-specific settings and limits #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ModelConfig { @@ -27,6 +55,13 @@ pub struct ModelConfig { pub toolshim_model: Option, } +/// Struct to represent model pattern matches and their limits +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ModelLimitConfig { + pub pattern: String, + pub context_limit: usize, +} + impl ModelConfig { /// Create a new ModelConfig with the specified model name /// @@ -70,29 +105,23 @@ impl ModelConfig { /// Get model-specific context limit based on model name fn get_model_specific_limit(model_name: &str) -> Option { - // Implement some sensible defaults - match model_name { - // OpenAI models, https://platform.openai.com/docs/models#models-overview - name if name.contains("gpt-4o") => Some(128_000), - name if name.contains("gpt-4-turbo") => Some(128_000), - name if name.contains("o1-mini") || name.contains("o1-preview") => Some(128_000), - name if name.contains("o1") => Some(200_000), - name if name.contains("o3-mini") => Some(200_000), - name if name.contains("gpt-4.1") => Some(1_000_000), - name if name.contains("gpt-4-1") => Some(1_000_000), - - // Anthropic models, https://docs.anthropic.com/en/docs/about-claude/models - name if name.contains("claude-3") => Some(200_000), - - // Google models, https://ai.google/get-started/our-models/ - name if name.contains("gemini-2.5") => Some(1_000_000), - name if name.contains("gemini-2-5") => Some(1_000_000), - - // Meta Llama models, https://github.com/meta-llama/llama-models/tree/main?tab=readme-ov-file#llama-models-1 - name if name.contains("llama3.2") => Some(128_000), - name if name.contains("llama3.3") => Some(128_000), - _ => None, + for (pattern, &limit) in MODEL_SPECIFIC_LIMITS.iter() { + if model_name.contains(pattern) { + return Some(limit); + } } + None + } + + /// Get all model pattern matches and their limits + pub fn get_all_model_limits() -> Vec { + MODEL_SPECIFIC_LIMITS + .iter() + .map(|(&pattern, &context_limit)| ModelLimitConfig { + pattern: pattern.to_string(), + context_limit, + }) + .collect() } /// Set an explicit context limit @@ -215,4 +244,15 @@ mod tests { let config = ModelConfig::new("test-model".to_string()); assert_eq!(config.temperature, None); } + + #[test] + fn test_get_all_model_limits() { + let limits = ModelConfig::get_all_model_limits(); + assert!(!limits.is_empty()); + + // Test that we can find specific patterns + let gpt4_limit = limits.iter().find(|l| l.pattern == "gpt-4o"); + assert!(gpt4_limit.is_some()); + assert_eq!(gpt4_limit.unwrap().context_limit, 128_000); + } } diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 980de584cb..bd8f3ea5ce 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -1,26 +1,18 @@ -import React, { useEffect, useRef, useState, useCallback } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { IpcRendererEvent } from 'electron'; -import { addExtensionFromDeepLink } from './extensions'; import { openSharedSessionFromDeepLink } from './sessionLinks'; -import { getStoredModel } from './utils/providerUtils'; -import { getStoredProvider, initializeSystem } from './utils/providerUtils'; -import { useModel } from './components/settings/models/ModelContext'; -import { useRecentModels } from './components/settings/models/RecentModels'; -import { createSelectedModel } from './components/settings/models/utils'; -import { getDefaultModel } from './components/settings/models/hardcoded_stuff'; +import { initializeSystem } from './utils/providerUtils'; import { ErrorUI } from './components/ErrorBoundary'; import { ConfirmationModal } from './components/ui/ConfirmationModal'; import { ToastContainer } from 'react-toastify'; import { toastService } from './toasts'; -import { settingsV2Enabled } from './flags'; import { extractExtensionName } from './components/settings/extensions/utils'; import { GoosehintsModal } from './components/GoosehintsModal'; import { SessionDetails } from './sessions'; -import WelcomeView from './components/WelcomeView'; import ChatView from './components/ChatView'; import SuspenseLoader from './suspense-loader'; -import SettingsView, { type SettingsViewOptions } from './components/settings/SettingsView'; +import { type SettingsViewOptions } from './components/settings/SettingsView'; import SettingsViewV2 from './components/settings_v2/SettingsView'; import MoreModelsView from './components/settings/models/MoreModelsView'; import ConfigureProvidersView from './components/settings/providers/ConfigureProvidersView'; @@ -29,7 +21,6 @@ import SharedSessionView from './components/sessions/SharedSessionView'; import ProviderSettings from './components/settings_v2/providers/ProviderSettingsPage'; import RecipeEditor from './components/RecipeEditor'; import { useChat } from './hooks/useChat'; -import { addExtension as addExtensionDirect, FullExtensionConfig } from './extensions'; import 'react-toastify/dist/ReactToastify.css'; import { useConfig, MalformedConfigError } from './components/ConfigContext'; @@ -101,7 +92,7 @@ export default function App() { const [extensionConfirmLabel, setExtensionConfirmLabel] = useState(''); const [extensionConfirmTitle, setExtensionConfirmTitle] = useState(''); const [{ view, viewOptions }, setInternalView] = useState(getInitialView()); - const { getExtensions, addExtension, disableAllExtensions, read } = useConfig(); + const { getExtensions, addExtension, read } = useConfig(); const initAttemptedRef = useRef(false); // Utility function to extract the command from the link @@ -123,158 +114,7 @@ export default function App() { setInternalView({ view, viewOptions }); }; - const disableAllStoredExtensions = () => { - const userSettingsStr = localStorage.getItem('user_settings'); - if (!userSettingsStr) return; - - try { - const userSettings = JSON.parse(userSettingsStr); - // Store original state before modifying - localStorage.setItem('user_settings_backup', userSettingsStr); - console.log('Backing up user_settings'); - - // Disable all extensions - userSettings.extensions = userSettings.extensions.map((ext) => ({ - ...ext, - enabled: false, - })); - - localStorage.setItem('user_settings', JSON.stringify(userSettings)); - console.log('Disabled all stored extensions'); - window.electron.emit('settings-updated'); - } catch (error) { - console.error('Error disabling stored extensions:', error); - } - }; - - // Function to restore original extension states for new non-recipe windows - const restoreOriginalExtensionStates = () => { - const backupStr = localStorage.getItem('user_settings_backup'); - if (backupStr) { - localStorage.setItem('user_settings', backupStr); - console.log('Restored original extension states'); - } - }; - - const updateUserSettingsWithConfig = (extensions: FullExtensionConfig[]) => { - try { - const userSettingsStr = localStorage.getItem('user_settings'); - const userSettings = userSettingsStr ? JSON.parse(userSettingsStr) : { extensions: [] }; - - // For each extension in the passed in config - extensions.forEach((newExtension) => { - // Find if this extension already exists - const existingIndex = userSettings.extensions.findIndex( - (ext) => ext.id === newExtension.id - ); - - if (existingIndex !== -1) { - // Extension exists - just set its enabled to true - userSettings.extensions[existingIndex].enabled = true; - } else { - // Extension is new - add it to the array - userSettings.extensions.push({ - ...newExtension, - enabled: true, - }); - } - }); - - localStorage.setItem('user_settings', JSON.stringify(userSettings)); - console.log('Updated user settings with new/enabled extensions:', userSettings.extensions); - - // Notify any listeners (like the settings page) that settings have changed - window.electron.emit('settings-updated'); - } catch (error) { - console.error('Error updating user settings:', error); - } - }; - - const enableRecipeConfigExtensions = async (extensions: FullExtensionConfig[]) => { - if (!extensions?.length) { - console.log('No extensions to enable from bot config'); - return; - } - - console.log(`Enabling ${extensions.length} extensions from bot config:`, extensions); - - disableAllStoredExtensions(); - - // Wait for initial server readiness - await new Promise((resolve) => setTimeout(resolve, 2000)); - - for (const extension of extensions) { - try { - console.log(`Enabling extension: ${extension.name}`); - const extensionConfig = { - ...extension, - enabled: true, - }; - - // Try to add the extension - const response = await addExtensionDirect(extensionConfig, false); - - if (!response.ok) { - console.error( - `Failed to enable extension ${extension.name}: Server returned ${response.status}` - ); - // If it's a 428, retry once - if (response.status === 428) { - console.log('Server not ready, waiting and will retry...'); - await new Promise((resolve) => setTimeout(resolve, 2000)); - try { - await addExtensionDirect(extensionConfig, true); - console.log(`Successfully enabled extension ${extension.name} on retry`); - } catch (retryError) { - console.error(`Failed to enable extension ${extension.name} on retry:`, retryError); - } - } - continue; - } - updateUserSettingsWithConfig(extensions); - - console.log(`Successfully enabled extension: ${extension.name}`); - } catch (error) { - console.error(`Failed to enable extension ${extension.name}:`, error); - } - } - - console.log('Finished enabling bot config extensions'); - }; - - const _enableRecipeConfigExtensionsV2 = useCallback( - async (extensions: FullExtensionConfig[]) => { - if (!extensions?.length) { - console.log('No extensions to enable from bot config'); - return; - } - - try { - await disableAllExtensions(); - console.log('Disabled all existing extensions'); - - for (const extension of extensions) { - try { - console.log(`Enabling extension: ${extension.name}`); - await addExtension(extension.name, extension, true); - } catch (error) { - console.error(`Failed to enable extension ${extension.name}:`, error); - } - } - } catch (error) { - console.error('Failed to enable bot extensions'); - } - console.log('Finished enabling bot config extensions'); - }, - [disableAllExtensions, addExtension] - ); - - // settings v2 initialization useEffect(() => { - if (!settingsV2Enabled) { - return; - } - // Guard against multiple initialization attempts if (initAttemptedRef.current) { console.log('Initialization already attempted, skipping...'); @@ -326,12 +166,6 @@ export default function App() { return; } - // Handle bot config extensions first - // if (recipeConfig?.extensions?.length > 0 && viewType != 'recipeEditor') { - // console.log('Found extensions in bot config:', recipeConfig.extensions); - // await enableRecipeConfigExtensionsV2(recipeConfig.extensions); - // } - const config = window.electron.getConfig(); const provider = (await read('GOOSE_PROVIDER', false)) ?? config.GOOSE_DEFAULT_PROVIDER; @@ -622,11 +456,7 @@ export default function App() { console.log(`Confirming installation of extension from: ${pendingLink}`); setModalVisible(false); // Dismiss modal immediately try { - if (settingsV2Enabled) { - await addExtensionFromDeepLinkV2(pendingLink, addExtension, setView); - } else { - await addExtensionFromDeepLink(pendingLink, setView); - } + await addExtensionFromDeepLinkV2(pendingLink, addExtension, setView); console.log('Extension installation successful'); } catch (error) { console.error('Failed to add extension:', error); @@ -648,112 +478,6 @@ export default function App() { setPendingLink(null); }; - // TODO: remove -- careful removal of these and the useEffect below breaks - // reloading to chat view using stored provider - const { switchModel } = useModel(); // TODO: remove - const { addRecentModel } = useRecentModels(); // TODO: remove - - useEffect(() => { - const urlParams = new URLSearchParams(window.location.search); - const viewType = urlParams.get('view'); - const recipeConfig = window.appConfig.get('recipeConfig'); - - if (settingsV2Enabled) { - return; - } - - console.log(`Initializing app with settings v1`); - - // Handle bot config extensions first - if (recipeConfig?.extensions?.length > 0 && viewType != 'recipeEditor') { - console.log('Found extensions in bot config:', recipeConfig.extensions); - enableRecipeConfigExtensions(recipeConfig.extensions); - } - - // If we have a specific view type in the URL, use that and skip provider detection - if (viewType) { - if (viewType === 'recipeEditor' && recipeConfig) { - console.log('Setting view to recipeEditor with config:', recipeConfig); - setView('recipeEditor', { config: recipeConfig }); - } else { - setView(viewType as View); - } - return; - } - - // if not in any of the states above (in a regular chat) - if (!recipeConfig) { - restoreOriginalExtensionStates(); - } - - console.log(`Initializing app with settings v1`); - - // Attempt to detect config for a stored provider - const detectStoredProvider = () => { - try { - const config = window.electron.getConfig(); - console.log('Loaded config:', JSON.stringify(config)); - - const storedProvider = getStoredProvider(config); - console.log('Stored provider:', storedProvider); - - if (storedProvider) { - setView('chat'); - } else { - setView('welcome'); - } - } catch (error) { - console.error('DETECTION ERROR:', error); - setFatalError(`Config detection error: ${error.message || 'Unknown error'}`); - } - }; - - // Initialize system if we have a stored provider - const setupStoredProvider = async () => { - try { - const config = window.electron.getConfig(); - - if (config.GOOSE_PROVIDER && config.GOOSE_MODEL) { - console.log('using GOOSE_PROVIDER and GOOSE_MODEL from config'); - await initializeSystem(config.GOOSE_PROVIDER, config.GOOSE_MODEL); - return; - } - - const storedProvider = getStoredProvider(config); - const storedModel = getStoredModel(); - - if (storedProvider) { - try { - await initializeSystem(storedProvider, storedModel); - console.log('Setup using locally stored provider:', storedProvider); - console.log('Setup using locally stored model:', storedModel); - - if (!storedModel) { - const modelName = getDefaultModel(storedProvider.toLowerCase()); - const model = createSelectedModel(storedProvider.toLowerCase(), modelName); - switchModel(model); - addRecentModel(model); - } - } catch (error) { - console.error('Failed to initialize with stored provider:', error); - setFatalError(`Initialization failed: ${error.message || 'Unknown error'}`); - } - } - } catch (error) { - console.error('SETUP ERROR:', error); - setFatalError(`Setup error: ${error.message || 'Unknown error'}`); - } - }; - - // Execute the functions with better error handling - detectStoredProvider(); - setupStoredProvider().catch((error) => { - console.error('ASYNC SETUP ERROR:', error); - setFatalError(`Async setup error: ${error.message || 'Unknown error'}`); - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - if (fatalError) { return ; } @@ -796,34 +520,18 @@ export default function App() {
{view === 'loading' && } - {view === 'welcome' && - (settingsV2Enabled ? ( - setView('chat')} isOnboarding={true} /> - ) : ( - { - setView('chat'); - }} - /> - ))} - {view === 'settings' && - (settingsV2Enabled ? ( - { - setView('chat'); - }} - setView={setView} - viewOptions={viewOptions as SettingsViewOptions} - /> - ) : ( - { - setView('chat'); - }} - setView={setView} - viewOptions={viewOptions as SettingsViewOptions} - /> - ))} + {view === 'welcome' && ( + setView('chat')} isOnboarding={true} /> + )} + {view === 'settings' && ( + { + setView('chat'); + }} + setView={setView} + viewOptions={viewOptions as SettingsViewOptions} + /> + )} {view === 'moreModels' && ( { diff --git a/ui/desktop/src/components/bottom_menu/BottomMenu.tsx b/ui/desktop/src/components/bottom_menu/BottomMenu.tsx index 061340faf4..a9466c3c62 100644 --- a/ui/desktop/src/components/bottom_menu/BottomMenu.tsx +++ b/ui/desktop/src/components/bottom_menu/BottomMenu.tsx @@ -1,13 +1,9 @@ import { useState, useEffect, useRef } from 'react'; import { useModel } from '../settings/models/ModelContext'; -import { Sliders } from 'lucide-react'; import { AlertType, useAlerts } from '../alerts'; import { useToolCount } from '../alerts/useToolCount'; import BottomMenuAlertPopover from './BottomMenuAlertPopover'; -import { ModelRadioList } from '../settings/models/ModelRadioList'; -import { ChevronUp, ChevronDown } from '../icons'; import type { View, ViewOptions } from '../../App'; -import { settingsV2Enabled } from '../../flags'; import { BottomMenuModeSelection } from './BottomMenuModeSelection'; import ModelsBottomBar from '../settings_v2/models/bottom_bar/ModelsBottomBar'; import { useConfig } from '../ConfigContext'; @@ -19,6 +15,11 @@ const TOKEN_LIMIT_DEFAULT = 128000; // fallback for custom models that the backe const TOKEN_WARNING_THRESHOLD = 0.8; // warning shows at 80% of the token limit const TOOLS_MAX_SUGGESTED = 60; // max number of tools before we show a warning +interface ModelLimit { + pattern: string; + context_limit: number; +} + export default function BottomMenu({ setView, numTokens = 0, @@ -40,40 +41,28 @@ export default function BottomMenu({ const { getProviders, read } = useConfig(); const [tokenLimit, setTokenLimit] = useState(TOKEN_LIMIT_DEFAULT); - // Model-specific token limits that match the backend implementation - const MODEL_SPECIFIC_LIMITS: { [key: string]: number } = { - // OpenAI models - 'gpt-4o': 128_000, - 'gpt-4-turbo': 128_000, - 'o1-mini': 128_000, - 'o1-preview': 128_000, - o1: 200_000, - 'o3-mini': 200_000, - 'gpt-4.1': 1_000_000, - 'gpt-4-1': 1_000_000, - - // Anthropic models - 'claude-3': 200_000, - - // Google models - 'gemini-2.5': 1_000_000, - 'gemini-2-5': 1_000_000, - - // Meta Llama models - 'llama3.2': 128_000, - 'llama3.3': 128_000, + // Load model limits from the API + const getModelLimits = async () => { + try { + const response = await read('model-limits', false); + if (response) { + // The response is already parsed, no need for JSON.parse + return response as ModelLimit[]; + } + } catch (err) { + console.error('Error fetching model limits:', err); + } + return []; }; - // Helper function to replicate Rust's get_model_specific_limit logic - function getModelSpecificLimit(modelName: string): number | null { - // Check each pattern against the model name - for (const [pattern, limit] of Object.entries(MODEL_SPECIFIC_LIMITS)) { - if (modelName.toLowerCase().includes(pattern.toLowerCase())) { - return limit; - } - } - return null; - } + // Helper function to find model limit using pattern matching + const findModelLimit = (modelName: string, modelLimits: ModelLimit[]): number | null => { + if (!modelName) return null; + const matchingLimit = modelLimits.find((limit) => + modelName.toLowerCase().includes(limit.pattern.toLowerCase()) + ); + return matchingLimit ? matchingLimit.context_limit : null; + }; // Load providers and get current model's token limit const loadProviderDetails = async () => { @@ -90,7 +79,7 @@ export default function BottomMenu({ // Find the provider details for the current provider const currentProvider = providers.find((p) => p.name === provider); if (currentProvider?.metadata?.known_models) { - // Find the model's token limit + // Find the model's token limit from the backend response const modelConfig = currentProvider.metadata.known_models.find((m) => m.name === model); if (modelConfig?.context_limit) { setTokenLimit(modelConfig.context_limit); @@ -98,15 +87,15 @@ export default function BottomMenu({ } } - // Fallback: Use the pattern matching logic if no exact match was found - const fallbackLimit = getModelSpecificLimit(model); + // Fallback: Use pattern matching logic if no exact model match was found + const modelLimit = await getModelLimits(); + const fallbackLimit = findModelLimit(model as string, modelLimit); if (fallbackLimit !== null) { - console.log(`Using fallback token limit for model ${model}: ${fallbackLimit}`); setTokenLimit(fallbackLimit); return; } - // If no match found, use the default + // If no match found, use the default model limit setTokenLimit(TOKEN_LIMIT_DEFAULT); } catch (err) { console.error('Error loading providers or token limit:', err); @@ -199,76 +188,7 @@ export default function BottomMenu({ {} {/* Model Selector Dropdown */} - {settingsV2Enabled ? ( - - ) : ( -
-
setIsModelMenuOpen(!isModelMenuOpen)} - > - {(currentModel?.alias ?? currentModel?.name) || 'Select Model'} - {isModelMenuOpen ? ( - - ) : ( - - )} -
- - {/* Dropdown Menu */} - {isModelMenuOpen && ( -
-
- ( - - )} - /> -
{ - setIsModelMenuOpen(false); - setView('settings'); - }} - > - Tools and Settings - -
-
-
- )} -
- )} + {/* Separator */}
diff --git a/ui/desktop/src/components/bottom_menu/BottomMenuModeSelection.tsx b/ui/desktop/src/components/bottom_menu/BottomMenuModeSelection.tsx index c0f6a30978..092a6c0be3 100644 --- a/ui/desktop/src/components/bottom_menu/BottomMenuModeSelection.tsx +++ b/ui/desktop/src/components/bottom_menu/BottomMenuModeSelection.tsx @@ -1,8 +1,6 @@ import React, { useEffect, useRef, useState, useCallback } from 'react'; -import { getApiUrl, getSecretKey } from '../../config'; import { all_goose_modes, ModeSelectionItem } from '../settings_v2/mode/ModeSelectionItem'; import { useConfig } from '../ConfigContext'; -import { settingsV2Enabled } from '../../flags'; import { View, ViewOptions } from '../../App'; import { Orbit } from 'lucide-react'; @@ -18,23 +16,8 @@ export const BottomMenuModeSelection = ({ setView }: BottomMenuModeSelectionProp const fetchCurrentMode = useCallback(async () => { try { - if (!settingsV2Enabled) { - const response = await fetch(getApiUrl('/configs/get?key=GOOSE_MODE'), { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': getSecretKey(), - }, - }); - - if (response.ok) { - const { value } = await response.json(); - if (value) { - setGooseMode(value); - } - } - } else { - const mode = (await read('GOOSE_MODE', false)) as string; + const mode = (await read('GOOSE_MODE', false)) as string; + if (mode) { setGooseMode(mode); } } catch (error) { @@ -86,29 +69,12 @@ export const BottomMenuModeSelection = ({ setView }: BottomMenuModeSelectionProp return; } - if (!settingsV2Enabled) { - const storeResponse = await fetch(getApiUrl('/configs/store'), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': getSecretKey(), - }, - body: JSON.stringify({ - key: 'GOOSE_MODE', - value: newMode, - isSecret: false, - }), - }); - - if (!storeResponse.ok) { - const errorText = await storeResponse.text(); - console.error('Store response error:', errorText); - throw new Error(`Failed to store new goose mode: ${newMode}`); - } - setGooseMode(newMode); - } else { + try { await upsert('GOOSE_MODE', newMode, false); setGooseMode(newMode); + } catch (error) { + console.error('Error updating goose mode:', error); + throw new Error(`Failed to store new goose mode: ${newMode}`); } }; @@ -125,14 +91,6 @@ export const BottomMenuModeSelection = ({ setView }: BottomMenuModeSelectionProp > {getValueByKey(gooseMode).toLowerCase()} - {/**/} - {/* Goose Mode: {getValueByKey(gooseMode)}*/} - {/**/} - {/*{isGooseModeMenuOpen ? (*/} - {/* */} - {/*) : (*/} - {/* */} - {/*)}*/} {/* Dropdown Menu */} diff --git a/ui/desktop/src/components/more_menu/MoreMenu.tsx b/ui/desktop/src/components/more_menu/MoreMenu.tsx index 86c827ae1c..5820093d01 100644 --- a/ui/desktop/src/components/more_menu/MoreMenu.tsx +++ b/ui/desktop/src/components/more_menu/MoreMenu.tsx @@ -3,7 +3,6 @@ import React, { useEffect, useState } from 'react'; import { ChatSmart, Idea, Refresh, Time, Send, Settings } from '../icons'; import { FolderOpen, Moon, Sliders, Sun } from 'lucide-react'; import { useConfig } from '../ConfigContext'; -import { settingsV2Enabled } from '../../flags'; import { ViewOptions, View } from '../../App'; interface MenuButtonProps { @@ -270,40 +269,21 @@ export default function MoreMenu({ - {settingsV2Enabled && ( - { - await remove('GOOSE_PROVIDER', false); - await remove('GOOSE_MODEL', false); - setOpen(false); - setView('welcome'); - }} - danger - subtitle="Clear selected model and restart (alpha)" - icon={} - className="border-b-0" - > - Reset provider and model - - )} - - {!settingsV2Enabled && ( - { - localStorage.removeItem('GOOSE_PROVIDER'); - setOpen(false); - window.electron.createChatWindow(); - }} - danger - subtitle="Clear selected model and restart" - icon={} - className="border-b-0" - > - Reset provider and model - - )} + { + await remove('GOOSE_PROVIDER', false); + await remove('GOOSE_MODEL', false); + setOpen(false); + setView('welcome'); + }} + danger + subtitle="Clear selected model and restart (alpha)" + icon={} + className="border-b-0" + > + Reset provider and model +
diff --git a/ui/desktop/src/components/settings/basic/ModeSelection.tsx b/ui/desktop/src/components/settings/basic/ModeSelection.tsx index ba6e651358..963074966e 100644 --- a/ui/desktop/src/components/settings/basic/ModeSelection.tsx +++ b/ui/desktop/src/components/settings/basic/ModeSelection.tsx @@ -1,61 +1,40 @@ -import React, { useEffect, useState } from 'react'; -import { getApiUrl, getSecretKey } from '../../../config'; +import React, { useEffect, useState, useCallback } from 'react'; import { all_goose_modes, filterGooseModes, ModeSelectionItem } from './ModeSelectionItem'; +import { useConfig } from '../../ConfigContext'; export const ModeSelection = () => { const [currentMode, setCurrentMode] = useState('auto'); const [previousApproveModel, setPreviousApproveModel] = useState(''); + const { read, upsert } = useConfig(); const handleModeChange = async (newMode: string) => { - const storeResponse = await fetch(getApiUrl('/configs/store'), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': getSecretKey(), - }, - body: JSON.stringify({ - key: 'GOOSE_MODE', - value: newMode, - isSecret: false, - }), - }); - - if (!storeResponse.ok) { - const errorText = await storeResponse.text(); - console.error('Store response error:', errorText); + try { + await upsert('GOOSE_MODE', newMode, false); + // Only track the previous approve if current mode is approve related but new mode is not. + if (currentMode.includes('approve') && !newMode.includes('approve')) { + setPreviousApproveModel(currentMode); + } + setCurrentMode(newMode); + } catch (error) { + console.error('Error updating goose mode:', error); throw new Error(`Failed to store new goose mode: ${newMode}`); } - // Only track the previous approve if current mode is approve related but new mode is not. - if (currentMode.includes('approve') && !newMode.includes('approve')) { - setPreviousApproveModel(currentMode); - } - setCurrentMode(newMode); }; - useEffect(() => { - const fetchCurrentMode = async () => { - try { - const response = await fetch(getApiUrl('/configs/get?key=GOOSE_MODE'), { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': getSecretKey(), - }, - }); - - if (response.ok) { - const { value } = await response.json(); - if (value) { - setCurrentMode(value); - } - } - } catch (error) { - console.error('Error fetching current mode:', error); + const fetchCurrentMode = useCallback(async () => { + try { + const mode = (await read('GOOSE_MODE', false)) as string; + if (mode) { + setCurrentMode(mode); } - }; + } catch (error) { + console.error('Error fetching current mode:', error); + } + }, [read]); + useEffect(() => { fetchCurrentMode(); - }, []); + }, [fetchCurrentMode]); return (
diff --git a/ui/desktop/src/components/settings_v2/extensions/deeplink.ts b/ui/desktop/src/components/settings_v2/extensions/deeplink.ts index 51dc070910..4191423684 100644 --- a/ui/desktop/src/components/settings_v2/extensions/deeplink.ts +++ b/ui/desktop/src/components/settings_v2/extensions/deeplink.ts @@ -1,8 +1,7 @@ -import type { ExtensionConfig } from '../../../api/types.gen'; +import type { ExtensionConfig } from '../../../api'; import { toastService } from '../../../toasts'; import { activateExtension } from './extension-manager'; -import { DEFAULT_EXTENSION_TIMEOUT, nameToKey } from './utils'; -import { settingsV2Enabled } from '../../../flags'; +import { DEFAULT_EXTENSION_TIMEOUT } from './utils'; /** * Build an extension config for stdio from the deeplink URL @@ -129,11 +128,7 @@ export async function addExtensionFromDeepLink( // Check if extension requires env vars and go to settings if so if (config.envs && Object.keys(config.envs).length > 0) { console.log('Environment variables required, redirecting to settings'); - if (settingsV2Enabled) { - setView('settings', { deepLinkConfig: config, showEnvVars: true }); - } else { - setView('settings', { extensionId: nameToKey(name), showEnvVars: true }); - } + setView('settings', { deepLinkConfig: config, showEnvVars: true }); return; } diff --git a/ui/desktop/src/components/settings_v2/mode/ModeSection.tsx b/ui/desktop/src/components/settings_v2/mode/ModeSection.tsx index ef0d9d0ad7..73d4197f26 100644 --- a/ui/desktop/src/components/settings_v2/mode/ModeSection.tsx +++ b/ui/desktop/src/components/settings_v2/mode/ModeSection.tsx @@ -1,7 +1,7 @@ -import { useEffect, useState } from 'react'; -import { getApiUrl, getSecretKey } from '../../../config'; +import { useEffect, useState, useCallback } from 'react'; import { all_goose_modes, ModeSelectionItem } from './ModeSelectionItem'; import { View, ViewOptions } from '../../../App'; +import { useConfig } from '../../ConfigContext'; interface ModeSectionProps { setView: (view: View, viewOptions?: ViewOptions) => void; @@ -9,53 +9,32 @@ interface ModeSectionProps { export const ModeSection = ({ setView }: ModeSectionProps) => { const [currentMode, setCurrentMode] = useState('auto'); + const { read, upsert } = useConfig(); const handleModeChange = async (newMode: string) => { - const storeResponse = await fetch(getApiUrl('/configs/store'), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': getSecretKey(), - }, - body: JSON.stringify({ - key: 'GOOSE_MODE', - value: newMode, - isSecret: false, - }), - }); - - if (!storeResponse.ok) { - const errorText = await storeResponse.text(); - console.error('Store response error:', errorText); + try { + await upsert('GOOSE_MODE', newMode, false); + setCurrentMode(newMode); + } catch (error) { + console.error('Error updating goose mode:', error); throw new Error(`Failed to store new goose mode: ${newMode}`); } - setCurrentMode(newMode); }; - useEffect(() => { - const fetchCurrentMode = async () => { - try { - const response = await fetch(getApiUrl('/configs/get?key=GOOSE_MODE'), { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': getSecretKey(), - }, - }); - - if (response.ok) { - const { value } = await response.json(); - if (value) { - setCurrentMode(value); - } - } - } catch (error) { - console.error('Error fetching current mode:', error); + const fetchCurrentMode = useCallback(async () => { + try { + const mode = (await read('GOOSE_MODE', false)) as string; + if (mode) { + setCurrentMode(mode); } - }; + } catch (error) { + console.error('Error fetching current mode:', error); + } + }, [read]); + useEffect(() => { fetchCurrentMode(); - }, []); + }, [fetchCurrentMode]); return (
diff --git a/ui/desktop/src/extensions.tsx b/ui/desktop/src/extensions.tsx index d7215e2907..d86369a0a8 100644 --- a/ui/desktop/src/extensions.tsx +++ b/ui/desktop/src/extensions.tsx @@ -1,6 +1,4 @@ import { getApiUrl, getSecretKey } from './config'; -import { type View } from './App'; -import { type SettingsViewOptions } from './components/settings/SettingsView'; import { toast } from 'react-toastify'; import builtInExtensionsData from './built-in-extensions.json'; @@ -274,158 +272,3 @@ export async function replaceWithShims(cmd: string) { return cmd; } - -function envVarsRequired(config: ExtensionConfig) { - return config.env_keys?.length > 0; -} - -function handleError(message: string, shouldThrow = false): void { - toastError({ - title: 'Failed to install extension', - msg: message, - traceback: message, - toastOptions: { autoClose: false }, - }); - console.error(message); - if (shouldThrow) { - throw new Error(message); - } -} - -export async function addExtensionFromDeepLink( - url: string, - setView: (view: View, options: SettingsViewOptions) => void -) { - if (!url.startsWith('goose://extension')) { - handleError('Invalid URL: URL must use the goose://extension scheme'); - return; - } - - const parsedUrl = new URL(url); - - if (parsedUrl.protocol !== 'goose:') { - handleError('Invalid protocol: URL must use the goose:// scheme', true); - } - - // Check that all required fields are present and not empty - const requiredFields = ['name', 'description']; - - for (const field of requiredFields) { - const value = parsedUrl.searchParams.get(field); - if (!value || value.trim() === '') { - handleError(`The link is missing required field '${field}'`, true); - } - } - - const cmd = parsedUrl.searchParams.get('cmd'); - const remoteUrl = parsedUrl.searchParams.get('url'); - - if (!cmd && !remoteUrl) { - handleError( - "Failed to install extension: Missing required 'cmd' or 'url' parameter in the URL", - true - ); - } - - const id = parsedUrl.searchParams.get('id'); - const name = parsedUrl.searchParams.get('name'); - const description = parsedUrl.searchParams.get('description'); - const timeout = parsedUrl.searchParams.get('timeout'); - - // Create a ExtensionConfig from the URL parameters - // Parse timeout if provided, otherwise use default - const parsedTimeout = timeout ? parseInt(timeout, 10) : null; - - const config: FullExtensionConfig = cmd - ? getStdioConfig(cmd, parsedUrl, id, name, description, parsedTimeout) - : getSseConfig(remoteUrl, id, name, description, parsedTimeout); - - // Store the extension config regardless of env vars status - storeExtensionConfig(config); - - // Check if extension requires env vars and go to settings if so - if (envVarsRequired(config)) { - console.log('Environment variables required, redirecting to settings'); - setView('settings', { extensionId: config.id, showEnvVars: true }); - return; - } - - // If no env vars are required, proceed with extending Goosed - await addExtension(config); -} - -function getStdioConfig( - cmd: string, - parsedUrl: URL, - id: string, - name: string, - description: string, - parsedTimeout: number -) { - const allowedCommands = ['docker', 'jbang', 'npx', 'uvx', 'goosed']; - if (!allowedCommands.includes(cmd)) { - handleError( - `Failed to install extension: Invalid command: ${cmd}. Only ${allowedCommands.join(', ')} are allowed.`, - true - ); - } - - // Check for security risk with npx -c command - const args = parsedUrl.searchParams.getAll('arg'); - if (cmd === 'npx' && args.includes('-c')) { - handleError('npx with -c argument can lead to code injection', true); - } - - const envList = parsedUrl.searchParams.getAll('env'); - - // split env based on delimiter to a map - const envs = envList.reduce( - (acc, env) => { - const [key, value] = env.split('='); - acc[key] = value; - return acc; - }, - {} as Record - ); - - const config: FullExtensionConfig = { - id, - name, - type: 'stdio', - cmd, - args, - description, - enabled: true, - env_keys: Object.keys(envs).length > 0 ? Object.keys(envs) : [], - timeout: - parsedTimeout !== null && !isNaN(parsedTimeout) && Number.isInteger(parsedTimeout) - ? parsedTimeout - : DEFAULT_EXTENSION_TIMEOUT, - }; - - return config; -} - -function getSseConfig( - remoteUrl: string, - id: string, - name: string, - description: string, - parsedTimeout: number -) { - const config: FullExtensionConfig = { - id, - name, - type: 'sse', - uri: remoteUrl, - description, - enabled: true, - env_keys: [], - timeout: - parsedTimeout !== null && !isNaN(parsedTimeout) && Number.isInteger(parsedTimeout) - ? parsedTimeout - : DEFAULT_EXTENSION_TIMEOUT, - }; - - return config; -} diff --git a/ui/desktop/src/flags.ts b/ui/desktop/src/flags.ts index e9360e3e29..e69de29bb2 100644 --- a/ui/desktop/src/flags.ts +++ b/ui/desktop/src/flags.ts @@ -1 +0,0 @@ -export const settingsV2Enabled = true; diff --git a/ui/desktop/src/utils/providerUtils.ts b/ui/desktop/src/utils/providerUtils.ts index 3077b8f285..370ed38995 100644 --- a/ui/desktop/src/utils/providerUtils.ts +++ b/ui/desktop/src/utils/providerUtils.ts @@ -1,10 +1,6 @@ import { getApiUrl, getSecretKey } from '../config'; -import { FullExtensionConfig, loadAndAddStoredExtensions } from '../extensions'; -import { GOOSE_PROVIDER, GOOSE_MODEL } from '../env_vars'; -import { Model } from '../components/settings/models/ModelContext'; -import { gooseModels } from '../components/settings/models/GooseModels'; +import { FullExtensionConfig } from '../extensions'; import { initializeAgent } from '../agent'; -import { settingsV2Enabled } from '../flags'; import { initializeBundledExtensions, syncBundledExtensions, @@ -16,31 +12,6 @@ import type { ExtensionConfig, FixedExtensionEntry } from '../components/ConfigC import { toastService } from '../toasts'; import { ExtensionQuery, addExtension as apiAddExtension } from '../api'; -interface AppConfig { - GOOSE_PROVIDER?: string; - [key: string]: unknown; -} - -export function getStoredProvider(config: AppConfig): string | null { - return config.GOOSE_PROVIDER || localStorage.getItem(GOOSE_PROVIDER); -} - -export function getStoredModel(): string | null { - const storedModel = localStorage.getItem('GOOSE_MODEL'); // Adjust key name if necessary - - if (storedModel) { - try { - const modelInfo: Model = JSON.parse(storedModel); - return modelInfo.name || null; // Return name if it exists, otherwise null - } catch (error) { - console.error('Error parsing GOOSE_MODEL from local storage:', error); - return null; // Return null if parsing fails - } - } - - return null; // Return null if storedModel is not found -} - export interface Provider { id: string; // Lowercase key (e.g., "openai") name: string; // Provider name (e.g., "OpenAI") @@ -166,13 +137,6 @@ export const initializeSystem = async ( console.log('initializing agent with provider', provider, 'model', model); await initializeAgent({ provider, model }); - // This will go away after the release of settings v2 as this is now handled in config.yaml - if (!settingsV2Enabled) { - // Sync the model state with React - const syncedModel = syncModelWithAgent(provider, model); - console.log('Model synced with React state:', syncedModel); - } - // Get recipeConfig directly here const recipeConfig = window.appConfig?.get?.('recipeConfig'); const botPrompt = recipeConfig?.instructions; @@ -200,93 +164,47 @@ export const initializeSystem = async ( } } - if (settingsV2Enabled) { - if (!options?.getExtensions || !options?.addExtension) { - console.warn('Extension helpers not provided in alpha mode'); - return; - } + if (!options?.getExtensions || !options?.addExtension) { + console.warn('Extension helpers not provided in alpha mode'); + return; + } - // NOTE: remove when we want to stop migration logic - // Check if we need to migrate extensions from localStorage to config.yaml - const configVersion = localStorage.getItem('configVersion'); - const shouldMigrateExtensions = !configVersion || parseInt(configVersion, 10) < 3; + // NOTE: remove when we want to stop migration logic + // Check if we need to migrate extensions from localStorage to config.yaml + const configVersion = localStorage.getItem('configVersion'); + const shouldMigrateExtensions = !configVersion || parseInt(configVersion, 10) < 3; - console.log(`shouldMigrateExtensions is ${shouldMigrateExtensions}`); - if (shouldMigrateExtensions) { - await migrateExtensionsToSettingsV3(); - } + console.log(`shouldMigrateExtensions is ${shouldMigrateExtensions}`); + if (shouldMigrateExtensions) { + await migrateExtensionsToSettingsV3(); + } - /* NOTE: - * If we've migrated and this is a version update, refreshedExtensions should be > 0 - * and we'll want to syncBundledExtensions to ensure any new extensions are added. - * Otherwise if the user has never opened goose - refreshedExtensions will be 0 - * and we want to fall into the case to initializeBundledExtensions. - */ + /* NOTE: + * If we've migrated and this is a version update, refreshedExtensions should be > 0 + * and we'll want to syncBundledExtensions to ensure any new extensions are added. + * Otherwise if the user has never opened goose - refreshedExtensions will be 0 + * and we want to fall into the case to initializeBundledExtensions. + */ - // Initialize or sync built-in extensions into config.yaml - let refreshedExtensions = await options.getExtensions(false); + // Initialize or sync built-in extensions into config.yaml + let refreshedExtensions = await options.getExtensions(false); - if (refreshedExtensions.length === 0) { - await initializeBundledExtensions(options.addExtension); - refreshedExtensions = await options.getExtensions(false); - } else { - await syncBundledExtensions(refreshedExtensions, options.addExtension); - } - - // Add enabled extensions to agent - for (const extensionEntry of refreshedExtensions) { - if (extensionEntry.enabled) { - const extensionConfig = extractExtensionConfig(extensionEntry); - await addToAgentOnStartup({ addToConfig: options.addExtension, extensionConfig }); - } - } + if (refreshedExtensions.length === 0) { + await initializeBundledExtensions(options.addExtension); + refreshedExtensions = await options.getExtensions(false); } else { - loadAndAddStoredExtensions().catch((error) => { - console.error('Failed to load and add stored extension configs:', error); - }); + await syncBundledExtensions(refreshedExtensions, options.addExtension); + } + + // Add enabled extensions to agent + for (const extensionEntry of refreshedExtensions) { + if (extensionEntry.enabled) { + const extensionConfig = extractExtensionConfig(extensionEntry); + await addToAgentOnStartup({ addToConfig: options.addExtension, extensionConfig }); + } } } catch (error) { console.error('Failed to initialize agent:', error); throw error; } }; - -// This function ensures the agent initialization values and React model state stay in sync -const syncModelWithAgent = (provider: string, modelName: string): Model | null => { - console.log('Syncing model state with agent:', { provider, modelName }); - - // First, try to find a matching model in our predefined list - let matchingModel = gooseModels.find( - (m) => m.name === modelName && m.provider.toLowerCase() === provider.toLowerCase() - ); - - // If no match by exact name and provider, try just by provider - if (!matchingModel) { - matchingModel = gooseModels.find((m) => m.provider.toLowerCase() === provider.toLowerCase()); - - if (matchingModel) { - console.log('Found model by provider only:', matchingModel); - } - } - - // If still no match, create a custom model - if (!matchingModel) { - console.log('No matching model found, creating custom model'); - matchingModel = { - id: Date.now(), - name: modelName, - provider: provider, - alias: `${provider} - ${modelName}`, - }; - } - - // Update localStorage with the model - if (matchingModel) { - localStorage.setItem(GOOSE_PROVIDER, matchingModel.provider.toLowerCase()); - localStorage.setItem(GOOSE_MODEL, JSON.stringify(matchingModel)); - - return matchingModel; - } - - return null; -};