diff --git a/ui/desktop/src/components/settings/api_keys/utils.tsx b/ui/desktop/src/components/settings/api_keys/utils.tsx index ceae29d888..2140a99ee3 100644 --- a/ui/desktop/src/components/settings/api_keys/utils.tsx +++ b/ui/desktop/src/components/settings/api_keys/utils.tsx @@ -1,24 +1,38 @@ import { Provider, ProviderResponse } from './types'; import { getApiUrl, getSecretKey } from '../../../config'; +import { special_provider_cases } from '../providers/utils'; export async function getActiveProviders(): Promise { try { // Fetch the secrets settings const secretsSettings = await getSecretsSettings(); + // Check for special provider cases (e.g. ollama needs to be installed in Applications folder) + const specialCasesResults = await Promise.all( + Object.entries(special_provider_cases).map(async ([providerName, checkFunction]) => { + const isActive = await checkFunction(); // Dynamically re-check status + console.log(`Special case result for ${providerName}:`, isActive); + return isActive ? providerName : null; + }) + ); + // Extract active providers based on `is_set` in `secret_status` or providers with no keys const activeProviders = Object.values(secretsSettings) // Convert object to array .filter((provider) => { const apiKeyStatus = Object.values(provider.secret_status || {}); // Get all key statuses // Include providers if: - // - They have at least one key set (`is_set: true`), OR - // - They have no keys (`secret_status` is empty or undefined) - return apiKeyStatus.some((key) => key.is_set) || apiKeyStatus.length === 0; + // - They have at least one key set (`is_set: true`) + return apiKeyStatus.some((key) => key.is_set); }) .map((provider) => provider.name || 'Unknown Provider'); // Extract provider name - return activeProviders; + // Combine active providers from secrets settings and special cases + const allActiveProviders = [ + ...activeProviders, + ...specialCasesResults.filter((provider) => provider !== null), // Filter out null results + ]; + return allActiveProviders; } catch (error) { console.error('Failed to get active providers:', error); return []; diff --git a/ui/desktop/src/components/settings/models/hardcoded_stuff.tsx b/ui/desktop/src/components/settings/models/hardcoded_stuff.tsx index 829ef7271e..4078cfc8e2 100644 --- a/ui/desktop/src/components/settings/models/hardcoded_stuff.tsx +++ b/ui/desktop/src/components/settings/models/hardcoded_stuff.tsx @@ -109,15 +109,3 @@ export const provider_aliases = [ { provider: 'OpenRouter', alias: 'openrouter' }, { provider: 'Google', alias: 'google' }, ]; - -export const recent_models = [ - { name: 'gpt-4o', provider: 'OpenAI', lastUsed: '2 hours ago' }, - { name: 'claude-3-5-sonnet-latest', provider: 'Anthropic', lastUsed: 'Yesterday' }, - { name: 'o1-mini', provider: 'OpenAI', lastUsed: '3 days ago' }, -]; - -export const model_env_vars = { - OpenAI: 'OPENAI_MODEL', - Anthropic: 'ANTHROPIC_MODEL', - Databricks: 'DATABRICKS_MODEL', -}; diff --git a/ui/desktop/src/components/settings/providers/BaseProviderGrid.tsx b/ui/desktop/src/components/settings/providers/BaseProviderGrid.tsx index 41b12d2adb..acb50b9252 100644 --- a/ui/desktop/src/components/settings/providers/BaseProviderGrid.tsx +++ b/ui/desktop/src/components/settings/providers/BaseProviderGrid.tsx @@ -1,9 +1,12 @@ -import React from 'react'; -import { Check, Plus, Settings, X, Rocket } from 'lucide-react'; +import React, { useEffect, useState } from 'react'; +import { Check, Plus, Settings, X, Rocket, RefreshCw, AlertCircle } from 'lucide-react'; import { Button } from '../../ui/button'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../ui/Tooltip'; import { Portal } from '@radix-ui/react-portal'; import { required_keys } from '../models/hardcoded_stuff'; +import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; +import { useActiveKeys } from '../api_keys/ActiveKeysContext'; +import { getActiveProviders } from '../api_keys/utils'; // Common interfaces and helper functions interface Provider { @@ -27,6 +30,7 @@ interface BaseProviderCardProps { showDelete?: boolean; hasRequiredKeys?: boolean; onTakeoff?: () => void; + showTakeoff?: boolean; } function getArticle(word: string): string { @@ -60,9 +64,11 @@ function BaseProviderCard({ showDelete = false, hasRequiredKeys = false, onTakeoff, + showTakeoff, }: BaseProviderCardProps) { const numRequiredKeys = required_keys[name]?.length || 0; const tooltipText = numRequiredKeys === 1 ? `Add ${name} API Key` : `Add ${name} API Keys`; + const { activeKeys, setActiveKeys } = useActiveKeys(); return (
@@ -85,6 +91,7 @@ function BaseProviderCard({

{name}

+ {/* Configured state: Green check */} {isConfigured && ( @@ -98,7 +105,36 @@ function BaseProviderCard({

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

+ + +
+
+ )} + + {/* Not Configured state: Red exclamation mark for Ollama */} + {!isConfigured && name === 'Ollama' && ( + + + +
+ ! +
+
+ + +

+ To use, the{' '} + + Ollama app + {' '} + must be installed on your machine and open.

@@ -106,7 +142,6 @@ function BaseProviderCard({
)}
-

{description}

@@ -114,6 +149,42 @@ function BaseProviderCard({
+ {!isConfigured && name === 'Ollama' && ( + + + + + + + +

Re-check for active Ollama app running in the background.

+
+
+
+
+ )} + + {/* Default "Add Keys" Button for other providers */} {!isConfigured && onAddKeys && hasRequiredKeys && ( @@ -187,7 +258,7 @@ function BaseProviderCard({ )}
- {isConfigured && onTakeoff && ( + {isConfigured && onTakeoff && showTakeoff !== false && ( @@ -228,6 +299,7 @@ interface BaseProviderGridProps { onConfigure?: (provider: Provider) => void; onDelete?: (provider: Provider) => void; onTakeoff?: (provider: Provider) => void; + showTakeoff?: boolean; } export function BaseProviderGrid({ @@ -240,6 +312,7 @@ export function BaseProviderGrid({ onAddKeys, onConfigure, onDelete, + showTakeoff, onTakeoff, }: BaseProviderGridProps) { return ( @@ -262,6 +335,7 @@ export function BaseProviderGrid({ 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 index 2e83621f88..12ffe1d10e 100644 --- a/ui/desktop/src/components/settings/providers/ConfigureProvidersGrid.tsx +++ b/ui/desktop/src/components/settings/providers/ConfigureProvidersGrid.tsx @@ -205,6 +205,7 @@ export function ConfigureProvidersGrid() { onAddKeys={handleAddKeys} onConfigure={handleConfigure} onDelete={handleDelete} + showTakeoff={false} /> {showSetupModal && selectedForSetup && ( diff --git a/ui/desktop/src/components/settings/providers/utils.tsx b/ui/desktop/src/components/settings/providers/utils.tsx new file mode 100644 index 0000000000..821b7865ca --- /dev/null +++ b/ui/desktop/src/components/settings/providers/utils.tsx @@ -0,0 +1,14 @@ +export const special_provider_cases = { + Ollama: async () => await checkForOllama(), // Dynamically re-check +}; + +export async function checkForOllama() { + console.log('Invoking check-ollama IPC handler...'); + try { + const ollamaInstalled = await window.electron.checkForOllama(); + return ollamaInstalled; + } catch (error) { + console.error('Error invoking check-ollama:', error); + return false; + } +} diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 42c69496fc..e7beab6ee7 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -26,6 +26,7 @@ import { saveSettings, updateEnvironmentVariables, } from './utils/settings'; +const { exec } = require('child_process'); // Handle creating/removing shortcuts on Windows when installing/uninstalling. if (started) app.quit(); @@ -347,6 +348,37 @@ ipcMain.handle('select-file-or-directory', async () => { return null; }); +ipcMain.handle('check-ollama', async () => { + try { + return new Promise((resolve, reject) => { + // Run `ps` and filter for "ollama" + exec('ps aux | grep -iw "[o]llama"', (error, stdout, stderr) => { + if (error) { + console.error('Error executing ps command:', error); + return resolve(false); // Process is not running + } + + if (stderr) { + console.error('Standard error output from ps command:', stderr); + return resolve(false); // Process is not running + } + + console.log('Raw stdout from ps command:', stdout); + + // Trim and check if output contains a match + const trimmedOutput = stdout.trim(); + console.log('Trimmed stdout:', trimmedOutput); + + const isRunning = trimmedOutput.length > 0; // True if there's any output + resolve(isRunning); // Resolve true if running, false otherwise + }); + }); + } catch (err) { + console.error('Error checking for Ollama:', err); + return false; // Return false on error + } +}); + app.whenReady().then(async () => { // Test error feature - only enabled with GOOSE_TEST_ERROR=true if (process.env.GOOSE_TEST_ERROR === 'true') { diff --git a/ui/desktop/src/preload.js b/ui/desktop/src/preload.js index 734d1ac79e..d78ae3bb67 100644 --- a/ui/desktop/src/preload.js +++ b/ui/desktop/src/preload.js @@ -18,6 +18,7 @@ contextBridge.exposeInMainWorld('electron', { openInChrome: (url) => ipcRenderer.send('open-in-chrome', url), fetchMetadata: (url) => ipcRenderer.invoke('fetch-metadata', url), reloadApp: () => ipcRenderer.send('reload-app'), + checkForOllama: () => ipcRenderer.invoke('check-ollama'), selectFileOrDirectory: () => ipcRenderer.invoke('select-file-or-directory'), startPowerSaveBlocker: () => ipcRenderer.invoke('start-power-save-blocker'), stopPowerSaveBlocker: () => ipcRenderer.invoke('stop-power-save-blocker'),