diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index e4418b1caaef..d312764b9c1f 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -14,22 +14,20 @@ import { type Recipe } from './recipe'; import ChatView from './components/ChatView'; import SuspenseLoader from './suspense-loader'; -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'; +import SettingsView, { SettingsViewOptions } from './components/settings/SettingsView'; import SessionsView from './components/sessions/SessionsView'; import SharedSessionView from './components/sessions/SharedSessionView'; import SchedulesView from './components/schedule/SchedulesView'; -import ProviderSettings from './components/settings_v2/providers/ProviderSettingsPage'; +import ProviderSettings from './components/settings/providers/ProviderSettingsPage'; import RecipeEditor from './components/RecipeEditor'; import { useChat } from './hooks/useChat'; import 'react-toastify/dist/ReactToastify.css'; import { useConfig, MalformedConfigError } from './components/ConfigContext'; -import { addExtensionFromDeepLink as addExtensionFromDeepLinkV2 } from './components/settings_v2/extensions'; +import { ModelAndProviderProvider } from './components/ModelAndProviderContext'; +import { addExtensionFromDeepLink as addExtensionFromDeepLinkV2 } from './components/settings/extensions'; import { backupConfig, initConfig, readAllConfig } from './api/sdk.gen'; -import PermissionSettingsView from './components/settings_v2/permission/PermissionSetting'; +import PermissionSettingsView from './components/settings/permission/PermissionSetting'; import { type SessionDetails } from './sessions'; @@ -462,7 +460,7 @@ export default function App() { ); return ( - <> + @@ -496,7 +494,7 @@ export default function App() { setView('chat')} isOnboarding={true} /> )} {view === 'settings' && ( - { setView('chat'); }} @@ -504,21 +502,6 @@ export default function App() { viewOptions={viewOptions as SettingsViewOptions} /> )} - {view === 'moreModels' && ( - { - setView('settings'); - }} - setView={setView} - /> - )} - {view === 'configureProviders' && ( - { - setView('settings'); - }} - /> - )} {view === 'ConfigureProviders' && ( setView('chat')} isOnboarding={false} /> )} @@ -579,6 +562,6 @@ export default function App() { setIsGoosehintsModalOpen={setIsGoosehintsModalOpen} /> )} - + ); } diff --git a/ui/desktop/src/components/ConfigContext.tsx b/ui/desktop/src/components/ConfigContext.tsx index 6e2a8032dfbf..be4fefc94518 100644 --- a/ui/desktop/src/components/ConfigContext.tsx +++ b/ui/desktop/src/components/ConfigContext.tsx @@ -19,7 +19,7 @@ import type { ExtensionQuery, ExtensionConfig, } from '../api/types.gen'; -import { removeShims } from './settings_v2/extensions/utils'; +import { removeShims } from './settings/extensions/utils'; export type { ExtensionConfig } from '../api/types.gen'; diff --git a/ui/desktop/src/components/ModelAndProviderContext.tsx b/ui/desktop/src/components/ModelAndProviderContext.tsx new file mode 100644 index 000000000000..729467a6ff08 --- /dev/null +++ b/ui/desktop/src/components/ModelAndProviderContext.tsx @@ -0,0 +1,190 @@ +import React, { createContext, useContext, useState, useEffect, useMemo, useCallback } from 'react'; +import { initializeAgent } from '../agent'; +import { toastError, toastSuccess } from '../toasts'; +import Model, { getProviderMetadata } from './settings/models/modelInterface'; +import { ProviderMetadata } from '../api'; +import { useConfig } from './ConfigContext'; + +// titles +export const UNKNOWN_PROVIDER_TITLE = 'Provider name lookup'; + +// errors +const CHANGE_MODEL_ERROR_TITLE = 'Change failed'; +const SWITCH_MODEL_AGENT_ERROR_MSG = + 'Failed to start agent with selected model -- please try again'; +const CONFIG_UPDATE_ERROR_MSG = 'Failed to update configuration settings -- please try again'; +export const UNKNOWN_PROVIDER_MSG = 'Unknown provider in config -- please inspect your config.yaml'; + +// success +const CHANGE_MODEL_TOAST_TITLE = 'Model changed'; +const SWITCH_MODEL_SUCCESS_MSG = 'Successfully switched models'; + +interface ModelAndProviderContextType { + currentModel: string | null; + currentProvider: string | null; + changeModel: (model: Model) => Promise; + getCurrentModelAndProvider: () => Promise<{ model: string; provider: string }>; + getFallbackModelAndProvider: () => Promise<{ model: string; provider: string }>; + getCurrentModelAndProviderForDisplay: () => Promise<{ model: string; provider: string }>; + refreshCurrentModelAndProvider: () => Promise; +} + +interface ModelAndProviderProviderProps { + children: React.ReactNode; +} + +const ModelAndProviderContext = createContext(undefined); + +export const ModelAndProviderProvider: React.FC = ({ children }) => { + const [currentModel, setCurrentModel] = useState(null); + const [currentProvider, setCurrentProvider] = useState(null); + const { read, upsert, getProviders } = useConfig(); + + const changeModel = useCallback( + async (model: Model) => { + const modelName = model.name; + const providerName = model.provider; + try { + await initializeAgent({ + model: model.name, + provider: model.provider, + }); + } catch (error) { + console.error(`Failed to change model at agent step -- ${modelName} ${providerName}`); + toastError({ + title: CHANGE_MODEL_ERROR_TITLE, + msg: SWITCH_MODEL_AGENT_ERROR_MSG, + traceback: error instanceof Error ? error.message : String(error), + }); + // don't write to config + return; + } + + try { + await upsert('GOOSE_PROVIDER', providerName, false); + await upsert('GOOSE_MODEL', modelName, false); + + // Update local state + setCurrentProvider(providerName); + setCurrentModel(modelName); + } catch (error) { + console.error(`Failed to change model at config step -- ${modelName} ${providerName}}`); + toastError({ + title: CHANGE_MODEL_ERROR_TITLE, + msg: CONFIG_UPDATE_ERROR_MSG, + traceback: error instanceof Error ? error.message : String(error), + }); + // agent and config will be out of sync at this point + // TODO: reset agent to use current config settings + } finally { + // show toast + toastSuccess({ + title: CHANGE_MODEL_TOAST_TITLE, + msg: `${SWITCH_MODEL_SUCCESS_MSG} -- using ${model.alias ?? modelName} from ${model.subtext ?? providerName}`, + }); + } + }, + [upsert] + ); + + const getFallbackModelAndProvider = useCallback(async () => { + const provider = window.appConfig.get('GOOSE_DEFAULT_PROVIDER') as string; + const model = window.appConfig.get('GOOSE_DEFAULT_MODEL') as string; + if (provider && model) { + try { + await upsert('GOOSE_MODEL', model, false); + await upsert('GOOSE_PROVIDER', provider, false); + } catch (error) { + console.error('[getFallbackModelAndProvider] Failed to write to config', error); + } + } + return { model: model, provider: provider }; + }, [upsert]); + + const getCurrentModelAndProvider = useCallback(async () => { + let model: string; + let provider: string; + + // read from config + try { + model = (await read('GOOSE_MODEL', false)) as string; + provider = (await read('GOOSE_PROVIDER', false)) as string; + } catch (error) { + console.error(`Failed to read GOOSE_MODEL or GOOSE_PROVIDER from config`); + throw error; + } + if (!model || !provider) { + console.log('[getCurrentModelAndProvider] Checking app environment as fallback'); + return getFallbackModelAndProvider(); + } + return { model: model, provider: provider }; + }, [read, getFallbackModelAndProvider]); + + const getCurrentModelAndProviderForDisplay = useCallback(async () => { + const modelProvider = await getCurrentModelAndProvider(); + const gooseModel = modelProvider.model; + const gooseProvider = modelProvider.provider; + + // lookup display name + let metadata: ProviderMetadata; + + try { + metadata = await getProviderMetadata(String(gooseProvider), getProviders); + } catch (error) { + return { model: gooseModel, provider: gooseProvider }; + } + const providerDisplayName = metadata.display_name; + + return { model: gooseModel, provider: providerDisplayName }; + }, [getCurrentModelAndProvider, getProviders]); + + const refreshCurrentModelAndProvider = useCallback(async () => { + try { + const { model, provider } = await getCurrentModelAndProvider(); + setCurrentModel(model); + setCurrentProvider(provider); + } catch (error) { + console.error('Failed to refresh current model and provider:', error); + } + }, [getCurrentModelAndProvider]); + + // Load initial model and provider on mount + useEffect(() => { + refreshCurrentModelAndProvider(); + }, [refreshCurrentModelAndProvider]); + + const contextValue = useMemo( + () => ({ + currentModel, + currentProvider, + changeModel, + getCurrentModelAndProvider, + getFallbackModelAndProvider, + getCurrentModelAndProviderForDisplay, + refreshCurrentModelAndProvider, + }), + [ + currentModel, + currentProvider, + changeModel, + getCurrentModelAndProvider, + getFallbackModelAndProvider, + getCurrentModelAndProviderForDisplay, + refreshCurrentModelAndProvider, + ] + ); + + return ( + + {children} + + ); +}; + +export const useModelAndProvider = () => { + const context = useContext(ModelAndProviderContext); + if (context === undefined) { + throw new Error('useModelAndProvider must be used within a ModelAndProviderProvider'); + } + return context; +}; diff --git a/ui/desktop/src/components/ProviderGrid.tsx b/ui/desktop/src/components/ProviderGrid.tsx deleted file mode 100644 index 553bd0a5decf..000000000000 --- a/ui/desktop/src/components/ProviderGrid.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import React from 'react'; -import { - supported_providers, - required_keys, - provider_aliases, -} from './settings/models/hardcoded_stuff'; -import { useActiveKeys } from './settings/api_keys/ActiveKeysContext'; -import { ProviderSetupModal } from './settings/ProviderSetupModal'; -import { useModel } from './settings/models/ModelContext'; -import { useRecentModels } from './settings/models/RecentModels'; -import { createSelectedModel } from './settings/models/utils'; -import { getDefaultModel } from './settings/models/hardcoded_stuff'; -import { initializeSystem } from '../utils/providerUtils'; -import { getApiUrl, getSecretKey } from '../config'; -import { getActiveProviders, isSecretKey } from './settings/api_keys/utils'; -import { BaseProviderGrid, getProviderDescription } from './settings/providers/BaseProviderGrid'; -import { toastError, toastSuccess } from '../toasts'; - -interface ProviderGridProps { - onSubmit?: () => void; -} - -export function ProviderGrid({ onSubmit }: ProviderGridProps) { - const { activeKeys, setActiveKeys } = useActiveKeys(); - const [selectedId, setSelectedId] = React.useState(null); - const [showSetupModal, setShowSetupModal] = React.useState(false); - const { switchModel } = useModel(); - const { addRecentModel } = useRecentModels(); - - const providers = React.useMemo(() => { - return supported_providers.map((providerName) => { - const alias = - provider_aliases.find((p) => p.provider === providerName)?.alias || - providerName.toLowerCase(); - const isConfigured = activeKeys.includes(providerName); - - return { - id: alias, - name: providerName, - isConfigured, - description: getProviderDescription(providerName), - }; - }); - }, [activeKeys]); - - const handleConfigure = async (provider: { - id: string; - name: string; - isConfigured: boolean; - description: string; - }) => { - const providerId = provider.id.toLowerCase(); - - const modelName = getDefaultModel(providerId) || 'default-model'; - const model = createSelectedModel(providerId, modelName); - - await initializeSystem(providerId, model.name); - - switchModel(model); - addRecentModel(model); - localStorage.setItem('GOOSE_PROVIDER', providerId); - - toastSuccess({ - title: provider.name, - msg: `Starting Goose with default model: ${getDefaultModel(provider.name.toLowerCase().replace(/ /g, '_'))}.`, - }); - - onSubmit?.(); - }; - - const handleAddKeys = (provider: { - id: string; - name: string; - isConfigured: boolean; - description: string; - }) => { - setSelectedId(provider.id); - setShowSetupModal(true); - }; - - const handleModalSubmit = async (configValues: { [key: string]: string }) => { - if (!selectedId) return; - - const provider = providers.find((p) => p.id === selectedId)?.name; - if (!provider) return; - - const requiredKeys = required_keys[provider as keyof typeof required_keys]; - if (!requiredKeys || requiredKeys.length === 0) { - console.error(`No keys found for provider ${provider}`); - return; - } - - try { - // Delete existing keys if provider is already configured - const isUpdate = providers.find((p) => p.id === selectedId)?.isConfigured; - if (isUpdate) { - for (const keyName of requiredKeys) { - const isSecret = isSecretKey(keyName); - const deleteResponse = await fetch(getApiUrl('/configs/delete'), { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': getSecretKey(), - }, - body: JSON.stringify({ - key: keyName, - isSecret, - }), - }); - - if (!deleteResponse.ok) { - const errorText = await deleteResponse.text(); - console.error('Delete response error:', errorText); - throw new Error(`Failed to delete old key: ${keyName}`); - } - } - } - - // Store new keys - for (const keyName of requiredKeys) { - const value = configValues[keyName]; - if (!value) { - console.error(`Missing value for required key: ${keyName}`); - continue; - } - - const isSecret = isSecretKey(keyName); - const storeResponse = await fetch(getApiUrl('/configs/store'), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': getSecretKey(), - }, - body: JSON.stringify({ - key: keyName, - value: value, - isSecret, - }), - }); - - if (!storeResponse.ok) { - const errorText = await storeResponse.text(); - console.error('Store response error:', errorText); - throw new Error(`Failed to store new key: ${keyName}`); - } - } - - toastSuccess({ - title: provider, - msg: isUpdate ? `Successfully updated configuration` : `Successfully added configuration`, - }); - - const updatedKeys = await getActiveProviders(); - setActiveKeys(updatedKeys); - - setShowSetupModal(false); - setSelectedId(null); - } catch (err) { - console.error('Error handling modal submit:', err); - const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'; - toastError({ - title: provider, - msg: `Failed to ${providers.find((p) => p.id === selectedId)?.isConfigured ? 'update' : 'add'} configuration`, - traceback: errorMessage, - }); - } - }; - - const handleSelect = (providerId: string) => { - setSelectedId(selectedId === providerId ? null : providerId); - }; - - // Add useEffect for Esc key handling - React.useEffect(() => { - const handleEsc = (event: KeyboardEvent) => { - if (event.key === 'Escape') { - setSelectedId(null); - } - }; - window.addEventListener('keydown', handleEsc); - return () => { - window.removeEventListener('keydown', handleEsc); - }; - }, []); - - return ( -
- { - handleConfigure(provider); - }} - /> - - {showSetupModal && selectedId && ( -
- p.id === selectedId)?.name || 'Unknown Provider'} - _model="Example Model" - _endpoint="Example Endpoint" - onSubmit={handleModalSubmit} - onCancel={() => { - setShowSetupModal(false); - setSelectedId(null); - }} - /> -
- )} -
- ); -} diff --git a/ui/desktop/src/components/RecipeEditor.tsx b/ui/desktop/src/components/RecipeEditor.tsx index 2c8ac36e62c3..676f44bc0575 100644 --- a/ui/desktop/src/components/RecipeEditor.tsx +++ b/ui/desktop/src/components/RecipeEditor.tsx @@ -11,7 +11,6 @@ import RecipeActivityEditor from './RecipeActivityEditor'; import RecipeInfoModal from './RecipeInfoModal'; import RecipeExpandableInfo from './RecipeExpandableInfo'; import { ScheduleFromRecipeModal } from './schedule/ScheduleFromRecipeModal'; -// import ExtensionList from './settings_v2/extensions/subcomponents/ExtensionList'; interface RecipeEditorProps { config?: Recipe; @@ -365,7 +364,14 @@ export default function RecipeEditor({ config }: RecipeEditorProps) { recipe={getCurrentConfig()} onCreateSchedule={(deepLink) => { // Open the schedules view with the deep link pre-filled - window.electron.createChatWindow(undefined, undefined, undefined, undefined, undefined, 'schedules'); + window.electron.createChatWindow( + undefined, + undefined, + undefined, + undefined, + undefined, + 'schedules' + ); // Store the deep link in localStorage for the schedules view to pick up localStorage.setItem('pendingScheduleDeepLink', deepLink); }} diff --git a/ui/desktop/src/components/ToolCallConfirmation.tsx b/ui/desktop/src/components/ToolCallConfirmation.tsx index 606923a31683..cdffaed951f7 100644 --- a/ui/desktop/src/components/ToolCallConfirmation.tsx +++ b/ui/desktop/src/components/ToolCallConfirmation.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { snakeToTitleCase } from '../utils'; -import PermissionModal from './settings_v2/permission/PermissionModal'; +import PermissionModal from './settings/permission/PermissionModal'; import { ChevronRight } from 'lucide-react'; import { confirmPermission } from '../api'; diff --git a/ui/desktop/src/components/WelcomeView.tsx b/ui/desktop/src/components/WelcomeView.tsx deleted file mode 100644 index 2e5779e350c0..000000000000 --- a/ui/desktop/src/components/WelcomeView.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { ProviderGrid } from './ProviderGrid'; -import { ScrollArea } from './ui/scroll-area'; -import { Button } from './ui/button'; -import WelcomeGooseLogo from './WelcomeGooseLogo'; -import MoreMenuLayout from './more_menu/MoreMenuLayout'; - -// Extending React CSSProperties to include custom webkit property -declare module 'react' { - interface CSSProperties { - WebkitAppRegion?: string; - } -} - -interface WelcomeScreenProps { - onSubmit?: () => void; -} - -export default function WelcomeScreen({ onSubmit }: WelcomeScreenProps) { - return ( -
- - - {/* Content area - explicitly set as non-draggable */} -
- -
- {/* Header Section */} -
-
-
- -
-
-

