From 274e9521c68d168e95f894d920ef33cac7e2486b Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Thu, 25 Jun 2026 06:52:47 +1000 Subject: [PATCH] chore: removed mesh on ui (#9982) --- ui/desktop/src/App.tsx | 17 - .../src/components/settings/SettingsView.tsx | 37 +- .../components/settings/mesh/MeshSection.tsx | 9 - .../components/settings/mesh/MeshSettings.tsx | 660 ------------------ ui/desktop/src/main.ts | 23 - ui/desktop/src/mesh.ts | 319 --------- ui/desktop/src/preload.ts | 14 - 7 files changed, 4 insertions(+), 1075 deletions(-) delete mode 100644 ui/desktop/src/components/settings/mesh/MeshSection.tsx delete mode 100644 ui/desktop/src/components/settings/mesh/MeshSettings.tsx delete mode 100644 ui/desktop/src/mesh.ts diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 64128f965..f536fcdab 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -506,23 +506,6 @@ export function AppInner() { }; }, []); - // Show a toast if mesh is the configured provider but isn't running. - useEffect(() => { - const handler = () => { - toast.warn( - "Inference Mesh is set as your provider but isn't running. Open Settings → Mesh to start it. Keep goose running to stay connected.", - { - autoClose: false, - toastId: 'mesh-not-running', - } - ); - }; - window.electron.on('mesh-not-running', handler); - return () => { - window.electron.off('mesh-not-running', handler); - }; - }, []); - // Prevent default drag and drop behavior globally to avoid opening files in new windows // but allow our React components to handle drops in designated areas useEffect(() => { diff --git a/ui/desktop/src/components/settings/SettingsView.tsx b/ui/desktop/src/components/settings/SettingsView.tsx index 318f49e28..24c21f790 100644 --- a/ui/desktop/src/components/settings/SettingsView.tsx +++ b/ui/desktop/src/components/settings/SettingsView.tsx @@ -17,7 +17,6 @@ import { FileText, Keyboard, HardDrive, - Network, KeyRound, } from 'lucide-react'; import { useState, useEffect, useRef } from 'react'; @@ -27,7 +26,6 @@ import ChatSettingsSection from './chat/ChatSettingsSection'; import KeyboardShortcutsSection from './keyboard/KeyboardShortcutsSection'; import AuthSettingsSection from './auth/AuthSettingsSection'; import LocalInferenceSection from './localInference/LocalInferenceSection'; -import MeshSection from './mesh/MeshSection'; import { CONFIGURATION_ENABLED } from '../../updates'; import { trackSettingsTabViewed } from '../../utils/analytics'; import { useFeatures } from '../../contexts/FeaturesContext'; @@ -117,29 +115,21 @@ export default function SettingsView({ auth: 'auth', gateway: 'sharing', 'local-inference': 'local-inference', - mesh: 'mesh', }; const targetTab = sectionToTab[viewOptions.section]; - if ( - targetTab && - (targetTab !== 'local-inference' || localInference) && - (targetTab !== 'mesh' || !tunnelDisabled) - ) { + if (targetTab && (targetTab !== 'local-inference' || localInference)) { setActiveTab(targetTab); } } - }, [viewOptions.section, localInference, tunnelDisabled]); + }, [viewOptions.section, localInference]); - // Reset active tab if local-inference or mesh becomes unavailable + // Reset active tab if local-inference becomes unavailable useEffect(() => { if (!localInference && activeTab === 'local-inference') { setActiveTab('models'); } - if (tunnelDisabled && activeTab === 'mesh') { - setActiveTab('models'); - } - }, [localInference, tunnelDisabled, activeTab]); + }, [localInference, activeTab]); useEffect(() => { if (!hasTrackedInitialTab.current) { @@ -210,16 +200,6 @@ export default function SettingsView({ {intl.formatMessage(i18n.tabLocalInference)} )} - {!tunnelDisabled && ( - - - Mesh - - )} {intl.formatMessage(i18n.tabChat)} @@ -276,15 +256,6 @@ export default function SettingsView({ )} - {!tunnelDisabled && ( - - - - )} - - - - ); -} diff --git a/ui/desktop/src/components/settings/mesh/MeshSettings.tsx b/ui/desktop/src/components/settings/mesh/MeshSettings.tsx deleted file mode 100644 index bba79c37f..000000000 --- a/ui/desktop/src/components/settings/mesh/MeshSettings.tsx +++ /dev/null @@ -1,660 +0,0 @@ -import { useState, useEffect, useCallback, useRef } from 'react'; -import { - RefreshCw, - ExternalLink, - Zap, - Play, - Square, - Copy, - Check, - ChevronDown, - ChevronRight, -} from 'lucide-react'; -import { Button } from '../../ui/button'; -import { Input } from '../../ui/input'; -import { - setConfigProvider, - updateCustomProvider, - createCustomProvider, - getCustomProvider, -} from '../../../api'; -import { useModelAndProvider } from '../../ModelAndProviderContext'; -const MESH_API_PORT = 9337; -const MESH_CONSOLE_PORT = 3131; -const MESH_DEFAULT_MODEL = 'Qwen3-30B-A3B-Q4_K_M'; - -// Popular models from mesh-llm catalog, grouped by size -const MODEL_CATALOG = [ - { name: 'Qwen3-4B-Q4_K_M', size: '~3 GB', tier: 'small' }, - { name: 'Qwen3-8B-Q4_K_M', size: '~5 GB', tier: 'small' }, - { name: 'Qwen3-14B-Q4_K_M', size: '~9 GB', tier: 'medium' }, - { name: 'Devstral-Small-2505-Q4_K_M', size: '~14 GB', tier: 'medium' }, - { name: 'Qwen3-30B-A3B-Q4_K_M', size: '~17 GB', tier: 'large' }, - { name: 'GLM-4.7-Flash-Q4_K_M', size: '~17 GB', tier: 'large' }, - { name: 'Qwen3-32B-Q4_K_M', size: '~20 GB', tier: 'large' }, - { name: 'Qwen2.5-Coder-32B-Instruct-Q4_K_M', size: '~20 GB', tier: 'large' }, - { name: 'Qwen2.5-72B-Instruct-Q4_K_M', size: '~42 GB', tier: 'xlarge' }, -]; - -type MeshMode = 'new' | 'join' | 'auto'; -type MeshStatus = 'unknown' | 'running' | 'stopped' | 'starting' | 'not-installed' | 'downloading'; - -interface MeshStatusInfo { - running: boolean; - installed: boolean; - models: string[]; - token?: string; - peerCount?: number; - nodeStatus?: string; - binaryPath?: string; -} - -export const MeshSettings = () => { - const { refreshCurrentModelAndProvider } = useModelAndProvider(); - const isMacOS = window.electron.platform === 'darwin' && window.electron.arch === 'arm64'; - const [status, setStatus] = useState('unknown'); - const [statusInfo, setStatusInfo] = useState({ - running: false, - installed: true, - models: [], - }); - const [mode, setMode] = useState('auto'); - const [selectedModel, setSelectedModel] = useState(MESH_DEFAULT_MODEL); - const [joinToken, setJoinToken] = useState(''); - const [contributeGpu, setContributeGpu] = useState(false); - const [copiedToken, setCopiedToken] = useState(false); - const [showAdvanced, setShowAdvanced] = useState(false); - const [saving, setSaving] = useState(false); - const [error, setError] = useState(null); - const [activeModel, setActiveModel] = useState(null); - const [meshProviderId, setMeshProviderIdState] = useState( - () => localStorage.getItem('mesh-provider-id') ?? 'mesh' - ); - const setMeshProviderId = (id: string) => { - setMeshProviderIdState(id); - localStorage.setItem('mesh-provider-id', id); - }; - const [checking, setChecking] = useState(false); - const startTimeoutRef = useRef | null>(null); - - const checkStatus = useCallback(async () => { - setChecking(true); - try { - const result = await window.electron.checkMesh(); - if (result.running) { - setStatus('running'); - setStatusInfo(result); - } else if (!result.installed && !isMacOS) { - // On non-macOS, binary must be manually installed. - setStatus((prev) => (prev === 'downloading' ? prev : 'not-installed')); - setStatusInfo({ running: false, installed: false, models: [] }); - } else { - // On macOS, start-mesh handles downloading, so treat not-installed as stopped. - setStatus((prev) => (prev === 'starting' || prev === 'downloading' ? prev : 'stopped')); - setStatusInfo({ ...result, models: [] }); - } - } catch { - setStatus((prev) => (prev === 'starting' || prev === 'downloading' ? prev : 'stopped')); - } finally { - setChecking(false); - } - }, [isMacOS]); - - useEffect(() => { - checkStatus(); - const interval = setInterval(checkStatus, status === 'starting' ? 3000 : 10000); - return () => clearInterval(interval); - }, [checkStatus, status]); - - const meshProviderBody = (models: string[]) => ({ - engine: 'openai_compatible' as const, - display_name: 'Inference Mesh', - api_url: `http://localhost:${MESH_API_PORT}`, - api_key: '', - models, - supports_streaming: true, - requires_auth: false, - }); - - // Create or update the mesh custom provider via the REST API, - // which handles file writes and registry refresh atomically. - // Returns the provider ID to use with setConfigProvider. - const ensureMeshProvider = async (models: string[]): Promise => { - const modelList = models.length > 0 ? models : [MESH_DEFAULT_MODEL]; - const body = meshProviderBody(modelList); - - // Try the last-known provider ID first, then fall back to 'mesh' - const idsToTry = meshProviderId === 'mesh' ? ['mesh'] : [meshProviderId, 'mesh']; - - for (const id of idsToTry) { - const existing = await getCustomProvider({ path: { id } }); - if (existing.data) { - await updateCustomProvider({ - path: { id }, - body, - throwOnError: true, - }); - setMeshProviderId(id); - return id; - } - } - - // Provider doesn't exist yet — create it - const result = await createCustomProvider({ - body, - throwOnError: true, - }); - const newId = result.data?.provider_name ?? 'mesh'; - setMeshProviderId(newId); - return newId; - }; - - const activateModel = async (modelId: string) => { - setSaving(true); - setError(null); - try { - const providerId = await ensureMeshProvider(statusInfo.models); - await setConfigProvider({ - body: { provider: providerId, model: modelId }, - throwOnError: true, - }); - await refreshCurrentModelAndProvider(); - setActiveModel(modelId); - } catch (err) { - setError(`Failed to activate model: ${err}`); - } finally { - setSaving(false); - } - }; - - const startMesh = async () => { - setError(null); - // On macOS, start-mesh downloads the latest binary first. - setStatus(isMacOS ? 'downloading' : 'starting'); - try { - const args: string[] = []; - - if (mode === 'new') { - args.push('serve', '--model', selectedModel); - } else if (mode === 'join') { - if (!joinToken.trim()) { - setError('Paste an invite token to join a mesh'); - setStatus('stopped'); - return; - } - if (contributeGpu) { - args.push('serve', '--join', joinToken.trim()); - } else { - args.push('client', '--join', joinToken.trim()); - } - } else { - // auto - if (contributeGpu) { - args.push('serve', '--auto'); - } else { - args.push('client', '--auto'); - } - } - - const result = await window.electron.startMesh(args); - if (!result.started) { - setError(result.error || 'Failed to start mesh-llm'); - setStatus('stopped'); - return; - } - setStatus('starting'); - // Polling will pick up when it's ready. Timeout after 5 min so - // the UI doesn't get stuck in "starting" if the daemon crashes. - if (startTimeoutRef.current) { - clearTimeout(startTimeoutRef.current); - } - startTimeoutRef.current = setTimeout(() => { - startTimeoutRef.current = null; - setStatus((prev) => { - if (prev === 'starting') { - setError('mesh-llm did not become ready. Check ~/.mesh-llm/mesh-llm.log'); - return 'stopped'; - } - return prev; - }); - }, 300000); - } catch (err) { - setError(`Failed to start: ${err}`); - setStatus('stopped'); - } - }; - - const stopMesh = async () => { - try { - const result = await window.electron.stopMesh(); - if (result.stopped) { - setStatus('stopped'); - setStatusInfo((prev) => ({ ...prev, running: false, models: [], token: undefined })); - } else { - setError('Failed to stop mesh-llm'); - } - } catch { - setError('Failed to stop mesh-llm'); - } - }; - - const copyToken = () => { - if (statusInfo.token) { - navigator.clipboard.writeText(statusInfo.token); - setCopiedToken(true); - setTimeout(() => setCopiedToken(false), 2000); - } - }; - - const StatusIndicator = () => { - switch (status) { - case 'running': - return ( - - - Running — {statusInfo.models.length} model - {statusInfo.models.length !== 1 ? 's' : ''} available - {statusInfo.peerCount !== undefined && statusInfo.peerCount > 0 && ( - - · {statusInfo.peerCount} peer{statusInfo.peerCount !== 1 ? 's' : ''} - - )} - - ); - case 'starting': - return ( - - - Starting — this may take a minute if downloading a model... - - ); - case 'downloading': - return ( - - - Downloading latest mesh-llm (~19 MB)... - - ); - case 'not-installed': - return ( - - - mesh-llm not installed - - ); - case 'stopped': - return ( - - {checking ? ( - - ) : ( - - )} - Not running - - ); - default: - return checking ? ( - - - Checking... - - ) : null; - } - }; - - return ( -
- {/* Header */} -
-
-

Inference Mesh

- - - Learn more - -
-

- Experimental. Pool GPUs with others - for decentralized LLM inference — no API keys, no cloud. Start a private mesh, join one - with an invite token, or discover public meshes.{' '} - - docs.anarchai.org - -

-
- -
- {error &&

{error}

} -
- - {/* Not installed — non-macOS only; on macOS start-mesh handles the download */} - {status === 'not-installed' && ( -
-

Get started

-

- mesh-llm is not installed. Follow the install guide to set it up, or connect to a mesh - already running on this machine. -

-
- - - - -
-
- )} - - {/* Downloading */} - {status === 'downloading' && ( -
-

Downloading latest mesh-llm...

-

- Fetching the latest version to ~/.mesh-llm/. This should only take a moment. -

-
- )} - - {/* Setup panel — shown when stopped and installed */} - {(status === 'stopped' || status === 'unknown') && ( -
- {/* Mode selector */} -
- - - - - -
- - {/* Mode-specific options */} - {mode === 'new' && ( -
- - -

- Downloads automatically if not already cached. Larger models need more VRAM. -

-
- )} - - {mode === 'join' && ( -
- - setJoinToken(e.target.value)} - placeholder="Paste invite token here" - className="max-w-md" - /> -
- )} - - {(mode === 'auto' || mode === 'join') && ( - - )} - - - -

- When you start the mesh, keep goose running to stay connected. -

-
- )} - - {/* Starting indicator */} - {status === 'starting' && ( -
-

Starting mesh-llm...

-

- Connecting to the mesh and loading models. This may take a minute on first run. -

-
- )} - - {/* Running state */} - {status === 'running' && ( - <> - {/* Invite token */} - {statusInfo.token && ( -
-
-
-

Invite token

-

- Share this with others so they can join your mesh. -

-
- -
- - {statusInfo.token} - -
- )} - - {/* Model list */} - {statusInfo.models.length > 0 && ( -
-

Available Models

-

- Select a model to use it as your Goose provider. -

-
- {statusInfo.models.map((modelId) => { - const isActive = activeModel === modelId; - return ( -
!saving && activateModel(modelId)} - > -
-
- {modelId} - live -
- {isActive ? ( - Active - ) : ( - - )} -
-
- ); - })} -
-
- )} - - {statusInfo.models.length === 0 && ( -

- Mesh is running but no models are available yet. A model may still be loading. -

- )} - -

- Keep goose running to stay connected to the mesh. -

- - {/* Actions row */} -
- - - - Open Console - -
- - )} - - {/* Advanced settings */} -
- - - {showAdvanced && ( -
- {statusInfo.binaryPath && ( -
- - {statusInfo.binaryPath} -
- )} -
- - http://localhost:{MESH_API_PORT}/v1 -
-
- - - http://localhost:{MESH_CONSOLE_PORT} - -
-
- )} -
- - {/* Refresh */} -
- -
-
- ); -}; diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index cd8b69ac3..fc06dbb69 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -50,7 +50,6 @@ import { UPDATES_ENABLED } from './updates'; import './utils/recipeHash'; import { Client } from './api/client'; import { GooseApp } from './api'; -import * as mesh from './mesh'; import installExtension, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer'; import { BLOCKED_PROTOCOLS, WEB_PROTOCOLS } from './utils/urlSecurity'; import { buildCSP } from './utils/csp'; @@ -1093,19 +1092,6 @@ const createChat = async (app: App, options: CreateChatOptions = {}) => { stopErrorLogCollection(); errorLog.length = 0; - // Nudge the user if mesh is their provider but isn't running. - // Delay to let the renderer mount before sending the IPC event. - setTimeout(() => { - mesh - .checkProviderRunning(goosedClient) - .then((ok) => { - if (!ok && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('mesh-not-running'); - } - }) - .catch(() => {}); - }, 5000); - // Let windowStateKeeper manage the window mainWindowState.manage(mainWindow); @@ -2037,12 +2023,6 @@ ipcMain.handle('select-import-session-file', async () => { } }); -// ── Mesh-LLM lifecycle (see mesh.ts) ──────────────────────────────── - -ipcMain.handle('check-mesh', () => mesh.check()); -ipcMain.handle('start-mesh', (_event, args: string[]) => mesh.start(args)); -ipcMain.handle('stop-mesh', () => mesh.stop()); - ipcMain.handle('check-ollama', async () => { try { return new Promise((resolve) => { @@ -2925,9 +2905,6 @@ async function getAllowList(): Promise { } app.on('will-quit', async () => { - // Stop the mesh child process if we spawned one. - mesh.cleanup(); - const goosedLeases = new Set(goosedLeasesByWindowId.values()); if (goosedLeases.size > 0) { log.info(`App quitting, terminating ${goosedLeases.size} goosed server(s)`); diff --git a/ui/desktop/src/mesh.ts b/ui/desktop/src/mesh.ts deleted file mode 100644 index d45a204e9..000000000 --- a/ui/desktop/src/mesh.ts +++ /dev/null @@ -1,319 +0,0 @@ -/** - * mesh-llm process lifecycle — download, start, stop, auto-start. - * - * macOS (Apple Silicon) only for download/spawn; other platforms can still - * probe the API port to detect an externally-running mesh. - */ - -import { execFile, execFileSync, spawn } from 'child_process'; -import path from 'node:path'; -import os from 'node:os'; -import fsSync from 'node:fs'; -import http from 'node:http'; -import { Buffer } from 'node:buffer'; -import log from './utils/logger'; -import { Client } from './api/client'; -import { readConfig } from './api/sdk.gen'; - -const API_PORT = 9337; -const CONSOLE_PORT = 3131; -const DOWNLOAD_URL = - 'https://github.com/michaelneale/mesh-llm/releases/latest/download/mesh-bundle.tar.gz'; - -let childProcess: ReturnType | null = null; - -function execFileP(cmd: string, args: string[], opts: { timeout: number }): Promise { - return new Promise((resolve, reject) => { - execFile(cmd, args, opts, (err) => (err ? reject(err) : resolve())); - }); -} - -// ── Binary discovery ──────────────────────────────────────────────── - -export async function findBinary(): Promise { - try { - const binPath = execFileSync('which', ['mesh-llm'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); - if (binPath) return binPath; - } catch { - // ignore — which returns non-zero if not found - } - - const meshDir = path.join(os.homedir(), '.mesh-llm', 'mesh-llm'); - if (fsSync.existsSync(meshDir)) return meshDir; - - const localBin = path.join(os.homedir(), '.local', 'bin', 'mesh-llm'); - if (fsSync.existsSync(localBin)) return localBin; - - return null; -} - -export async function downloadBinary(): Promise<{ binary: string } | { error: string }> { - if (process.platform !== 'darwin' || process.arch !== 'arm64') { - return { error: 'Auto-download is only available on macOS (Apple Silicon)' }; - } - - const installDir = path.join(os.homedir(), '.mesh-llm'); - if (!fsSync.existsSync(installDir)) { - fsSync.mkdirSync(installDir, { recursive: true }); - } - - const tarball = path.join(installDir, 'mesh-bundle.tar.gz'); - try { - await execFileP('curl', ['-fsSL', '-o', tarball, DOWNLOAD_URL], { timeout: 120000 }); - await execFileP('tar', ['xz', '--strip-components=1', '-C', installDir, '-f', tarball], { - timeout: 30000, - }); - - const binary = path.join(installDir, 'mesh-llm'); - if (!fsSync.existsSync(binary)) { - return { error: 'Download succeeded but mesh-llm binary not found' }; - } - - for (const name of ['mesh-llm', 'rpc-server', 'llama-server']) { - const bin = path.join(installDir, name); - if (fsSync.existsSync(bin)) { - try { - await execFileP('codesign', ['-s', '-', bin], { timeout: 10000 }); - } catch { - /* codesign may fail if already signed */ - } - try { - await execFileP('xattr', ['-cr', bin], { timeout: 10000 }); - } catch { - /* xattr may fail */ - } - } - } - - return { binary }; - } catch (err) { - return { error: `Download failed: ${err}` }; - } finally { - try { - fsSync.unlinkSync(tarball); - } catch { - /* ignore */ - } - } -} - -// ── Port probing ──────────────────────────────────────────────────── - -export function isRunning(): Promise { - return new Promise((resolve) => { - const req = http.get(`http://localhost:${API_PORT}/v1/models`, { timeout: 2000 }, (res) => { - res.resume(); - resolve(res.statusCode === 200); - }); - req.on('error', () => resolve(false)); - req.on('timeout', () => { - req.destroy(); - resolve(false); - }); - }); -} - -// ── Status check (used by check-mesh IPC) ─────────────────────────── - -export interface MeshStatus { - running: boolean; - installed: boolean; - models: string[]; - token?: string; - peerCount?: number; - nodeStatus?: string; - binaryPath?: string; -} - -export async function check(): Promise { - const result: MeshStatus = { running: false, installed: true, models: [] }; - - const binary = await findBinary(); - if (binary) { - result.binaryPath = binary; - } else { - result.installed = false; - } - - // Probe the API - try { - const modelsData = await new Promise<{ running: boolean; models: string[] }>((resolve) => { - const req = http.get(`http://localhost:${API_PORT}/v1/models`, { timeout: 3000 }, (res) => { - let body = ''; - res.on('data', (chunk: Buffer) => { - body += chunk.toString(); - }); - res.on('end', () => { - try { - if (res.statusCode !== 200) { - resolve({ running: false, models: [] }); - return; - } - const data = JSON.parse(body); - if (!Array.isArray(data.data)) { - resolve({ running: false, models: [] }); - return; - } - const models = data.data - .filter((m: { id?: unknown }) => typeof m.id === 'string') - .map((m: { id: string }) => m.id); - resolve({ running: true, models }); - } catch { - resolve({ running: false, models: [] }); - } - }); - }); - req.on('error', () => resolve({ running: false, models: [] })); - req.on('timeout', () => { - req.destroy(); - resolve({ running: false, models: [] }); - }); - }); - - result.running = modelsData.running; - result.models = modelsData.models; - } catch { - // API not reachable - } - - if (result.running) { - try { - const statusData = await new Promise<{ - token?: string; - peerCount?: number; - nodeStatus?: string; - }>((resolve) => { - const req = http.get( - `http://localhost:${CONSOLE_PORT}/api/status`, - { timeout: 3000 }, - (res) => { - let body = ''; - res.on('data', (chunk: Buffer) => { - body += chunk.toString(); - }); - res.on('end', () => { - try { - const data = JSON.parse(body); - resolve({ - token: data.token, - peerCount: Array.isArray(data.peers) ? data.peers.length : undefined, - nodeStatus: data.node_status, - }); - } catch { - resolve({}); - } - }); - } - ); - req.on('error', () => resolve({})); - req.on('timeout', () => { - req.destroy(); - resolve({}); - }); - }); - result.token = statusData.token; - result.peerCount = statusData.peerCount; - result.nodeStatus = statusData.nodeStatus; - } catch { - // console not available - } - } - - return result; -} - -// ── Start / stop ──────────────────────────────────────────────────── - -function spawnAttached( - binary: string, - args: string[] -): Promise<{ started: boolean; error?: string; pid?: number }> { - const logDir = path.join(os.homedir(), '.mesh-llm'); - if (!fsSync.existsSync(logDir)) fsSync.mkdirSync(logDir, { recursive: true }); - const logPath = path.join(logDir, 'mesh-llm.log'); - const out = fsSync.openSync(logPath, 'a'); - - const child = spawn(binary, args, { stdio: ['ignore', out, out] }); - childProcess = child; - child.on('exit', () => { - if (childProcess === child) childProcess = null; - }); - - return new Promise((resolve) => { - const timeout = setTimeout(() => { - child.removeAllListeners('error'); - resolve({ started: true, pid: child.pid }); - }, 500); - - child.once('error', (err) => { - clearTimeout(timeout); - childProcess = null; - resolve({ started: false, error: `Failed to spawn mesh-llm: ${err.message}` }); - }); - }).then((result) => { - fsSync.closeSync(out); - return result as { started: boolean; error?: string; pid?: number }; - }); -} - -export async function start( - args: string[] -): Promise<{ started: boolean; error?: string; pid?: number; alreadyRunning?: boolean }> { - if (await isRunning()) { - return { started: true, alreadyRunning: true }; - } - - const dlResult = await downloadBinary(); - let binary: string; - if ('error' in dlResult) { - const existing = await findBinary(); - if (!existing) { - return { started: false, error: dlResult.error }; - } - binary = existing; - } else { - binary = dlResult.binary; - } - - return spawnAttached(binary, args); -} - -export async function stop(): Promise<{ stopped: boolean }> { - if (childProcess) { - cleanup(); - return { stopped: true }; - } - try { - const binary = await findBinary(); - if (!binary) return { stopped: false }; - execFileSync(binary, ['stop'], { timeout: 5000, encoding: 'utf8' }); - return { stopped: true }; - } catch { - return { stopped: false }; - } -} - -export function cleanup(): void { - if (!childProcess) return; - try { - childProcess.kill('SIGTERM'); - } catch { - /* already dead */ - } - childProcess = null; -} - -// ── Startup check ─────────────────────────────────────────────────── - -export async function checkProviderRunning(goosedClient: Client): Promise { - const res = await readConfig({ - body: { key: 'GOOSE_PROVIDER', is_secret: false }, - client: goosedClient, - }); - const provider = typeof res.data === 'string' ? res.data : String(res.data ?? ''); - if (!provider.includes('mesh')) return true; - if (await isRunning()) return true; - - log.info('Mesh provider configured but not running'); - return false; -} diff --git a/ui/desktop/src/preload.ts b/ui/desktop/src/preload.ts index 9f0bda06d..4a4d90655 100644 --- a/ui/desktop/src/preload.ts +++ b/ui/desktop/src/preload.ts @@ -114,17 +114,6 @@ type ElectronAPI = { openInChrome: (url: string) => void; reloadApp: () => void; checkForOllama: () => Promise; - checkMesh: () => Promise<{ - running: boolean; - installed: boolean; - models: string[]; - token?: string; - peerCount?: number; - nodeStatus?: string; - binaryPath?: string; - }>; - startMesh: (args: string[]) => Promise<{ started: boolean; error?: string; pid?: number }>; - stopMesh: () => Promise<{ stopped: boolean }>; selectFileOrDirectory: (defaultPath?: string) => Promise; selectImportSessionFile: () => Promise<{ filePath: string; @@ -223,9 +212,6 @@ const electronAPI: ElectronAPI = { openInChrome: (url: string) => ipcRenderer.send('open-in-chrome', url), reloadApp: () => ipcRenderer.send('reload-app'), checkForOllama: () => ipcRenderer.invoke('check-ollama'), - checkMesh: () => ipcRenderer.invoke('check-mesh'), - startMesh: (args: string[]) => ipcRenderer.invoke('start-mesh', args), - stopMesh: () => ipcRenderer.invoke('stop-mesh'), selectFileOrDirectory: (defaultPath?: string) => ipcRenderer.invoke('select-file-or-directory', defaultPath),