settings about tab will now display driver and hardware info, allow re-downloading on binary version mismatches, clean up openwebui "itsdangerous" requirements, display a system monitor icon badge when monitoring is off

This commit is contained in:
lone-cloud 2025-09-25 23:19:31 -07:00
parent 47b7138e7c
commit ee710c4ac1
22 changed files with 719 additions and 323 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "gerbil", "name": "gerbil",
"productName": "Gerbil", "productName": "Gerbil",
"version": "1.5.5", "version": "1.6.0-beta-1",
"description": "Run Large Language Models locally", "description": "Run Large Language Models locally",
"main": "out/main/index.js", "main": "out/main/index.js",
"homepage": "./", "homepage": "./",

View file

@ -1,15 +1,18 @@
import { Button, Tooltip } from '@mantine/core'; import { Button, Tooltip, ActionIcon } from '@mantine/core';
import { Activity } from 'lucide-react';
interface PerformanceBadgeProps { interface PerformanceBadgeProps {
label: string; label?: string;
value: string; value?: string;
tooltipLabel: string; tooltipLabel: string;
iconOnly?: boolean;
} }
export const PerformanceBadge = ({ export const PerformanceBadge = ({
label, label,
value, value,
tooltipLabel, tooltipLabel,
iconOnly = false,
}: PerformanceBadgeProps) => { }: PerformanceBadgeProps) => {
const handlePerformanceClick = async () => { const handlePerformanceClick = async () => {
const result = await window.electronAPI.app.openPerformanceManager(); const result = await window.electronAPI.app.openPerformanceManager();
@ -21,6 +24,16 @@ export const PerformanceBadge = ({
} }
}; };
if (iconOnly) {
return (
<Tooltip label={tooltipLabel} position="top">
<ActionIcon size="sm" variant="subtle" onClick={handlePerformanceClick}>
<Activity size="1.125rem" />
</ActionIcon>
</Tooltip>
);
}
return ( return (
<Tooltip label={tooltipLabel} position="top"> <Tooltip label={tooltipLabel} position="top">
<Button <Button

View file

@ -93,37 +93,43 @@ export const StatusBar = ({ maxDataPoints = 60 }: StatusBarProps) => {
</Group> </Group>
<Group gap="xs"> <Group gap="xs">
{cpuMetrics && memoryMetrics && ( {systemMonitoringEnabled ? (
<> <>
<PerformanceBadge {cpuMetrics && memoryMetrics && (
label="CPU" <>
value={`${cpuMetrics.usage}%`} <PerformanceBadge
tooltipLabel={`${cpuMetrics.usage}%${cpuMetrics.temperature ? `${cpuMetrics.temperature}°C` : ''}`} label="CPU"
/> value={`${cpuMetrics.usage}%`}
tooltipLabel={`${cpuMetrics.usage}%${cpuMetrics.temperature ? `${cpuMetrics.temperature}°C` : ''}`}
/>
<PerformanceBadge <PerformanceBadge
label="RAM" label="RAM"
value={`${memoryMetrics.usage}%`} value={`${memoryMetrics.usage}%`}
tooltipLabel={`${memoryMetrics.used.toFixed(2)} GB / ${memoryMetrics.total.toFixed(2)} GB (${memoryMetrics.usage}%)`} tooltipLabel={`${memoryMetrics.used.toFixed(2)} GB / ${memoryMetrics.total.toFixed(2)} GB (${memoryMetrics.usage}%)`}
/> />
</>
)}
{gpuMetrics?.gpus.map((gpu, index) => (
<Group gap="xs" key={`gpu-${index}`}>
<PerformanceBadge
label={`GPU${gpuMetrics.gpus.length > 1 ? ` ${index + 1}` : ''}`}
value={`${gpu.usage}%`}
tooltipLabel={`${gpu.usage}%${gpu.temperature ? `${gpu.temperature}°C` : ''}`}
/>
<PerformanceBadge
label={`VRAM${gpuMetrics.gpus.length > 1 ? ` ${index + 1}` : ''}`}
value={`${gpu.memoryUsage}%`}
tooltipLabel={`${gpu.memoryUsed.toFixed(2)} GB / ${gpu.memoryTotal.toFixed(2)} GB (${gpu.memoryUsage}%)`}
/>
</Group>
))}
</> </>
) : (
<PerformanceBadge tooltipLabel="System resource manager" iconOnly />
)} )}
{gpuMetrics?.gpus.map((gpu, index) => (
<Group gap="xs" key={`gpu-${index}`}>
<PerformanceBadge
label={`GPU${gpuMetrics.gpus.length > 1 ? ` ${index + 1}` : ''}`}
value={`${gpu.usage}%`}
tooltipLabel={`${gpu.usage}%${gpu.temperature ? `${gpu.temperature}°C` : ''}`}
/>
<PerformanceBadge
label={`VRAM${gpuMetrics.gpus.length > 1 ? ` ${index + 1}` : ''}`}
value={`${gpu.memoryUsage}%`}
tooltipLabel={`${gpu.memoryUsed.toFixed(2)} GB / ${gpu.memoryTotal.toFixed(2)} GB (${gpu.memoryUsage}%)`}
/>
</Group>
))}
</Group> </Group>
</Group> </Group>
</AppShell.Footer> </AppShell.Footer>

View file

@ -13,45 +13,43 @@ import { Download } from 'lucide-react';
import { MouseEvent } from 'react'; import { MouseEvent } from 'react';
import { pretifyBinName, isWindowsROCmBuild } from '@/utils/assets'; import { pretifyBinName, isWindowsROCmBuild } from '@/utils/assets';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import type { VersionInfo } from '@/types';
interface DownloadCardProps { interface DownloadCardProps {
name: string; version: VersionInfo;
size: string; size: string;
version?: string;
description?: string; description?: string;
isCurrent?: boolean;
isInstalled?: boolean;
isDownloading?: boolean; isDownloading?: boolean;
downloadProgress?: number; downloadProgress?: number;
disabled?: boolean; disabled?: boolean;
hasUpdate?: boolean;
newerVersion?: string;
onDownload: (e: MouseEvent<HTMLButtonElement>) => void; onDownload: (e: MouseEvent<HTMLButtonElement>) => void;
onMakeCurrent?: () => void; onMakeCurrent?: () => void;
onUpdate?: (e: MouseEvent<HTMLButtonElement>) => void; onUpdate?: (e: MouseEvent<HTMLButtonElement>) => void;
onRedownload?: (e: MouseEvent<HTMLButtonElement>) => void;
} }
export const DownloadCard = ({ export const DownloadCard = ({
name, version: versionInfo,
size, size,
version,
description, description,
isCurrent = false,
isInstalled = false,
isDownloading = false, isDownloading = false,
downloadProgress = 0, downloadProgress = 0,
disabled = false, disabled = false,
hasUpdate = false,
newerVersion,
onDownload, onDownload,
onMakeCurrent, onMakeCurrent,
onUpdate, onUpdate,
onRedownload,
}: DownloadCardProps) => { }: DownloadCardProps) => {
const { resolvedColorScheme: colorScheme } = usePreferencesStore(); const { resolvedColorScheme: colorScheme } = usePreferencesStore();
const hasVersionMismatch = Boolean(
versionInfo.version &&
versionInfo.actualVersion &&
versionInfo.version !== versionInfo.actualVersion
);
const renderActionButtons = () => { const renderActionButtons = () => {
const buttons = []; const buttons = [];
if (!isInstalled) { if (!versionInfo.isInstalled) {
return ( return (
<Button <Button
key="download" key="download"
@ -73,20 +71,21 @@ export const DownloadCard = ({
); );
} }
if (!isCurrent && onMakeCurrent) { if (!versionInfo.isCurrent && onMakeCurrent) {
buttons.push( buttons.push(
<Button <Button
key="makeCurrent" key="makeCurrent"
variant="filled" variant="filled"
size="xs" size="xs"
onClick={onMakeCurrent} onClick={onMakeCurrent}
disabled={disabled}
> >
Make Current Make Current
</Button> </Button>
); );
} }
if (hasUpdate && onUpdate) { if (versionInfo.hasUpdate && onUpdate) {
buttons.push( buttons.push(
<Button <Button
key="update" key="update"
@ -104,7 +103,32 @@ export const DownloadCard = ({
) )
} }
> >
{isDownloading ? 'Updating...' : `Update to ${newerVersion}`} {isDownloading
? 'Updating...'
: `Update to ${versionInfo.newerVersion}`}
</Button>
);
}
if (hasVersionMismatch && onRedownload) {
buttons.push(
<Button
key="redownload"
variant="filled"
size="xs"
onClick={onRedownload}
loading={isDownloading}
disabled={disabled}
color="red"
leftSection={
isDownloading ? (
<Loader size="1rem" />
) : (
<Download style={{ width: rem(14), height: rem(14) }} />
)
}
>
{isDownloading ? 'Re-downloading...' : 'Re-download'}
</Button> </Button>
); );
} }
@ -117,7 +141,7 @@ export const DownloadCard = ({
withBorder withBorder
radius="sm" radius="sm"
padding="sm" padding="sm"
{...(isCurrent && { {...(versionInfo.isCurrent && {
bg: colorScheme === 'dark' ? 'dark.6' : 'gray.0', bg: colorScheme === 'dark' ? 'dark.6' : 'gray.0',
bd: `2px solid var(--mantine-color-${colorScheme === 'dark' ? 'blue-4' : 'blue-6'})`, bd: `2px solid var(--mantine-color-${colorScheme === 'dark' ? 'blue-4' : 'blue-6'})`,
})} })}
@ -126,19 +150,19 @@ export const DownloadCard = ({
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<Group gap="xs" align="center" mb="xs"> <Group gap="xs" align="center" mb="xs">
<Text fw={500} size="sm"> <Text fw={500} size="sm">
{pretifyBinName(name)} {pretifyBinName(versionInfo.name)}
</Text> </Text>
{isCurrent && ( {versionInfo.isCurrent && (
<Badge variant="light" color="blue" size="sm"> <Badge variant="light" color="blue" size="sm">
Current Current
</Badge> </Badge>
)} )}
{hasUpdate && ( {versionInfo.hasUpdate && (
<Badge variant="light" color="orange" size="sm"> <Badge variant="light" color="orange" size="sm">
Update Available Update Available
</Badge> </Badge>
)} )}
{isWindowsROCmBuild(name) && ( {isWindowsROCmBuild(versionInfo.name) && (
<Badge variant="light" color="yellow" size="sm"> <Badge variant="light" color="yellow" size="sm">
Experimental Experimental
</Badge> </Badge>
@ -150,9 +174,15 @@ export const DownloadCard = ({
</Text> </Text>
)} )}
<Group gap="xs" align="center"> <Group gap="xs" align="center">
{version && ( {versionInfo.version && (
<Text size="xs" c="dimmed"> <Text size="xs" c="dimmed">
Version {version} Version {versionInfo.version}
{hasVersionMismatch && versionInfo.actualVersion && (
<span style={{ color: 'var(--mantine-color-red-6)' }}>
{' '}
(actual: {versionInfo.actualVersion})
</span>
)}
</Text> </Text>
)} )}
{size && ( {size && (

View file

@ -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 (
<Card withBorder radius="md" p="xs">
<Text size="md" fw={600} mb="sm">
{title}
</Text>
<Text size="sm" c="dimmed">
Loading...
</Text>
</Card>
);
}
return (
<Card withBorder radius="md" p="xs" style={{ position: 'relative' }}>
<Tooltip label="Copy">
<ActionIcon
variant="subtle"
size="sm"
onClick={copyInfo}
aria-label={`Copy ${title}`}
style={{
position: 'absolute',
top: 8,
right: 8,
zIndex: 1,
}}
>
<Copy style={{ width: rem(14), height: rem(14) }} />
</ActionIcon>
</Tooltip>
<Text size="md" fw={600} mb="sm">
{title}
</Text>
<Stack gap="xs">
{items.map((item, index) => (
<Group key={index} gap="md" align="center" wrap="nowrap">
<Text size="sm" fw={500} c="dimmed" style={{ minWidth: '7.5rem' }}>
{item.label}:
</Text>
<Text
size="sm"
ff="monospace"
style={{
wordBreak: 'break-all',
flex: 1,
}}
>
{item.value}
</Text>
</Group>
))}
</Stack>
</Card>
);
};

View file

@ -82,12 +82,19 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
ref={isDownloading ? downloadingItemRef : null} ref={isDownloading ? downloadingItemRef : null}
> >
<DownloadCard <DownloadCard
name={download.name} version={{
name: download.name,
version: download.version || '',
size: download.size,
isInstalled: false,
isCurrent: false,
downloadUrl: download.url,
hasUpdate: false,
}}
size={formatDownloadSize( size={formatDownloadSize(
download.size, download.size,
download.url download.url
)} )}
version={download.version}
description={getAssetDescription(download.name)} description={getAssetDescription(download.name)}
isDownloading={isDownloading} isDownloading={isDownloading}
downloadProgress={ downloadProgress={

View file

@ -1,4 +1,10 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import {
createSoftwareItems,
createDriverItems,
createHardwareItems,
type HardwareInfo,
} from '@/utils/systemInfo';
import { import {
Text, Text,
Stack, Stack,
@ -9,19 +15,22 @@ import {
Badge, Badge,
Button, Button,
rem, rem,
ActionIcon,
Tooltip,
} from '@mantine/core'; } from '@mantine/core';
import { Github, FolderOpen, Copy, FileText } from 'lucide-react'; import { Github, FolderOpen, FileText } from 'lucide-react';
import { safeExecute } from '@/utils/logger';
import { useLogoClickSounds } from '@/hooks/useLogoClickSounds'; import { useLogoClickSounds } from '@/hooks/useLogoClickSounds';
import type { VersionInfo } from '@/types/electron'; import type { SystemVersionInfo } from '@/types/electron';
import { PRODUCT_NAME, GITHUB_API } from '@/constants'; import { PRODUCT_NAME, GITHUB_API } from '@/constants';
import { InfoCard } from '@/components/InfoCard';
import icon from '/icon.png'; import icon from '/icon.png';
export const AboutTab = () => { export const AboutTab = () => {
const [versionInfo, setVersionInfo] = useState<VersionInfo | null>(null); const [versionInfo, setVersionInfo] = useState<SystemVersionInfo | null>(
null
);
const [hardwareInfo, setHardwareInfo] = useState<HardwareInfo | null>(null);
const { handleLogoClick, getLogoStyles } = useLogoClickSounds(); const { handleLogoClick, getLogoStyles } = useLogoClickSounds();
useEffect(() => { useEffect(() => {
const loadVersionInfo = async () => { const loadVersionInfo = async () => {
const info = await window.electronAPI.app.getVersionInfo(); const info = await window.electronAPI.app.getVersionInfo();
@ -29,7 +38,35 @@ export const AboutTab = () => {
setVersionInfo(info); 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(); loadVersionInfo();
loadHardwareInfo();
}, []); }, []);
if (!versionInfo) { if (!versionInfo) {
@ -40,47 +77,9 @@ export const AboutTab = () => {
); );
} }
const versionItems = [ const softwareItems = createSoftwareItems(versionInfo);
{ const driverItems = hardwareInfo ? createDriverItems(hardwareInfo) : [];
label: PRODUCT_NAME, const hardwareItems = hardwareInfo ? createHardwareItems(hardwareInfo) : [];
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 actionButtons = [ const actionButtons = [
{ {
@ -158,48 +157,15 @@ export const AboutTab = () => {
</Group> </Group>
</Card> </Card>
<Card withBorder radius="md" p="xs" style={{ position: 'relative' }}> <InfoCard title="Software" items={softwareItems} />
<Tooltip label="Copy Info">
<ActionIcon <InfoCard title="Drivers" items={driverItems} loading={!hardwareInfo} />
variant="subtle"
size="sm" <InfoCard
onClick={copyVersionInfo} title="Hardware"
aria-label="Copy Version Info" items={hardwareItems}
style={{ loading={!hardwareInfo}
position: 'absolute', />
top: 8,
right: 8,
zIndex: 1,
}}
>
<Copy style={{ width: rem(14), height: rem(14) }} />
</ActionIcon>
</Tooltip>
<Stack gap="xs">
{versionItems.map((item, index) => (
<Group key={index} gap="md" align="center" wrap="nowrap">
<Text
size="sm"
fw={500}
c="dimmed"
style={{ minWidth: '7.5rem' }}
>
{item.label}:
</Text>
<Text
size="sm"
ff="monospace"
style={{
wordBreak: 'break-all',
flex: 1,
}}
>
{item.value}
</Text>
</Group>
))}
</Stack>
</Card>
</Stack> </Stack>
); );
}; };

View file

@ -20,18 +20,7 @@ import { formatDownloadSize } from '@/utils/format';
import { useKoboldVersionsStore } from '@/stores/koboldVersions'; import { useKoboldVersionsStore } from '@/stores/koboldVersions';
import type { InstalledVersion, ReleaseWithStatus } from '@/types/electron'; import type { InstalledVersion, ReleaseWithStatus } from '@/types/electron';
import type { VersionInfo } from '@/types';
interface VersionInfo {
name: string;
version: string;
size?: number;
isInstalled: boolean;
isCurrent: boolean;
downloadUrl?: string;
installedPath?: string;
hasUpdate?: boolean;
newerVersion?: string;
}
export const VersionsTab = () => { export const VersionsTab = () => {
const { const {
@ -119,6 +108,7 @@ export const VersionsTab = () => {
installedPath: installedVersion.path, installedPath: installedVersion.path,
hasUpdate, hasUpdate,
newerVersion: hasUpdate ? download.version : undefined, newerVersion: hasUpdate ? download.version : undefined,
actualVersion: installedVersion.actualVersion,
}); });
} else { } else {
versions.push({ versions.push({
@ -146,6 +136,7 @@ export const VersionsTab = () => {
isInstalled: true, isInstalled: true,
isCurrent, isCurrent,
installedPath: installed.path, installedPath: installed.path,
actualVersion: installed.actualVersion,
}); });
} }
}); });
@ -194,16 +185,31 @@ export const VersionsTab = () => {
await loadInstalledVersions(); 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; if (!version.installedPath) return;
const success = await window.electronAPI.kobold.setCurrentVersion( const targetInstalledVersion = installedVersions.find(
version.installedPath (v) => v.path === version.installedPath
); );
if (targetInstalledVersion) {
if (success) { setCurrentVersion(targetInstalledVersion);
await loadInstalledVersions();
} }
window.electronAPI.kobold.setCurrentVersion(version.installedPath);
}; };
if (loadingInstalled || loadingPlatform || loadingRemote) { if (loadingInstalled || loadingPlatform || loadingRemote) {
@ -257,21 +263,16 @@ export const VersionsTab = () => {
ref={isDownloading ? downloadingItemRef : null} ref={isDownloading ? downloadingItemRef : null}
> >
<DownloadCard <DownloadCard
name={version.name} version={version}
size={ size={
version.size version.size
? formatDownloadSize(version.size, version.downloadUrl) ? formatDownloadSize(version.size, version.downloadUrl)
: '' : ''
} }
version={version.version}
description={getAssetDescription(version.name)} description={getAssetDescription(version.name)}
isCurrent={version.isCurrent}
isInstalled={version.isInstalled}
isDownloading={isDownloading} isDownloading={isDownloading}
downloadProgress={downloadProgress[version.name]} downloadProgress={downloadProgress[version.name]}
disabled={downloading !== null} disabled={downloading !== null}
hasUpdate={version.hasUpdate}
newerVersion={version.newerVersion}
onDownload={(e) => { onDownload={(e) => {
e.stopPropagation(); e.stopPropagation();
handleDownload(version); handleDownload(version);
@ -280,6 +281,10 @@ export const VersionsTab = () => {
e.stopPropagation(); e.stopPropagation();
handleUpdate(version); handleUpdate(version);
}} }}
onRedownload={(e) => {
e.stopPropagation();
handleRedownload(version);
}}
onMakeCurrent={() => makeCurrent(version)} onMakeCurrent={() => makeCurrent(version)}
/> />
</div> </div>

View file

@ -116,7 +116,7 @@ const checkVramWarnings = async (backend: string): Promise<Warning[]> => {
if (lowVramGpus.length > 0) { if (lowVramGpus.length > 0) {
const memoryDetails = lowVramGpus const memoryDetails = lowVramGpus
.map((gpu) => `${gpu.deviceName}: ${gpu.totalMemoryGB!.toFixed(1)}GB`) .map((gpu) => `${gpu.totalMemoryGB!.toFixed(1)}GB`)
.join(', '); .join(', ');
warnings.push({ warnings.push({

View file

@ -57,6 +57,7 @@ import {
detectGPUCapabilities, detectGPUCapabilities,
detectGPUMemory, detectGPUMemory,
detectROCm, detectROCm,
detectSystemMemory,
} from '@/main/modules/hardware'; } from '@/main/modules/hardware';
import { import {
detectBackendSupport, detectBackendSupport,
@ -121,6 +122,8 @@ export function setupIPCHandlers() {
ipcMain.handle('kobold:detectGPUMemory', () => detectGPUMemory()); ipcMain.handle('kobold:detectGPUMemory', () => detectGPUMemory());
ipcMain.handle('kobold:detectSystemMemory', () => detectSystemMemory());
ipcMain.handle('kobold:detectROCm', () => detectROCm()); ipcMain.handle('kobold:detectROCm', () => detectROCm());
ipcMain.handle('kobold:detectBackendSupport', () => detectBackendSupport()); ipcMain.handle('kobold:detectBackendSupport', () => detectBackendSupport());

View file

@ -15,14 +15,14 @@ import type { ChildProcess } from 'child_process';
import yauzl from 'yauzl'; import yauzl from 'yauzl';
import { createWriteStream } from 'fs'; import { createWriteStream } from 'fs';
import { logError, safeExecute } from '@/utils/node/logging'; import { logError, safeExecute, tryExecute } from '@/utils/node/logging';
import { sendKoboldOutput } from './window'; import { sendKoboldOutput } from './window';
import { getInstallDir } from './config'; import { getInstallDir } from './config';
import { COMFYUI, SERVER_READY_SIGNALS, GITHUB_API } from '@/constants'; import { COMFYUI, SERVER_READY_SIGNALS, GITHUB_API } from '@/constants';
import { terminateProcess } from '@/utils/node/process'; import { terminateProcess } from '@/utils/node/process';
import { parseKoboldConfig } from '@/utils/node/kobold'; import { parseKoboldConfig } from '@/utils/node/kobold';
import { getAppVersion, ensureDir } from '@/utils/node/fs'; import { getAppVersion, ensureDir } from '@/utils/node/fs';
import { getGPUData } from '@/utils/node/gpu'; import { detectGPU } from './hardware';
import { getUvEnvironment } from './dependencies'; import { getUvEnvironment } from './dependencies';
interface ComfyUIVersionInfo { interface ComfyUIVersionInfo {
@ -61,15 +61,10 @@ async function saveComfyUIVersion(
workspaceDir: string, workspaceDir: string,
version: ComfyUIVersionInfo version: ComfyUIVersionInfo
) { ) {
try { await tryExecute(async () => {
const versionFile = join(workspaceDir, '.version.json'); const versionFile = join(workspaceDir, '.version.json');
await writeFile(versionFile, JSON.stringify(version, null, 2)); await writeFile(versionFile, JSON.stringify(version, null, 2));
} catch (error) { }, 'Failed to save ComfyUI version info');
logError(
'Failed to save ComfyUI version info',
error instanceof Error ? error : undefined
);
}
} }
async function shouldUpdateComfyUI(workspaceDir: string) { async function shouldUpdateComfyUI(workspaceDir: string) {
@ -102,9 +97,8 @@ on('SIGTERM', () => {
async function shouldForceCPUMode() { async function shouldForceCPUMode() {
try { try {
const gpus = await getGPUData(); const gpuInfo = await detectGPU();
const hasAMD = gpus.some((gpu) => gpu.deviceName.includes('AMD')); const { hasAMD, hasNVIDIA } = gpuInfo;
const hasNVIDIA = gpus.some((gpu) => gpu.deviceName.includes('NVIDIA'));
const isWindows = platform === 'win32'; const isWindows = platform === 'win32';
return (hasAMD && isWindows) || (!hasAMD && !hasNVIDIA); return (hasAMD && isWindows) || (!hasAMD && !hasNVIDIA);
@ -125,9 +119,8 @@ async function getPyTorchInstallArgs(pythonPath: string) {
]; ];
try { try {
const gpus = await getGPUData(); const gpuInfo = await detectGPU();
const hasAMD = gpus.some((gpu) => gpu.deviceName.includes('AMD')); const { hasAMD, hasNVIDIA } = gpuInfo;
const hasNVIDIA = gpus.some((gpu) => gpu.deviceName.includes('NVIDIA'));
const isWindows = platform === 'win32'; const isWindows = platform === 'win32';
if (hasAMD && !isWindows) { if (hasAMD && !isWindows) {
@ -597,10 +590,7 @@ export async function startFrontend(args: string[]) {
await waitForComfyUIToStart(); await waitForComfyUIToStart();
} catch (error) { } catch (error) {
logError( logError('Failed to start ComfyUI', error as Error);
'Failed to start ComfyUI',
error instanceof Error ? error : undefined
);
throw error; throw error;
} }
} }

View file

@ -27,7 +27,9 @@ export async function detectCPU() {
const devices: string[] = []; const devices: string[] = [];
if (cpu.brand) { 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'); const avx = flags.includes('avx') || flags.includes('AVX');
@ -59,35 +61,28 @@ export async function detectGPU() {
} }
const result = await safeExecute(async () => { const result = await safeExecute(async () => {
const gpuData = await getGPUData(); const graphics = await si.graphics();
let hasAMD = false; let hasAMD = false;
let hasNVIDIA = false; let hasNVIDIA = false;
const gpuInfo: string[] = []; const gpuInfo: string[] = [];
for (const gpu of gpuData) { for (const controller of graphics.controllers) {
if (gpu.deviceName) { // Check vendor for AMD
gpuInfo.push(formatDeviceName(gpu.deviceName));
}
const deviceName = gpu.deviceName?.toLowerCase() || '';
if ( if (
deviceName.includes('amd') || controller.vendor?.toLowerCase().includes('amd') ||
deviceName.includes('ati') || controller.vendor?.toLowerCase().includes('ati')
deviceName.includes('radeon')
) { ) {
hasAMD = true; hasAMD = true;
} }
if ( if (controller.vendor?.toLowerCase().includes('nvidia')) {
deviceName.includes('nvidia') ||
deviceName.includes('geforce') ||
deviceName.includes('gtx') ||
deviceName.includes('rtx')
) {
hasNVIDIA = true; hasNVIDIA = true;
} }
if (controller.model) {
gpuInfo.push(controller.model);
}
} }
const basicInfo = { const basicInfo = {
@ -95,7 +90,6 @@ export async function detectGPU() {
hasNVIDIA, hasNVIDIA,
gpuInfo: gpuInfo.length > 0 ? gpuInfo : ['No GPU information available'], gpuInfo: gpuInfo.length > 0 ? gpuInfo : ['No GPU information available'],
}; };
basicGPUInfoCache = basicInfo; basicGPUInfoCache = basicInfo;
return basicInfo; return basicInfo;
}, 'GPU detection failed'); }, 'GPU detection failed');
@ -133,7 +127,7 @@ async function detectCUDA() {
try { try {
const { stdout } = await execa( const { stdout } = await execa(
'nvidia-smi', 'nvidia-smi',
['--query-gpu=name,memory.total,memory.free', '--format=csv,noheader'], ['--query-gpu=name,driver_version', '--format=csv,noheader,nounits'],
{ {
timeout: 5000, timeout: 5000,
reject: false, reject: false,
@ -141,19 +135,41 @@ async function detectCUDA() {
); );
if (stdout.trim()) { if (stdout.trim()) {
const devices = stdout // Check for error messages that indicate nvidia-smi failed
.trim() const errorPatterns = [
.split('\n') 'NVIDIA-SMI has failed',
.map((line) => { 'No devices found',
const parts = line.split(','); 'Unable to determine the device handle',
const rawName = parts[0]?.trim() || 'Unknown NVIDIA GPU'; "couldn't communicate with the NVIDIA driver",
return formatDeviceName(rawName); 'No NVIDIA GPU found',
}) ];
.filter(Boolean);
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 { return {
supported: devices.length > 0, supported: devices.length > 0,
devices, devices,
version,
} as const; } 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 { return {
supported: devices.length > 0, supported: devices.length > 0,
devices, 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 { return {
supported: devices.length > 0, supported: devices.length > 0,
devices, devices,
version,
}; };
} }
return { supported: false, devices: [] }; return { supported: false, devices: [], version: 'Unknown' };
} catch { } catch {
return { supported: false, devices: [] }; return { supported: false, devices: [], version: 'Unknown' };
} }
} }
@ -336,15 +385,34 @@ async function detectCLBlast() {
if (stdout.trim()) { if (stdout.trim()) {
const devices = parseClInfoOutput(stdout); 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 { return {
supported: devices.length > 0, supported: devices.length > 0,
devices, devices,
version,
}; };
} }
return { supported: false, devices: [] }; return { supported: false, devices: [], version: 'Unknown' };
} catch { } catch {
return { supported: false, devices: [] }; return { supported: false, devices: [], version: 'Unknown' };
} }
} }
@ -358,18 +426,15 @@ export async function detectGPUMemory() {
const memoryInfo: GPUMemoryInfo[] = []; const memoryInfo: GPUMemoryInfo[] = [];
for (const gpu of gpuData) { for (const gpu of gpuData) {
if (gpu.deviceName) { let vram: number | null = gpu.memoryTotal;
let vram: number | null = gpu.memoryTotal;
if (!vram || vram <= 1) { if (!vram || vram <= 1) {
vram = null; vram = null;
}
memoryInfo.push({
deviceName: formatDeviceName(gpu.deviceName),
totalMemoryGB: vram,
});
} }
memoryInfo.push({
totalMemoryGB: vram,
});
} }
return memoryInfo; return memoryInfo;
@ -378,3 +443,44 @@ export async function detectGPUMemory() {
gpuMemoryInfoCache = result || []; gpuMemoryInfoCache = result || [];
return gpuMemoryInfoCache; 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),
};
}
};

View file

@ -43,14 +43,18 @@ export async function getInstalledVersions() {
const versionPromises = launchers.map(async (launcher) => { const versionPromises = launchers.map(async (launcher) => {
try { try {
const detectedVersion = await getVersionFromBinary(launcher.path); const versionInfo = await getVersionFromBinary(launcher.path);
const version = detectedVersion || 'unknown';
if (!versionInfo) {
return null;
}
return { return {
version, version: versionInfo.version,
path: launcher.path, path: launcher.path,
filename: launcher.filename, filename: launcher.filename,
size: launcher.size, size: launcher.size,
actualVersion: versionInfo.actualVersion,
} as InstalledVersion; } as InstalledVersion;
} catch (error) { } catch (error) {
logError( logError(
@ -131,42 +135,56 @@ export async function getVersionFromBinary(launcherPath: string) {
return null; return null;
} }
let folderVersion: string | null = null;
let actualVersion: string | null = null;
const folderName = launcherPath.split(/[/\\]/).slice(-2, -1)[0]; const folderName = launcherPath.split(/[/\\]/).slice(-2, -1)[0];
if (folderName) { if (folderName) {
const versionMatch = folderName.match( const versionMatch = folderName.match(
/-(\d+\.\d+(?:\.\d+)?(?:\.[a-zA-Z0-9]+)*(?:-[a-zA-Z0-9]+)*)$/ /-(\d+\.\d+(?:\.\d+)?(?:\.[a-zA-Z0-9]+)*(?:-[a-zA-Z0-9]+)*)$/
); );
if (versionMatch) { if (versionMatch) {
return versionMatch[1]; folderVersion = versionMatch[1];
} }
} }
const result = await execa(launcherPath, ['--version'], { try {
timeout: 30000, const result = await execa(launcherPath, ['--version'], {
stdio: ['ignore', 'pipe', 'pipe'], 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)) { if (/^\d+\.\d+/.test(allOutput)) {
const versionParts = allOutput.split(/\s+/)[0]; const versionParts = allOutput.split(/\s+/)[0];
if (versionParts && /^\d+\.\d+/.test(versionParts)) { if (versionParts && /^\d+\.\d+/.test(versionParts)) {
return versionParts; actualVersion = 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;
} }
} }
}
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 { } catch {
return null; return null;
} }

View file

@ -3,6 +3,7 @@ import { BrowserWindow } from 'electron';
import { platform } from 'process'; import { platform } from 'process';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import { getGPUData } from '@/utils/node/gpu'; import { getGPUData } from '@/utils/node/gpu';
import { detectGPU } from './hardware';
import { tryExecute, safeExecute } from '@/utils/node/logging'; import { tryExecute, safeExecute } from '@/utils/node/logging';
export interface CpuMetrics { export interface CpuMetrics {
@ -134,18 +135,18 @@ async function collectAndSendMemoryMetrics() {
async function collectAndSendGpuMetrics() { async function collectAndSendGpuMetrics() {
await tryExecute(async () => { await tryExecute(async () => {
const gpuData = await getGPUData(); const [gpuData, gpuInfo] = await Promise.all([getGPUData(), detectGPU()]);
const metrics: GpuMetrics = { const metrics: GpuMetrics = {
gpus: gpuData.map((gpuInfo) => ({ gpus: gpuData.map((gpuData, index) => ({
name: gpuInfo.deviceName, name: gpuInfo.gpuInfo[index] || `GPU ${index}`,
usage: Math.round(gpuInfo.usage), usage: Math.round(gpuData.usage),
memoryUsed: gpuInfo.memoryUsed, memoryUsed: gpuData.memoryUsed,
memoryTotal: gpuInfo.memoryTotal, memoryTotal: gpuData.memoryTotal,
memoryUsage: memoryUsage:
gpuInfo.memoryTotal > 0 gpuData.memoryTotal > 0
? Math.round((gpuInfo.memoryUsed / gpuInfo.memoryTotal) * 100) ? Math.round((gpuData.memoryUsed / gpuData.memoryTotal) * 100)
: 0, : 0,
temperature: gpuInfo.temperature, temperature: gpuData.temperature,
})), })),
}; };

View file

@ -14,14 +14,7 @@ import { getUvEnvironment } from './dependencies';
let openWebUIProcess: ChildProcess | null = null; let openWebUIProcess: ChildProcess | null = null;
const OPENWEBUI_BASE_ARGS = [ const OPENWEBUI_BASE_ARGS = ['--python', '3.11', 'open-webui@latest', 'serve'];
'--python',
'3.11',
'--with',
'itsdangerous',
'open-webui@latest',
'serve',
];
on('SIGINT', () => { on('SIGINT', () => {
void stopFrontend(); void stopFrontend();

View file

@ -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<PlatformGPUInfo> {
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,
}
);
}

View file

@ -26,6 +26,7 @@ const koboldAPI: KoboldAPI = {
detectGPUCapabilities: () => detectGPUCapabilities: () =>
ipcRenderer.invoke('kobold:detectGPUCapabilities'), ipcRenderer.invoke('kobold:detectGPUCapabilities'),
detectGPUMemory: () => ipcRenderer.invoke('kobold:detectGPUMemory'), detectGPUMemory: () => ipcRenderer.invoke('kobold:detectGPUMemory'),
detectSystemMemory: () => ipcRenderer.invoke('kobold:detectSystemMemory'),
detectROCm: () => ipcRenderer.invoke('kobold:detectROCm'), detectROCm: () => ipcRenderer.invoke('kobold:detectROCm'),
detectBackendSupport: () => ipcRenderer.invoke('kobold:detectBackendSupport'), detectBackendSupport: () => ipcRenderer.invoke('kobold:detectBackendSupport'),
getAvailableBackends: (includeDisabled) => getAvailableBackends: (includeDisabled) =>

View file

@ -3,6 +3,7 @@ import type {
GPUCapabilities, GPUCapabilities,
BasicGPUInfo, BasicGPUInfo,
GPUMemoryInfo, GPUMemoryInfo,
SystemMemoryInfo,
} from '@/types/hardware'; } from '@/types/hardware';
import type { BackendOption, BackendSupport } from '@/types'; import type { BackendOption, BackendSupport } from '@/types';
import type { MantineColorScheme } from '@mantine/core'; import type { MantineColorScheme } from '@mantine/core';
@ -55,6 +56,7 @@ export interface InstalledVersion {
path: string; path: string;
filename: string; filename: string;
size?: number; size?: number;
actualVersion?: string;
} }
export interface DownloadItem { export interface DownloadItem {
@ -112,6 +114,7 @@ export interface KoboldAPI {
detectCPU: () => Promise<CPUCapabilities>; detectCPU: () => Promise<CPUCapabilities>;
detectGPUCapabilities: () => Promise<GPUCapabilities>; detectGPUCapabilities: () => Promise<GPUCapabilities>;
detectGPUMemory: () => Promise<GPUMemoryInfo[]>; detectGPUMemory: () => Promise<GPUMemoryInfo[]>;
detectSystemMemory: () => Promise<SystemMemoryInfo>;
detectROCm: () => Promise<{ supported: boolean; devices: string[] }>; detectROCm: () => Promise<{ supported: boolean; devices: string[] }>;
detectBackendSupport: () => Promise<BackendSupport | null>; detectBackendSupport: () => Promise<BackendSupport | null>;
getAvailableBackends: (includeDisabled?: boolean) => Promise<BackendOption[]>; getAvailableBackends: (includeDisabled?: boolean) => Promise<BackendOption[]>;
@ -141,7 +144,7 @@ export interface KoboldAPI {
onKoboldOutput: (callback: (data: string) => void) => () => void; onKoboldOutput: (callback: (data: string) => void) => () => void;
} }
export interface VersionInfo { export interface SystemVersionInfo {
appVersion: string; appVersion: string;
electronVersion: string; electronVersion: string;
nodeVersion: string; nodeVersion: string;
@ -160,7 +163,7 @@ export interface AppAPI {
viewConfigFile: () => Promise<void>; viewConfigFile: () => Promise<void>;
openPath: (path: string) => Promise<void>; openPath: (path: string) => Promise<void>;
getVersion: () => Promise<string>; getVersion: () => Promise<string>;
getVersionInfo: () => Promise<VersionInfo>; getVersionInfo: () => Promise<SystemVersionInfo>;
minimizeWindow: () => void; minimizeWindow: () => void;
maximizeWindow: () => void; maximizeWindow: () => void;
closeWindow: () => void; closeWindow: () => void;

View file

@ -5,26 +5,35 @@ export interface CPUCapabilities {
} }
export interface GPUMemoryInfo { export interface GPUMemoryInfo {
deviceName: string;
totalMemoryGB: number | null; totalMemoryGB: number | null;
} }
export interface SystemMemoryInfo {
totalGB: number;
speed?: number;
type?: string;
}
export interface GPUCapabilities { export interface GPUCapabilities {
cuda: { cuda: {
readonly supported: boolean; readonly supported: boolean;
readonly devices: readonly string[]; readonly devices: readonly string[];
readonly version?: string;
}; };
rocm: { rocm: {
readonly supported: boolean; readonly supported: boolean;
readonly devices: readonly string[]; readonly devices: readonly string[];
readonly version?: string;
}; };
vulkan: { vulkan: {
readonly supported: boolean; readonly supported: boolean;
readonly devices: readonly string[]; readonly devices: readonly string[];
readonly version?: string;
}; };
clblast: { clblast: {
readonly supported: boolean; readonly supported: boolean;
readonly devices: readonly string[]; readonly devices: readonly string[];
readonly version?: string;
}; };
} }
@ -43,9 +52,6 @@ export interface HardwareInfo {
cpu: CPUCapabilities; cpu: CPUCapabilities;
gpu: BasicGPUInfo; gpu: BasicGPUInfo;
gpuCapabilities?: GPUCapabilities; gpuCapabilities?: GPUCapabilities;
} gpuMemory?: GPUMemoryInfo[];
systemMemory?: SystemMemoryInfo;
export interface SystemCapabilities {
hardware: HardwareInfo;
platform: string;
} }

14
src/types/index.d.ts vendored
View file

@ -45,6 +45,20 @@ export interface InstalledVersion {
path: string; path: string;
filename: string; filename: string;
size?: number; 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 { export interface DismissedUpdate {

View file

@ -3,14 +3,12 @@ import { join } from 'path';
import { platform } from 'process'; import { platform } from 'process';
interface CachedGPUInfo { interface CachedGPUInfo {
deviceName: string;
devicePath: string; devicePath: string;
memoryTotal: number; memoryTotal: number;
hwmonPath?: string; hwmonPath?: string;
} }
interface GPUData { interface GPUData {
deviceName: string;
usage: number; usage: number;
memoryUsed: number; memoryUsed: number;
memoryTotal: number; memoryTotal: number;
@ -38,7 +36,6 @@ async function initializeLinuxGPUCache() {
return linuxCachePromise; return linuxCachePromise;
} }
// eslint-disable-next-line sonarjs/cognitive-complexity
linuxCachePromise = (async () => { linuxCachePromise = (async () => {
try { try {
const drmPath = '/sys/class/drm'; const drmPath = '/sys/class/drm';
@ -50,42 +47,6 @@ async function initializeLinuxGPUCache() {
const gpus = []; const gpus = [];
for (const card of cardEntries) { for (const card of cardEntries) {
const devicePath = join(drmPath, card, 'device'); 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 { try {
const memTotalData = await readFile( const memTotalData = await readFile(
`${devicePath}/mem_info_vram_total`, `${devicePath}/mem_info_vram_total`,
@ -109,7 +70,6 @@ async function initializeLinuxGPUCache() {
} catch {} } catch {}
gpus.push({ gpus.push({
deviceName,
devicePath, devicePath,
memoryTotal, memoryTotal,
hwmonPath, hwmonPath,
@ -167,7 +127,6 @@ async function getLinuxGPUData() {
} }
gpus.push({ gpus.push({
deviceName: cachedGPU.deviceName,
usage, usage,
memoryUsed: parseFloat(memoryUsed.toFixed(2)), memoryUsed: parseFloat(memoryUsed.toFixed(2)),
memoryTotal: parseFloat(cachedGPU.memoryTotal.toFixed(2)), memoryTotal: parseFloat(cachedGPU.memoryTotal.toFixed(2)),

149
src/utils/systemInfo.ts Normal file
View file

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