- Welcome to goose -

-

- Your intelligent AI assistant for seamless productivity and creativity. -

-
-
-
- - {/* ProviderGrid */} -
-

- Choose a Provider -

-

- Select an AI model provider to get started with goose. -

-

- Click on a provider to configure its API keys and start using goose. Your keys are - stored securely and encrypted locally. You can change your provider and select - specific models in the settings. -

- -
- - {/* Get started (now less prominent) */} -
-

- Not sure where to start?{' '} - -

-
-
-
-
-
- ); -} diff --git a/ui/desktop/src/components/bottom_menu/BottomMenu.tsx b/ui/desktop/src/components/bottom_menu/BottomMenu.tsx index 43c4714abfed..587670ec07f7 100644 --- a/ui/desktop/src/components/bottom_menu/BottomMenu.tsx +++ b/ui/desktop/src/components/bottom_menu/BottomMenu.tsx @@ -1,13 +1,12 @@ import { useState, useEffect, useRef } from 'react'; -import { useModel } from '../settings/models/ModelContext'; import { AlertType, useAlerts } from '../alerts'; import { useToolCount } from '../alerts/useToolCount'; import BottomMenuAlertPopover from './BottomMenuAlertPopover'; import type { View, ViewOptions } from '../../App'; import { BottomMenuModeSelection } from './BottomMenuModeSelection'; -import ModelsBottomBar from '../settings_v2/models/bottom_bar/ModelsBottomBar'; +import ModelsBottomBar from '../settings/models/bottom_bar/ModelsBottomBar'; import { useConfig } from '../ConfigContext'; -import { getCurrentModelAndProvider } from '../settings_v2/models'; +import { useModelAndProvider } from '../ModelAndProviderContext'; import { Message } from '../../types/message'; import { ManualSummarizeButton } from '../context_management/ManualSummaryButton'; @@ -34,11 +33,11 @@ export default function BottomMenu({ setMessages: (messages: Message[]) => void; }) { const [isModelMenuOpen, setIsModelMenuOpen] = useState(false); - const { currentModel } = useModel(); const { alerts, addAlert, clearAlerts } = useAlerts(); const dropdownRef = useRef(null); const toolCount = useToolCount(); const { getProviders, read } = useConfig(); + const { getCurrentModelAndProvider, currentModel, currentProvider } = useModelAndProvider(); const [tokenLimit, setTokenLimit] = useState(TOKEN_LIMIT_DEFAULT); const [isTokenLimitLoaded, setIsTokenLimitLoaded] = useState(false); @@ -72,7 +71,7 @@ export default function BottomMenu({ setIsTokenLimitLoaded(false); // Get current model and provider first to avoid unnecessary provider fetches - const { model, provider } = await getCurrentModelAndProvider({ readFromConfig: read }); + const { model, provider } = await getCurrentModelAndProvider(); if (!model || !provider) { console.log('No model or provider found'); setIsTokenLimitLoaded(true); @@ -117,7 +116,7 @@ export default function BottomMenu({ useEffect(() => { loadProviderDetails(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentModel]); + }, [currentModel, currentProvider]); // Handle tool count alerts and token usage useEffect(() => { diff --git a/ui/desktop/src/components/bottom_menu/BottomMenuModeSelection.tsx b/ui/desktop/src/components/bottom_menu/BottomMenuModeSelection.tsx index eb9418dc0efa..852420852051 100644 --- a/ui/desktop/src/components/bottom_menu/BottomMenuModeSelection.tsx +++ b/ui/desktop/src/components/bottom_menu/BottomMenuModeSelection.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState, useCallback } from 'react'; -import { all_goose_modes, ModeSelectionItem } from '../settings_v2/mode/ModeSelectionItem'; +import { all_goose_modes, ModeSelectionItem } from '../settings/mode/ModeSelectionItem'; import { useConfig } from '../ConfigContext'; import { View, ViewOptions } from '../../App'; import { Orbit } from 'lucide-react'; diff --git a/ui/desktop/src/components/settings/OllamaBattleGame.tsx b/ui/desktop/src/components/settings/OllamaBattleGame.tsx deleted file mode 100644 index 729329bd3087..000000000000 --- a/ui/desktop/src/components/settings/OllamaBattleGame.tsx +++ /dev/null @@ -1,472 +0,0 @@ -import { useState, useEffect, useRef } from 'react'; - -// Import actual PNG images -import llamaSprite from '../../assets/battle-game/llama.png'; -import gooseSprite from '../../assets/battle-game/goose.png'; -import battleBackground from '../../assets/battle-game/background.png'; -import battleMusic from '../../assets/battle-game/battle.mp3'; - -interface BattleState { - currentStep: number; - gooseHp: number; - llamaHp: number; - message: string; - animation: string | null; - lastChoice?: string; - showHostInput?: boolean; - processingAction?: boolean; -} - -interface OllamaBattleGameProps { - onComplete: (configValues: { [key: string]: string }) => void; - requiredKeys: string[]; -} - -export function OllamaBattleGame({ onComplete, requiredKeys: _ }: OllamaBattleGameProps) { - // Use Audio element type for audioRef - // eslint-disable-next-line no-undef - const audioRef = useRef(null); - const [isMuted, setIsMuted] = useState(false); - - const [battleState, setBattleState] = useState({ - currentStep: 0, - gooseHp: 100, - llamaHp: 100, - message: 'A wild Ollama appeared!', - animation: null, - processingAction: false, - }); - - const [configValues, setConfigValues] = useState<{ [key: string]: string }>({}); - - // Initialize audio when component mounts - useEffect(() => { - if (typeof window !== 'undefined') { - audioRef.current = new window.Audio(battleMusic); - audioRef.current.loop = true; - audioRef.current.volume = 0.2; - audioRef.current.play().catch((e) => console.log('Audio autoplay prevented:', e)); - } - - return () => { - if (audioRef.current) { - audioRef.current.pause(); - audioRef.current = null; - } - }; - }, []); - - const toggleMute = () => { - if (audioRef.current) { - if (isMuted) { - audioRef.current.volume = 0.2; - } else { - audioRef.current.volume = 0; - } - setIsMuted(!isMuted); - } - }; - - const battleSteps = [ - { - message: 'A wild Ollama appeared!', - action: null, - animation: 'appear', - }, - { - message: 'What will GOOSE do?', - action: 'choice', - choices: ['Pacify', 'HONK!'], - animation: 'attack', - followUpMessages: ["It's not very effective...", 'But OLLAMA is confused!'], - }, - { - message: 'OLLAMA used YAML Confusion!', - action: null, - animation: 'counter', - followUpMessages: ['OLLAMA hurt itself in confusion!', 'GOOSE maintained composure!'], - }, - { - message: 'What will GOOSE do?', - action: 'final_choice', - choices: (previousChoice: string) => [ - previousChoice === 'Pacify' ? 'HONK!' : 'Pacify', - 'Configure Host', - ], - animation: 'attack', - }, - { - message: 'OLLAMA used Docker Dependency!', - action: null, - animation: 'counter', - followUpMessages: ["It's not very effective...", 'GOOSE knows containerization!'], - }, - { - message: 'What will GOOSE do?', - action: 'host_choice', - choices: ['Configure Host'], - animation: 'finish', - }, - { - message: '', // Will be set dynamically based on choice - action: 'host_input', - prompt: 'Enter your Ollama host address:', - configKey: 'OLLAMA_HOST', - animation: 'finish', - followUpMessages: [ - "It's super effective!", - 'OLLAMA has been configured!', - 'OLLAMA joined your team!', - ], - }, - { - message: 'Configuration complete!\nOLLAMA will remember this friendship!', - action: 'complete', - }, - ]; - - const animateHit = (isLlama: boolean) => { - const element = document.querySelector(isLlama ? '.llama-sprite' : '.goose-sprite'); - if (element) { - element.classList.add('hit-flash'); - setTimeout(() => { - element.classList.remove('hit-flash'); - }, 500); - } - }; - - useEffect(() => { - // Add CSS for the hit animation and defeat animation - const style = document.createElement('style'); - style.textContent = ` - @keyframes hitFlash { - 0%, 100% { opacity: 1; } - 50% { opacity: 0; } - } - .hit-flash { - animation: hitFlash 0.5s; - } - @keyframes defeat { - 0% { transform: translateY(0); opacity: 1; } - 20% { transform: translateY(-30px); opacity: 1; } - 100% { transform: translateY(500px); opacity: 0; } - } - .defeated { - animation: defeat 1.3s cubic-bezier(.36,.07,.19,.97) both; - } - `; - document.head.appendChild(style); - return () => { - document.head.removeChild(style); - }; - }, []); - - const handleAction = async (value: string) => { - const currentStep = - battleState.currentStep < battleSteps.length ? battleSteps[battleState.currentStep] : null; - - if (!currentStep) return; - - // Handle host input - if (currentStep.action === 'host_input' && value && currentStep.configKey) { - setConfigValues((prev) => ({ - ...prev, - [currentStep.configKey!]: value, - })); - return; - } - - // Handle host submit - if (currentStep.action === 'host_input' && !value) { - setBattleState((prev) => ({ - ...prev, - processingAction: true, - llamaHp: 0, - message: "It's super effective!", - })); - animateHit(true); - - // Add defeat class to llama sprite and health bar - const llamaContainer = document.querySelector('.llama-container'); - if (llamaContainer) { - await new Promise((resolve) => setTimeout(resolve, 500)); - llamaContainer.classList.add('defeated'); - } - - // Show victory messages with delays - if (currentStep.followUpMessages) { - for (const msg of currentStep.followUpMessages) { - await new Promise((resolve) => setTimeout(resolve, 1000)); - setBattleState((prev) => ({ ...prev, message: msg })); - } - } - - await new Promise((resolve) => setTimeout(resolve, 1000)); - onComplete(configValues); - return; - } - - // Handle continue button for messages - if (!currentStep.action) { - setBattleState((prev) => ({ - ...prev, - currentStep: prev.currentStep + 1, - message: battleSteps[prev.currentStep + 1]?.message || prev.message, - processingAction: false, - })); - return; - } - - // Handle choices (Pacify/HONK/Configure Host) - if ( - (currentStep.action === 'choice' || - currentStep.action === 'final_choice' || - currentStep.action === 'host_choice') && - value - ) { - // Set processing flag to hide buttons - setBattleState((prev) => ({ - ...prev, - processingAction: true, - })); - - if (value === 'Configure Host') { - setBattleState((prev) => ({ - ...prev, - message: 'GOOSE used Configure Host!', - showHostInput: true, - currentStep: battleSteps.findIndex((step) => step.action === 'host_input'), - processingAction: false, - })); - return; - } - - // Handle Pacify or HONK attacks - setBattleState((prev) => ({ - ...prev, - lastChoice: value, - llamaHp: Math.max(0, prev.llamaHp - 25), - message: `GOOSE used ${value}!`, - })); - animateHit(true); - - // Show follow-up messages - if (currentStep.followUpMessages) { - for (const msg of currentStep.followUpMessages) { - await new Promise((resolve) => setTimeout(resolve, 1000)); - setBattleState((prev) => ({ ...prev, message: msg })); - } - } - - // Proceed to counter-attack - await new Promise((resolve) => setTimeout(resolve, 1000)); - const isFirstCycle = currentStep.action === 'choice'; - const nextStep = battleSteps[battleState.currentStep + 1]; - setBattleState((prev) => ({ - ...prev, - gooseHp: Math.max(0, prev.gooseHp - 25), - message: isFirstCycle ? 'OLLAMA used YAML Confusion!' : 'OLLAMA used Docker Dependency!', - currentStep: prev.currentStep + 1, - processingAction: false, - })); - animateHit(false); - - // Show counter-attack messages - if (nextStep?.followUpMessages) { - await new Promise((resolve) => setTimeout(resolve, 1000)); - for (const msg of nextStep.followUpMessages) { - await new Promise((resolve) => setTimeout(resolve, 1000)); - setBattleState((prev) => ({ ...prev, message: msg })); - } - } - - return; - } - - // Check for battle completion - if (battleState.currentStep === battleSteps.length - 2) { - onComplete(configValues); - } - }; - - return ( -
- {/* Battle Scene */} -
- {/* Llama sprite */} -
-
-
- OLLAMA - Lv.1 -
-
-
-
50 - ? '#10B981' - : battleState.llamaHp > 20 - ? '#F59E0B' - : '#EF4444', - }} - /> -
- - {Math.floor(battleState.llamaHp)}/100 - -
-
- Llama -
- - {/* Goose sprite */} -
- Goose -
-
- GOOSE - Lv.99 -
-
-
-
50 - ? '#10B981' - : battleState.gooseHp > 20 - ? '#F59E0B' - : '#EF4444', - }} - /> -
- - {Math.floor(battleState.gooseHp)}/100 - -
-
-
-
- - {/* Dialog Box */} -
-
-
- -
-

- {battleState.message} -

