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 (
);
}
@@ -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',
+ }))
+ : []),
+];