From 4deebf7550810acb8e8a59799ae422f654833f44 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Tue, 31 Mar 2026 22:03:18 +1100 Subject: [PATCH] Add Inference Mesh settings tab (#8094) Signed-off-by: Michael Neale Signed-off-by: jh-block Co-authored-by: jh-block --- .../src/components/settings/SettingsView.tsx | 47 +- .../components/settings/mesh/MeshSection.tsx | 9 + .../components/settings/mesh/MeshSettings.tsx | 670 ++++++++++++++++++ ui/desktop/src/main.ts | 245 ++++++- ui/desktop/src/preload.ts | 18 + 5 files changed, 983 insertions(+), 6 deletions(-) create mode 100644 ui/desktop/src/components/settings/mesh/MeshSection.tsx create mode 100644 ui/desktop/src/components/settings/mesh/MeshSettings.tsx diff --git a/ui/desktop/src/components/settings/SettingsView.tsx b/ui/desktop/src/components/settings/SettingsView.tsx index fcdf9544..312c3747 100644 --- a/ui/desktop/src/components/settings/SettingsView.tsx +++ b/ui/desktop/src/components/settings/SettingsView.tsx @@ -9,7 +9,16 @@ import ConfigSettings from './config/ConfigSettings'; import PromptsSettingsSection from './PromptsSettingsSection'; import { ExtensionConfig } from '../../api'; import { MainPanelLayout } from '../Layout/MainPanelLayout'; -import { Bot, Share2, Monitor, MessageSquare, FileText, Keyboard, HardDrive } from 'lucide-react'; +import { + Bot, + Share2, + Monitor, + MessageSquare, + FileText, + Keyboard, + HardDrive, + Network, +} from 'lucide-react'; import { useState, useEffect, useRef } from 'react'; import TunnelSection from './tunnel/TunnelSection'; import GatewaySettingsSection from './gateways/GatewaySettingsSection'; @@ -17,6 +26,7 @@ import { getTunnelStatus } from '../../api/sdk.gen'; import ChatSettingsSection from './chat/ChatSettingsSection'; import KeyboardShortcutsSection from './keyboard/KeyboardShortcutsSection'; 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'; @@ -101,21 +111,29 @@ export default function SettingsView({ keyboard: 'keyboard', gateway: 'sharing', 'local-inference': 'local-inference', + mesh: 'mesh', }; const targetTab = sectionToTab[viewOptions.section]; - if (targetTab && (targetTab !== 'local-inference' || localInference)) { + if ( + targetTab && + (targetTab !== 'local-inference' || localInference) && + (targetTab !== 'mesh' || !tunnelDisabled) + ) { setActiveTab(targetTab); } } - }, [viewOptions.section, localInference]); + }, [viewOptions.section, localInference, tunnelDisabled]); - // Reset active tab if local-inference becomes unavailable + // Reset active tab if local-inference or mesh becomes unavailable useEffect(() => { if (!localInference && activeTab === 'local-inference') { setActiveTab('models'); } - }, [localInference, activeTab]); + if (tunnelDisabled && activeTab === 'mesh') { + setActiveTab('models'); + } + }, [localInference, tunnelDisabled, activeTab]); useEffect(() => { if (!hasTrackedInitialTab.current) { @@ -186,6 +204,16 @@ export default function SettingsView({ {intl.formatMessage(i18n.tabLocalInference)} )} + {!tunnelDisabled && ( + + + Mesh + + )} {intl.formatMessage(i18n.tabChat)} @@ -238,6 +266,15 @@ 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 new file mode 100644 index 00000000..ce60edba --- /dev/null +++ b/ui/desktop/src/components/settings/mesh/MeshSettings.tsx @@ -0,0 +1,670 @@ +import { useState, useEffect, useCallback, useRef } from 'react'; +import { + RefreshCw, + ExternalLink, + Zap, + Play, + Square, + Copy, + Check, + ChevronDown, + ChevronRight, + Download, +} 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 canAutoDownload = 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(true); + 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) { + setStatus((prev) => (prev === 'downloading' ? prev : 'not-installed')); + setStatusInfo({ running: false, installed: false, models: [] }); + } else { + setStatus((prev) => (prev === 'starting' || prev === 'downloading' ? prev : 'stopped')); + setStatusInfo({ ...result, models: [] }); + } + } catch { + setStatus((prev) => (prev === 'starting' || prev === 'downloading' ? prev : 'stopped')); + } finally { + setChecking(false); + } + }, []); + + 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); + setStatus('starting'); + try { + const args: string[] = []; + + if (mode === 'new') { + args.push('--model', selectedModel); + } else if (mode === 'join') { + if (!joinToken.trim()) { + setError('Paste an invite token to join a mesh'); + setStatus('stopped'); + return; + } + args.push('--join', joinToken.trim()); + if (!contributeGpu) { + args.push('--client'); + } + } else { + // auto + args.push('--auto'); + if (!contributeGpu) { + args.push('--client'); + } + } + + const result = await window.electron.startMesh(args); + if (!result.started) { + setError(result.error || 'Failed to start mesh-llm'); + setStatus('stopped'); + return; + } + // 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 downloadMesh = async () => { + setError(null); + setStatus('downloading'); + try { + const result = await window.electron.downloadMesh(); + if (result.downloaded) { + setStatusInfo((prev) => ({ + ...prev, + installed: true, + binaryPath: result.binaryPath, + })); + setStatus('stopped'); + } else { + setError(result.error || 'Download failed'); + setStatus('not-installed'); + } + } catch (err) { + setError(`Download failed: ${err}`); + setStatus('not-installed'); + } + }; + + 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 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 — offer download or install link */} + {status === 'not-installed' && ( +
+

Get started

+

+ mesh-llm is a small download (~19 MB) that manages local inference and mesh networking. + Models are downloaded separately when you start a mesh. +

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

Downloading mesh-llm...

+

+ Downloading and installing 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') && ( + + )} + + +
+ )} + + {/* 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. +

+ )} + + {/* 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 04b0db97..e9de61e9 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -23,7 +23,8 @@ import fsSync from 'node:fs'; import started from 'electron-squirrel-startup'; import path from 'node:path'; import os from 'node:os'; -import { spawn } from 'child_process'; +import { execFile, execFileSync, execSync, spawn } from 'child_process'; +import http from 'node:http'; import 'dotenv/config'; import { checkServerStatus } from './goosed'; import { startGoosed } from './goosed'; @@ -1564,6 +1565,248 @@ ipcMain.handle('select-file-or-directory', async (_event, defaultPath?: string) return null; }); +// ── Mesh-LLM lifecycle ────────────────────────────────────────────── + +const MESH_API_PORT = 9337; +const MESH_CONSOLE_PORT = 3131; +const MESH_DOWNLOAD_URL = + 'https://github.com/michaelneale/mesh-llm/releases/latest/download/mesh-bundle.tar.gz'; + +async function findMeshBinary(): Promise { + // 1. PATH + try { + const binPath = execSync('which mesh-llm 2>/dev/null || echo ""', { encoding: 'utf8' }).trim(); + if (binPath) return binPath; + } catch { + // ignore + } + + // 2. ~/.mesh-llm/ (our download location) + const meshDir = path.join(os.homedir(), '.mesh-llm', 'mesh-llm'); + if (fsSync.existsSync(meshDir)) return meshDir; + + // 3. ~/.local/bin/ + const localBin = path.join(os.homedir(), '.local', 'bin', 'mesh-llm'); + if (fsSync.existsSync(localBin)) return localBin; + + return null; +} + +ipcMain.handle('check-mesh', async () => { + const result: { + running: boolean; + installed: boolean; + models: string[]; + token?: string; + peerCount?: number; + nodeStatus?: string; + binaryPath?: string; + } = { running: false, installed: true, models: [] }; + + // Check if mesh-llm binary exists + const binary = await findMeshBinary(); + if (binary) { + result.binaryPath = binary; + } else { + result.installed = false; + // Still probe the API — maybe it's running from somewhere unexpected + } + + // Probe the API + try { + const modelsData: { running: boolean; models: string[] } = await new Promise((resolve) => { + const req = http.get(`http://localhost:${MESH_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 running, also grab console status for invite token + peer info + if (result.running) { + try { + const statusData: { token?: string; peerCount?: number; nodeStatus?: string } = + await new Promise((resolve) => { + const req = http.get( + `http://localhost:${MESH_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 — that's fine + } + } + + return result; +}); + +ipcMain.handle('start-mesh', async (_event, args: string[]) => { + const binary = await findMeshBinary(); + if (!binary) { + return { + started: false, + error: 'mesh-llm not found. Download it first from the Mesh settings tab.', + }; + } + + // Log to ~/.mesh-llm/mesh-llm.log + 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'); + + // Spawn detached — mesh-llm outlives Goose. + // Wait briefly for early spawn errors (bad permissions, missing binary, etc.) + const child = spawn(binary, args, { + detached: true, + stdio: ['ignore', out, out], + }); + + const result = await new Promise<{ started: boolean; error?: string; pid?: number }>( + (resolve) => { + const timeout = setTimeout(() => { + child.removeAllListeners('error'); + child.unref(); + resolve({ started: true, pid: child.pid }); + }, 500); + + child.once('error', (err) => { + clearTimeout(timeout); + resolve({ started: false, error: `Failed to spawn mesh-llm: ${err.message}` }); + }); + } + ); + + fsSync.closeSync(out); + return result; +}); + +ipcMain.handle('stop-mesh', async () => { + try { + const binary = await findMeshBinary(); + if (!binary) { + return { stopped: false }; + } + execFileSync(binary, ['stop'], { timeout: 5000, encoding: 'utf8' }); + return { stopped: true }; + } catch { + return { stopped: false }; + } +}); + +function execFileP(cmd: string, args: string[], opts: { timeout: number }): Promise { + return new Promise((resolve, reject) => { + execFile(cmd, args, opts, (err) => (err ? reject(err) : resolve())); + }); +} + +ipcMain.handle('download-mesh', async () => { + if (process.platform !== 'darwin' || process.arch !== 'arm64') { + return { downloaded: false, 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 { + // Download and extract — mesh-bundle.tar.gz contains mesh-bundle/{mesh-llm,rpc-server,llama-server} + await execFileP('curl', ['-fsSL', '-o', tarball, MESH_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 { downloaded: false, error: 'Download succeeded but mesh-llm binary not found' }; + } + + // macOS: ad-hoc codesign + clear quarantine to avoid Gatekeeper prompts + if (process.platform === 'darwin') { + 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 { downloaded: true, binaryPath: binary }; + } catch (err) { + return { downloaded: false, error: `Download failed: ${err}` }; + } finally { + try { fsSync.unlinkSync(tarball); } catch { /* ignore */ } + } +}); + ipcMain.handle('check-ollama', async () => { try { return new Promise((resolve) => { diff --git a/ui/desktop/src/preload.ts b/ui/desktop/src/preload.ts index bf1a620c..269a1e40 100644 --- a/ui/desktop/src/preload.ts +++ b/ui/desktop/src/preload.ts @@ -101,6 +101,7 @@ export interface CreateChatWindowOptions { // Define the API types in a single place type ElectronAPI = { platform: string; + arch: string; reactReady: () => void; getConfig: () => Record; hideWindow: () => void; @@ -114,6 +115,18 @@ type ElectronAPI = { fetchMetadata: (url: string) => Promise; 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 }>; + downloadMesh: () => Promise<{ downloaded: boolean; error?: string; binaryPath?: string }>; selectFileOrDirectory: (defaultPath?: string) => Promise; getBinaryPath: (binaryName: string) => Promise; readFile: (directory: string) => Promise; @@ -180,6 +193,7 @@ type AppConfigAPI = { const electronAPI: ElectronAPI = { platform: process.platform, + arch: process.arch, reactReady: () => ipcRenderer.send('react-ready'), getConfig: () => { if (!config || Object.keys(config).length === 0) { @@ -201,6 +215,10 @@ const electronAPI: ElectronAPI = { fetchMetadata: (url: string) => ipcRenderer.invoke('fetch-metadata', 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'), + downloadMesh: () => ipcRenderer.invoke('download-mesh'), selectFileOrDirectory: (defaultPath?: string) => ipcRenderer.invoke('select-file-or-directory', defaultPath), getBinaryPath: (binaryName: string) => ipcRenderer.invoke('get-binary-path', binaryName),