- - {battleState.currentStep < battleSteps.length && ( -
- {/* Show battle choices */} - {(battleSteps[battleState.currentStep].action === 'choice' || - battleSteps[battleState.currentStep].action === 'final_choice' || - battleSteps[battleState.currentStep].action === 'host_choice') && - !battleState.showHostInput && - !battleState.processingAction && ( -
- {(typeof battleSteps[battleState.currentStep].choices === 'function' - ? ( - battleSteps[battleState.currentStep].choices as ( - choice: string - ) => string[] - )(battleState.lastChoice || '') - : (battleSteps[battleState.currentStep].choices as string[]) - )?.map((choice: string) => ( - - ))} -
- )} - - {/* Show host input when needed */} - {battleState.showHostInput && !battleState.processingAction && ( -
-

- Enter your Ollama host address: -

-
- handleAction(e.target.value)} - /> - -
-
- )} - - {/* Continue button for messages */} - {!battleSteps[battleState.currentStep].action && !battleState.processingAction && ( - - )} -
- )} -
- - {/* Black corners for that classic Pokemon feel */} -
-
-
-
-
-
- ); -} diff --git a/ui/desktop/src/components/settings/ProviderSetupModal.tsx b/ui/desktop/src/components/settings/ProviderSetupModal.tsx deleted file mode 100644 index 28566e9815c1..000000000000 --- a/ui/desktop/src/components/settings/ProviderSetupModal.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import React from 'react'; -import { Card } from '../ui/card'; -import { Lock } from 'lucide-react'; -import { Input } from '../ui/input'; -import { Button } from '../ui/button'; -import { required_keys, default_key_value } from './models/hardcoded_stuff'; -import { isSecretKey } from './api_keys/utils'; -import { OllamaBattleGame } from './OllamaBattleGame'; - -interface ProviderSetupModalProps { - provider: string; - _model: string; - _endpoint: string; - title?: string; - onSubmit: (configValues: { [key: string]: string }) => void; - onCancel: () => void; - forceBattle?: boolean; -} - -export function ProviderSetupModal({ - provider, - _model: _, - _endpoint: __, - title, - onSubmit, - onCancel, - forceBattle = false, -}: ProviderSetupModalProps) { - const [configValues, setConfigValues] = React.useState<{ [key: string]: string }>( - default_key_value - ); - const requiredKeys = (required_keys as Record)[provider] || ['API Key']; - const headerText = title || `Setup ${provider}`; - - const shouldShowBattle = React.useMemo(() => { - if (forceBattle) return true; - if (provider.toLowerCase() !== 'ollama') return false; - - const now = new Date(); - return now.getMinutes() === 0; - }, [provider, forceBattle]); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - onSubmit(configValues); - }; - - return ( -
- -
- {/* Header */} -
-

{headerText}

-
- - {provider.toLowerCase() === 'ollama' && shouldShowBattle ? ( - - ) : ( -
-
- {requiredKeys.map((keyName: string) => ( -
- - setConfigValues((prev) => ({ - ...prev, - [keyName]: e.target.value, - })) - } - placeholder={keyName} - className="w-full h-14 px-4 font-regular rounded-lg border shadow-none border-gray-300 bg-white text-lg placeholder:text-gray-400 font-regular text-gray-900" - required - /> -
- ))} -
{ - if (provider.toLowerCase() === 'ollama') { - onCancel(); - onSubmit({ forceBattle: 'true' }); - } - }} - > - - {`Your configuration values will be stored securely in the keychain and used only for making requests to ${provider}`} -
-
- - {/* Actions */} -
- - -
-
- )} -
-
-
- ); -} diff --git a/ui/desktop/src/components/settings/SettingsView.tsx b/ui/desktop/src/components/settings/SettingsView.tsx index b0c0c225f318..54d95b92279d 100644 --- a/ui/desktop/src/components/settings/SettingsView.tsx +++ b/ui/desktop/src/components/settings/SettingsView.tsx @@ -1,57 +1,19 @@ -import React, { useState, useEffect } from 'react'; -import { IpcRendererEvent } from 'electron'; import { ScrollArea } from '../ui/scroll-area'; -import { Settings as SettingsType } from './types'; -import { - FullExtensionConfig, - addExtension, - removeExtension, - BUILT_IN_EXTENSIONS, -} from '../../extensions'; -import { ConfigureExtensionModal } from './extensions/ConfigureExtensionModal'; -import { ManualExtensionModal } from './extensions/ManualExtensionModal'; -import { ConfigureBuiltInExtensionModal } from './extensions/ConfigureBuiltInExtensionModal'; import BackButton from '../ui/BackButton'; -import { RecentModelsRadio } from './models/RecentModels'; -import { ExtensionItem } from './extensions/ExtensionItem'; -import { View, ViewOptions } from '../../App'; -import { ModeSelection } from './basic/ModeSelection'; -import SessionSharingSection from './session/SessionSharingSection'; -import { toastSuccess } from '../../toasts'; +import type { View, ViewOptions } from '../../App'; +import ExtensionsSection from './extensions/ExtensionsSection'; +import ModelsSection from './models/ModelsSection'; +import { ModeSection } from './mode/ModeSection'; +import { ToolSelectionStrategySection } from './tool_selection_strategy/ToolSelectionStrategySection'; +import SessionSharingSection from './sessions/SessionSharingSection'; +import { ResponseStylesSection } from './response_styles/ResponseStylesSection'; +import AppSettingsSection from './app/AppSettingsSection'; +import { ExtensionConfig } from '../../api'; import MoreMenuLayout from '../more_menu/MoreMenuLayout'; -const EXTENSIONS_DESCRIPTION = - 'The Model Context Protocol (MCP) is a system that allows AI models to securely connect with local or remote resources using standard server setups. It works like a client-server setup and expands AI capabilities using three main components: Prompts, Resources, and Tools.'; - -const EXTENSIONS_SITE_LINK = 'https://block.github.io/goose/v1/extensions/'; - -const DEFAULT_SETTINGS: SettingsType = { - models: [ - { - id: 'gpt4', - name: 'GPT 4.0', - description: 'Standard config', - enabled: false, - }, - { - id: 'gpt4lite', - name: 'GPT 4.0 lite', - description: 'Standard config', - enabled: false, - }, - { - id: 'claude', - name: 'Claude', - description: 'Standard config', - enabled: true, - }, - ], - extensions: BUILT_IN_EXTENSIONS, -}; - export type SettingsViewOptions = { - extensionId: string; - showEnvVars: boolean; + deepLinkConfig?: ExtensionConfig; + showEnvVars?: boolean; }; export default function SettingsView({ @@ -63,133 +25,8 @@ export default function SettingsView({ setView: (view: View, viewOptions?: ViewOptions) => void; viewOptions: SettingsViewOptions; }) { - const [settings, setSettings] = React.useState(() => { - const saved = localStorage.getItem('user_settings'); - window.electron.logInfo('Settings: ' + saved); - let currentSettings = saved ? JSON.parse(saved) : DEFAULT_SETTINGS; - - // Ensure built-in extensions are included if not already present - BUILT_IN_EXTENSIONS.forEach((builtIn) => { - const exists = currentSettings.extensions.some( - (ext: FullExtensionConfig) => ext.id === builtIn.id - ); - if (!exists) { - currentSettings.extensions.push(builtIn); - } - }); - - return currentSettings; - }); - - const [extensionBeingConfigured, setExtensionBeingConfigured] = - useState(null); - - const [isManualModalOpen, setIsManualModalOpen] = useState(false); - - // Persist settings changes - useEffect(() => { - localStorage.setItem('user_settings', JSON.stringify(settings)); - }, [settings]); - - // Listen for settings updates from extension storage - useEffect(() => { - const handleSettingsUpdate = (_event: IpcRendererEvent) => { - const saved = localStorage.getItem('user_settings'); - if (saved) { - let currentSettings = JSON.parse(saved); - setSettings(currentSettings); - } - }; - - window.electron.on('settings-updated', handleSettingsUpdate); - return () => { - window.electron.off('settings-updated', handleSettingsUpdate); - }; - }, []); - - // Handle URL parameters for auto-opening extension configuration - useEffect(() => { - const extensionId = viewOptions.extensionId; - const showEnvVars = viewOptions.showEnvVars; - - if (extensionId && showEnvVars === true) { - // Find the extension in settings - const extension = settings.extensions.find((ext) => ext.id === extensionId); - if (extension) { - // Auto-open the configuration modal - setExtensionBeingConfigured(extension); - // Scroll to extensions section - const element = document.getElementById('extensions'); - if (element) { - element.scrollIntoView({ behavior: 'smooth' }); - } - } - } - // We only run this once on load - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [settings.extensions]); - - const handleExtensionToggle = async (extensionId: string) => { - // Find the extension to get its current state - const extension = settings.extensions.find((ext) => ext.id === extensionId); - - if (!extension) return; - - const newEnabled = !extension.enabled; - - const originalSettings = settings; - - // Optimistically update local component state - setSettings((prev) => ({ - ...prev, - extensions: prev.extensions.map((ext) => - ext.id === extensionId ? { ...ext, enabled: newEnabled } : ext - ), - })); - - let response: Response; - - if (newEnabled) { - response = await addExtension(extension); - } else { - response = await removeExtension(extension.name); - } - - if (!response.ok) { - setSettings(originalSettings); - } - }; - - const handleExtensionRemove = async () => { - if (!extensionBeingConfigured) return; - - const response = await removeExtension(extensionBeingConfigured.name, true); - - if (response.ok) { - toastSuccess({ - title: extensionBeingConfigured.name, - msg: `Successfully removed extension`, - }); - - // Remove from localstorage - setSettings((prev) => ({ - ...prev, - extensions: prev.extensions.filter((ext) => ext.id !== extensionBeingConfigured.id), - })); - setExtensionBeingConfigured(null); - } - }; - - const handleExtensionConfigSubmit = () => { - setExtensionBeingConfigured(null); - }; - - const isBuiltIn = (extensionId: string) => { - return BUILT_IN_EXTENSIONS.some((builtIn) => builtIn.id === extensionId); - }; - return ( -
+
@@ -200,114 +37,29 @@ export default function SettingsView({
{/* Content Area */} -
+
- {/*Models Section*/} -
- -
-
-
-

Extensions

- - Browse - -
- -
-

{EXTENSIONS_DESCRIPTION}

- - {settings.extensions.length === 0 ? ( -

No Extensions Added

- ) : ( -
- {settings.extensions.map((ext) => ( - setExtensionBeingConfigured(extension)} - /> - ))} - -
- )} -
-
- -
-
-

Mode Selection

-
- -
-

- Configure how Goose interacts with tools and extensions -

- - -
-
-
- -
+ {/* Models Section */} + + {/* Extensions Section */} + + {/* Goose Modes */} + + {/*Session sharing*/} + + {/* Response Styles */} + + {/* Tool Selection Strategy */} + + {/* App Settings */} +
- - {extensionBeingConfigured && isBuiltIn(extensionBeingConfigured.id) ? ( - { - setExtensionBeingConfigured(null); - }} - extension={extensionBeingConfigured} - onSubmit={handleExtensionConfigSubmit} - /> - ) : ( - { - setExtensionBeingConfigured(null); - }} - extension={extensionBeingConfigured} - onSubmit={handleExtensionConfigSubmit} - onRemove={handleExtensionRemove} - /> - )} - - setIsManualModalOpen(false)} - onSubmit={async (extension) => { - const response = await addExtension(extension); - - if (response.ok) { - setSettings((prev) => ({ - ...prev, - extensions: [...prev.extensions, extension], - })); - setIsManualModalOpen(false); - } else { - // TODO - Anything for the UI state beyond validation? - } - }} - />
); } diff --git a/ui/desktop/src/components/settings/api_keys/ActiveKeysContext.tsx b/ui/desktop/src/components/settings/api_keys/ActiveKeysContext.tsx deleted file mode 100644 index 7533352de426..000000000000 --- a/ui/desktop/src/components/settings/api_keys/ActiveKeysContext.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { createContext, useContext, useState, ReactNode, useEffect } from 'react'; -import { getActiveProviders } from './utils'; -import SuspenseLoader from '../../../suspense-loader'; - -// Create a context for active keys -const ActiveKeysContext = createContext< - | { - activeKeys: string[]; - setActiveKeys: (keys: string[]) => void; - } - | undefined ->(undefined); - -export const ActiveKeysProvider = ({ children }: { children: ReactNode }) => { - const [activeKeys, setActiveKeys] = useState([]); // Start with an empty list - const [isLoading, setIsLoading] = useState(true); // Track loading state - - // Fetch active keys from the backend - useEffect(() => { - const fetchActiveProviders = async () => { - try { - const providers = await getActiveProviders(); // Fetch the active providers - setActiveKeys(providers); // Update state with fetched providers - } catch (error) { - console.error('Error fetching active providers:', error); - } finally { - setIsLoading(false); // Ensure loading is marked as complete - } - }; - - fetchActiveProviders(); // Call the async function - }, []); - - // Provide active keys and ability to update them - return ( - - {!isLoading ? children : } - - ); -}; - -// Custom hook to access active keys -export const useActiveKeys = () => { - const context = useContext(ActiveKeysContext); - if (!context) { - throw new Error('useActiveKeys must be used within an ActiveKeysProvider'); - } - return context; -}; diff --git a/ui/desktop/src/components/settings/api_keys/types.ts b/ui/desktop/src/components/settings/api_keys/types.ts deleted file mode 100644 index 9d8b22451014..000000000000 --- a/ui/desktop/src/components/settings/api_keys/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface ProviderResponse { - supported: boolean; - name?: string; - description?: string; - models?: string[]; - config_status: Record; -} - -export interface ConfigDetails { - key: string; - is_set: boolean; - location?: string; -} diff --git a/ui/desktop/src/components/settings/api_keys/utils.tsx b/ui/desktop/src/components/settings/api_keys/utils.tsx deleted file mode 100644 index 1f7d9d1536fe..000000000000 --- a/ui/desktop/src/components/settings/api_keys/utils.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { ProviderResponse, ConfigDetails } from './types'; -import { getApiUrl, getSecretKey } from '../../../config'; -import { default_key_value, required_keys } from '../models/hardcoded_stuff'; // e.g. { OPENAI_HOST: '', OLLAMA_HOST: '' } - -// Backend API response types -interface ProviderMetadata { - description: string; - models: string[]; -} - -interface ProviderDetails { - name: string; - metadata: ProviderMetadata; - is_configured: boolean; -} - -export function isSecretKey(keyName: string): boolean { - // Endpoints and hosts should not be stored as secrets - const nonSecretKeys = [ - 'ANTHROPIC_HOST', - 'DATABRICKS_HOST', - 'OLLAMA_HOST', - 'OPENAI_HOST', - 'OPENAI_BASE_PATH', - 'AZURE_OPENAI_ENDPOINT', - 'AZURE_OPENAI_DEPLOYMENT_NAME', - 'AZURE_OPENAI_API_VERSION', - 'GCP_PROJECT_ID', - 'GCP_LOCATION', - ]; - return !nonSecretKeys.includes(keyName); -} - -export async function getActiveProviders(): Promise { - try { - const configSettings = await getConfigSettings(); - const activeProviders = Object.values(configSettings) - .filter((provider) => { - const providerName = provider.name; - const configStatus = provider.config_status ?? {}; - - // Skip if provider isn't in required_keys - if (!required_keys[providerName as keyof typeof required_keys]) return false; - - // Get all required keys for this provider - const providerRequiredKeys = required_keys[providerName as keyof typeof required_keys]; - - // Special case: If a provider has exactly one required key and that key - // has a default value, check if it's explicitly set - if (providerRequiredKeys.length === 1 && providerRequiredKeys[0] in default_key_value) { - const key = providerRequiredKeys[0]; - // Only consider active if the key is explicitly set - return configStatus[key]?.is_set === true; - } - - // For providers with multiple keys or keys without defaults: - // Check if all required keys without defaults are set - const requiredNonDefaultKeys = providerRequiredKeys.filter( - (key: string) => !(key in default_key_value) - ); - - // If there are no non-default keys, this provider needs at least one key explicitly set - if (requiredNonDefaultKeys.length === 0) { - return providerRequiredKeys.some((key: string) => configStatus[key]?.is_set === true); - } - - // Otherwise, all non-default keys must be set - return requiredNonDefaultKeys.every((key: string) => configStatus[key]?.is_set === true); - }) - .map((provider) => provider.name || 'Unknown Provider'); - - console.log('[GET ACTIVE PROVIDERS]:', activeProviders); - return activeProviders; - } catch (error) { - console.error('Failed to get active providers:', error); - return []; - } -} - -export async function getConfigSettings(): Promise> { - // Fetch provider config status - const response = await fetch(getApiUrl('/config/providers'), { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': getSecretKey(), - }, - }); - - if (!response.ok) { - throw new Error('Failed to fetch provider configuration status'); - } - - const providers: ProviderDetails[] = await response.json(); - - // Convert the response to the expected format - const data: Record = {}; - providers.forEach((provider) => { - const providerRequiredKeys = required_keys[provider.name as keyof typeof required_keys] || []; - - data[provider.name] = { - name: provider.name, - supported: true, - description: provider.metadata.description, - models: provider.metadata.models, - config_status: providerRequiredKeys.reduce>( - (acc: Record, key: string) => { - acc[key] = { - key, - is_set: provider.is_configured, - location: provider.is_configured ? 'config' : undefined, - }; - return acc; - }, - {} - ), - }; - }); - - return data; -} diff --git a/ui/desktop/src/components/settings_v2/app/AppSettingsSection.tsx b/ui/desktop/src/components/settings/app/AppSettingsSection.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/app/AppSettingsSection.tsx rename to ui/desktop/src/components/settings/app/AppSettingsSection.tsx diff --git a/ui/desktop/src/components/settings/basic/ConfigureApproveMode.tsx b/ui/desktop/src/components/settings/basic/ConfigureApproveMode.tsx deleted file mode 100644 index a7b61e2b3788..000000000000 --- a/ui/desktop/src/components/settings/basic/ConfigureApproveMode.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Card } from '../../ui/card'; -import { Button } from '../../ui/button'; -import { GooseMode, ModeSelectionItem } from './ModeSelectionItem'; - -interface ConfigureApproveModeProps { - onClose: () => void; - handleModeChange: (newMode: string) => void; - currentMode: string | null; -} - -export function ConfigureApproveMode({ - onClose, - handleModeChange, - currentMode, -}: ConfigureApproveModeProps) { - const approveModes: GooseMode[] = [ - { - key: 'approve', - label: 'Manual Approval', - description: 'All tools, extensions and file modifications will require human approval', - }, - { - key: 'smart_approve', - label: 'Smart Approval', - description: 'Intelligently determine which actions need approval based on risk level ', - }, - ]; - - const [isSubmitting, setIsSubmitting] = useState(false); - const [approveMode, setApproveMode] = useState(currentMode); - - useEffect(() => { - setApproveMode(currentMode); - }, [currentMode]); - - const handleModeSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - setIsSubmitting(true); - try { - handleModeChange(approveMode || ''); - onClose(); - } catch (error) { - console.error('Error configuring goose mode:', error); - } finally { - setIsSubmitting(false); - } - }; - - return ( -
- -
-
- {/* Header */} -
-

- Configure Approve Mode -

-
- -
-

- Approve requests can either be given to all tool requests or determine which actions - may need integration -

-
- {approveModes.map((mode) => ( - { - setApproveMode(newMode); - }} - /> - ))} -
-
-
-
- - {/* Actions */} -
- - -
-
-
- ); -} diff --git a/ui/desktop/src/components/settings/basic/ModeSelection.tsx b/ui/desktop/src/components/settings/basic/ModeSelection.tsx deleted file mode 100644 index b38aec1266af..000000000000 --- a/ui/desktop/src/components/settings/basic/ModeSelection.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { 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) => { - 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}`); - } - }; - - 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 ( -
-
- {filterGooseModes(currentMode, all_goose_modes, previousApproveModel).map((mode) => ( - - ))} -
-
- ); -}; diff --git a/ui/desktop/src/components/settings/basic/ModeSelectionItem.tsx b/ui/desktop/src/components/settings/basic/ModeSelectionItem.tsx deleted file mode 100644 index d52ca600026a..000000000000 --- a/ui/desktop/src/components/settings/basic/ModeSelectionItem.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { useEffect, useState } from 'react'; -import { Gear } from '../../icons'; -import { ConfigureApproveMode } from './ConfigureApproveMode'; - -export interface GooseMode { - key: string; - label: string; - description: string; -} - -export const all_goose_modes: GooseMode[] = [ - { - key: 'auto', - label: 'Autonomous', - description: 'Full file modification capabilities, edit, create, and delete files freely.', - }, - { - key: 'approve', - label: 'Manual Approval', - description: 'All tools, extensions and file modifications will require human approval', - }, - { - key: 'smart_approve', - label: 'Smart Approval', - description: 'Intelligently determine which actions need approval based on risk level ', - }, - { - key: 'chat', - label: 'Chat Only', - description: 'Engage with the selected provider without using tools or extensions.', - }, -]; - -export function filterGooseModes( - currentMode: string, - modes: GooseMode[], - previousApproveMode: string -) { - return modes.filter((mode) => { - const approveList = ['approve', 'smart_approve']; - const nonApproveList = ['auto', 'chat']; - // Always keep 'auto' and 'chat' - if (nonApproveList.includes(mode.key)) { - return true; - } - // If current mode is non approve mode, we display write approve by default. - if (nonApproveList.includes(currentMode) && !previousApproveMode) { - return mode.key === 'smart_approve'; - } - - // Always include the current and previou approve mode - if (mode.key === currentMode) { - return true; - } - - // Current mode and previous approve mode cannot exist at the same time. - if (approveList.includes(currentMode) && approveList.includes(previousApproveMode)) { - return false; - } - - if (mode.key === previousApproveMode) { - return true; - } - - return false; - }); -} - -interface ModeSelectionItemProps { - currentMode: string; - mode: GooseMode; - showDescription: boolean; - isApproveModeConfigure: boolean; - handleModeChange: (newMode: string) => void; -} - -export function ModeSelectionItem({ - currentMode, - mode, - showDescription, - isApproveModeConfigure, - handleModeChange, -}: ModeSelectionItemProps) { - const [checked, setChecked] = useState(currentMode == mode.key); - const [isDislogOpen, setIsDislogOpen] = useState(false); - - useEffect(() => { - setChecked(currentMode === mode.key); - }, [currentMode, mode.key]); - - return ( -
-
handleModeChange(mode.key)}> -
-
-

{mode.label}

- {showDescription && ( -

{mode.description}

- )} -
-
-
- {!isApproveModeConfigure && (mode.key == 'approve' || mode.key == 'smart_approve') && ( - - )} - handleModeChange(mode.key)} - className="peer sr-only" - /> -
-
-
-
-
- {isDislogOpen ? ( - { - setIsDislogOpen(false); - }} - handleModeChange={handleModeChange} - currentMode={currentMode} - /> - ) : null} -
-
-
- ); -} diff --git a/ui/desktop/src/components/settings/extensions/ConfigureBuiltInExtensionModal.tsx b/ui/desktop/src/components/settings/extensions/ConfigureBuiltInExtensionModal.tsx deleted file mode 100644 index a8af5dcdafc3..000000000000 --- a/ui/desktop/src/components/settings/extensions/ConfigureBuiltInExtensionModal.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import React from 'react'; -import { Card } from '../../ui/card'; -import { Button } from '../../ui/button'; -import { Input } from '../../ui/input'; -import { FullExtensionConfig } from '../../../extensions'; -import { getApiUrl, getSecretKey } from '../../../config'; -import { addExtension } from '../../../extensions'; -import { toastError, toastSuccess } from '../../../toasts'; - -interface ConfigureExtensionModalProps { - isOpen: boolean; - onClose: () => void; - onSubmit: () => void; - extension: FullExtensionConfig | null; -} - -export function ConfigureBuiltInExtensionModal({ - isOpen, - onClose, - onSubmit, - extension, -}: ConfigureExtensionModalProps) { - const [envValues, setEnvValues] = React.useState>({}); - const [isSubmitting, setIsSubmitting] = React.useState(false); - - // Reset form when dialog closes or extension changes - React.useEffect(() => { - if (!isOpen || !extension) { - setEnvValues({}); - } - }, [isOpen, extension]); - - const handleExtensionConfigSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - if (!extension) return; - - setIsSubmitting(true); - try { - // First store all environment variables - if (extension.env_keys && extension.env_keys.length > 0) { - for (const envKey of extension.env_keys) { - const value = envValues[envKey]; - if (!value) continue; - - const storeResponse = await fetch(getApiUrl('/configs/store'), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': getSecretKey(), - }, - body: JSON.stringify({ - key: envKey, - value: value.trim(), - isSecret: true, - }), - }); - - if (!storeResponse.ok) { - throw new Error(`Failed to store environment variable: ${envKey}`); - } - } - } - - const response = await addExtension(extension); - - if (!response.ok) { - throw new Error('Failed to add system configuration'); - } - - toastSuccess({ - title: extension.name, - msg: `Successfully configured extension`, - }); - onSubmit(); - onClose(); - } catch (err) { - console.error('Error configuring extension:', err); - const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'; - toastError({ - title: extension.name, - msg: `Failed to configure the extension`, - traceback: errorMessage, - }); - } finally { - setIsSubmitting(false); - } - }; - - if (!extension || !isOpen) return null; - - return ( -
- -
- {/* Header */} -
-

- Configure {extension.name} -

-
- - {/* Form */} -
-
- {extension.env_keys && extension.env_keys.length > 0 ? ( - <> -

- Please provide the required environment variables for this extension: -

-
- {extension.env_keys.map((envVarName) => ( -
- - - setEnvValues((prev) => ({ - ...prev, - [envVarName]: e.target.value, - })) - } - className="w-full h-14 px-4 font-regular rounded-lg border shadow-none border-gray-300 bg-white text-lg placeholder:text-gray-400 font-regular text-gray-900 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:placeholder:text-gray-500" - required - /> -
- ))} -
- - ) : ( -

- This extension doesn't require any environment variables. -

- )} -
- - {/* Actions */} -
- - -
-
-
-
-
- ); -} diff --git a/ui/desktop/src/components/settings/extensions/ConfigureExtensionModal.tsx b/ui/desktop/src/components/settings/extensions/ConfigureExtensionModal.tsx deleted file mode 100644 index 56e929b9e9cc..000000000000 --- a/ui/desktop/src/components/settings/extensions/ConfigureExtensionModal.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import React from 'react'; -import { Card } from '../../ui/card'; -import { Button } from '../../ui/button'; -import { Input } from '../../ui/input'; -import { FullExtensionConfig } from '../../../extensions'; -import { getApiUrl, getSecretKey } from '../../../config'; -import { addExtension } from '../../../extensions'; -import { toastError, toastSuccess } from '../../../toasts'; - -interface ConfigureExtensionModalProps { - isOpen: boolean; - onClose: () => void; - onSubmit: () => void; - onRemove: () => void; - extension: FullExtensionConfig | null; -} - -export function ConfigureExtensionModal({ - isOpen, - onClose, - onSubmit, - onRemove, - extension, -}: ConfigureExtensionModalProps) { - const [envValues, setEnvValues] = React.useState>({}); - const [isSubmitting, setIsSubmitting] = React.useState(false); - - // Reset form when dialog closes or extension changes - React.useEffect(() => { - if (!isOpen || !extension) { - setEnvValues({}); - } - }, [isOpen, extension]); - - const handleExtensionConfigSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - if (!extension) return; - - setIsSubmitting(true); - try { - // First store all environment variables - if (extension.env_keys && extension.env_keys.length > 0) { - for (const envKey of extension.env_keys) { - const value = envValues[envKey]; - if (!value) continue; - - const storeResponse = await fetch(getApiUrl('/configs/store'), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': getSecretKey(), - }, - body: JSON.stringify({ - key: envKey, - value: value.trim(), - isSecret: true, - }), - }); - - if (!storeResponse.ok) { - throw new Error(`Failed to store environment variable: ${envKey}`); - } - } - } - - const response = await addExtension(extension); - - if (!response.ok) { - throw new Error('Failed to add system configuration'); - } - - toastSuccess({ - title: extension.name, - msg: `Successfully configured extension`, - }); - onSubmit(); - onClose(); - } catch (err) { - console.error('Error configuring extension:', err); - const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'; - toastError({ - title: extension.name, - msg: `Failed to configure extension`, - traceback: errorMessage, - }); - } finally { - setIsSubmitting(false); - } - }; - - if (!extension || !isOpen) return null; - - return ( -
- -
- {/* Header */} -
-

- Configure {extension.name} -

-
- - {/* Form */} -
-
- {extension.env_keys && extension.env_keys.length > 0 ? ( - <> -

- Please provide the required environment variables for this extension: -

-
- {extension.env_keys.map((envVarName) => ( -
- - - setEnvValues((prev) => ({ - ...prev, - [envVarName]: e.target.value, - })) - } - className="w-full h-14 px-4 font-regular rounded-lg border shadow-none border-gray-300 bg-white text-lg placeholder:text-gray-400 font-regular text-gray-900 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:placeholder:text-gray-500" - required - /> -
- ))} -
- - ) : ( -

- This extension doesn't require any environment variables. -

- )} -
- - {/* Actions */} -
- - - -
-
-
-
-
- ); -} diff --git a/ui/desktop/src/components/settings/extensions/ExtensionItem.tsx b/ui/desktop/src/components/settings/extensions/ExtensionItem.tsx deleted file mode 100644 index 29b701f7cd5b..000000000000 --- a/ui/desktop/src/components/settings/extensions/ExtensionItem.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import { FullExtensionConfig } from '../../../extensions'; -import { Gear } from '../../icons'; - -type ExtensionItemProps = FullExtensionConfig & { - onToggle: (id: string) => void; - onConfigure: (extension: FullExtensionConfig) => void; - canConfigure?: boolean; // Added optional prop here -}; - -export const ExtensionItem: React.FC = (props) => { - const { id, name, description, enabled, onToggle, onConfigure, canConfigure } = props; - - return ( -
-
-
-
-

{name}

-
-

{description}

-
-
- {canConfigure && ( // Conditionally render the gear icon - - )} - -
-
-
- ); -}; diff --git a/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx b/ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx rename to ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx diff --git a/ui/desktop/src/components/settings/extensions/ManualExtensionModal.tsx b/ui/desktop/src/components/settings/extensions/ManualExtensionModal.tsx deleted file mode 100644 index ab5fcf99477f..000000000000 --- a/ui/desktop/src/components/settings/extensions/ManualExtensionModal.tsx +++ /dev/null @@ -1,315 +0,0 @@ -import React, { useState } from 'react'; -import { Card } from '../../ui/card'; -import { Button } from '../../ui/button'; -import { Input } from '../../ui/input'; -import { FullExtensionConfig, DEFAULT_EXTENSION_TIMEOUT } from '../../../extensions'; -import { Select } from '../../ui/Select'; -import { getApiUrl, getSecretKey } from '../../../config'; -import { toastError } from '../../../toasts'; - -interface ManualExtensionModalProps { - isOpen: boolean; - onClose: () => void; - onSubmit: (extension: FullExtensionConfig) => void; -} - -export function ManualExtensionModal({ isOpen, onClose, onSubmit }: ManualExtensionModalProps) { - const [formData, setFormData] = useState< - Partial & { commandInput?: string } - >({ - type: 'stdio', - enabled: true, - args: [], - commandInput: '', - timeout: DEFAULT_EXTENSION_TIMEOUT, - }); - const [envKey, setEnvKey] = useState(''); - const [envValue, setEnvValue] = useState(''); - const [envVars, setEnvVars] = useState>([]); - - const typeOptions = [ - { value: 'stdio', label: 'Standard IO' }, - { value: 'sse', label: 'Server-Sent Events' }, - { value: 'builtin', label: 'Built-in' }, - ]; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - if (!formData.id || !formData.name || !formData.description) { - toastError({ title: 'Please fill in all required fields' }); - return; - } - - if (formData.type === 'stdio' && !formData.commandInput) { - toastError({ title: 'Command is required for stdio type' }); - return; - } - - if (formData.type === 'sse' && !formData.uri) { - toastError({ title: 'URI is required for SSE type' }); - return; - } - - if (formData.type === 'builtin' && !formData.name) { - toastError({ title: 'Name is required for builtin type' }); - return; - } - - try { - // Store environment variables as secrets - for (const envVar of envVars) { - const storeResponse = await fetch(getApiUrl('/configs/store'), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': getSecretKey(), - }, - body: JSON.stringify({ - key: envVar.key, - value: envVar.value.trim(), - isSecret: true, - }), - }); - - if (!storeResponse.ok) { - throw new Error(`Failed to store environment variable: ${envVar.key}`); - } - } - - // Parse command input into cmd and args - let cmd = ''; - let args: string[] = []; - if (formData.type === 'stdio' && formData.commandInput) { - const parts = formData.commandInput.trim().split(/\s+/); - [cmd, ...args] = parts; - } - - const extension: FullExtensionConfig = { - ...formData, - type: formData.type!, - enabled: true, - env_keys: envVars.map((v) => v.key), - ...(formData.type === 'stdio' && { cmd, args }), - } as FullExtensionConfig; - - onSubmit(extension); - resetForm(); - } catch (error) { - console.error('Error configuring extension:', error); - toastError({ - title: 'Failed to configure extension', - traceback: error instanceof Error ? error.message : String(error), - }); - } - }; - - const resetForm = () => { - setFormData({ - type: 'stdio', - enabled: true, - args: [], - commandInput: '', - }); - setEnvVars([]); - setEnvKey(''); - setEnvValue(''); - }; - - const handleAddEnvVar = () => { - if (envKey && !envVars.some((v) => v.key === envKey)) { - setEnvVars([...envVars, { key: envKey, value: envValue }]); - setEnvKey(''); - setEnvValue(''); - } - }; - - const handleRemoveEnvVar = (key: string) => { - setEnvVars(envVars.filter((v) => v.key !== key)); - }; - - if (!isOpen) return null; - - return ( -
- -
-
-

Add custom extension

-
- -
-
-
- - setFormData({ ...formData, id: e.target.value })} - className="w-full" - required - /> -
- -
- - setFormData({ ...formData, name: e.target.value })} - className="w-full" - required - /> -
- -
- - setFormData({ ...formData, description: e.target.value })} - className="w-full" - required - /> -
- - {formData.type === 'stdio' && ( -
- - setFormData({ ...formData, commandInput: e.target.value })} - placeholder="e.g. goosed mcp example" - className="w-full" - required - /> -
- )} - - {formData.type === 'sse' && ( -
- - setFormData({ ...formData, uri: e.target.value })} - className="w-full" - required - /> -
- )} - -
- -
- setEnvKey(e.target.value)} - placeholder="Environment variable name" - className="flex-1" - /> - setEnvValue(e.target.value)} - placeholder="Value" - className="flex-1" - /> - - -
- {envVars.length > 0 && ( -
- {envVars.map((envVar) => ( -
-
-
- {envVar.key} - - = {envVar.value} - -
-
-
- -
-
- ))} -
- )} -
- -
- - setFormData({ ...formData, timeout: parseInt(e.target.value) })} - className="w-full" - required - /> -
-
-
- - -
-
-
-
-
- ); -} diff --git a/ui/desktop/src/components/settings_v2/extensions/agent-api.ts b/ui/desktop/src/components/settings/extensions/agent-api.ts similarity index 100% rename from ui/desktop/src/components/settings_v2/extensions/agent-api.ts rename to ui/desktop/src/components/settings/extensions/agent-api.ts diff --git a/ui/desktop/src/components/settings_v2/extensions/bundled-extensions.json b/ui/desktop/src/components/settings/extensions/bundled-extensions.json similarity index 100% rename from ui/desktop/src/components/settings_v2/extensions/bundled-extensions.json rename to ui/desktop/src/components/settings/extensions/bundled-extensions.json diff --git a/ui/desktop/src/components/settings_v2/extensions/bundled-extensions.ts b/ui/desktop/src/components/settings/extensions/bundled-extensions.ts similarity index 100% rename from ui/desktop/src/components/settings_v2/extensions/bundled-extensions.ts rename to ui/desktop/src/components/settings/extensions/bundled-extensions.ts diff --git a/ui/desktop/src/components/settings_v2/extensions/deeplink.ts b/ui/desktop/src/components/settings/extensions/deeplink.ts similarity index 100% rename from ui/desktop/src/components/settings_v2/extensions/deeplink.ts rename to ui/desktop/src/components/settings/extensions/deeplink.ts diff --git a/ui/desktop/src/components/settings_v2/extensions/extension-manager.ts b/ui/desktop/src/components/settings/extensions/extension-manager.ts similarity index 100% rename from ui/desktop/src/components/settings_v2/extensions/extension-manager.ts rename to ui/desktop/src/components/settings/extensions/extension-manager.ts diff --git a/ui/desktop/src/components/settings_v2/extensions/index.ts b/ui/desktop/src/components/settings/extensions/index.ts similarity index 100% rename from ui/desktop/src/components/settings_v2/extensions/index.ts rename to ui/desktop/src/components/settings/extensions/index.ts diff --git a/ui/desktop/src/components/settings_v2/extensions/modal/EnvVarsSection.tsx b/ui/desktop/src/components/settings/extensions/modal/EnvVarsSection.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/extensions/modal/EnvVarsSection.tsx rename to ui/desktop/src/components/settings/extensions/modal/EnvVarsSection.tsx diff --git a/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionConfigFields.tsx b/ui/desktop/src/components/settings/extensions/modal/ExtensionConfigFields.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/extensions/modal/ExtensionConfigFields.tsx rename to ui/desktop/src/components/settings/extensions/modal/ExtensionConfigFields.tsx diff --git a/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionInfoFields.tsx b/ui/desktop/src/components/settings/extensions/modal/ExtensionInfoFields.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/extensions/modal/ExtensionInfoFields.tsx rename to ui/desktop/src/components/settings/extensions/modal/ExtensionInfoFields.tsx diff --git a/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionModal.tsx b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/extensions/modal/ExtensionModal.tsx rename to ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx diff --git a/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionTimeoutField.tsx b/ui/desktop/src/components/settings/extensions/modal/ExtensionTimeoutField.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/extensions/modal/ExtensionTimeoutField.tsx rename to ui/desktop/src/components/settings/extensions/modal/ExtensionTimeoutField.tsx diff --git a/ui/desktop/src/components/settings_v2/extensions/subcomponents/ExtensionItem.tsx b/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionItem.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/extensions/subcomponents/ExtensionItem.tsx rename to ui/desktop/src/components/settings/extensions/subcomponents/ExtensionItem.tsx diff --git a/ui/desktop/src/components/settings_v2/extensions/subcomponents/ExtensionList.tsx b/ui/desktop/src/components/settings/extensions/subcomponents/ExtensionList.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/extensions/subcomponents/ExtensionList.tsx rename to ui/desktop/src/components/settings/extensions/subcomponents/ExtensionList.tsx diff --git a/ui/desktop/src/components/settings_v2/extensions/utils.ts b/ui/desktop/src/components/settings/extensions/utils.ts similarity index 91% rename from ui/desktop/src/components/settings_v2/extensions/utils.ts rename to ui/desktop/src/components/settings/extensions/utils.ts index 87e3e4f275fa..b7205cd9ec4e 100644 --- a/ui/desktop/src/components/settings_v2/extensions/utils.ts +++ b/ui/desktop/src/components/settings/extensions/utils.ts @@ -184,3 +184,18 @@ export function removeShims(cmd: string) { // If it's not a shim, return the original command return cmd; } + +export function extractCommand(link: string): string { + const url = new URL(link); + const cmd = url.searchParams.get('cmd') || 'Unknown Command'; + const args = url.searchParams.getAll('arg').map(decodeURIComponent); + + // Combine the command and its arguments into a reviewable format + return `${cmd} ${args.join(' ')}`.trim(); +} + +export function extractExtensionName(link: string): string { + const url = new URL(link); + const name = url.searchParams.get('name'); + return name ? decodeURIComponent(name) : 'Unknown Extension'; +} diff --git a/ui/desktop/src/components/settings/extensions/utils.tsx b/ui/desktop/src/components/settings/extensions/utils.tsx deleted file mode 100644 index 39603d11baf5..000000000000 --- a/ui/desktop/src/components/settings/extensions/utils.tsx +++ /dev/null @@ -1,14 +0,0 @@ -export function extractCommand(link: string): string { - const url = new URL(link); - const cmd = url.searchParams.get('cmd') || 'Unknown Command'; - const args = url.searchParams.getAll('arg').map(decodeURIComponent); - - // Combine the command and its arguments into a reviewable format - return `${cmd} ${args.join(' ')}`.trim(); -} - -export function extractExtensionName(link: string): string { - const url = new URL(link); - const name = url.searchParams.get('name'); - return name ? decodeURIComponent(name) : 'Unknown Extension'; -} diff --git a/ui/desktop/src/components/settings_v2/mode/ConfigureApproveMode.tsx b/ui/desktop/src/components/settings/mode/ConfigureApproveMode.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/mode/ConfigureApproveMode.tsx rename to ui/desktop/src/components/settings/mode/ConfigureApproveMode.tsx diff --git a/ui/desktop/src/components/settings_v2/mode/ModeSection.tsx b/ui/desktop/src/components/settings/mode/ModeSection.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/mode/ModeSection.tsx rename to ui/desktop/src/components/settings/mode/ModeSection.tsx diff --git a/ui/desktop/src/components/settings_v2/mode/ModeSelectionItem.tsx b/ui/desktop/src/components/settings/mode/ModeSelectionItem.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/mode/ModeSelectionItem.tsx rename to ui/desktop/src/components/settings/mode/ModeSelectionItem.tsx diff --git a/ui/desktop/src/components/settings/models/AddModelInline.tsx b/ui/desktop/src/components/settings/models/AddModelInline.tsx deleted file mode 100644 index f9c474133478..000000000000 --- a/ui/desktop/src/components/settings/models/AddModelInline.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { useState, useEffect } from 'react'; -import { Button } from '../../ui/button'; -import { Input } from '../../ui/input'; -import Select from 'react-select'; -import { Plus } from 'lucide-react'; -import { createSelectedModel, useHandleModelSelection } from './utils'; -import { useActiveKeys } from '../api_keys/ActiveKeysContext'; -import { gooseModels } from './GooseModels'; -import { createDarkSelectStyles, darkSelectTheme } from '../../ui/select-styles'; - -export function AddModelInline() { - const { activeKeys } = useActiveKeys(); // Access active keys from context - - // Convert active keys to dropdown options - const providerOptions = activeKeys.map((key) => ({ - value: key.toLowerCase(), - label: key, - })); - - const [selectedProvider, setSelectedProvider] = useState(null); - const [modelName, setModelName] = useState(''); - const [filteredModels, setFilteredModels] = useState< - { id: string; name: string; provider: string }[] - >([]); - const [showSuggestions, setShowSuggestions] = useState(false); - const handleModelSelection = useHandleModelSelection(); - - // Filter models by selected provider and input text - useEffect(() => { - if (!selectedProvider || !modelName) { - setFilteredModels([]); - setShowSuggestions(false); - return; - } - - const filtered = gooseModels - .filter( - (model) => - model.provider.toLowerCase() === selectedProvider && - model.name.toLowerCase().includes(modelName.toLowerCase()) - ) - .slice(0, 5) // Limit suggestions to top 5 - .map((model) => ({ - id: String(model.id || ''), - name: model.name, - provider: model.provider, - })); - setFilteredModels(filtered); - setShowSuggestions(filtered.length > 0); - }, [modelName, selectedProvider]); - - const handleSubmit = () => { - if (!selectedProvider || !modelName) { - console.error('Both provider and model name are required.'); - return; - } - - // Find the selected model from the filtered models - const selectedModel = createSelectedModel(selectedProvider, modelName); - - // Trigger the model selection logic - handleModelSelection(selectedModel, 'AddModelInline'); - - // Reset form state - setSelectedProvider(null); // Clear the provider selection - setModelName(''); // Clear the model name - setFilteredModels([]); - setShowSuggestions(false); - }; - - const handleSelectSuggestion = (suggestion: { provider: string; name: string }) => { - setModelName(suggestion.name); - setShowSuggestions(false); // Hide suggestions after selection - }; - - const handleBlur = () => { - setTimeout(() => setShowSuggestions(false), 150); // Delay to allow click to register - }; - - return ( -
-
- setModelName(e.target.value)} - onBlur={handleBlur} - /> - {showSuggestions && ( -
- {filteredModels.map((model) => ( -
handleSelectSuggestion(model)} - > - {model.name} -
- ))} -
- )} -
- - -
- ); -} diff --git a/ui/desktop/src/components/settings/models/GooseModels.tsx b/ui/desktop/src/components/settings/models/GooseModels.tsx deleted file mode 100644 index 4badf402839a..000000000000 --- a/ui/desktop/src/components/settings/models/GooseModels.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Model } from './ModelContext'; - -// TODO: move into backends / fetch dynamically -// this is used by ModelContext -export const gooseModels: Model[] = [ - { id: 1, name: 'gpt-4o-mini', provider: 'OpenAI' }, - { id: 2, name: 'gpt-4o', provider: 'OpenAI' }, - { id: 3, name: 'gpt-4-turbo', provider: 'OpenAI' }, - { id: 5, name: 'o1', provider: 'OpenAI' }, - { id: 7, name: 'claude-3-5-sonnet-latest', provider: 'Anthropic' }, - { id: 8, name: 'claude-3-5-haiku-latest', provider: 'Anthropic' }, - { id: 9, name: 'claude-3-opus-latest', provider: 'Anthropic' }, - { id: 10, name: 'gemini-1.5-pro', provider: 'Google' }, - { id: 11, name: 'gemini-1.5-flash', provider: 'Google' }, - { id: 12, name: 'gemini-2.0-flash', provider: 'Google' }, - { id: 13, name: 'gemini-2.0-flash-lite-preview-02-05', provider: 'Google' }, - { id: 14, name: 'gemini-2.0-flash-thinking-exp-01-21', provider: 'Google' }, - { id: 15, name: 'gemini-2.0-pro-exp-02-05', provider: 'Google' }, - { id: 16, name: 'gemini-2.5-pro-exp-03-25', provider: 'Google' }, - { id: 17, name: 'llama-3.3-70b-versatile', provider: 'Groq' }, - { id: 18, name: 'qwen2.5', provider: 'Ollama' }, - { id: 19, name: 'anthropic/claude-3.5-sonnet', provider: 'OpenRouter' }, - { id: 20, name: 'gpt-4o', provider: 'Azure OpenAI' }, - { id: 21, name: 'claude-3-7-sonnet@20250219', provider: 'GCP Vertex AI' }, - { id: 22, name: 'claude-3-5-sonnet-v2@20241022', provider: 'GCP Vertex AI' }, - { id: 23, name: 'claude-3-5-sonnet@20240620', provider: 'GCP Vertex AI' }, - { id: 24, name: 'claude-3-5-haiku@20241022', provider: 'GCP Vertex AI' }, - { id: 25, name: 'claude-sonnet-4@20250514', provider: 'GCP Vertex AI' }, - { id: 26, name: 'gemini-2.0-pro-exp-02-05', provider: 'GCP Vertex AI' }, - { id: 27, name: 'gemini-2.0-flash-001', provider: 'GCP Vertex AI' }, - { id: 28, name: 'gemini-1.5-pro-002', provider: 'GCP Vertex AI' }, - { id: 29, name: 'gemini-2.5-pro-exp-03-25', provider: 'GCP Vertex AI' }, -]; diff --git a/ui/desktop/src/components/settings/models/ModelContext.tsx b/ui/desktop/src/components/settings/models/ModelContext.tsx deleted file mode 100644 index b179188a3300..000000000000 --- a/ui/desktop/src/components/settings/models/ModelContext.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { createContext, useContext, useState, ReactNode } from 'react'; -import { GOOSE_MODEL, GOOSE_PROVIDER } from '../../../env_vars'; -import { gooseModels } from './GooseModels'; // Assuming hardcoded models are here - -// TODO: API keys -export interface Model { - id?: number; // Make `id` optional to allow user-defined models - name: string; - provider: string; - lastUsed?: string; - alias?: string; // optional model display name - subtext?: string; // goes below model name if not the provider -} - -interface ModelContextValue { - currentModel: Model | null; - setCurrentModel: (model: Model) => void; - switchModel: (model: Model) => void; // Add the reusable switch function -} - -const ModelContext = createContext(undefined); - -export const ModelProvider = ({ children }: { children: ReactNode }) => { - const [currentModel, setCurrentModel] = useState( - JSON.parse(localStorage.getItem(GOOSE_MODEL) || 'null') - ); - - const updateModel = (model: Model) => { - setCurrentModel(model); - localStorage.setItem(GOOSE_PROVIDER, model.provider.toLowerCase()); - localStorage.setItem(GOOSE_MODEL, JSON.stringify(model)); - }; - - const switchModel = (model: Model) => { - const newModel = model.id - ? gooseModels.find((m) => m.id === model.id) || model - : { id: Date.now(), ...model }; // Assign unique ID for user-defined models - updateModel(newModel); - }; - - return ( - - {children} - - ); -}; - -export const useModel = () => { - const context = useContext(ModelContext); - if (!context) throw new Error('useModel must be used within a ModelProvider'); - return context; -}; diff --git a/ui/desktop/src/components/settings/models/ModelRadioList.tsx b/ui/desktop/src/components/settings/models/ModelRadioList.tsx deleted file mode 100644 index d4bb7cb44201..000000000000 --- a/ui/desktop/src/components/settings/models/ModelRadioList.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useRecentModels } from './RecentModels'; -import { useModel, Model } from './ModelContext'; -import { useHandleModelSelection } from './utils'; -import type { View } from '../../../App'; - -interface ModelRadioListProps { - renderItem: (props: { - model: Model; - isSelected: boolean; - onSelect: () => void; - }) => React.ReactNode; - className?: string; -} - -export function SeeMoreModelsButtons({ setView }: { setView: (view: View) => void }) { - return ( -
-

Models

- -
- ); -} - -export function ModelRadioList({ renderItem, className = '' }: ModelRadioListProps) { - const { recentModels } = useRecentModels(); - const { currentModel } = useModel(); - const handleModelSelection = useHandleModelSelection(); - const [selectedModel, setSelectedModel] = useState(null); - - useEffect(() => { - if (currentModel) { - setSelectedModel(currentModel.name); - } - }, [currentModel]); - - const handleRadioChange = async (model: Model) => { - if (selectedModel === model.name) { - console.log(`Model "${model.name}" is already active.`); - return; - } - - setSelectedModel(model.name); - await handleModelSelection(model, 'ModelList'); - }; - - return ( -
- {recentModels.map((model) => - renderItem({ - model, - isSelected: selectedModel === model.name, - onSelect: () => handleRadioChange(model), - }) - )} -
- ); -} diff --git a/ui/desktop/src/components/settings_v2/models/ModelsSection.tsx b/ui/desktop/src/components/settings/models/ModelsSection.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/models/ModelsSection.tsx rename to ui/desktop/src/components/settings/models/ModelsSection.tsx diff --git a/ui/desktop/src/components/settings/models/MoreModelsView.tsx b/ui/desktop/src/components/settings/models/MoreModelsView.tsx deleted file mode 100644 index 3a5b511000f0..000000000000 --- a/ui/desktop/src/components/settings/models/MoreModelsView.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { RecentModels } from './RecentModels'; -import { ProviderButtons } from './ProviderButtons'; -import BackButton from '../../ui/BackButton'; -import { SearchBar } from './Search'; -import { AddModelInline } from './AddModelInline'; -import { ScrollArea } from '../../ui/scroll-area'; -import type { View } from '../../../App'; -import MoreMenuLayout from '../../more_menu/MoreMenuLayout'; - -export default function MoreModelsView({ - onClose, - setView, -}: { - onClose: () => void; - setView: (view: View) => void; -}) { - return ( -
- - - -
- -

Browse models

-
- - {/* Content Area */} -
-
-
-

Models

- -
- -
- {/* Search Section */} -
-

Search Models

- -
- - {/* Add Model Section */} -
-

Add Model

- -
- - {/* Provider Section */} -
-

Browse by Provider

-
- -
-
- - {/* Recent Models Section */} -
-
-

Recently used

-
-
- -
-
-
-
-
-
-
- ); -} diff --git a/ui/desktop/src/components/settings/models/ProviderButtons.tsx b/ui/desktop/src/components/settings/models/ProviderButtons.tsx deleted file mode 100644 index 542366513bce..000000000000 --- a/ui/desktop/src/components/settings/models/ProviderButtons.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { useState, useEffect } from 'react'; -import { Button } from '../../ui/button'; -import { Switch } from '../../ui/switch'; -import { useActiveKeys } from '../api_keys/ActiveKeysContext'; -import { model_docs_link } from './hardcoded_stuff'; -import { gooseModels } from './GooseModels'; -import { useModel } from './ModelContext'; -import { useHandleModelSelection } from './utils'; - -// Create a mapping from provider name to href -const providerLinks: Record = model_docs_link.reduce( - (acc, { name, href }) => { - acc[name] = href; - return acc; - }, - {} as Record -); - -export function ProviderButtons() { - const { activeKeys } = useActiveKeys(); - const [selectedProvider, setSelectedProvider] = useState(null); - const { currentModel } = useModel(); - const handleModelSelection = useHandleModelSelection(); - - // Handle Escape key press - useEffect(() => { - const handleEsc = (event: KeyboardEvent) => { - if (event.key === 'Escape') { - setSelectedProvider(null); - } - }; - window.addEventListener('keydown', handleEsc); - return () => window.removeEventListener('keydown', handleEsc); - }, []); - - // Filter models by provider - const providerModels = selectedProvider - ? gooseModels.filter((model) => model.provider === selectedProvider) - : []; - - return ( -
-
-
- {activeKeys.map((provider) => ( - - ))} -
-
- - {/* Models List */} - {selectedProvider && ( -
-
- {providerModels.map((model) => ( -
- {model.name} - handleModelSelection(model, 'ProviderButtons')} - /> -
- ))} -
- - - Browse more {selectedProvider} models - -
- )} -
- ); -} diff --git a/ui/desktop/src/components/settings/models/RecentModels.tsx b/ui/desktop/src/components/settings/models/RecentModels.tsx deleted file mode 100644 index 2091a0077987..000000000000 --- a/ui/desktop/src/components/settings/models/RecentModels.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import { useState, useEffect } from 'react'; -import { Clock } from 'lucide-react'; -import { Model } from './ModelContext'; -import { ModelRadioList, SeeMoreModelsButtons } from './ModelRadioList'; -import { useModel } from './ModelContext'; -import { useHandleModelSelection } from './utils'; -import type { View } from '../../../App'; - -const MAX_RECENT_MODELS = 3; - -export function useRecentModels() { - const [recentModels, setRecentModels] = useState([]); - - useEffect(() => { - const storedModels = localStorage.getItem('recentModels'); - if (storedModels) { - setRecentModels(JSON.parse(storedModels)); - } - }, []); - - const addRecentModel = (model: Model) => { - const modelWithTimestamp = { ...model, lastUsed: new Date().toISOString() }; // Add lastUsed field - setRecentModels((prevModels) => { - const updatedModels = [ - modelWithTimestamp, - ...prevModels.filter((m) => m.name !== model.name), - ].slice(0, MAX_RECENT_MODELS); - - localStorage.setItem('recentModels', JSON.stringify(updatedModels)); - return updatedModels; - }); - }; - - return { recentModels, addRecentModel }; -} - -function getRelativeTimeString(date: string | Date): string { - const now = new Date(); - const then = new Date(date); - const diffInSeconds = Math.floor((now.getTime() - then.getTime()) / 1000); - - if (diffInSeconds < 60) { - return 'Just now'; - } - - const diffInMinutes = Math.floor(diffInSeconds / 60); - if (diffInMinutes < 60) { - return `${diffInMinutes}m ago`; - } - - const diffInHours = Math.floor(diffInMinutes / 60); - if (diffInHours < 24) { - return `${diffInHours}h ago`; - } - - const diffInDays = Math.floor(diffInHours / 24); - if (diffInDays < 7) { - return `${diffInDays}d ago`; - } - - if (diffInDays < 30) { - const weeks = Math.floor(diffInDays / 7); - return `${weeks}w ago`; - } - - const months = Math.floor(diffInDays / 30); - if (months < 12) { - return `${months}mo ago`; - } - - const years = Math.floor(months / 12); - return `${years}y ago`; -} - -export function RecentModels() { - const { recentModels } = useRecentModels(); - const { currentModel } = useModel(); - const handleModelSelection = useHandleModelSelection(); - const [selectedModel, setSelectedModel] = useState(null); - - useEffect(() => { - if (currentModel) { - setSelectedModel(currentModel.name); - } - }, [currentModel]); - - const handleRadioChange = async (model: Model) => { - if (selectedModel === model.name) { - console.log(`Model "${model.name}" is already active.`); - return; - } - - setSelectedModel(model.name); - await handleModelSelection(model, 'RecentModels'); - }; - - return ( -
- {recentModels.map((model) => ( - - ))} -
- ); -} - -export function RecentModelsRadio({ setView }: { setView: (view: View) => void }) { - return ( -
- -
-
- ( - - )} - /> -
-
-
- ); -} diff --git a/ui/desktop/src/components/settings/models/Search.tsx b/ui/desktop/src/components/settings/models/Search.tsx deleted file mode 100644 index 5f7439377dd5..000000000000 --- a/ui/desktop/src/components/settings/models/Search.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { Search } from 'lucide-react'; -import { Switch } from '../../ui/switch'; -import { gooseModels } from './GooseModels'; -import { useModel } from './ModelContext'; -import { useHandleModelSelection } from './utils'; -import { useActiveKeys } from '../api_keys/ActiveKeysContext'; - -// TODO: dark mode (p1) -// FIXME: arrow keys do not work to select a model (p1) -export function SearchBar() { - const [search, setSearch] = useState(''); - const [focusedIndex, setFocusedIndex] = useState(-1); - const [showResults, setShowResults] = useState(false); - const resultsRef = useRef<(HTMLDivElement | null)[]>([]); - const searchBarRef = useRef(null); - - const { currentModel } = useModel(); // Access global state - const handleModelSelection = useHandleModelSelection(); - - // search results filtering - // results set will only include models that have a configured provider - const { activeKeys } = useActiveKeys(); // Access active keys from context - - const model_options = gooseModels.filter((model) => activeKeys.includes(model.provider)); - - const filteredModels = model_options - .filter((model) => model.name.toLowerCase().includes(search.toLowerCase())) - .slice(0, 5); - - useEffect(() => { - setFocusedIndex(-1); - }, [search]); - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (searchBarRef.current && !searchBarRef.current.contains(event.target as Node)) { - setShowResults(false); - } - }; - - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, []); - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'ArrowDown') { - e.preventDefault(); - setFocusedIndex((prev) => (prev < filteredModels.length - 1 ? prev + 1 : prev)); - setShowResults(true); - } else if (e.key === 'ArrowUp') { - e.preventDefault(); - setFocusedIndex((prev) => (prev > 0 ? prev - 1 : prev)); - setShowResults(true); - } else if (e.key === 'Enter' && focusedIndex >= 0) { - e.preventDefault(); - const selectedModel = filteredModels[focusedIndex]; - handleModelSelection(selectedModel, 'SearchBar'); - } else if (e.key === 'Escape') { - e.preventDefault(); - setShowResults(false); - } - }; - - useEffect(() => { - if (focusedIndex >= 0 && focusedIndex < resultsRef.current.length) { - resultsRef.current[focusedIndex]?.scrollIntoView({ - block: 'nearest', - }); - } - }, [focusedIndex]); - - return ( -
- - { - setSearch(e.target.value); - setShowResults(true); - }} - onKeyDown={handleKeyDown} - onFocus={() => setShowResults(true)} - className="w-full pl-9 py-2 text-black dark:text-white bg-bgApp border border-borderSubtle rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" - /> - {showResults && search && ( -
- {filteredModels.length > 0 ? ( - filteredModels.map((model, index) => ( -
(resultsRef.current[index] = el)} - className={`p-2 flex justify-between items-center hover:bg-bgSubtle/50 dark:hover:bg-gray-700 cursor-pointer ${ - model.id === currentModel?.id ? 'bg-bgSubtle/50 dark:bg-gray-700' : '' - }`} - > -
- {model.name} - - {model.provider} - -
- handleModelSelection(model, 'SearchBar')} - /> -
- )) - ) : ( -
No models found
- )} -
- )} -
- ); -} diff --git a/ui/desktop/src/components/settings_v2/models/bottom_bar/ModelsBottomBar.tsx b/ui/desktop/src/components/settings/models/bottom_bar/ModelsBottomBar.tsx similarity index 84% rename from ui/desktop/src/components/settings_v2/models/bottom_bar/ModelsBottomBar.tsx rename to ui/desktop/src/components/settings/models/bottom_bar/ModelsBottomBar.tsx index 21bb47fa7e29..2f37ce87f674 100644 --- a/ui/desktop/src/components/settings_v2/models/bottom_bar/ModelsBottomBar.tsx +++ b/ui/desktop/src/components/settings/models/bottom_bar/ModelsBottomBar.tsx @@ -1,7 +1,6 @@ import { Sliders } from 'lucide-react'; import React, { useEffect, useState, useRef } from 'react'; -import { useConfig } from '../../../ConfigContext'; -import { getCurrentModelAndProviderForDisplay } from '../index'; +import { useModelAndProvider } from '../../../ModelAndProviderContext'; import { AddModelModal } from '../subcomponents/AddModelModal'; import { View } from '../../../../App'; import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '../../../ui/Tooltip'; @@ -11,10 +10,10 @@ interface ModelsBottomBarProps { setView: (view: View) => void; } export default function ModelsBottomBar({ dropdownRef, setView }: ModelsBottomBarProps) { - const { read, getProviders } = useConfig(); + const { currentModel, currentProvider, getCurrentModelAndProviderForDisplay } = + useModelAndProvider(); const [isModelMenuOpen, setIsModelMenuOpen] = useState(false); - const [provider, setProvider] = useState(null); - const [model, setModel] = useState(''); + const [displayProvider, setDisplayProvider] = useState(null); const [isAddModelModalOpen, setIsAddModelModalOpen] = useState(false); const menuRef = useRef(null); const [isModelTruncated, setIsModelTruncated] = useState(false); @@ -22,16 +21,15 @@ export default function ModelsBottomBar({ dropdownRef, setView }: ModelsBottomBa const modelRef = useRef(null); const [isTooltipOpen, setIsTooltipOpen] = useState(false); + // Update display provider when current provider changes useEffect(() => { - (async () => { - const modelProvider = await getCurrentModelAndProviderForDisplay({ - readFromConfig: read, - getProviders, - }); - setProvider(modelProvider.provider as string | null); - setModel(modelProvider.model as string); - })(); - }); + if (currentProvider) { + (async () => { + const modelProvider = await getCurrentModelAndProviderForDisplay(); + setDisplayProvider(modelProvider.provider); + })(); + } + }, [currentProvider, getCurrentModelAndProviderForDisplay]); useEffect(() => { const checkTruncation = () => { @@ -42,7 +40,7 @@ export default function ModelsBottomBar({ dropdownRef, setView }: ModelsBottomBa checkTruncation(); window.addEventListener('resize', checkTruncation); return () => window.removeEventListener('resize', checkTruncation); - }, [model]); + }, [currentModel]); useEffect(() => { setIsTooltipOpen(false); @@ -81,12 +79,12 @@ export default function ModelsBottomBar({ dropdownRef, setView }: ModelsBottomBa ref={modelRef} className="truncate max-w-[130px] md:max-w-[200px] lg:max-w-[360px] min-w-0 block" > - {model || 'Select Model'} + {currentModel || 'Select Model'} {isModelTruncated && ( - {model || 'Select Model'} + {currentModel || 'Select Model'} )} @@ -99,7 +97,7 @@ export default function ModelsBottomBar({ dropdownRef, setView }: ModelsBottomBa
Current:
- {model} -- {provider} + {currentModel} -- {displayProvider}
- {/* Glowing ring */} -
-
isSelectable && isConfigured && onSelect?.()} - className={`relative bg-bgApp rounded-lg - p-3 transition-all duration-200 h-[160px] flex flex-col justify-between - ${isSelectable && isConfigured ? 'cursor-pointer' : ''} - ${!isSelectable ? 'hover:border-borderStandard' : ''} - ${isSelectable && isConfigured ? 'hover:border-borderStandard' : ''} - `} - > -
-
-

{name}

- - {/* Configured state: Green check */} - {isConfigured && ( - - - -
- -
-
- - -

- {hasRequiredKeys - ? `You have ${getArticle(name)} ${name} API Key set in your environment` - : `${name} is installed and running on your machine`} -

-
-
-
-
- )} -
-

- {description} -

-
- -
-
- {/* Default "Add Keys" Button for other providers */} - {!isConfigured && onAddKeys && hasRequiredKeys && ( - - - - - - - -

{tooltipText}

-
-
-
-
- )} - {isConfigured && showSettings && hasRequiredKeys && ( - - - - - - - -

Configure {name} settings

-
-
-
-
- )} - {showDelete && hasRequiredKeys && isConfigured && ( - - - - - - - -

Remove {name} API Key or Host

-
-
-
-
- )} -
- {isConfigured && onTakeoff && showTakeoff !== false && ( - - - - - - - -

Launch goose with {name}

-
-
-
-
- )} -
-
-
- ); -} - -interface BaseProviderGridProps { - providers: Provider[]; - isSelectable?: boolean; - showSettings?: boolean; - showDelete?: boolean; - selectedId?: string | null; - onSelect?: (providerId: string) => void; - onAddKeys?: (provider: Provider) => void; - onConfigure?: (provider: Provider) => void; - onDelete?: (provider: Provider) => void; - onTakeoff?: (provider: Provider) => void; - showTakeoff?: boolean; -} - -export function BaseProviderGrid({ - providers, - isSelectable = false, - showSettings = false, - showDelete = false, - selectedId = null, - onSelect, - onAddKeys, - onConfigure, - onDelete, - showTakeoff, - onTakeoff, -}: BaseProviderGridProps) { - return ( -
- {providers.map((provider) => { - const hasRequiredKeys = - (required_keys as Record)[provider.name]?.length > 0; - return ( - onSelect?.(provider.id)} - onAddKeys={() => onAddKeys?.(provider)} - onConfigure={() => onConfigure?.(provider)} - onDelete={() => onDelete?.(provider)} - onTakeoff={() => onTakeoff?.(provider)} - showSettings={showSettings} - showDelete={showDelete} - hasRequiredKeys={hasRequiredKeys} - showTakeoff={showTakeoff} - /> - ); - })} -
- ); -} diff --git a/ui/desktop/src/components/settings/providers/ConfigureProvidersGrid.tsx b/ui/desktop/src/components/settings/providers/ConfigureProvidersGrid.tsx deleted file mode 100644 index f9f3d860db8f..000000000000 --- a/ui/desktop/src/components/settings/providers/ConfigureProvidersGrid.tsx +++ /dev/null @@ -1,308 +0,0 @@ -import { useMemo, useState } from 'react'; -import { useActiveKeys } from '../api_keys/ActiveKeysContext'; -import { BaseProviderGrid, getProviderDescription } from './BaseProviderGrid'; -import { supported_providers, provider_aliases, required_keys } from '../models/hardcoded_stuff'; -import { ProviderSetupModal } from '../ProviderSetupModal'; -import { getApiUrl, getSecretKey } from '../../../config'; -import { getActiveProviders, isSecretKey } from '../api_keys/utils'; -import { useModel } from '../models/ModelContext'; -import { Button } from '../../ui/button'; -import { toastError, toastSuccess } from '../../../toasts'; - -function ConfirmationModal({ - message, - onConfirm, - onCancel, -}: { - message: string; - onConfirm: () => void; - onCancel: () => void; -}) { - return ( -
-
-
-

- Confirm Delete -

-

{message}

-
- - -
-
-
-
- ); -} - -// Settings version - non-selectable cards with settings gear -export function ConfigureProvidersGrid() { - const { activeKeys, setActiveKeys } = useActiveKeys(); - const [showSetupModal, setShowSetupModal] = useState(false); - const [selectedForSetup, setSelectedForSetup] = useState(null); - const [modalMode, setModalMode] = useState<'edit' | 'setup' | 'battle'>('setup'); - const [isConfirmationOpen, setIsConfirmationOpen] = useState(false); - const [providerToDelete, setProviderToDelete] = useState<{ - name: string; - id: string; - isConfigured: boolean; - description: string; - } | null>(null); - const { currentModel } = useModel(); - - const providers = useMemo(() => { - return supported_providers.map((providerName) => { - const alias = - provider_aliases.find((p) => p.provider === providerName)?.alias || - providerName.toLowerCase(); - const isConfigured = activeKeys.includes(providerName); - - return { - id: alias, - name: providerName, - isConfigured, - description: getProviderDescription(providerName), - }; - }); - }, [activeKeys]); - - const handleAddKeys = (provider: { - id: string; - name: string; - isConfigured: boolean; - description: string; - }) => { - setSelectedForSetup(provider.id); - setModalMode('setup'); - setShowSetupModal(true); - }; - - const handleConfigure = (provider: { - id: string; - name: string; - isConfigured: boolean; - description: string; - }) => { - setSelectedForSetup(provider.id); - setModalMode('edit'); - setShowSetupModal(true); - }; - - const handleModalSubmit = async (configValues: { [key: string]: string }) => { - if (!selectedForSetup) return; - - const provider = providers.find((p) => p.id === selectedForSetup)?.name; - if (!provider) return; - - const requiredKeys = (required_keys as Record)[provider]; - if (!requiredKeys || requiredKeys.length === 0) { - console.error(`No keys found for provider ${provider}`); - return; - } - - try { - // Delete existing keys if provider is already configured - const isUpdate = providers.find((p) => p.id === selectedForSetup)?.isConfigured; - if (isUpdate) { - for (const keyName of requiredKeys) { - const isSecret = isSecretKey(keyName); - const deleteResponse = await fetch(getApiUrl('/configs/delete'), { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': getSecretKey(), - }, - body: JSON.stringify({ - key: keyName, - isSecret, - }), - }); - - if (!deleteResponse.ok) { - const errorText = await deleteResponse.text(); - console.error('Delete response error:', errorText); - throw new Error(`Failed to delete old key: ${keyName}`); - } - } - } - - // Store new keys - for (const keyName of requiredKeys) { - const value = configValues[keyName]; - if (!value) { - console.error(`Missing value for required key: ${keyName}`); - continue; - } - - const isSecret = isSecretKey(keyName); - const storeResponse = await fetch(getApiUrl('/configs/store'), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': getSecretKey(), - }, - body: JSON.stringify({ - key: keyName, - value: value, - isSecret, - }), - }); - - if (!storeResponse.ok) { - const errorText = await storeResponse.text(); - console.error('Store response error:', errorText); - throw new Error(`Failed to store new key: ${keyName}`); - } - } - - toastSuccess({ - title: provider, - msg: isUpdate ? `Successfully updated configuration` : `Successfully added configuration`, - }); - - const updatedKeys = await getActiveProviders(); - setActiveKeys(updatedKeys); - - setShowSetupModal(false); - setSelectedForSetup(null); - setModalMode('setup'); - } catch (error) { - console.error('Error handling modal submit:', error); - toastError({ - title: provider, - msg: `Failed to ${providers.find((p) => p.id === selectedForSetup)?.isConfigured ? 'update' : 'add'} configuration`, - traceback: error instanceof Error ? error.message : String(error), - }); - } - }; - - const handleDelete = async (provider: { - id: string; - name: string; - isConfigured: boolean; - description: string; - }) => { - setProviderToDelete(provider); - setIsConfirmationOpen(true); - }; - - const confirmDelete = async () => { - if (!providerToDelete) return; - - const requiredKeys = required_keys[providerToDelete.name as keyof typeof required_keys]; - if (!requiredKeys || requiredKeys.length === 0) { - console.error(`No keys found for provider ${providerToDelete.name}`); - return; - } - - try { - // Check if the selected provider is currently active - if (currentModel?.provider === providerToDelete.name) { - const msg = `Cannot delete the configuration because it's the provider of the current model (${currentModel.name}). Please switch to a different model first.`; - toastError({ title: providerToDelete.name, msg, traceback: msg }); - setIsConfirmationOpen(false); - return; - } - - // Delete all keys for the provider - for (const keyName of requiredKeys) { - const isSecret = isSecretKey(keyName); - const deleteResponse = await fetch(getApiUrl('/configs/delete'), { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': getSecretKey(), - }, - body: JSON.stringify({ - key: keyName, - isSecret, - }), - }); - - if (!deleteResponse.ok) { - const errorText = await deleteResponse.text(); - console.error('Delete response error:', errorText); - throw new Error(`Failed to delete key: ${keyName}`); - } - } - - console.log('Configuration deleted successfully.'); - toastSuccess({ - title: providerToDelete.name, - msg: 'Successfully deleted configuration', - }); - - const updatedKeys = await getActiveProviders(); - setActiveKeys(updatedKeys); - } catch (error) { - console.error('Error deleting configuration:', error); - toastError({ - title: providerToDelete.name, - msg: 'Failed to delete configuration', - traceback: error instanceof Error ? error.message : String(error), - }); - } - setIsConfirmationOpen(false); - }; - - return ( -
- - - {showSetupModal && selectedForSetup && ( -
- p.id === selectedForSetup)?.name || ''} - _model="Example Model" - _endpoint="Example Endpoint" - title={ - modalMode === 'edit' - ? `Edit ${providers.find((p) => p.id === selectedForSetup)?.name} Configuration` - : undefined - } - onSubmit={(configValues) => { - if (configValues.forceBattle === 'true') { - setSelectedForSetup(selectedForSetup); - setModalMode('battle'); - setShowSetupModal(true); - return; - } - handleModalSubmit(configValues); - }} - onCancel={() => { - setShowSetupModal(false); - setSelectedForSetup(null); - setModalMode('setup'); - }} - forceBattle={modalMode === 'battle'} - /> -
- )} - - {isConfirmationOpen && providerToDelete && ( - setIsConfirmationOpen(false)} - /> - )} -
- ); -} diff --git a/ui/desktop/src/components/settings/providers/ConfigureProvidersView.tsx b/ui/desktop/src/components/settings/providers/ConfigureProvidersView.tsx deleted file mode 100644 index 8890fe7108a9..000000000000 --- a/ui/desktop/src/components/settings/providers/ConfigureProvidersView.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { ScrollArea } from '../../ui/scroll-area'; -import BackButton from '../../ui/BackButton'; -import { ConfigureProvidersGrid } from './ConfigureProvidersGrid'; -import MoreMenuLayout from '../../more_menu/MoreMenuLayout'; - -export default function ConfigureProvidersView({ onClose }: { onClose: () => void }) { - return ( -
- - - -
- -

Configure

-
- -
-
-

Providers

-
- - {/* Content Area */} -
-
- -
-
-
-
-
- ); -} diff --git a/ui/desktop/src/components/settings_v2/providers/ProviderGrid.tsx b/ui/desktop/src/components/settings/providers/ProviderGrid.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/ProviderGrid.tsx rename to ui/desktop/src/components/settings/providers/ProviderGrid.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/ProviderRegistry.tsx b/ui/desktop/src/components/settings/providers/ProviderRegistry.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/ProviderRegistry.tsx rename to ui/desktop/src/components/settings/providers/ProviderRegistry.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/ProviderSettingsPage.tsx b/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/ProviderSettingsPage.tsx rename to ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/interfaces/ButtonCallbacks.tsx b/ui/desktop/src/components/settings/providers/interfaces/ButtonCallbacks.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/interfaces/ButtonCallbacks.tsx rename to ui/desktop/src/components/settings/providers/interfaces/ButtonCallbacks.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/interfaces/ConfigurationAction.tsx b/ui/desktop/src/components/settings/providers/interfaces/ConfigurationAction.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/interfaces/ConfigurationAction.tsx rename to ui/desktop/src/components/settings/providers/interfaces/ConfigurationAction.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/interfaces/OllamaMetadata.tsx b/ui/desktop/src/components/settings/providers/interfaces/OllamaMetadata.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/interfaces/OllamaMetadata.tsx rename to ui/desktop/src/components/settings/providers/interfaces/OllamaMetadata.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/interfaces/ParameterSchema.ts b/ui/desktop/src/components/settings/providers/interfaces/ParameterSchema.ts similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/interfaces/ParameterSchema.ts rename to ui/desktop/src/components/settings/providers/interfaces/ParameterSchema.ts diff --git a/ui/desktop/src/components/settings_v2/providers/interfaces/ProviderDetails.tsx b/ui/desktop/src/components/settings/providers/interfaces/ProviderDetails.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/interfaces/ProviderDetails.tsx rename to ui/desktop/src/components/settings/providers/interfaces/ProviderDetails.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/interfaces/ProviderState.tsx b/ui/desktop/src/components/settings/providers/interfaces/ProviderState.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/interfaces/ProviderState.tsx rename to ui/desktop/src/components/settings/providers/interfaces/ProviderState.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/modal/ProviderConfiguationModal.tsx b/ui/desktop/src/components/settings/providers/modal/ProviderConfiguationModal.tsx similarity index 96% rename from ui/desktop/src/components/settings_v2/providers/modal/ProviderConfiguationModal.tsx rename to ui/desktop/src/components/settings/providers/modal/ProviderConfiguationModal.tsx index 74deaca77199..36e4245cf0ad 100644 --- a/ui/desktop/src/components/settings_v2/providers/modal/ProviderConfiguationModal.tsx +++ b/ui/desktop/src/components/settings/providers/modal/ProviderConfiguationModal.tsx @@ -10,8 +10,8 @@ import { DefaultSubmitHandler } from './subcomponents/handlers/DefaultSubmitHand import OllamaSubmitHandler from './subcomponents/handlers/OllamaSubmitHandler'; import OllamaForm from './subcomponents/forms/OllamaForm'; import { useConfig } from '../../../ConfigContext'; +import { useModelAndProvider } from '../../../ModelAndProviderContext'; import { AlertTriangle } from 'lucide-react'; -import { getCurrentModelAndProvider } from '../../models'; // Import the utility interface FormValues { [key: string]: string | number | boolean | null; @@ -27,7 +27,8 @@ const customFormsMap: Record = { export default function ProviderConfigurationModal() { const [validationErrors, setValidationErrors] = useState>({}); - const { upsert, remove, read } = useConfig(); // Add read to the destructured values + const { upsert, remove } = useConfig(); + const { getCurrentModelAndProvider } = useModelAndProvider(); const { isOpen, currentProvider, modalProps, closeModal } = useProviderModal(); const [configValues, setConfigValues] = useState>({}); const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false); @@ -126,10 +127,7 @@ export default function ProviderConfigurationModal() { const handleDelete = async () => { // Check if this is the currently active provider try { - const providerModel = await getCurrentModelAndProvider({ - readFromConfig: read, - writeToConfig: upsert, - }); + const providerModel = await getCurrentModelAndProvider(); if (currentProvider.name === providerModel.provider) { // It's the active provider - set state and show warning setIsActiveProvider(true); diff --git a/ui/desktop/src/components/settings_v2/providers/modal/ProviderModalProvider.tsx b/ui/desktop/src/components/settings/providers/modal/ProviderModalProvider.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/ProviderModalProvider.tsx rename to ui/desktop/src/components/settings/providers/modal/ProviderModalProvider.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/modal/constants.tsx b/ui/desktop/src/components/settings/providers/modal/constants.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/constants.tsx rename to ui/desktop/src/components/settings/providers/modal/constants.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/modal/interfaces/ProviderSetupFormProps.tsx b/ui/desktop/src/components/settings/providers/modal/interfaces/ProviderSetupFormProps.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/interfaces/ProviderSetupFormProps.tsx rename to ui/desktop/src/components/settings/providers/modal/interfaces/ProviderSetupFormProps.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/ProviderLogo.tsx b/ui/desktop/src/components/settings/providers/modal/subcomponents/ProviderLogo.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/ProviderLogo.tsx rename to ui/desktop/src/components/settings/providers/modal/subcomponents/ProviderLogo.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/ProviderSetupActions.tsx b/ui/desktop/src/components/settings/providers/modal/subcomponents/ProviderSetupActions.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/ProviderSetupActions.tsx rename to ui/desktop/src/components/settings/providers/modal/subcomponents/ProviderSetupActions.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/ProviderSetupHeader.tsx b/ui/desktop/src/components/settings/providers/modal/subcomponents/ProviderSetupHeader.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/ProviderSetupHeader.tsx rename to ui/desktop/src/components/settings/providers/modal/subcomponents/ProviderSetupHeader.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/SecureStorageNotice.tsx b/ui/desktop/src/components/settings/providers/modal/subcomponents/SecureStorageNotice.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/SecureStorageNotice.tsx rename to ui/desktop/src/components/settings/providers/modal/subcomponents/SecureStorageNotice.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/forms/DefaultProviderSetupForm.tsx b/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/DefaultProviderSetupForm.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/forms/DefaultProviderSetupForm.tsx rename to ui/desktop/src/components/settings/providers/modal/subcomponents/forms/DefaultProviderSetupForm.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/forms/OllamaForm.tsx b/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/OllamaForm.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/forms/OllamaForm.tsx rename to ui/desktop/src/components/settings/providers/modal/subcomponents/forms/OllamaForm.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/handlers/DefaultSubmitHandler.tsx b/ui/desktop/src/components/settings/providers/modal/subcomponents/handlers/DefaultSubmitHandler.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/handlers/DefaultSubmitHandler.tsx rename to ui/desktop/src/components/settings/providers/modal/subcomponents/handlers/DefaultSubmitHandler.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/handlers/OllamaSubmitHandler.tsx b/ui/desktop/src/components/settings/providers/modal/subcomponents/handlers/OllamaSubmitHandler.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/handlers/OllamaSubmitHandler.tsx rename to ui/desktop/src/components/settings/providers/modal/subcomponents/handlers/OllamaSubmitHandler.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/anthropic.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/anthropic.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/anthropic.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/anthropic.png diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/anthropic@2x.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/anthropic@2x.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/anthropic@2x.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/anthropic@2x.png diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/anthropic@3x.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/anthropic@3x.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/anthropic@3x.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/anthropic@3x.png diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/databricks.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/databricks.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/databricks.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/databricks.png diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/databricks@2x.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/databricks@2x.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/databricks@2x.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/databricks@2x.png diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/databricks@3x.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/databricks@3x.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/databricks@3x.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/databricks@3x.png diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/default.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/default.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/default.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/default.png diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/default@2x.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/default@2x.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/default@2x.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/default@2x.png diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/default@3x.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/default@3x.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/default@3x.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/default@3x.png diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/google.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/google.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/google.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/google.png diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/google@2x.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/google@2x.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/google@2x.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/google@2x.png diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/google@3x.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/google@3x.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/google@3x.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/google@3x.png diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/groq.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/groq.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/groq.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/groq.png diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/groq@2x.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/groq@2x.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/groq@2x.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/groq@2x.png diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/groq@3x.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/groq@3x.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/groq@3x.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/groq@3x.png diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/ollama.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/ollama.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/ollama.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/ollama.png diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/ollama@2x.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/ollama@2x.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/ollama@2x.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/ollama@2x.png diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/ollama@3x.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/ollama@3x.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/ollama@3x.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/ollama@3x.png diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/openai.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/openai.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/openai.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/openai.png diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/openai.svg b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/openai.svg similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/openai.svg rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/openai.svg diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/openai@2x.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/openai@2x.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/openai@2x.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/openai@2x.png diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/openai@3x.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/openai@3x.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/openai@3x.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/openai@3x.png diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/openrouter.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/openrouter.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/openrouter.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/openrouter.png diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/openrouter@2x.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/openrouter@2x.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/openrouter@2x.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/openrouter@2x.png diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/openrouter@3x.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/openrouter@3x.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/openrouter@3x.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/openrouter@3x.png diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/snowflake.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/snowflake.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/snowflake.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/snowflake.png diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/snowflake@2x.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/snowflake@2x.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/snowflake@2x.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/snowflake@2x.png diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/snowflake@3x.png b/ui/desktop/src/components/settings/providers/modal/subcomponents/icons/snowflake@3x.png similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/modal/subcomponents/icons/snowflake@3x.png rename to ui/desktop/src/components/settings/providers/modal/subcomponents/icons/snowflake@3x.png diff --git a/ui/desktop/src/components/settings_v2/providers/parameters/UpdateSecrets.tsx b/ui/desktop/src/components/settings/providers/parameters/UpdateSecrets.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/parameters/UpdateSecrets.tsx rename to ui/desktop/src/components/settings/providers/parameters/UpdateSecrets.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/state/providerState.tsx b/ui/desktop/src/components/settings/providers/state/providerState.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/state/providerState.tsx rename to ui/desktop/src/components/settings/providers/state/providerState.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/subcomponents/CardActions.tsx b/ui/desktop/src/components/settings/providers/subcomponents/CardActions.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/subcomponents/CardActions.tsx rename to ui/desktop/src/components/settings/providers/subcomponents/CardActions.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/subcomponents/CardBody.tsx b/ui/desktop/src/components/settings/providers/subcomponents/CardBody.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/subcomponents/CardBody.tsx rename to ui/desktop/src/components/settings/providers/subcomponents/CardBody.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/subcomponents/CardContainer.tsx b/ui/desktop/src/components/settings/providers/subcomponents/CardContainer.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/subcomponents/CardContainer.tsx rename to ui/desktop/src/components/settings/providers/subcomponents/CardContainer.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/subcomponents/CardHeader.tsx b/ui/desktop/src/components/settings/providers/subcomponents/CardHeader.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/subcomponents/CardHeader.tsx rename to ui/desktop/src/components/settings/providers/subcomponents/CardHeader.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/subcomponents/ProviderCard.tsx b/ui/desktop/src/components/settings/providers/subcomponents/ProviderCard.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/subcomponents/ProviderCard.tsx rename to ui/desktop/src/components/settings/providers/subcomponents/ProviderCard.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/subcomponents/buttons/CardButtons.tsx b/ui/desktop/src/components/settings/providers/subcomponents/buttons/CardButtons.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/subcomponents/buttons/CardButtons.tsx rename to ui/desktop/src/components/settings/providers/subcomponents/buttons/CardButtons.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/subcomponents/buttons/DefaultCardButtons.tsx b/ui/desktop/src/components/settings/providers/subcomponents/buttons/DefaultCardButtons.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/subcomponents/buttons/DefaultCardButtons.tsx rename to ui/desktop/src/components/settings/providers/subcomponents/buttons/DefaultCardButtons.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/subcomponents/buttons/TooltipWrapper.tsx b/ui/desktop/src/components/settings/providers/subcomponents/buttons/TooltipWrapper.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/subcomponents/buttons/TooltipWrapper.tsx rename to ui/desktop/src/components/settings/providers/subcomponents/buttons/TooltipWrapper.tsx diff --git a/ui/desktop/src/components/settings_v2/providers/subcomponents/utils/StringUtils.tsx b/ui/desktop/src/components/settings/providers/subcomponents/utils/StringUtils.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/providers/subcomponents/utils/StringUtils.tsx rename to ui/desktop/src/components/settings/providers/subcomponents/utils/StringUtils.tsx diff --git a/ui/desktop/src/components/settings_v2/recipes/ViewRecipe.tsx b/ui/desktop/src/components/settings/recipes/ViewRecipe.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/recipes/ViewRecipe.tsx rename to ui/desktop/src/components/settings/recipes/ViewRecipe.tsx diff --git a/ui/desktop/src/components/settings_v2/response_styles/ResponseStyleSelectionItem.tsx b/ui/desktop/src/components/settings/response_styles/ResponseStyleSelectionItem.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/response_styles/ResponseStyleSelectionItem.tsx rename to ui/desktop/src/components/settings/response_styles/ResponseStyleSelectionItem.tsx diff --git a/ui/desktop/src/components/settings_v2/response_styles/ResponseStylesSection.tsx b/ui/desktop/src/components/settings/response_styles/ResponseStylesSection.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/response_styles/ResponseStylesSection.tsx rename to ui/desktop/src/components/settings/response_styles/ResponseStylesSection.tsx diff --git a/ui/desktop/src/components/settings/session/SessionSharingSection.tsx b/ui/desktop/src/components/settings/session/SessionSharingSection.tsx deleted file mode 100644 index de2a080d124d..000000000000 --- a/ui/desktop/src/components/settings/session/SessionSharingSection.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Input } from '../../ui/input'; -import { Check, Lock } from 'lucide-react'; - -export default function SessionSharingSection() { - const envBaseUrlShare = window.appConfig.get('GOOSE_BASE_URL_SHARE') as string | undefined; - console.log('envBaseUrlShare', envBaseUrlShare); - - // If env is set, force sharing enabled and set the baseUrl accordingly. - const [sessionSharingConfig, setSessionSharingConfig] = useState({ - enabled: envBaseUrlShare ? true : false, - baseUrl: envBaseUrlShare || '', - }); - const [urlError, setUrlError] = useState(''); - // isUrlConfigured is true if the user has configured a baseUrl and it is valid. - const isUrlConfigured = - !envBaseUrlShare && sessionSharingConfig.enabled && isValidUrl(sessionSharingConfig.baseUrl); - - // Only load saved config from localStorage if the env variable is not provided. - useEffect(() => { - if (envBaseUrlShare) { - // If env variable is set, save the forced configuration to localStorage - const forcedConfig = { - enabled: true, - baseUrl: envBaseUrlShare, - }; - localStorage.setItem('session_sharing_config', JSON.stringify(forcedConfig)); - } else { - const savedSessionConfig = localStorage.getItem('session_sharing_config'); - if (savedSessionConfig) { - try { - const config = JSON.parse(savedSessionConfig); - setSessionSharingConfig(config); - } catch (error) { - console.error('Error parsing session sharing config:', error); - } - } - } - }, [envBaseUrlShare]); - - // Helper to check if the user's input is a valid URL - function isValidUrl(value: string): boolean { - if (!value) return false; - try { - new URL(value); - return true; - } catch { - return false; - } - } - - // Toggle sharing (only allowed when env is not set). - const toggleSharing = () => { - if (envBaseUrlShare) { - return; // Do nothing if the environment variable forces sharing. - } - setSessionSharingConfig((prev) => { - const updated = { ...prev, enabled: !prev.enabled }; - localStorage.setItem('session_sharing_config', JSON.stringify(updated)); - return updated; - }); - }; - - // Handle changes to the base URL field - const handleBaseUrlChange = (e: React.ChangeEvent) => { - const newBaseUrl = e.target.value; - setSessionSharingConfig((prev) => ({ - ...prev, - baseUrl: newBaseUrl, - })); - - if (isValidUrl(newBaseUrl)) { - setUrlError(''); - const updated = { ...sessionSharingConfig, baseUrl: newBaseUrl }; - localStorage.setItem('session_sharing_config', JSON.stringify(updated)); - } else { - setUrlError('Invalid URL format. Please enter a valid URL (e.g. https://example.com/api).'); - } - }; - - return ( - <> -
-

Session Sharing

-
- -
- {envBaseUrlShare ? ( -

- Session sharing is configured but fully opt-in — your sessions are only shared when you - explicitly click the share button. -

- ) : ( -

- You can enable session sharing to share your sessions with others. You'll then need to - enter the base URL for the session sharing API endpoint. Anyone with access to the same - API and sharing session enabled will be able to see your sessions. -

- )} - -
- {/* Toggle for enabling session sharing */} -
- - {envBaseUrlShare ? ( - - ) : ( - - )} -
- - {/* Base URL field (only visible if enabled) */} - {sessionSharingConfig.enabled && ( -
-
- - {isUrlConfigured && } -
-
- {} : handleBaseUrlChange} - /> -
- {urlError &&

{urlError}

} -
- )} -
-
- - ); -} diff --git a/ui/desktop/src/components/settings_v2/sessions/SessionSharingSection.tsx b/ui/desktop/src/components/settings/sessions/SessionSharingSection.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/sessions/SessionSharingSection.tsx rename to ui/desktop/src/components/settings/sessions/SessionSharingSection.tsx diff --git a/ui/desktop/src/components/settings_v2/tool_selection_strategy/ToolSelectionStrategySection.tsx b/ui/desktop/src/components/settings/tool_selection_strategy/ToolSelectionStrategySection.tsx similarity index 100% rename from ui/desktop/src/components/settings_v2/tool_selection_strategy/ToolSelectionStrategySection.tsx rename to ui/desktop/src/components/settings/tool_selection_strategy/ToolSelectionStrategySection.tsx diff --git a/ui/desktop/src/components/settings/types.ts b/ui/desktop/src/components/settings/types.ts deleted file mode 100644 index 140c233e94ec..000000000000 --- a/ui/desktop/src/components/settings/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { FullExtensionConfig } from '../../extensions'; - -export interface Model { - id: string; - name: string; - description: string; - enabled: boolean; -} - -export interface Settings { - models: Model[]; - extensions: FullExtensionConfig[]; -} diff --git a/ui/desktop/src/components/settings_v2/SettingsView.tsx b/ui/desktop/src/components/settings_v2/SettingsView.tsx deleted file mode 100644 index 54d95b92279d..000000000000 --- a/ui/desktop/src/components/settings_v2/SettingsView.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { ScrollArea } from '../ui/scroll-area'; -import BackButton from '../ui/BackButton'; -import type { View, ViewOptions } from '../../App'; -import ExtensionsSection from './extensions/ExtensionsSection'; -import ModelsSection from './models/ModelsSection'; -import { ModeSection } from './mode/ModeSection'; -import { ToolSelectionStrategySection } from './tool_selection_strategy/ToolSelectionStrategySection'; -import SessionSharingSection from './sessions/SessionSharingSection'; -import { ResponseStylesSection } from './response_styles/ResponseStylesSection'; -import AppSettingsSection from './app/AppSettingsSection'; -import { ExtensionConfig } from '../../api'; -import MoreMenuLayout from '../more_menu/MoreMenuLayout'; - -export type SettingsViewOptions = { - deepLinkConfig?: ExtensionConfig; - showEnvVars?: boolean; -}; - -export default function SettingsView({ - onClose, - setView, - viewOptions, -}: { - onClose: () => void; - setView: (view: View, viewOptions?: ViewOptions) => void; - viewOptions: SettingsViewOptions; -}) { - return ( -
- - - -
-
- onClose()} /> -

Settings

-
- - {/* Content Area */} -
-
- {/* Models Section */} - - {/* Extensions Section */} - - {/* Goose Modes */} - - {/*Session sharing*/} - - {/* Response Styles */} - - {/* Tool Selection Strategy */} - - {/* App Settings */} - -
-
-
-
-
- ); -} diff --git a/ui/desktop/src/json.d.ts b/ui/desktop/src/json.d.ts index 894b1fe78e1f..5bb9ca45aa2c 100644 --- a/ui/desktop/src/json.d.ts +++ b/ui/desktop/src/json.d.ts @@ -37,3 +37,10 @@ declare module '*.mp4' { const value: string; export default value; } + +// Extend CSS properties to include Electron-specific properties +declare namespace React { + interface CSSProperties { + WebkitAppRegion?: 'drag' | 'no-drag'; + } +} diff --git a/ui/desktop/src/renderer.tsx b/ui/desktop/src/renderer.tsx index d08734f77c18..71a90179f19d 100644 --- a/ui/desktop/src/renderer.tsx +++ b/ui/desktop/src/renderer.tsx @@ -1,9 +1,7 @@ import React, { Suspense, lazy } from 'react'; import ReactDOM from 'react-dom/client'; -import { ModelProvider } from './components/settings/models/ModelContext'; import { ConfigProvider } from './components/ConfigContext'; import { ErrorBoundary } from './components/ErrorBoundary'; -import { ActiveKeysProvider } from './components/settings/api_keys/ActiveKeysContext'; import { patchConsoleLogging } from './utils'; import SuspenseLoader from './suspense-loader'; @@ -15,13 +13,9 @@ ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - - - + + + diff --git a/ui/desktop/src/utils/deleteAllKeys.tsx b/ui/desktop/src/utils/deleteAllKeys.tsx deleted file mode 100644 index 3972e9cc154b..000000000000 --- a/ui/desktop/src/utils/deleteAllKeys.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { getApiUrl, getSecretKey } from '../config'; -import { required_keys } from '../components/settings/models/hardcoded_stuff'; - -export async function DeleteProviderKeysFromKeychain() { - for (const [_provider, keys] of Object.entries(required_keys)) { - for (const keyName of keys) { - try { - const deleteResponse = await fetch(getApiUrl('/configs/delete'), { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': getSecretKey(), - }, - body: JSON.stringify({ - key: keyName, - is_secret: true, // get rid of keychain keys only - }), - }); - - if (!deleteResponse.ok) { - const errorText = await deleteResponse.text(); - console.error('Delete response error:', errorText); - throw new Error('Failed to delete key: ' + keyName); - } else { - console.log('Successfully deleted key:', keyName); - } - } catch (error) { - console.error('Error deleting key:', keyName, error); - } - } - } -} diff --git a/ui/desktop/src/utils/providerUtils.ts b/ui/desktop/src/utils/providerUtils.ts index 0c0f5a94aa4d..a26f9f1a542a 100644 --- a/ui/desktop/src/utils/providerUtils.ts +++ b/ui/desktop/src/utils/providerUtils.ts @@ -5,8 +5,8 @@ import { initializeBundledExtensions, syncBundledExtensions, addToAgentOnStartup, -} from '../components/settings_v2/extensions'; -import { extractExtensionConfig } from '../components/settings_v2/extensions/utils'; +} from '../components/settings/extensions'; +import { extractExtensionConfig } from '../components/settings/extensions/utils'; import type { ExtensionConfig, FixedExtensionEntry } from '../components/ConfigContext'; // TODO: remove when removing migration logic import { toastService } from '../toasts'; diff --git a/ui/desktop/tests/e2e/app.spec.ts b/ui/desktop/tests/e2e/app.spec.ts index b33f265feb51..1c7ba1ce04eb 100644 --- a/ui/desktop/tests/e2e/app.spec.ts +++ b/ui/desktop/tests/e2e/app.spec.ts @@ -179,7 +179,13 @@ test.describe('Goose App', () => { // Get the main window once for all tests mainWindow = await electronApp.firstWindow(); await mainWindow.waitForLoadState('domcontentloaded'); - await mainWindow.waitForLoadState('networkidle'); + + // Try to wait for networkidle, but don't fail if it times out due to MCP activity + try { + await mainWindow.waitForLoadState('networkidle', { timeout: 10000 }); + } catch (error) { + console.log('NetworkIdle timeout (likely due to MCP activity), continuing with test...'); + } // Wait for React app to be ready by checking for the root element to have content await mainWindow.waitForFunction(() => { @@ -417,7 +423,12 @@ test.describe('Goose App', () => { try { // Reload the page to ensure settings are fresh await mainWindow.reload(); - await mainWindow.waitForLoadState('networkidle'); + // Try to wait for networkidle, but don't fail if it times out due to MCP activity + try { + await mainWindow.waitForLoadState('networkidle', { timeout: 10000 }); + } catch (error) { + console.log('NetworkIdle timeout (likely due to MCP activity), continuing with test...'); + } await mainWindow.waitForLoadState('domcontentloaded'); // Wait for React app to be ready @@ -687,9 +698,18 @@ test.describe('Goose App', () => { }, initialMessages, { timeout: 30000 }); // Get the latest response - const response = await mainWindow.locator('[data-testid="message-container"]').last(); + const response = await mainWindow.waitForSelector('.goose-message-tool', { timeout: 5000 }); expect(await response.isVisible()).toBe(true); + // Click the Output dropdown to reveal the actual quote + await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-quote-response-debug.png` }); + const element = await mainWindow.$('.goose-message-tool'); + const html = await element.innerHTML(); + console.log('HTML content:', html); + // Click the Runningquote dropdown to reveal the actual quote + const runningQuoteButton = await mainWindow.waitForSelector('div.goose-message-tool svg.rotate-90', { timeout: 5000 }); + await runningQuoteButton.click(); + // Click the Output dropdown to reveal the actual quote const outputButton = await mainWindow.waitForSelector('button:has-text("Output")', { timeout: 5000 }); await outputButton.click();