diff --git a/package.json b/package.json index 78cdd77..2f0415c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "gerbil", "productName": "Gerbil", - "version": "1.5.5", + "version": "1.6.0-beta-1", "description": "Run Large Language Models locally", "main": "out/main/index.js", "homepage": "./", diff --git a/src/components/App/PerformanceBadge.tsx b/src/components/App/PerformanceBadge.tsx index 0606615..77b476d 100644 --- a/src/components/App/PerformanceBadge.tsx +++ b/src/components/App/PerformanceBadge.tsx @@ -1,15 +1,18 @@ -import { Button, Tooltip } from '@mantine/core'; +import { Button, Tooltip, ActionIcon } from '@mantine/core'; +import { Activity } from 'lucide-react'; interface PerformanceBadgeProps { - label: string; - value: string; + label?: string; + value?: string; tooltipLabel: string; + iconOnly?: boolean; } export const PerformanceBadge = ({ label, value, tooltipLabel, + iconOnly = false, }: PerformanceBadgeProps) => { const handlePerformanceClick = async () => { const result = await window.electronAPI.app.openPerformanceManager(); @@ -21,6 +24,16 @@ export const PerformanceBadge = ({ } }; + if (iconOnly) { + return ( + + + + + + ); + } + return ( ); } - if (hasUpdate && onUpdate) { + if (versionInfo.hasUpdate && onUpdate) { buttons.push( + ); + } + + if (hasVersionMismatch && onRedownload) { + buttons.push( + ); } @@ -117,7 +141,7 @@ export const DownloadCard = ({ withBorder radius="sm" padding="sm" - {...(isCurrent && { + {...(versionInfo.isCurrent && { bg: colorScheme === 'dark' ? 'dark.6' : 'gray.0', bd: `2px solid var(--mantine-color-${colorScheme === 'dark' ? 'blue-4' : 'blue-6'})`, })} @@ -126,19 +150,19 @@ export const DownloadCard = ({
- {pretifyBinName(name)} + {pretifyBinName(versionInfo.name)} - {isCurrent && ( + {versionInfo.isCurrent && ( Current )} - {hasUpdate && ( + {versionInfo.hasUpdate && ( Update Available )} - {isWindowsROCmBuild(name) && ( + {isWindowsROCmBuild(versionInfo.name) && ( Experimental @@ -150,9 +174,15 @@ export const DownloadCard = ({ )} - {version && ( + {versionInfo.version && ( - Version {version} + Version {versionInfo.version} + {hasVersionMismatch && versionInfo.actualVersion && ( + + {' '} + (actual: {versionInfo.actualVersion}) + + )} )} {size && ( diff --git a/src/components/InfoCard.tsx b/src/components/InfoCard.tsx new file mode 100644 index 0000000..a6fea01 --- /dev/null +++ b/src/components/InfoCard.tsx @@ -0,0 +1,91 @@ +import { + rem, + ActionIcon, + Tooltip, + Card, + Stack, + Group, + Text, +} from '@mantine/core'; +import { Copy } from 'lucide-react'; +import { safeExecute } from '@/utils/logger'; + +export interface InfoItem { + label: string; + value: string; +} + +interface InfoCardProps { + title: string; + items: InfoItem[]; + loading?: boolean; +} + +export const InfoCard = ({ title, items, loading = false }: InfoCardProps) => { + const copyInfo = async () => { + const info = items.map((item) => `${item.label}: ${item.value}`).join('\n'); + + await safeExecute( + () => navigator.clipboard.writeText(info), + `Failed to copy ${title.toLowerCase()}` + ); + }; + + if (loading) { + return ( + + + {title} + + + Loading... + + + ); + } + + return ( + + + + + + + + + {title} + + + + {items.map((item, index) => ( + + + {item.label}: + + + {item.value} + + + ))} + + + ); +}; diff --git a/src/components/screens/Download.tsx b/src/components/screens/Download.tsx index 60e6b0c..de09fa7 100644 --- a/src/components/screens/Download.tsx +++ b/src/components/screens/Download.tsx @@ -82,12 +82,19 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => { ref={isDownloading ? downloadingItemRef : null} > { - const [versionInfo, setVersionInfo] = useState(null); + const [versionInfo, setVersionInfo] = useState( + null + ); + const [hardwareInfo, setHardwareInfo] = useState(null); const { handleLogoClick, getLogoStyles } = useLogoClickSounds(); + useEffect(() => { const loadVersionInfo = async () => { const info = await window.electronAPI.app.getVersionInfo(); @@ -29,7 +38,35 @@ export const AboutTab = () => { setVersionInfo(info); } }; + + const loadHardwareInfo = async () => { + try { + const [cpu, gpu, gpuCapabilities, gpuMemory, systemMemory] = + await Promise.all([ + window.electronAPI.kobold.detectCPU(), + window.electronAPI.kobold.detectGPU(), + window.electronAPI.kobold.detectGPUCapabilities(), + window.electronAPI.kobold.detectGPUMemory(), + window.electronAPI.kobold.detectSystemMemory(), + ]); + + setHardwareInfo({ + cpu, + gpu, + gpuCapabilities, + gpuMemory, + systemMemory, + }); + } catch (error) { + window.electronAPI.logs.logError( + 'Failed to load hardware info', + error as Error + ); + } + }; + loadVersionInfo(); + loadHardwareInfo(); }, []); if (!versionInfo) { @@ -40,47 +77,9 @@ export const AboutTab = () => { ); } - const versionItems = [ - { - label: PRODUCT_NAME, - value: versionInfo.aurPackageVersion - ? (() => { - const pkgrel = versionInfo.aurPackageVersion.split('-')[1]; - - return pkgrel - ? `${versionInfo.appVersion} (AUR${pkgrel !== '1' ? ' r' + pkgrel : ''})` - : versionInfo.appVersion; - })() - : versionInfo.appVersion, - }, - { label: 'Electron', value: versionInfo.electronVersion }, - { - label: 'Node.js', - value: versionInfo.nodeJsSystemVersion - ? `${versionInfo.nodeVersion} (System: ${versionInfo.nodeJsSystemVersion})` - : versionInfo.nodeVersion, - }, - { label: 'Chromium', value: versionInfo.chromeVersion }, - { label: 'V8', value: versionInfo.v8Version }, - { - label: 'OS', - value: `${versionInfo.platform} ${versionInfo.arch} (${versionInfo.osVersion})`, - }, - ...(versionInfo.uvVersion - ? [{ label: 'uv', value: versionInfo.uvVersion }] - : []), - ]; - - const copyVersionInfo = async () => { - const info = versionItems - .map((item) => `${item.label}: ${item.value}`) - .join('\n'); - - await safeExecute( - () => navigator.clipboard.writeText(info), - 'Failed to copy version info' - ); - }; + const softwareItems = createSoftwareItems(versionInfo); + const driverItems = hardwareInfo ? createDriverItems(hardwareInfo) : []; + const hardwareItems = hardwareInfo ? createHardwareItems(hardwareInfo) : []; const actionButtons = [ { @@ -158,48 +157,15 @@ export const AboutTab = () => { - - - - - - - - {versionItems.map((item, index) => ( - - - {item.label}: - - - {item.value} - - - ))} - - + + + + + ); }; diff --git a/src/components/settings/VersionsTab.tsx b/src/components/settings/VersionsTab.tsx index 00606af..1c1c7ec 100644 --- a/src/components/settings/VersionsTab.tsx +++ b/src/components/settings/VersionsTab.tsx @@ -20,18 +20,7 @@ import { formatDownloadSize } from '@/utils/format'; import { useKoboldVersionsStore } from '@/stores/koboldVersions'; import type { InstalledVersion, ReleaseWithStatus } from '@/types/electron'; - -interface VersionInfo { - name: string; - version: string; - size?: number; - isInstalled: boolean; - isCurrent: boolean; - downloadUrl?: string; - installedPath?: string; - hasUpdate?: boolean; - newerVersion?: string; -} +import type { VersionInfo } from '@/types'; export const VersionsTab = () => { const { @@ -119,6 +108,7 @@ export const VersionsTab = () => { installedPath: installedVersion.path, hasUpdate, newerVersion: hasUpdate ? download.version : undefined, + actualVersion: installedVersion.actualVersion, }); } else { versions.push({ @@ -146,6 +136,7 @@ export const VersionsTab = () => { isInstalled: true, isCurrent, installedPath: installed.path, + actualVersion: installed.actualVersion, }); } }); @@ -194,16 +185,31 @@ export const VersionsTab = () => { await loadInstalledVersions(); }; - const makeCurrent = async (version: VersionInfo) => { + const handleRedownload = async (version: VersionInfo) => { + const download = availableDownloads.find((d) => d.name === version.name); + if (!download) return; + + await handleDownloadFromStore({ + item: download, + isUpdate: true, + wasCurrentBinary: version.isCurrent, + oldVersionPath: version.installedPath, + }); + + await loadInstalledVersions(); + }; + + const makeCurrent = (version: VersionInfo) => { if (!version.installedPath) return; - const success = await window.electronAPI.kobold.setCurrentVersion( - version.installedPath + const targetInstalledVersion = installedVersions.find( + (v) => v.path === version.installedPath ); - - if (success) { - await loadInstalledVersions(); + if (targetInstalledVersion) { + setCurrentVersion(targetInstalledVersion); } + + window.electronAPI.kobold.setCurrentVersion(version.installedPath); }; if (loadingInstalled || loadingPlatform || loadingRemote) { @@ -257,21 +263,16 @@ export const VersionsTab = () => { ref={isDownloading ? downloadingItemRef : null} > { e.stopPropagation(); handleDownload(version); @@ -280,6 +281,10 @@ export const VersionsTab = () => { e.stopPropagation(); handleUpdate(version); }} + onRedownload={(e) => { + e.stopPropagation(); + handleRedownload(version); + }} onMakeCurrent={() => makeCurrent(version)} />
diff --git a/src/hooks/useWarnings.ts b/src/hooks/useWarnings.ts index b80312e..6dd9f86 100644 --- a/src/hooks/useWarnings.ts +++ b/src/hooks/useWarnings.ts @@ -116,7 +116,7 @@ const checkVramWarnings = async (backend: string): Promise => { if (lowVramGpus.length > 0) { const memoryDetails = lowVramGpus - .map((gpu) => `${gpu.deviceName}: ${gpu.totalMemoryGB!.toFixed(1)}GB`) + .map((gpu) => `${gpu.totalMemoryGB!.toFixed(1)}GB`) .join(', '); warnings.push({ diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 332bcc3..01f285b 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -57,6 +57,7 @@ import { detectGPUCapabilities, detectGPUMemory, detectROCm, + detectSystemMemory, } from '@/main/modules/hardware'; import { detectBackendSupport, @@ -121,6 +122,8 @@ export function setupIPCHandlers() { ipcMain.handle('kobold:detectGPUMemory', () => detectGPUMemory()); + ipcMain.handle('kobold:detectSystemMemory', () => detectSystemMemory()); + ipcMain.handle('kobold:detectROCm', () => detectROCm()); ipcMain.handle('kobold:detectBackendSupport', () => detectBackendSupport()); diff --git a/src/main/modules/comfyui.ts b/src/main/modules/comfyui.ts index 816e114..95ab36e 100644 --- a/src/main/modules/comfyui.ts +++ b/src/main/modules/comfyui.ts @@ -15,14 +15,14 @@ import type { ChildProcess } from 'child_process'; import yauzl from 'yauzl'; import { createWriteStream } from 'fs'; -import { logError, safeExecute } from '@/utils/node/logging'; +import { logError, safeExecute, tryExecute } from '@/utils/node/logging'; import { sendKoboldOutput } from './window'; import { getInstallDir } from './config'; import { COMFYUI, SERVER_READY_SIGNALS, GITHUB_API } from '@/constants'; import { terminateProcess } from '@/utils/node/process'; import { parseKoboldConfig } from '@/utils/node/kobold'; import { getAppVersion, ensureDir } from '@/utils/node/fs'; -import { getGPUData } from '@/utils/node/gpu'; +import { detectGPU } from './hardware'; import { getUvEnvironment } from './dependencies'; interface ComfyUIVersionInfo { @@ -61,15 +61,10 @@ async function saveComfyUIVersion( workspaceDir: string, version: ComfyUIVersionInfo ) { - try { + await tryExecute(async () => { const versionFile = join(workspaceDir, '.version.json'); await writeFile(versionFile, JSON.stringify(version, null, 2)); - } catch (error) { - logError( - 'Failed to save ComfyUI version info', - error instanceof Error ? error : undefined - ); - } + }, 'Failed to save ComfyUI version info'); } async function shouldUpdateComfyUI(workspaceDir: string) { @@ -102,9 +97,8 @@ on('SIGTERM', () => { async function shouldForceCPUMode() { try { - const gpus = await getGPUData(); - const hasAMD = gpus.some((gpu) => gpu.deviceName.includes('AMD')); - const hasNVIDIA = gpus.some((gpu) => gpu.deviceName.includes('NVIDIA')); + const gpuInfo = await detectGPU(); + const { hasAMD, hasNVIDIA } = gpuInfo; const isWindows = platform === 'win32'; return (hasAMD && isWindows) || (!hasAMD && !hasNVIDIA); @@ -125,9 +119,8 @@ async function getPyTorchInstallArgs(pythonPath: string) { ]; try { - const gpus = await getGPUData(); - const hasAMD = gpus.some((gpu) => gpu.deviceName.includes('AMD')); - const hasNVIDIA = gpus.some((gpu) => gpu.deviceName.includes('NVIDIA')); + const gpuInfo = await detectGPU(); + const { hasAMD, hasNVIDIA } = gpuInfo; const isWindows = platform === 'win32'; if (hasAMD && !isWindows) { @@ -597,10 +590,7 @@ export async function startFrontend(args: string[]) { await waitForComfyUIToStart(); } catch (error) { - logError( - 'Failed to start ComfyUI', - error instanceof Error ? error : undefined - ); + logError('Failed to start ComfyUI', error as Error); throw error; } } diff --git a/src/main/modules/hardware.ts b/src/main/modules/hardware.ts index cf0a0c1..43233a5 100644 --- a/src/main/modules/hardware.ts +++ b/src/main/modules/hardware.ts @@ -27,7 +27,9 @@ export async function detectCPU() { const devices: string[] = []; if (cpu.brand) { - devices.push(formatDeviceName(cpu.brand)); + devices.push( + `${formatDeviceName(cpu.brand)} (${cpu.physicalCores} cores, ${cpu.speed} GHz)` + ); } const avx = flags.includes('avx') || flags.includes('AVX'); @@ -59,35 +61,28 @@ export async function detectGPU() { } const result = await safeExecute(async () => { - const gpuData = await getGPUData(); + const graphics = await si.graphics(); let hasAMD = false; let hasNVIDIA = false; const gpuInfo: string[] = []; - for (const gpu of gpuData) { - if (gpu.deviceName) { - gpuInfo.push(formatDeviceName(gpu.deviceName)); - } - - const deviceName = gpu.deviceName?.toLowerCase() || ''; - + for (const controller of graphics.controllers) { + // Check vendor for AMD if ( - deviceName.includes('amd') || - deviceName.includes('ati') || - deviceName.includes('radeon') + controller.vendor?.toLowerCase().includes('amd') || + controller.vendor?.toLowerCase().includes('ati') ) { hasAMD = true; } - if ( - deviceName.includes('nvidia') || - deviceName.includes('geforce') || - deviceName.includes('gtx') || - deviceName.includes('rtx') - ) { + if (controller.vendor?.toLowerCase().includes('nvidia')) { hasNVIDIA = true; } + + if (controller.model) { + gpuInfo.push(controller.model); + } } const basicInfo = { @@ -95,7 +90,6 @@ export async function detectGPU() { hasNVIDIA, gpuInfo: gpuInfo.length > 0 ? gpuInfo : ['No GPU information available'], }; - basicGPUInfoCache = basicInfo; return basicInfo; }, 'GPU detection failed'); @@ -133,7 +127,7 @@ async function detectCUDA() { try { const { stdout } = await execa( 'nvidia-smi', - ['--query-gpu=name,memory.total,memory.free', '--format=csv,noheader'], + ['--query-gpu=name,driver_version', '--format=csv,noheader,nounits'], { timeout: 5000, reject: false, @@ -141,19 +135,41 @@ async function detectCUDA() { ); if (stdout.trim()) { - const devices = stdout - .trim() - .split('\n') - .map((line) => { - const parts = line.split(','); - const rawName = parts[0]?.trim() || 'Unknown NVIDIA GPU'; - return formatDeviceName(rawName); - }) - .filter(Boolean); + // Check for error messages that indicate nvidia-smi failed + const errorPatterns = [ + 'NVIDIA-SMI has failed', + 'No devices found', + 'Unable to determine the device handle', + "couldn't communicate with the NVIDIA driver", + 'No NVIDIA GPU found', + ]; + + const hasError = errorPatterns.some((pattern) => + stdout.toLowerCase().includes(pattern.toLowerCase()) + ); + + if (hasError) { + return { supported: false, devices: [] } as const; + } + + const lines = stdout.trim().split('\n'); + const devices: string[] = []; + let version: string | undefined; + + for (const line of lines) { + const parts = line.split(','); + const rawName = parts[0]?.trim() || 'Unknown NVIDIA GPU'; + devices.push(formatDeviceName(rawName)); + + if (!version && parts[1]?.trim()) { + version = parts[1].trim(); + } + } return { supported: devices.length > 0, devices, + version, } as const; } @@ -228,9 +244,28 @@ export async function detectROCm() { } } + let version: string | undefined; + try { + const { stdout: amdSmiOutput } = await execa('amd-smi', { + timeout: 3000, + reject: false, + }); + + if (amdSmiOutput.trim()) { + const match = + amdSmiOutput.match(/ROCm version:\s*(\d+\.\d+\.\d+)/i) || + amdSmiOutput.match(/version\s*(\d+\.\d+\.\d+)/i) || + amdSmiOutput.match(/(\d+\.\d+\.\d+)/); + if (match) { + version = match[1]; + } + } + } catch {} + return { supported: devices.length > 0, devices, + version, }; } @@ -263,15 +298,29 @@ async function detectVulkan() { } } + let version = 'Unknown'; + try { + const apiVersionLine = lines.find( + (line) => line.includes('apiVersion') && line.includes('=') + ); + if (apiVersionLine) { + const match = apiVersionLine.match(/=\s*(\d+\.\d+(?:\.\d+)?)/); + if (match) { + version = match[1]; + } + } + } catch {} + return { supported: devices.length > 0, devices, + version, }; } - return { supported: false, devices: [] }; + return { supported: false, devices: [], version: 'Unknown' }; } catch { - return { supported: false, devices: [] }; + return { supported: false, devices: [], version: 'Unknown' }; } } @@ -336,15 +385,34 @@ async function detectCLBlast() { if (stdout.trim()) { const devices = parseClInfoOutput(stdout); + + let version = 'Unknown'; + try { + const versionLine = stdout + .split('\n') + .find( + (line) => + line.includes('OpenCL C version') || + line.includes('CL_DEVICE_OPENCL_C_VERSION') + ); + if (versionLine) { + const match = versionLine.match(/OpenCL C\s+(\d+\.\d+)/); + if (match) { + version = match[1]; + } + } + } catch {} + return { supported: devices.length > 0, devices, + version, }; } - return { supported: false, devices: [] }; + return { supported: false, devices: [], version: 'Unknown' }; } catch { - return { supported: false, devices: [] }; + return { supported: false, devices: [], version: 'Unknown' }; } } @@ -358,18 +426,15 @@ export async function detectGPUMemory() { const memoryInfo: GPUMemoryInfo[] = []; for (const gpu of gpuData) { - if (gpu.deviceName) { - let vram: number | null = gpu.memoryTotal; + let vram: number | null = gpu.memoryTotal; - if (!vram || vram <= 1) { - vram = null; - } - - memoryInfo.push({ - deviceName: formatDeviceName(gpu.deviceName), - totalMemoryGB: vram, - }); + if (!vram || vram <= 1) { + vram = null; } + + memoryInfo.push({ + totalMemoryGB: vram, + }); } return memoryInfo; @@ -378,3 +443,44 @@ export async function detectGPUMemory() { gpuMemoryInfoCache = result || []; return gpuMemoryInfoCache; } + +export const detectSystemMemory = async () => { + try { + const [memInfo, memLayout] = await Promise.all([si.mem(), si.memLayout()]); + + const totalGB = Math.round(memInfo.total / 1024 ** 3); + + let speed: number | undefined; + let type: string | undefined; + + if (memLayout && memLayout.length > 0) { + const populatedSlot = memLayout.find( + (slot) => + slot.size > 0 && + ((slot.clockSpeed && slot.clockSpeed > 0) || + (slot.type && slot.type !== '')) + ); + + if (populatedSlot) { + speed = + populatedSlot.clockSpeed && populatedSlot.clockSpeed > 0 + ? populatedSlot.clockSpeed + : undefined; + type = + populatedSlot.type && populatedSlot.type !== '' + ? populatedSlot.type + : undefined; + } + } + + return { + totalGB, + speed, + type, + }; + } catch { + return { + totalGB: Math.round((await si.mem()).total / 1024 ** 3), + }; + } +}; diff --git a/src/main/modules/koboldcpp/version.ts b/src/main/modules/koboldcpp/version.ts index 81d08e1..eb3b316 100644 --- a/src/main/modules/koboldcpp/version.ts +++ b/src/main/modules/koboldcpp/version.ts @@ -43,14 +43,18 @@ export async function getInstalledVersions() { const versionPromises = launchers.map(async (launcher) => { try { - const detectedVersion = await getVersionFromBinary(launcher.path); - const version = detectedVersion || 'unknown'; + const versionInfo = await getVersionFromBinary(launcher.path); + + if (!versionInfo) { + return null; + } return { - version, + version: versionInfo.version, path: launcher.path, filename: launcher.filename, size: launcher.size, + actualVersion: versionInfo.actualVersion, } as InstalledVersion; } catch (error) { logError( @@ -131,42 +135,56 @@ export async function getVersionFromBinary(launcherPath: string) { return null; } + let folderVersion: string | null = null; + let actualVersion: string | null = null; + const folderName = launcherPath.split(/[/\\]/).slice(-2, -1)[0]; if (folderName) { const versionMatch = folderName.match( /-(\d+\.\d+(?:\.\d+)?(?:\.[a-zA-Z0-9]+)*(?:-[a-zA-Z0-9]+)*)$/ ); if (versionMatch) { - return versionMatch[1]; + folderVersion = versionMatch[1]; } } - const result = await execa(launcherPath, ['--version'], { - timeout: 30000, - stdio: ['ignore', 'pipe', 'pipe'], - }); + try { + const result = await execa(launcherPath, ['--version'], { + timeout: 30000, + stdio: ['ignore', 'pipe', 'pipe'], + }); - const allOutput = (result.stdout + result.stderr).trim(); + const allOutput = (result.stdout + result.stderr).trim(); - if (/^\d+\.\d+/.test(allOutput)) { - const versionParts = allOutput.split(/\s+/)[0]; - if (versionParts && /^\d+\.\d+/.test(versionParts)) { - return versionParts; - } - } - - const lines = allOutput.split('\n'); - for (const line of lines) { - const trimmedLine = line.trim(); - if (/^\d+\.\d+/.test(trimmedLine)) { - const versionPart = trimmedLine.split(/\s+/)[0]; - if (versionPart) { - return versionPart; + if (/^\d+\.\d+/.test(allOutput)) { + const versionParts = allOutput.split(/\s+/)[0]; + if (versionParts && /^\d+\.\d+/.test(versionParts)) { + actualVersion = versionParts; } } - } - return null; + if (!actualVersion) { + const lines = allOutput.split('\n'); + for (const line of lines) { + const trimmedLine = line.trim(); + if (/^\d+\.\d+/.test(trimmedLine)) { + const versionPart = trimmedLine.split(/\s+/)[0]; + if (versionPart) { + actualVersion = versionPart; + break; + } + } + } + } + } catch {} + + return { + version: folderVersion || actualVersion || 'unknown', + actualVersion: + folderVersion && actualVersion && folderVersion !== actualVersion + ? actualVersion + : undefined, + }; } catch { return null; } diff --git a/src/main/modules/monitoring.ts b/src/main/modules/monitoring.ts index 71b561f..878f264 100644 --- a/src/main/modules/monitoring.ts +++ b/src/main/modules/monitoring.ts @@ -3,6 +3,7 @@ import { BrowserWindow } from 'electron'; import { platform } from 'process'; import { spawn } from 'child_process'; import { getGPUData } from '@/utils/node/gpu'; +import { detectGPU } from './hardware'; import { tryExecute, safeExecute } from '@/utils/node/logging'; export interface CpuMetrics { @@ -134,18 +135,18 @@ async function collectAndSendMemoryMetrics() { async function collectAndSendGpuMetrics() { await tryExecute(async () => { - const gpuData = await getGPUData(); + const [gpuData, gpuInfo] = await Promise.all([getGPUData(), detectGPU()]); const metrics: GpuMetrics = { - gpus: gpuData.map((gpuInfo) => ({ - name: gpuInfo.deviceName, - usage: Math.round(gpuInfo.usage), - memoryUsed: gpuInfo.memoryUsed, - memoryTotal: gpuInfo.memoryTotal, + gpus: gpuData.map((gpuData, index) => ({ + name: gpuInfo.gpuInfo[index] || `GPU ${index}`, + usage: Math.round(gpuData.usage), + memoryUsed: gpuData.memoryUsed, + memoryTotal: gpuData.memoryTotal, memoryUsage: - gpuInfo.memoryTotal > 0 - ? Math.round((gpuInfo.memoryUsed / gpuInfo.memoryTotal) * 100) + gpuData.memoryTotal > 0 + ? Math.round((gpuData.memoryUsed / gpuData.memoryTotal) * 100) : 0, - temperature: gpuInfo.temperature, + temperature: gpuData.temperature, })), }; diff --git a/src/main/modules/openwebui.ts b/src/main/modules/openwebui.ts index 06c5c2b..8e26832 100644 --- a/src/main/modules/openwebui.ts +++ b/src/main/modules/openwebui.ts @@ -14,14 +14,7 @@ import { getUvEnvironment } from './dependencies'; let openWebUIProcess: ChildProcess | null = null; -const OPENWEBUI_BASE_ARGS = [ - '--python', - '3.11', - '--with', - 'itsdangerous', - 'open-webui@latest', - 'serve', -]; +const OPENWEBUI_BASE_ARGS = ['--python', '3.11', 'open-webui@latest', 'serve']; on('SIGINT', () => { void stopFrontend(); diff --git a/src/main/modules/platform-gpu.ts b/src/main/modules/platform-gpu.ts new file mode 100644 index 0000000..a9751bd --- /dev/null +++ b/src/main/modules/platform-gpu.ts @@ -0,0 +1,35 @@ +import { platform } from 'process'; +import { detectGPU } from './hardware'; +import { safeExecute } from '@/utils/node/logging'; + +export interface PlatformGPUInfo { + hasAMD: boolean; + hasNVIDIA: boolean; + isWindows: boolean; + shouldForceCPU: boolean; +} + +export async function getPlatformGPUInfo(): Promise { + const result = await safeExecute(async () => { + const gpuInfo = await detectGPU(); + const { hasAMD, hasNVIDIA } = gpuInfo; + const isWindows = platform === 'win32'; + const shouldForceCPU = (hasAMD && isWindows) || (!hasAMD && !hasNVIDIA); + + return { + hasAMD, + hasNVIDIA, + isWindows, + shouldForceCPU, + }; + }, 'Failed to detect platform GPU information'); + + return ( + result || { + hasAMD: false, + hasNVIDIA: false, + isWindows: platform === 'win32', + shouldForceCPU: true, + } + ); +} diff --git a/src/preload/index.ts b/src/preload/index.ts index 67c14cd..0bfb6e3 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -26,6 +26,7 @@ const koboldAPI: KoboldAPI = { detectGPUCapabilities: () => ipcRenderer.invoke('kobold:detectGPUCapabilities'), detectGPUMemory: () => ipcRenderer.invoke('kobold:detectGPUMemory'), + detectSystemMemory: () => ipcRenderer.invoke('kobold:detectSystemMemory'), detectROCm: () => ipcRenderer.invoke('kobold:detectROCm'), detectBackendSupport: () => ipcRenderer.invoke('kobold:detectBackendSupport'), getAvailableBackends: (includeDisabled) => diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index b896853..5a724ce 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -3,6 +3,7 @@ import type { GPUCapabilities, BasicGPUInfo, GPUMemoryInfo, + SystemMemoryInfo, } from '@/types/hardware'; import type { BackendOption, BackendSupport } from '@/types'; import type { MantineColorScheme } from '@mantine/core'; @@ -55,6 +56,7 @@ export interface InstalledVersion { path: string; filename: string; size?: number; + actualVersion?: string; } export interface DownloadItem { @@ -112,6 +114,7 @@ export interface KoboldAPI { detectCPU: () => Promise; detectGPUCapabilities: () => Promise; detectGPUMemory: () => Promise; + detectSystemMemory: () => Promise; detectROCm: () => Promise<{ supported: boolean; devices: string[] }>; detectBackendSupport: () => Promise; getAvailableBackends: (includeDisabled?: boolean) => Promise; @@ -141,7 +144,7 @@ export interface KoboldAPI { onKoboldOutput: (callback: (data: string) => void) => () => void; } -export interface VersionInfo { +export interface SystemVersionInfo { appVersion: string; electronVersion: string; nodeVersion: string; @@ -160,7 +163,7 @@ export interface AppAPI { viewConfigFile: () => Promise; openPath: (path: string) => Promise; getVersion: () => Promise; - getVersionInfo: () => Promise; + getVersionInfo: () => Promise; minimizeWindow: () => void; maximizeWindow: () => void; closeWindow: () => void; diff --git a/src/types/hardware.d.ts b/src/types/hardware.d.ts index a64a75f..d869968 100644 --- a/src/types/hardware.d.ts +++ b/src/types/hardware.d.ts @@ -5,26 +5,35 @@ export interface CPUCapabilities { } export interface GPUMemoryInfo { - deviceName: string; totalMemoryGB: number | null; } +export interface SystemMemoryInfo { + totalGB: number; + speed?: number; + type?: string; +} + export interface GPUCapabilities { cuda: { readonly supported: boolean; readonly devices: readonly string[]; + readonly version?: string; }; rocm: { readonly supported: boolean; readonly devices: readonly string[]; + readonly version?: string; }; vulkan: { readonly supported: boolean; readonly devices: readonly string[]; + readonly version?: string; }; clblast: { readonly supported: boolean; readonly devices: readonly string[]; + readonly version?: string; }; } @@ -43,9 +52,6 @@ export interface HardwareInfo { cpu: CPUCapabilities; gpu: BasicGPUInfo; gpuCapabilities?: GPUCapabilities; -} - -export interface SystemCapabilities { - hardware: HardwareInfo; - platform: string; + gpuMemory?: GPUMemoryInfo[]; + systemMemory?: SystemMemoryInfo; } diff --git a/src/types/index.d.ts b/src/types/index.d.ts index f694483..dd8c362 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -45,6 +45,20 @@ export interface InstalledVersion { path: string; filename: string; size?: number; + actualVersion?: string; +} + +export interface VersionInfo { + name: string; + version: string; + size?: number; + isInstalled: boolean; + isCurrent: boolean; + downloadUrl?: string; + installedPath?: string; + hasUpdate?: boolean; + newerVersion?: string; + actualVersion?: string; } export interface DismissedUpdate { diff --git a/src/utils/node/gpu.ts b/src/utils/node/gpu.ts index 031e548..e3b3dcc 100644 --- a/src/utils/node/gpu.ts +++ b/src/utils/node/gpu.ts @@ -3,14 +3,12 @@ import { join } from 'path'; import { platform } from 'process'; interface CachedGPUInfo { - deviceName: string; devicePath: string; memoryTotal: number; hwmonPath?: string; } interface GPUData { - deviceName: string; usage: number; memoryUsed: number; memoryTotal: number; @@ -38,7 +36,6 @@ async function initializeLinuxGPUCache() { return linuxCachePromise; } - // eslint-disable-next-line sonarjs/cognitive-complexity linuxCachePromise = (async () => { try { const drmPath = '/sys/class/drm'; @@ -50,42 +47,6 @@ async function initializeLinuxGPUCache() { const gpus = []; for (const card of cardEntries) { const devicePath = join(drmPath, card, 'device'); - - let deviceName = 'Unknown GPU'; - - try { - const modalias = await readFile( - `${devicePath}/modalias`, - 'utf8' - ).catch(() => ''); - - if (modalias.includes('amdgpu')) { - deviceName = 'AMD GPU'; - } else if ( - modalias.includes('nouveau') || - modalias.includes('nvidia') - ) { - deviceName = 'NVIDIA GPU'; - } else { - try { - const [vendorData, deviceData] = await Promise.all([ - readFile(`${devicePath}/vendor`, 'utf8').catch(() => ''), - readFile(`${devicePath}/device`, 'utf8').catch(() => ''), - ]); - - const vendorId = vendorData.trim(); - const deviceId = deviceData.trim(); - - if (vendorId === '0x1002') { - deviceName = 'AMD GPU'; - } else if (vendorId === '0x10de') { - deviceName = 'NVIDIA GPU'; - } else { - deviceName = `GPU (${vendorId}:${deviceId})`; - } - } catch {} - } - } catch {} try { const memTotalData = await readFile( `${devicePath}/mem_info_vram_total`, @@ -109,7 +70,6 @@ async function initializeLinuxGPUCache() { } catch {} gpus.push({ - deviceName, devicePath, memoryTotal, hwmonPath, @@ -167,7 +127,6 @@ async function getLinuxGPUData() { } gpus.push({ - deviceName: cachedGPU.deviceName, usage, memoryUsed: parseFloat(memoryUsed.toFixed(2)), memoryTotal: parseFloat(cachedGPU.memoryTotal.toFixed(2)), diff --git a/src/utils/systemInfo.ts b/src/utils/systemInfo.ts new file mode 100644 index 0000000..cce853b --- /dev/null +++ b/src/utils/systemInfo.ts @@ -0,0 +1,149 @@ +import type { SystemVersionInfo } from '@/types/electron'; +import type { + CPUCapabilities, + GPUCapabilities, + BasicGPUInfo, + GPUMemoryInfo, + SystemMemoryInfo, +} from '@/types/hardware'; +import { PRODUCT_NAME } from '@/constants'; +import type { InfoItem } from '@/components/InfoCard'; + +export interface HardwareInfo { + cpu: CPUCapabilities; + gpu: BasicGPUInfo; + gpuCapabilities: GPUCapabilities; + gpuMemory: GPUMemoryInfo[]; + systemMemory: SystemMemoryInfo; +} + +export const createSoftwareItems = (versionInfo: SystemVersionInfo) => [ + { + label: PRODUCT_NAME, + value: versionInfo.aurPackageVersion + ? (() => { + const pkgrel = versionInfo.aurPackageVersion.split('-')[1]; + return pkgrel + ? `${versionInfo.appVersion} (AUR${pkgrel !== '1' ? ' r' + pkgrel : ''})` + : versionInfo.appVersion; + })() + : versionInfo.appVersion, + }, + { label: 'Electron', value: versionInfo.electronVersion }, + { + label: 'Node.js', + value: versionInfo.nodeJsSystemVersion + ? `${versionInfo.nodeVersion} (System: ${versionInfo.nodeJsSystemVersion})` + : versionInfo.nodeVersion, + }, + { label: 'Chromium', value: versionInfo.chromeVersion }, + { label: 'V8', value: versionInfo.v8Version }, + { + label: 'OS', + value: `${versionInfo.platform} ${versionInfo.arch} (${versionInfo.osVersion})`, + }, + ...(versionInfo.uvVersion + ? [{ label: 'uv', value: versionInfo.uvVersion }] + : []), +]; + +export const createDriverItems = (hardwareInfo: HardwareInfo) => { + const items: InfoItem[] = []; + const { gpuCapabilities } = hardwareInfo; + + if (!gpuCapabilities) { + return [ + { + label: 'Driver Support', + value: 'Loading...', + }, + ]; + } + + if (gpuCapabilities.cuda.supported) { + items.push({ + label: 'CUDA', + value: gpuCapabilities.cuda.version + ? `v${gpuCapabilities.cuda.version}` + : 'Available', + }); + } + + if (gpuCapabilities.rocm.supported) { + items.push({ + label: 'ROCm', + value: gpuCapabilities.rocm.version + ? `v${gpuCapabilities.rocm.version}` + : 'Available', + }); + } + + if (gpuCapabilities.vulkan.supported) { + items.push({ + label: 'Vulkan', + value: gpuCapabilities.vulkan.version + ? `v${gpuCapabilities.vulkan.version}` + : 'Available', + }); + } else { + items.push({ + label: 'Vulkan', + value: 'Not available', + }); + } + + if (gpuCapabilities.clblast.supported) { + items.push({ + label: 'CLBlast', + value: gpuCapabilities.clblast.version + ? `v${gpuCapabilities.clblast.version}` + : 'Available', + }); + } else { + items.push({ + label: 'CLBlast', + value: 'Not available', + }); + } + + return items; +}; + +export const createHardwareItems = (hardwareInfo: HardwareInfo) => [ + { + label: 'CPU', + value: + hardwareInfo.cpu.devices.length > 0 + ? hardwareInfo.cpu.devices[0] + : 'Unknown', + }, + { + label: 'RAM', + value: + hardwareInfo.systemMemory && hardwareInfo.systemMemory.totalGB > 0 + ? `${hardwareInfo.systemMemory.totalGB.toFixed(1)} GB${ + hardwareInfo.systemMemory.type + ? ` ${hardwareInfo.systemMemory.type}` + : '' + }${ + hardwareInfo.systemMemory.speed + ? ` @ ${hardwareInfo.systemMemory.speed} MHz` + : '' + }` + : 'Detecting...', + }, + ...(hardwareInfo.gpu.gpuInfo.length > 0 + ? hardwareInfo.gpu.gpuInfo.map((gpu, index) => ({ + label: `GPU ${index > 0 ? index + 1 : ''}`.trim(), + value: gpu, + })) + : [{ label: 'GPU', value: 'No GPU detected' }]), + ...(hardwareInfo.gpuMemory && hardwareInfo.gpuMemory.length > 0 + ? hardwareInfo.gpuMemory.map((mem, index) => ({ + label: `VRAM ${index > 0 ? index + 1 : ''}`.trim(), + value: mem.totalMemoryGB + ? `${mem.totalMemoryGB.toFixed(1)} GB` + : 'Unknown', + })) + : []), +];