From 831cb9bb82de6dec9ec1561c1e260309de11b1e6 Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Fri, 13 Mar 2026 13:58:33 -0400 Subject: [PATCH] Remove dead OllamaSetup onboarding flow (#7861) Co-authored-by: Douwe Osinga --- ui/desktop/src/App.test.tsx | 4 - .../src/components/OllamaSetup.test.tsx | 266 --------------- ui/desktop/src/components/OllamaSetup.tsx | 258 -------------- ui/desktop/src/components/ProviderGuard.tsx | 19 -- ui/desktop/src/utils/ollamaDetection.test.ts | 323 ------------------ ui/desktop/src/utils/ollamaDetection.ts | 213 ------------ 6 files changed, 1083 deletions(-) delete mode 100644 ui/desktop/src/components/OllamaSetup.test.tsx delete mode 100644 ui/desktop/src/components/OllamaSetup.tsx delete mode 100644 ui/desktop/src/utils/ollamaDetection.test.ts delete mode 100644 ui/desktop/src/utils/ollamaDetection.ts diff --git a/ui/desktop/src/App.test.tsx b/ui/desktop/src/App.test.tsx index 0ca4eadec0..2907ae405c 100644 --- a/ui/desktop/src/App.test.tsx +++ b/ui/desktop/src/App.test.tsx @@ -65,10 +65,6 @@ vi.mock('./utils/openRouterSetup', () => ({ startOpenRouterSetup: vi.fn().mockResolvedValue({ success: false, message: 'Test' }), })); -vi.mock('./utils/ollamaDetection', () => ({ - checkOllamaStatus: vi.fn().mockResolvedValue({ isRunning: false }), -})); - // Mock the ConfigContext module vi.mock('./components/ConfigContext', () => ({ useConfig: () => ({ diff --git a/ui/desktop/src/components/OllamaSetup.test.tsx b/ui/desktop/src/components/OllamaSetup.test.tsx deleted file mode 100644 index fb2ac832a8..0000000000 --- a/ui/desktop/src/components/OllamaSetup.test.tsx +++ /dev/null @@ -1,266 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { render, screen, waitFor, fireEvent } from '@testing-library/react'; -import { OllamaSetup } from './OllamaSetup'; -import * as ollamaDetection from '../utils/ollamaDetection'; -import { toastService } from '../toasts'; - -// Mock dependencies -vi.mock('../utils/ollamaDetection'); -vi.mock('../toasts'); - -// Mock useConfig hook -const mockUpsert = vi.fn(); -const mockAddExtension = vi.fn(); -const mockGetExtensions = vi.fn(); - -vi.mock('./ConfigContext', () => ({ - useConfig: () => ({ - upsert: mockUpsert, - addExtension: mockAddExtension, - getExtensions: mockGetExtensions, - }), -})); - -describe('OllamaSetup', () => { - const mockOnSuccess = vi.fn(); - const mockOnCancel = vi.fn(); - - beforeEach(() => { - vi.clearAllMocks(); - // Default mocks - vi.mocked(ollamaDetection.getPreferredModel).mockReturnValue('gpt-oss:20b'); - vi.mocked(ollamaDetection.getOllamaDownloadUrl).mockReturnValue('https://ollama.com/download'); - }); - - describe('when Ollama is not detected', () => { - beforeEach(() => { - vi.mocked(ollamaDetection.checkOllamaStatus).mockResolvedValue({ - isRunning: false, - host: 'http://127.0.0.1:11434', - }); - }); - - it('should show installation instructions', async () => { - render(); - - await waitFor(() => { - expect(screen.getByText('Ollama Setup')).toBeInTheDocument(); - expect(screen.getByText(/Ollama is not detected on your system/)).toBeInTheDocument(); - }); - }); - - it('should provide download link', async () => { - render(); - - await waitFor(() => { - const downloadLink = screen.getByRole('link', { name: /Install Ollama/ }); - expect(downloadLink).toHaveAttribute('href', 'https://ollama.com/download'); - expect(downloadLink).toHaveAttribute('target', '_blank'); - }); - }); - - it('should show polling state when install link is clicked', async () => { - render(); - - // Mock pollForOllama - const mockStopPolling = vi.fn(); - vi.mocked(ollamaDetection.pollForOllama).mockReturnValue(mockStopPolling); - - await waitFor(() => { - const installLink = screen.getByText('Install Ollama'); - fireEvent.click(installLink); - }); - - expect(screen.getByText(/Waiting for Ollama to start/)).toBeInTheDocument(); - expect(ollamaDetection.pollForOllama).toHaveBeenCalled(); - }); - - it('should handle cancel button', async () => { - render(); - - await waitFor(() => { - fireEvent.click(screen.getByText('Cancel')); - }); - - expect(mockOnCancel).toHaveBeenCalled(); - }); - }); - - describe('when Ollama is detected but model is not available', () => { - beforeEach(() => { - vi.mocked(ollamaDetection.checkOllamaStatus).mockResolvedValue({ - isRunning: true, - host: 'http://127.0.0.1:11434', - }); - vi.mocked(ollamaDetection.hasModel).mockResolvedValue(false); - }); - - it('should show model download prompt', async () => { - render(); - - await waitFor(() => { - expect(screen.getByText(/The gpt-oss:20b model is not installed/)).toBeInTheDocument(); - expect(screen.getByText(/Download gpt-oss:20b/)).toBeInTheDocument(); - }); - }); - - it('should handle model download', async () => { - vi.mocked(ollamaDetection.pullOllamaModel).mockResolvedValue(true); - - render(); - - await waitFor(() => { - fireEvent.click(screen.getByText(/Download gpt-oss:20b/)); - }); - - await waitFor(() => { - expect(toastService.success).toHaveBeenCalledWith( - expect.objectContaining({ - title: 'Model Downloaded!', - }) - ); - }); - }); - - it('should handle download failure', async () => { - vi.mocked(ollamaDetection.pullOllamaModel).mockResolvedValue(false); - - render(); - - await waitFor(() => { - fireEvent.click(screen.getByText(/Download gpt-oss:20b/)); - }); - - await waitFor(() => { - expect(toastService.error).toHaveBeenCalledWith( - expect.objectContaining({ - title: 'Download Failed', - }) - ); - }); - }); - }); - - // TODO: re-enable when we have ollama back in the onboarding - describe.skip('when Ollama and model are both available', () => { - beforeEach(() => { - vi.mocked(ollamaDetection.checkOllamaStatus).mockResolvedValue({ - isRunning: true, - host: 'http://127.0.0.1:11434', - }); - vi.mocked(ollamaDetection.hasModel).mockResolvedValue(true); - }); - - it('should show ready state and connect button', async () => { - render(); - - await waitFor(() => { - expect(screen.getByText(/Ollama is detected and running/)).toBeInTheDocument(); - }); - }); - - it('should handle successful connection', async () => { - render(); - - await waitFor(() => { - fireEvent.click(screen.getByText(/Use Goose with Ollama/)); - }); - - await waitFor(() => { - expect(mockUpsert).toHaveBeenCalledWith('GOOSE_PROVIDER', 'ollama', false); - expect(mockUpsert).toHaveBeenCalledWith('GOOSE_MODEL', 'gpt-oss:20b', false); - expect(mockUpsert).toHaveBeenCalledWith('OLLAMA_HOST', 'localhost', false); - expect(toastService.success).toHaveBeenCalled(); - expect(mockOnSuccess).toHaveBeenCalled(); - }); - }); - - it('should handle connection failure', async () => { - render(); - - await waitFor(() => { - fireEvent.click(screen.getByText('Use Goose with Ollama')); - }); - - await waitFor(() => { - expect(toastService.error).toHaveBeenCalledWith( - expect.objectContaining({ - title: 'Connection Failed', - msg: expect.stringContaining('Initialization failed'), - }) - ); - }); - }); - }); - - describe('polling behavior', () => { - it('should clean up polling on unmount', async () => { - const mockStopPolling = vi.fn(); - vi.mocked(ollamaDetection.pollForOllama).mockReturnValue(mockStopPolling); - - vi.mocked(ollamaDetection.checkOllamaStatus).mockResolvedValue({ - isRunning: false, - host: 'http://127.0.0.1:11434', - }); - - const { unmount } = render(); - - await waitFor(() => { - fireEvent.click(screen.getByText('Install Ollama')); - }); - - expect(ollamaDetection.pollForOllama).toHaveBeenCalled(); - - unmount(); - - expect(mockStopPolling).toHaveBeenCalled(); - }); - - it('should handle Ollama detection during polling', async () => { - vi.mocked(ollamaDetection.checkOllamaStatus).mockResolvedValue({ - isRunning: false, - host: 'http://127.0.0.1:11434', - }); - - let pollCallback: ((status: { isRunning: boolean; host: string }) => void) | undefined; - vi.mocked(ollamaDetection.pollForOllama).mockImplementation((onDetected) => { - pollCallback = onDetected; - return vi.fn(); - }); - - render(); - - await waitFor(() => { - fireEvent.click(screen.getByText('Install Ollama')); - }); - - expect(screen.getByText(/Waiting for Ollama/)).toBeInTheDocument(); - - // Simulate Ollama being detected - vi.mocked(ollamaDetection.hasModel).mockResolvedValue(true); - pollCallback!({ isRunning: true, host: 'http://127.0.0.1:11434' }); - - await waitFor(() => { - expect(screen.getByText('Ollama is detected and running')).toBeInTheDocument(); - }); - }); - }); - - describe('error states', () => { - it('should handle errors during initial check', async () => { - // Mock checkOllamaStatus to resolve with isRunning: false after an error - vi.mocked(ollamaDetection.checkOllamaStatus).mockResolvedValue({ - isRunning: false, - host: 'http://127.0.0.1:11434', - error: 'Network error', - }); - - render(); - - await waitFor(() => { - // Should still show not detected state - expect(screen.getByText('Ollama is not detected on your system')).toBeInTheDocument(); - }); - }); - }); -}); diff --git a/ui/desktop/src/components/OllamaSetup.tsx b/ui/desktop/src/components/OllamaSetup.tsx deleted file mode 100644 index da2053ab4e..0000000000 --- a/ui/desktop/src/components/OllamaSetup.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import { useState, useEffect, useRef } from 'react'; -import { useConfig } from './ConfigContext'; -import { - checkOllamaStatus, - getOllamaDownloadUrl, - pollForOllama, - hasModel, - pullOllamaModel, - getPreferredModel, - type PullProgress, -} from '../utils/ollamaDetection'; -import { toastService } from '../toasts'; -import { Ollama } from './icons'; -import { errorMessage } from '../utils/conversionUtils'; - -interface OllamaSetupProps { - onSuccess: () => void; - onCancel: () => void; -} - -export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) { - //const { addExtension, getExtensions, upsert } = useConfig(); - const { upsert } = useConfig(); - const [isChecking, setIsChecking] = useState(true); - const [ollamaDetected, setOllamaDetected] = useState(false); - const [isPolling, setIsPolling] = useState(false); - const [isConnecting, setIsConnecting] = useState(false); - const [modelStatus, setModelStatus] = useState< - 'checking' | 'available' | 'not-available' | 'downloading' - >('checking'); - const [downloadProgress, setDownloadProgress] = useState(null); - const stopPollingRef = useRef<(() => void) | null>(null); - - useEffect(() => { - // Check if Ollama is already running - const checkInitial = async () => { - const status = await checkOllamaStatus(); - setOllamaDetected(status.isRunning); - - // If Ollama is running, check for the preferred model - if (status.isRunning) { - const modelAvailable = await hasModel(getPreferredModel()); - setModelStatus(modelAvailable ? 'available' : 'not-available'); - } - - setIsChecking(false); - }; - checkInitial(); - - // Cleanup polling on unmount - return () => { - if (stopPollingRef.current) { - stopPollingRef.current(); - } - }; - }, []); - - const handleInstallClick = () => { - setIsPolling(true); - - // Start polling for Ollama - stopPollingRef.current = pollForOllama( - async (status) => { - setOllamaDetected(status.isRunning); - setIsPolling(false); - - // Check for the model - const modelAvailable = await hasModel(getPreferredModel()); - setModelStatus(modelAvailable ? 'available' : 'not-available'); - - toastService.success({ - title: 'Ollama Detected!', - msg: 'Ollama is now running. You can connect to it.', - }); - }, - 3000 // Check every 3 seconds - ); - }; - - const handleDownloadModel = async () => { - setModelStatus('downloading'); - setDownloadProgress({ status: 'Starting download...' }); - - const success = await pullOllamaModel(getPreferredModel(), (progress) => { - setDownloadProgress(progress); - }); - - if (success) { - setModelStatus('available'); - toastService.success({ - title: 'Model Downloaded!', - msg: `Successfully downloaded ${getPreferredModel()}`, - }); - } else { - setModelStatus('not-available'); - toastService.error({ - title: 'Download Failed', - msg: `Failed to download ${getPreferredModel()}. Please try again.`, - traceback: '', - }); - } - setDownloadProgress(null); - }; - - const handleConnectOllama = async () => { - setIsConnecting(true); - try { - // Set up Ollama configuration - await upsert('GOOSE_PROVIDER', 'ollama', false); - await upsert('GOOSE_MODEL', getPreferredModel(), false); - await upsert('OLLAMA_HOST', 'localhost', false); - - toastService.success({ - title: 'Success!', - msg: `Connected to Ollama with ${getPreferredModel()} model.`, - }); - - onSuccess(); - } catch (error) { - console.error('Failed to connect to Ollama:', error); - toastService.error({ - title: 'Connection Failed', - msg: `Failed to connect to Ollama: ${errorMessage(error)}`, - traceback: error instanceof Error ? error.stack || '' : '', - }); - setIsConnecting(false); - } - }; - - if (isChecking) { - return ( -
-
-
-
-

Checking for Ollama...

-
- ); - } - - return ( -
- {/* Header with icon above heading - left aligned like onboarding cards */} -
- -

Ollama Setup

-

- Ollama lets you run AI models for free, private and locally on your computer. -

-
- - {ollamaDetected ? ( -
-
- - Ollama is detected and running - -
- - {modelStatus === 'checking' ? ( -
-
-
- ) : modelStatus === 'not-available' ? ( -
-
-

- The {getPreferredModel()} model is not installed -

-

- This model is recommended for the best experience with Goose -

-
- -
- ) : modelStatus === 'downloading' ? ( -
-
-

Downloading {getPreferredModel()}...

- {downloadProgress && ( - <> -

{downloadProgress.status}

- {downloadProgress.total && downloadProgress.completed && ( -
-
-
-
-

- {Math.round((downloadProgress.completed / downloadProgress.total) * 100)}% -

-
- )} - - )} -
-
- ) : ( - - )} -
- ) : ( -
-
- - Ollama is not detected on your system - -
- - {isPolling ? ( -
-
-
-
-

Waiting for Ollama to start...

-

- Once Ollama is installed and running, we'll automatically detect it. -

-
- ) : ( - - Install Ollama - - )} -
- )} - - -
- ); -} diff --git a/ui/desktop/src/components/ProviderGuard.tsx b/ui/desktop/src/components/ProviderGuard.tsx index 99b3f5d0f4..d211366640 100644 --- a/ui/desktop/src/components/ProviderGuard.tsx +++ b/ui/desktop/src/components/ProviderGuard.tsx @@ -7,7 +7,6 @@ import { startTetrateSetup } from '../utils/tetrateSetup'; import { startChatGptCodexSetup } from '../utils/chatgptCodexSetup'; import WelcomeGooseLogo from './WelcomeGooseLogo'; import { toastService } from '../toasts'; -import { OllamaSetup } from './OllamaSetup'; import { LocalModelSetup } from './LocalModelSetup'; import ApiKeyTester from './ApiKeyTester'; import { SwitchModelModal } from './settings/models/subcomponents/SwitchModelModal'; @@ -34,7 +33,6 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG const [isChecking, setIsChecking] = useState(true); const [hasProvider, setHasProvider] = useState(false); const [showFirstTimeSetup, setShowFirstTimeSetup] = useState(false); - const [showOllamaSetup, setShowOllamaSetup] = useState(false); const [showLocalModelSetup, setShowLocalModelSetup] = useState(false); const [userInActiveSetup, setUserInActiveSetup] = useState(false); const [showSwitchModelModal, setShowSwitchModelModal] = useState(false); @@ -189,19 +187,6 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG } }; - const handleOllamaComplete = () => { - trackOnboardingCompleted('ollama'); - setShowOllamaSetup(false); - setShowFirstTimeSetup(false); - setHasProvider(true); - navigate('/', { replace: true }); - }; - - const handleOllamaCancel = () => { - trackOnboardingAbandoned('ollama_setup'); - setShowOllamaSetup(false); - }; - const handleLocalModelComplete = () => { trackOnboardingCompleted('local'); setShowLocalModelSetup(false); @@ -296,10 +281,6 @@ export default function ProviderGuard({ didSelectProvider, children }: ProviderG ); } - if (showOllamaSetup) { - return ; - } - if (showLocalModelSetup) { return (
diff --git a/ui/desktop/src/utils/ollamaDetection.test.ts b/ui/desktop/src/utils/ollamaDetection.test.ts deleted file mode 100644 index 07da6071b0..0000000000 --- a/ui/desktop/src/utils/ollamaDetection.test.ts +++ /dev/null @@ -1,323 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* global AbortSignal, TextEncoder, EventListener */ - -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { - checkOllamaStatus, - getOllamaModels, - hasModel, - pullOllamaModel, - pollForOllama, - getOllamaDownloadUrl, - getPreferredModel, -} from './ollamaDetection'; - -// Mock fetch globally -globalThis.fetch = vi.fn(); - -// Define global objects for testing environment if they don't exist -if (typeof globalThis.AbortSignal === 'undefined') { - globalThis.AbortSignal = class AbortSignal { - aborted = false; - reason: any = undefined; - onabort: ((this: AbortSignal, ev: Event) => any) | null = null; - - addEventListener(_type: string, _listener: EventListener): void { - // Mock implementation - } - - removeEventListener(_type: string, _listener: EventListener): void { - // Mock implementation - } - - dispatchEvent(_event: Event): boolean { - return true; - } - } as any; -} - -if (typeof globalThis.TextEncoder === 'undefined') { - globalThis.TextEncoder = class TextEncoder { - encode(str: string): Uint8Array { - return new Uint8Array(str.split('').map((c) => c.charCodeAt(0))); - } - } as any; -} - -describe('ollamaDetection', () => { - beforeEach(() => { - vi.clearAllMocks(); - vi.useFakeTimers(); - }); - - afterEach(() => { - vi.useRealTimers(); - }); - - describe('checkOllamaStatus', () => { - it('should return isRunning: true when Ollama is accessible', async () => { - (globalThis.fetch as any).mockResolvedValueOnce({ - ok: true, - json: async () => ({ models: [] }), - }); - - const result = await checkOllamaStatus(); - - expect(result).toEqual({ - isRunning: true, - host: 'http://127.0.0.1:11434', - }); - expect(globalThis.fetch).toHaveBeenCalledWith('http://127.0.0.1:11434/api/tags', { - method: 'GET', - signal: expect.any(globalThis.AbortSignal), - }); - }); - - it('should return isRunning: false when Ollama is not accessible', async () => { - (globalThis.fetch as any).mockRejectedValueOnce(new Error('Connection refused')); - - const result = await checkOllamaStatus(); - - expect(result).toEqual({ - isRunning: false, - host: 'http://127.0.0.1:11434', - error: 'Connection refused', - }); - }); - - it('should timeout after 2 seconds', async () => { - let abortSignal: AbortSignal | undefined; - - (globalThis.fetch as any).mockImplementationOnce((_url: string, options: any) => { - abortSignal = options.signal; - return new Promise((_, reject) => { - // Listen for abort signal - options.signal.addEventListener('abort', () => { - reject(new Error('The operation was aborted')); - }); - }); - }); - - const checkPromise = checkOllamaStatus(); - - // Fast-forward 2 seconds - vi.advanceTimersByTime(2000); - - // The abort signal should be triggered - expect(abortSignal?.aborted).toBe(true); - - const result = await checkPromise; - expect(result.isRunning).toBe(false); - expect(result.error).toBe('The operation was aborted'); - }); - }); - - describe('getOllamaModels', () => { - it('should return models when API call is successful', async () => { - const mockModels = [ - { name: 'llama2:latest', size: 4733363377, digest: 'abc123', modified_at: '2023-10-01' }, - { name: 'gpt-oss:20b', size: 13780173839, digest: 'def456', modified_at: '2023-10-02' }, - ]; - - (globalThis.fetch as any).mockResolvedValueOnce({ - ok: true, - json: async () => ({ models: mockModels }), - }); - - const result = await getOllamaModels(); - - expect(result).toEqual(mockModels); - }); - - it('should return empty array when API call fails', async () => { - (globalThis.fetch as any).mockRejectedValueOnce(new Error('Network error')); - - const result = await getOllamaModels(); - - expect(result).toEqual([]); - }); - - it('should handle non-ok responses', async () => { - (globalThis.fetch as any).mockResolvedValueOnce({ - ok: false, - statusText: 'Not Found', - }); - - const result = await getOllamaModels(); - - expect(result).toEqual([]); - }); - }); - - describe('hasModel', () => { - it('should return true when model exists', async () => { - (globalThis.fetch as any).mockResolvedValueOnce({ - ok: true, - json: async () => ({ - models: [ - { - name: 'llama2:latest', - size: 4733363377, - digest: 'abc123', - modified_at: '2023-10-01', - }, - { name: 'gpt-oss:20b', size: 13780173839, digest: 'def456', modified_at: '2023-10-02' }, - ], - }), - }); - - const result = await hasModel('gpt-oss:20b'); - - expect(result).toBe(true); - }); - - it('should return false when model does not exist', async () => { - (globalThis.fetch as any).mockResolvedValueOnce({ - ok: true, - json: async () => ({ - models: [ - { - name: 'llama2:latest', - size: 4733363377, - digest: 'abc123', - modified_at: '2023-10-01', - }, - ], - }), - }); - - const result = await hasModel('gpt-oss:20b'); - - expect(result).toBe(false); - }); - }); - - describe('pullOllamaModel', () => { - it('should successfully pull a model and report progress', async () => { - const progressUpdates: any[] = []; - const onProgress = vi.fn((progress) => progressUpdates.push(progress)); - - const mockResponse = { - ok: true, - body: { - getReader: () => ({ - read: vi - .fn() - .mockResolvedValueOnce({ - done: false, - value: new TextEncoder().encode( - JSON.stringify({ status: 'downloading', completed: 100, total: 1000 }) + '\n' - ), - }) - .mockResolvedValueOnce({ - done: false, - value: new TextEncoder().encode( - JSON.stringify({ status: 'downloading', completed: 500, total: 1000 }) + '\n' - ), - }) - .mockResolvedValueOnce({ - done: false, - value: new TextEncoder().encode(JSON.stringify({ status: 'success' }) + '\n'), - }) - .mockResolvedValueOnce({ done: true }), - }), - }, - }; - - (globalThis.fetch as any).mockResolvedValueOnce(mockResponse); - - const result = await pullOllamaModel('gpt-oss:20b', onProgress); - - expect(result).toBe(true); - expect(onProgress).toHaveBeenCalledTimes(3); - expect(progressUpdates).toContainEqual({ - status: 'downloading', - completed: 100, - total: 1000, - }); - expect(progressUpdates).toContainEqual({ - status: 'downloading', - completed: 500, - total: 1000, - }); - expect(progressUpdates).toContainEqual({ status: 'success' }); - }); - - it('should return false on API error', async () => { - (globalThis.fetch as any).mockResolvedValueOnce({ - ok: false, - statusText: 'Model not found', - }); - - const result = await pullOllamaModel('invalid-model'); - - expect(result).toBe(false); - }); - }); - - describe('pollForOllama', () => { - it('should poll until Ollama is detected', async () => { - const onDetected = vi.fn(); - - // First call: Ollama not running - (globalThis.fetch as any).mockRejectedValueOnce(new Error('Connection refused')); - - const stopPolling = pollForOllama(onDetected, 100); - - // Verify initial call was made - expect(globalThis.fetch).toHaveBeenCalledTimes(1); - - // Second call: Still not running - (globalThis.fetch as any).mockRejectedValueOnce(new Error('Connection refused')); - vi.advanceTimersByTime(100); - - // Third call: Ollama is running - (globalThis.fetch as any).mockResolvedValueOnce({ - ok: true, - json: async () => ({ models: [] }), - }); - vi.advanceTimersByTime(100); - - // Wait for async operations - await vi.runAllTimersAsync(); - - expect(onDetected).toHaveBeenCalledWith({ - isRunning: true, - host: 'http://127.0.0.1:11434', - }); - - stopPolling(); - }); - - it('should stop polling when stop function is called', () => { - const onDetected = vi.fn(); - - (globalThis.fetch as any).mockRejectedValue(new Error('Connection refused')); - - const stopPolling = pollForOllama(onDetected, 100); - - // Should make initial call - expect(globalThis.fetch).toHaveBeenCalledTimes(1); - - // Stop polling - stopPolling(); - - // Advance time and verify no more calls are made - vi.advanceTimersByTime(500); - - // Only the initial call should have been made - expect(globalThis.fetch).toHaveBeenCalledTimes(1); - expect(onDetected).not.toHaveBeenCalled(); - }); - }); - - describe('utility functions', () => { - it('should return correct download URL', () => { - expect(getOllamaDownloadUrl()).toBe('https://ollama.com/download'); - }); - - it('should return correct preferred model', () => { - expect(getPreferredModel()).toBe('gpt-oss:20b'); - }); - }); -}); diff --git a/ui/desktop/src/utils/ollamaDetection.ts b/ui/desktop/src/utils/ollamaDetection.ts deleted file mode 100644 index f639d9028a..0000000000 --- a/ui/desktop/src/utils/ollamaDetection.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { errorMessage } from './conversionUtils'; - -const DEFAULT_OLLAMA_HOST = 'http://127.0.0.1:11434'; -const OLLAMA_DOWNLOAD_URL = 'https://ollama.com/download'; -const PREFERRED_MODEL = 'gpt-oss:20b'; - -export interface OllamaStatus { - isRunning: boolean; - host: string; - error?: string; -} - -export interface OllamaModel { - name: string; - size: number; - digest: string; - modified_at: string; -} - -export interface PullProgress { - status: string; - digest?: string; - total?: number; - completed?: number; -} - -/** - * Check if Ollama is running on the default port - */ -export async function checkOllamaStatus(): Promise { - try { - // Create an AbortController for timeout - const controller = new AbortController(); - const timeoutId = window.setTimeout(() => controller.abort(), 2000); - - try { - // Ollama exposes a health endpoint at /api/tags - const response = await fetch(`${DEFAULT_OLLAMA_HOST}/api/tags`, { - method: 'GET', - signal: controller.signal, - }); - - window.clearTimeout(timeoutId); - - return { - isRunning: response.ok, - host: DEFAULT_OLLAMA_HOST, - }; - } catch (err) { - window.clearTimeout(timeoutId); - throw err; - } - } catch (error) { - return { - isRunning: false, - host: DEFAULT_OLLAMA_HOST, - error: errorMessage(error, 'Unknown error'), - }; - } -} - -/** - * Get the Ollama download URL - */ -export function getOllamaDownloadUrl(): string { - return OLLAMA_DOWNLOAD_URL; -} - -/** - * Get the preferred model name - */ -export function getPreferredModel(): string { - return PREFERRED_MODEL; -} - -/** - * Check which models are available in Ollama - */ -export async function getOllamaModels(): Promise { - try { - const controller = new AbortController(); - const timeoutId = window.setTimeout(() => controller.abort(), 5000); - - try { - const response = await fetch(`${DEFAULT_OLLAMA_HOST}/api/tags`, { - method: 'GET', - signal: controller.signal, - }); - - window.clearTimeout(timeoutId); - - if (!response.ok) { - throw new Error(`Failed to get models: ${response.statusText}`); - } - - const data = await response.json(); - return data.models || []; - } catch (err) { - window.clearTimeout(timeoutId); - throw err; - } - } catch (error) { - console.error('Failed to get Ollama models:', error); - return []; - } -} - -/** - * Check if a specific model is available - */ -export async function hasModel(modelName: string): Promise { - const models = await getOllamaModels(); - return models.some((model) => model.name === modelName); -} - -/** - * Pull a model from Ollama - */ -export async function pullOllamaModel( - modelName: string, - onProgress?: (progress: PullProgress) => void -): Promise { - try { - const response = await fetch(`${DEFAULT_OLLAMA_HOST}/api/pull`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - name: modelName, - stream: true, - }), - }); - - if (!response.ok) { - throw new Error(`Failed to pull model: ${response.statusText}`); - } - - const reader = response.body?.getReader(); - if (!reader) { - throw new Error('No response body'); - } - - const decoder = new window.TextDecoder(); - let done = false; - - while (!done) { - const { value, done: readerDone } = await reader.read(); - done = readerDone; - - if (value) { - const text = decoder.decode(value); - const lines = text.split('\n').filter((line) => line.trim()); - - for (const line of lines) { - try { - const progress = JSON.parse(line) as PullProgress; - if (onProgress) { - onProgress(progress); - } - } catch { - // Ignore parse errors - } - } - } - } - - return true; - } catch (error) { - console.error('Failed to pull model:', error); - return false; - } -} - -/** - * Poll for Ollama availability - * @param onDetected Callback when Ollama is detected - * @param intervalMs Polling interval in milliseconds - * @returns A function to stop polling - */ -export function pollForOllama( - onDetected: (status: OllamaStatus) => void, - intervalMs: number = 5000 -): () => void { - let intervalId: number | null = null; - let isPolling = true; - - const poll = async () => { - if (!isPolling) return; - - const status = await checkOllamaStatus(); - if (status.isRunning) { - onDetected(status); - stopPolling(); - } - }; - - const stopPolling = () => { - isPolling = false; - if (intervalId) { - window.clearInterval(intervalId); - intervalId = null; - } - }; - - // Start polling immediately - poll(); - - // Then poll at intervals - intervalId = window.setInterval(poll, intervalMs); - - return stopPolling; -}