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;
-}