new startus bar to display system util info

This commit is contained in:
Egor 2025-09-10 03:06:02 -07:00
parent 63f388e696
commit 7455a5a270
39 changed files with 756 additions and 317 deletions

View file

@ -1,7 +1,7 @@
{
"name": "gerbil",
"productName": "Gerbil",
"version": "1.1.0",
"version": "1.1.1",
"description": "Run Large Language Models locally",
"main": "out/main/index.js",
"homepage": "./",
@ -63,8 +63,8 @@
"vite": "^7.1.5"
},
"dependencies": {
"@mantine/core": "^8.3.0",
"@mantine/hooks": "^8.3.0",
"@mantine/core": "^8.3.1",
"@mantine/hooks": "^8.3.1",
"axios": "^1.11.0",
"execa": "^9.6.0",
"lucide-react": "^0.543.0",

View file

@ -5,15 +5,15 @@ import { LaunchScreen } from '@/components/screens/Launch';
import { InterfaceScreen } from '@/components/screens/Interface';
import { WelcomeScreen } from '@/components/screens/Welcome';
import { UpdateAvailableModal } from '@/components/UpdateAvailableModal';
import { SettingsModal } from '@/components/settings/SettingsModal';
import { EjectConfirmModal } from '@/components/EjectConfirmModal';
import { ScreenTransition } from '@/components/ScreenTransition';
import { TitleBar } from '@/components/TitleBar';
import { StatusBar } from '@/components/StatusBar';
import { ErrorBoundary } from '@/components/ErrorBoundary';
import { useUpdateChecker } from '@/hooks/useUpdateChecker';
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
import { safeExecute } from '@/utils/logger';
import { TITLEBAR_HEIGHT } from '@/constants';
import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants';
import type { DownloadItem } from '@/types/electron';
import type { InterfaceTab, FrontendPreference, Screen } from '@/types';
@ -24,7 +24,6 @@ export const App = () => {
useState<InterfaceTab>('terminal');
const [frontendPreference, setFrontendPreference] =
useState<FrontendPreference>('koboldcpp');
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
const [ejectConfirmModalOpen, setEjectConfirmModalOpen] = useState(false);
const {
@ -146,6 +145,7 @@ export const App = () => {
return (
<AppShell
header={{ height: TITLEBAR_HEIGHT }}
footer={{ height: STATUSBAR_HEIGHT }}
padding={currentScreen === 'interface' ? 0 : 'md'}
>
<TitleBar
@ -153,7 +153,6 @@ export const App = () => {
currentTab={activeInterfaceTab}
onTabChange={setActiveInterfaceTab}
onEject={handleEject}
onOpenSettings={() => setSettingsModalOpen(true)}
frontendPreference={frontendPreference}
/>
@ -219,22 +218,15 @@ export const App = () => {
}
/>
</AppShell.Main>
<SettingsModal
isOnInterfaceScreen={currentScreen === 'interface'}
opened={settingsModalOpen}
onClose={async () => {
setSettingsModalOpen(false);
const preference = await safeExecute(
() =>
window.electronAPI.config.get(
'frontendPreference'
) as Promise<FrontendPreference>,
'Failed to load frontend preference:'
);
setFrontendPreference(preference || 'koboldcpp');
}}
currentScreen={currentScreen || undefined}
/>
<AppShell.Footer>
<StatusBar
currentScreen={currentScreen}
frontendPreference={frontendPreference}
onFrontendPreferenceChange={setFrontendPreference}
/>
</AppShell.Footer>
<EjectConfirmModal
opened={ejectConfirmModalOpen}
onClose={() => setEjectConfirmModalOpen(false)}

View file

@ -48,7 +48,7 @@ export const DownloadCard = ({
onUpdate,
}: DownloadCardProps) => {
const computedColorScheme = useComputedColorScheme('light', {
getInitialValueInEffect: false,
getInitialValueInEffect: true,
});
const isDark = computedColorScheme === 'dark';
const renderActionButtons = () => {

View file

@ -0,0 +1,147 @@
import { useEffect, useState } from 'react';
import {
Box,
Flex,
ActionIcon,
Tooltip,
Badge,
Group,
useMantineTheme,
useComputedColorScheme,
} from '@mantine/core';
import { Settings } from 'lucide-react';
import { SettingsModal } from '@/components/settings/SettingsModal';
import { safeExecute } from '@/utils/logger';
import type { FrontendPreference, Screen } from '@/types';
import type { SystemMetrics } from '@/main/modules/monitoring';
interface StatusBarProps {
maxDataPoints?: number;
currentScreen: Screen | null;
frontendPreference: FrontendPreference;
onFrontendPreferenceChange: (preference: FrontendPreference) => void;
}
export const StatusBar = ({
maxDataPoints = 60,
currentScreen,
frontendPreference: _frontendPreference,
onFrontendPreferenceChange,
}: StatusBarProps) => {
const [currentMetrics, setCurrentMetrics] = useState<SystemMetrics | null>(
null
);
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
const theme = useMantineTheme();
const colorScheme = useComputedColorScheme('light', {
getInitialValueInEffect: true,
});
useEffect(() => {
let isMounted = true;
const handleMetrics = async (newMetrics: SystemMetrics) => {
if (!isMounted) return;
setCurrentMetrics(newMetrics);
};
window.electronAPI.monitoring.onMetrics(handleMetrics);
void window.electronAPI.monitoring.start();
return () => {
isMounted = false;
window.electronAPI.monitoring.removeMetricsListener();
window.electronAPI.monitoring.stop();
};
}, [maxDataPoints]);
if (!currentMetrics) {
return null;
}
return (
<Box
pr="xs"
style={{
borderTop: `1px solid ${colorScheme === 'dark' ? theme.colors.dark[4] : theme.colors.gray[3]}`,
backgroundColor:
colorScheme === 'dark'
? 'var(--mantine-color-dark-8)'
: 'var(--mantine-color-gray-1)',
}}
>
<Flex align="center" justify="space-between">
<Group gap="xs" wrap="nowrap">
<Tooltip
label={`${currentMetrics.cpu.usage.toFixed(1)}%`}
position="top"
>
<Badge size="sm" variant="light">
CPU: {currentMetrics.cpu.usage.toFixed(1)}%
</Badge>
</Tooltip>
<Tooltip
label={`${(currentMetrics.memory.used / 1024 ** 3).toFixed(1)} GB / ${(currentMetrics.memory.total / 1024 ** 3).toFixed(1)} GB (${currentMetrics.memory.usage.toFixed(1)}%)`}
position="top"
>
<Badge size="sm" variant="light">
RAM: {currentMetrics.memory.usage.toFixed(1)}%
</Badge>
</Tooltip>
{currentMetrics.gpu?.map((gpu, index) => (
<Group key={`gpu-${index}`} gap={4} wrap="nowrap">
<Tooltip label={`${gpu.usage.toFixed(1)}%`} position="top">
<Badge size="sm" variant="light">
GPU: {gpu.usage.toFixed(1)}%
</Badge>
</Tooltip>
<Tooltip
label={`${(gpu.memoryUsed / 1024 ** 2).toFixed(1)} MB / ${(gpu.memoryTotal / 1024 ** 2).toFixed(1)} MB (${gpu.memoryUsage.toFixed(1)}%)`}
position="top"
>
<Badge size="sm" variant="light">
VRAM: {gpu.memoryUsage.toFixed(1)}%
</Badge>
</Tooltip>
</Group>
))}
</Group>
<Tooltip label="Settings" position="top">
<ActionIcon
variant="subtle"
size="sm"
onClick={() => setSettingsModalOpen(true)}
aria-label="Open settings"
style={{
borderRadius: '0.25rem',
}}
>
<Settings size="1rem" />
</ActionIcon>
</Tooltip>
</Flex>
<SettingsModal
isOnInterfaceScreen={currentScreen === 'interface'}
opened={settingsModalOpen}
onClose={async () => {
setSettingsModalOpen(false);
const preference = await safeExecute(
() =>
window.electronAPI.config.get(
'frontendPreference'
) as Promise<FrontendPreference>,
'Failed to load frontend preference:'
);
onFrontendPreferenceChange(preference || 'koboldcpp');
}}
currentScreen={currentScreen || undefined}
/>
</Box>
);
};

View file

@ -7,14 +7,7 @@ import {
useComputedColorScheme,
AppShell,
} from '@mantine/core';
import {
Minus,
Square,
X,
Settings,
Copy,
CircleFadingArrowUp,
} from 'lucide-react';
import { Minus, Square, X, Copy, CircleFadingArrowUp } from 'lucide-react';
import { useState } from 'react';
import { soundAssets, playSound, initializeAudio } from '@/utils/sounds';
import { useAppUpdateChecker } from '@/hooks/useAppUpdateChecker';
@ -28,7 +21,6 @@ interface TitleBarProps {
currentTab: InterfaceTab;
onTabChange: (tab: InterfaceTab) => void;
onEject: () => void;
onOpenSettings: () => void;
frontendPreference: FrontendPreference;
}
@ -37,11 +29,10 @@ export const TitleBar = ({
currentTab,
onTabChange,
onEject,
onOpenSettings,
frontendPreference,
}: TitleBarProps) => {
const computedColorScheme = useComputedColorScheme('light', {
getInitialValueInEffect: false,
getInitialValueInEffect: true,
});
const { hasUpdate, releaseUrl } = useAppUpdateChecker();
const { isImageGenerationMode } = useLaunchConfigStore();
@ -234,30 +225,6 @@ export const TitleBar = ({
</ActionIcon>
)}
<ActionIcon
variant="subtle"
size={TITLEBAR_HEIGHT}
onClick={onOpenSettings}
aria-label="Open settings"
tabIndex={-1}
style={{
borderRadius: '0.25rem',
margin: 0,
outline: 'none',
}}
>
<Settings size="1.25rem" />
</ActionIcon>
<Box
style={{
width: '0.0625rem',
height: '1.25rem',
backgroundColor: 'var(--mantine-color-default-border)',
margin: '0 0.25rem',
}}
/>
{[
{
icon: <Minus size="1rem" />,

View file

@ -2,7 +2,7 @@ import { useState, useCallback, useEffect, useRef } from 'react';
import { Card, Text, Title, Loader, Stack, Container } from '@mantine/core';
import { DownloadCard } from '@/components/DownloadCard';
import { getPlatformDisplayName } from '@/utils/platform';
import { formatDownloadSize } from '@/utils/download';
import { formatDownloadSize } from '@/utils/format';
import { getAssetDescription } from '@/utils/assets';
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
import { safeExecute } from '@/utils/logger';

View file

@ -30,7 +30,7 @@ export const TerminalTab = forwardRef<TerminalTabRef, TerminalTabProps>(
({ onServerReady, frontendPreference = 'koboldcpp' }, ref) => {
const { host, port, isImageGenerationMode } = useLaunchConfigStore();
const computedColorScheme = useComputedColorScheme('light', {
getInitialValueInEffect: false,
getInitialValueInEffect: true,
});
const [terminalContent, setTerminalContent] = useState<string>('');
const [isUserScrolling, setIsUserScrolling] = useState<boolean>(false);

View file

@ -1,4 +1,4 @@
import { Text, Group, Badge } from '@mantine/core';
import { Text, Group, Badge, Box } from '@mantine/core';
import type { BackendOption } from '@/types';
type BackendSelectItemProps = Omit<BackendOption, 'value'>;
@ -9,14 +9,16 @@ export const BackendSelectItem = ({
disabled = false,
}: BackendSelectItemProps) => (
<Group justify="space-between" wrap="nowrap">
<Text size="sm" truncate>
{label}
{disabled && (
<Text component="span" size="xs" ml="xs">
(Compatible devices not found)
</Text>
)}
</Text>
<Box w={!disabled ? '3rem' : 'auto'}>
<Text size="sm" truncate>
{label}
{disabled && (
<Text component="span" size="xs" ml="xs">
(Compatible devices not found)
</Text>
)}
</Text>
</Box>
{devices && devices.length > 0 && (
<Group gap={4}>
{devices.slice(0, 2).map((device, index) => (

View file

@ -1,6 +1,6 @@
import { Card, Container, Stack, Tabs, Group, Button } from '@mantine/core';
import { useState, useEffect, useCallback, useRef } from 'react';
import { tryExecute, error, safeExecute } from '@/utils/logger';
import { tryExecute, logError, safeExecute } from '@/utils/logger';
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import { useLaunchLogic } from '@/hooks/useLaunchLogic';
import { useWarnings } from '@/hooks/useWarnings';
@ -197,7 +197,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
setSelectedFile(fullConfigName);
await window.electronAPI.kobold.setSelectedConfig(fullConfigName);
} else {
error(
logError(
'Failed to create new configuration',
new Error('Save operation failed')
);
@ -207,7 +207,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
const handleSaveConfig = async () => {
if (!selectedFile) {
error(
logError(
'No configuration file selected for saving',
new Error('Selected file is null')
);
@ -221,7 +221,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
);
if (!saveSuccess) {
error(
logError(
'Failed to save configuration',
new Error('Save operation failed')
);

View file

@ -23,7 +23,7 @@ import { ZOOM } from '@/constants';
export const AppearanceTab = () => {
const { colorScheme, setColorScheme } = useMantineColorScheme();
const computedColorScheme = useComputedColorScheme('light', {
getInitialValueInEffect: false,
getInitialValueInEffect: true,
});
const isDark = computedColorScheme === 'dark';

View file

@ -17,7 +17,7 @@ import {
compareVersions,
} from '@/utils/version';
import { safeExecute } from '@/utils/logger';
import { formatDownloadSize } from '@/utils/download';
import { formatDownloadSize } from '@/utils/format';
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
import type { InstalledVersion, ReleaseWithStatus } from '@/types/electron';

View file

@ -4,6 +4,8 @@ export const CONFIG_FILE_NAME = 'config.json';
export const TITLEBAR_HEIGHT = '2.5rem';
export const STATUSBAR_HEIGHT = '1.5rem';
export const MODAL_STYLES_WITH_TITLEBAR = {
overlay: {
top: TITLEBAR_HEIGHT,

View file

@ -1,5 +1,5 @@
import { useState, useCallback, useEffect } from 'react';
import { error } from '@/utils/logger';
import { logError } from '@/utils/logger';
import { compareVersions } from '@/utils/version';
import { GITHUB_API } from '@/constants';
@ -50,7 +50,7 @@ export const useAppUpdateChecker = () => {
return updateInfo;
} catch (err) {
error('Failed to check for app updates:', err as Error);
logError('Failed to check for app updates:', err as Error);
return null;
} finally {
setIsChecking(false);

View file

@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react';
import { error } from '@/utils/logger';
import { logError } from '@/utils/logger';
import { getROCmDownload } from '@/utils/rocm';
import { GITHUB_API } from '@/constants';
import { filterAssetsByPlatform } from '@/utils/platform';
@ -48,7 +48,7 @@ const saveToCache = (releases: DownloadItem[]) => {
};
localStorage.setItem(CACHE_KEY, JSON.stringify(data));
} catch (err) {
error('Failed to save releases to cache', err as Error);
logError('Failed to save releases to cache', err as Error);
}
};
@ -126,7 +126,7 @@ const getLatestReleaseWithDownloadStatus =
availableAssets,
};
} catch (err) {
error('Failed to fetch latest release with status:', err as Error);
logError('Failed to fetch latest release with status:', err as Error);
return null;
}
};
@ -198,7 +198,7 @@ export const useKoboldVersions = (): UseKoboldVersionsReturn => {
setAvailableDownloads(sortDownloadsByType(allDownloads));
} catch (err) {
error('Failed to load remote versions:', err as Error);
logError('Failed to load remote versions:', err as Error);
const cached = loadFromCache();
if (cached) {
const rocm = await getROCmDownload().catch(() => null);
@ -236,7 +236,7 @@ export const useKoboldVersions = (): UseKoboldVersionsReturn => {
return result.success !== false;
} catch (err) {
error('Failed to download ${item.name}:', err as Error);
logError(`Failed to download ${item.name}:`, err as Error);
return false;
} finally {
setDownloading(null);

View file

@ -1,5 +1,5 @@
import { useState, useCallback } from 'react';
import { error } from '@/utils/logger';
import { logError } from '@/utils/logger';
import type { SdConvDirectMode } from '@/types';
interface UseLaunchLogicProps {
@ -283,7 +283,7 @@ export const useLaunchLogic = ({
);
}
} catch (err) {
error('Error launching:', err as Error);
logError('Error launching:', err as Error);
} finally {
setIsLaunching(false);
}

View file

@ -1,5 +1,5 @@
import { useState, useCallback, useEffect } from 'react';
import { error } from '@/utils/logger';
import { logError } from '@/utils/logger';
import {
getDisplayNameFromPath,
compareVersions,
@ -36,7 +36,7 @@ export const useUpdateChecker = () => {
}
setDismissedUpdatesLoaded(true);
} catch (err) {
error('Failed to load dismissed updates:', err as Error);
logError('Failed to load dismissed updates:', err as Error);
setDismissedUpdatesLoaded(true);
}
};
@ -51,7 +51,7 @@ export const useUpdateChecker = () => {
Array.from(updates)
);
} catch (err) {
error('Failed to save dismissed updates:', err as Error);
logError('Failed to save dismissed updates:', err as Error);
}
}, []);
@ -106,7 +106,7 @@ export const useUpdateChecker = () => {
}
}
} catch (err) {
error('Failed to check for updates:', err as Error);
logError('Failed to check for updates:', err as Error);
} finally {
setIsChecking(false);
}

View file

@ -42,6 +42,11 @@ import {
detectBackendSupport,
getAvailableBackends,
} from '@/main/modules/binary';
import {
setMainWindow,
startMonitoring,
stopMonitoring,
} from '@/main/modules/monitoring';
import type { FrontendPreference } from '@/types';
async function launchKoboldCppWithCustomFrontends(args: string[] = []) {
@ -229,4 +234,16 @@ export function setupIPCHandlers() {
);
ipcMain.handle('openwebui:isUvAvailable', () => isUvAvailable());
ipcMain.handle('monitoring:start', () => {
const mainWindow = getMainWindow();
if (mainWindow) {
setMainWindow(mainWindow);
startMonitoring();
}
});
ipcMain.handle('monitoring:stop', () => {
stopMonitoring();
});
}

View file

@ -1,6 +1,5 @@
/* eslint-disable no-comments/disallowComments */
import si from 'systeminformation';
import { shortenDeviceName } from '@/utils/hardware';
import { logError } from '@/main/modules/logging';
import { terminateProcess } from '@/utils/process';
import type {
@ -8,8 +7,10 @@ import type {
GPUCapabilities,
BasicGPUInfo,
GPUMemoryInfo,
HardwareDetectionResult,
} from '@/types/hardware';
import { spawn } from 'child_process';
import { formatDeviceName } from '@/utils/format';
let cpuCapabilitiesCache: CPUCapabilities | null = null;
let basicGPUInfoCache: BasicGPUInfo | null = null;
@ -26,7 +27,7 @@ export async function detectCPU() {
const devices: string[] = [];
if (cpu.brand) {
devices.push(shortenDeviceName(cpu.brand));
devices.push(formatDeviceName(cpu.brand));
}
const avx = flags.includes('avx') || flags.includes('AVX');
@ -65,7 +66,7 @@ export async function detectGPU() {
for (const controller of graphics.controllers) {
if (controller.model) {
gpuInfo.push(shortenDeviceName(controller.model));
gpuInfo.push(formatDeviceName(controller.model));
}
const vendor = controller.vendor?.toLowerCase() || '';
@ -142,10 +143,7 @@ async function detectCUDA() {
output += data.toString();
});
return new Promise<{
supported: boolean;
devices: string[];
}>((resolve) => {
return new Promise<HardwareDetectionResult>((resolve) => {
nvidia.on('close', (code) => {
if (code === 0 && output.trim()) {
const devices = output
@ -154,7 +152,7 @@ async function detectCUDA() {
.map((line) => {
const parts = line.split(',');
const rawName = parts[0]?.trim() || 'Unknown NVIDIA GPU';
return shortenDeviceName(rawName);
return formatDeviceName(rawName);
})
.filter(Boolean);
@ -206,10 +204,7 @@ export async function detectROCm() {
output += data.toString();
});
return new Promise<{
supported: boolean;
devices: string[];
}>((resolve) => {
return new Promise<HardwareDetectionResult>((resolve) => {
// eslint-disable-next-line sonarjs/cognitive-complexity
rocminfo.on('close', (code) => {
if (code === 0 && output.trim()) {
@ -225,9 +220,9 @@ export async function detectROCm() {
if (
name &&
!name.toLowerCase().includes('cpu') &&
!devices.includes(shortenDeviceName(name))
!devices.includes(formatDeviceName(name))
) {
devices.push(shortenDeviceName(name));
devices.push(formatDeviceName(name));
}
}
}
@ -262,7 +257,7 @@ export async function detectROCm() {
}
if (deviceType !== 'CPU') {
devices.push(shortenDeviceName(name));
devices.push(formatDeviceName(name));
}
}
}
@ -301,10 +296,7 @@ async function detectVulkan() {
output += data.toString();
});
return new Promise<{
supported: boolean;
devices: string[];
}>((resolve) => {
return new Promise<HardwareDetectionResult>((resolve) => {
vulkaninfo.on('close', (code) => {
if (code === 0 && output.trim()) {
const devices: string[] = [];
@ -316,7 +308,7 @@ async function detectVulkan() {
if (parts.length >= 2) {
const name = parts[1]?.trim();
if (name) {
devices.push(shortenDeviceName(name));
devices.push(formatDeviceName(name));
}
}
}
@ -363,7 +355,7 @@ function parseClInfoOutput(output: string) {
const deviceName = findDeviceNameInClInfo(lines, i);
if (deviceName && currentPlatform) {
const deviceLabel = `${shortenDeviceName(deviceName)} (${currentPlatform})`;
const deviceLabel = `${formatDeviceName(deviceName)} (${currentPlatform})`;
devices.push(deviceLabel);
}
}
@ -407,10 +399,7 @@ async function detectCLBlast() {
output += data.toString();
});
return new Promise<{
supported: boolean;
devices: string[];
}>((resolve) => {
return new Promise<HardwareDetectionResult>((resolve) => {
clinfo.on('close', (code) => {
if (code === 0 && output.trim()) {
const devices = parseClInfoOutput(output);
@ -456,7 +445,7 @@ export async function detectGPUMemory() {
}
memoryInfo.push({
deviceName: shortenDeviceName(controller.model),
deviceName: formatDeviceName(controller.model),
totalMemoryMB: vram,
});
}

View file

@ -0,0 +1,299 @@
import si from 'systeminformation';
import { BrowserWindow } from 'electron';
import { logError } from '@/main/modules/logging';
import { readFile, readdir } from 'fs/promises';
import { join } from 'path';
import { spawn } from 'child_process';
import { platform } from 'process';
export interface SystemMetrics {
timestamp: number;
cpu: {
usage: number;
};
memory: {
used: number;
total: number;
usage: number;
};
gpu?: {
name: string;
usage: number;
memoryUsed: number;
memoryTotal: number;
memoryUsage: number;
}[];
}
let interval: ReturnType<typeof setInterval> | null = null;
let isRunning = false;
let updateFrequency = 1000;
let mainWindow: BrowserWindow | null = null;
export function setMainWindow(window: BrowserWindow) {
mainWindow = window;
}
export function startMonitoring() {
if (isRunning) return;
isRunning = true;
collectAndSendMetrics();
interval = setInterval(() => {
collectAndSendMetrics();
}, updateFrequency);
}
export function stopMonitoring() {
if (interval) {
clearInterval(interval);
interval = null;
}
isRunning = false;
}
async function collectAndSendMetrics() {
try {
const metrics = await collectMetrics();
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('system-metrics', metrics);
}
} catch (error) {
logError('Failed to collect system metrics:', error as Error);
}
}
async function getWindowsGPUData() {
return new Promise<
{
deviceName: string;
usage: number;
memoryUsed: number;
memoryTotal: number;
}[]
>((resolve) => {
const gpus: {
deviceName: string;
usage: number;
memoryUsed: number;
memoryTotal: number;
}[] = [];
const powershellScript = `
# Get GPU information using WMI and Performance Counters
$gpuInfo = Get-WmiObject -Class Win32_VideoController | Where-Object {$_.Name -notlike "*Microsoft*" -and $_.Name -notlike "*Remote*"}
foreach ($gpu in $gpuInfo) {
$name = $gpu.Name
$vramTotal = if ($gpu.AdapterRAM) { $gpu.AdapterRAM } else { 0 }
# Try to get GPU usage from performance counters
$usage = 0
try {
$counter = Get-Counter "\\GPU Engine(*engtype_3D)\\Utilization Percentage" -ErrorAction SilentlyContinue
if ($counter) {
$usage = ($counter.CounterSamples | Measure-Object -Property CookedValue -Maximum).Maximum
}
} catch {}
# Try alternative performance counter for GPU usage
if ($usage -eq 0) {
try {
$counter = Get-Counter "\\GPU Process Memory(*)\\Dedicated Usage" -ErrorAction SilentlyContinue
if ($counter) {
$memUsed = ($counter.CounterSamples | Measure-Object -Property CookedValue -Sum).Sum
$usage = if ($vramTotal -gt 0) { [math]::Round(($memUsed / $vramTotal) * 100, 2) } else { 0 }
}
} catch {}
}
# Output as JSON
@{
Name = $name
Usage = $usage
MemoryUsed = 0
MemoryTotal = $vramTotal
} | ConvertTo-Json -Compress
}
`;
const powershell = spawn(
'powershell.exe',
[
'-NoProfile',
'-ExecutionPolicy',
'Bypass',
'-Command',
powershellScript,
],
{ timeout: 5000 }
);
let output = '';
powershell.stdout.on('data', (data) => {
output += data.toString();
});
powershell.on('close', (code) => {
if (code === 0 && output.trim()) {
try {
const lines = output
.trim()
.split('\n')
.filter((line) => line.trim());
for (const line of lines) {
try {
const gpuData = JSON.parse(line);
if (gpuData.Name) {
gpus.push({
deviceName: 'GPU',
usage: parseFloat(gpuData.Usage) || 0,
memoryUsed: parseInt(gpuData.MemoryUsed) || 0,
memoryTotal: parseInt(gpuData.MemoryTotal) || 0,
});
}
} catch {
continue;
}
}
} catch (error) {
logError('Failed to parse Windows GPU data:', error as Error);
}
}
resolve(gpus);
});
powershell.on('error', () => {
resolve(gpus);
});
setTimeout(() => {
powershell.kill();
resolve(gpus);
}, 5000);
});
}
async function getLinuxGPUData() {
try {
return getLinuxDrmMetrics();
} catch (error) {
logError('Failed to get Linux GPU data:', error as Error);
return [];
}
}
async function getLinuxDrmMetrics() {
try {
const drmPath = '/sys/class/drm';
const entries = await readdir(drmPath);
const cardEntries = entries.filter(
(entry) => entry.startsWith('card') && !entry.includes('-')
);
const gpus = [];
for (const card of cardEntries) {
const devicePath = join(drmPath, card, 'device');
try {
const usageData = await readFile(
`${devicePath}/gpu_busy_percent`,
'utf8'
);
const memUsedData = await readFile(
`${devicePath}/mem_info_vram_used`,
'utf8'
);
const memTotalData = await readFile(
`${devicePath}/mem_info_vram_total`,
'utf8'
);
const usage = parseInt(usageData.trim(), 10) || 0;
const memoryUsed = parseInt(memUsedData.trim(), 10) || 0;
const memoryTotal = parseInt(memTotalData.trim(), 10) || 0;
gpus.push({
deviceName: 'GPU',
usage,
memoryUsed,
memoryTotal,
});
} catch {
continue;
}
}
return gpus;
} catch {
return [];
}
}
async function getGPUData() {
if (platform === 'win32') {
return getWindowsGPUData();
} else if (platform === 'linux') {
return getLinuxGPUData();
} else {
return [];
}
}
async function collectMetrics(): Promise<SystemMetrics> {
const timestamp = Date.now();
const [cpuData, memData, gpuData] = await Promise.allSettled([
si.currentLoad(),
si.mem(),
getGPUData(),
]);
const cpu = {
usage: cpuData.status === 'fulfilled' ? cpuData.value.currentLoad : 0,
};
const memory = {
used:
memData.status === 'fulfilled'
? memData.value.active || memData.value.used
: 0,
total: memData.status === 'fulfilled' ? memData.value.total : 0,
usage:
memData.status === 'fulfilled'
? ((memData.value.active || memData.value.used) / memData.value.total) *
100
: 0,
};
let gpu: SystemMetrics['gpu'];
if (gpuData.status === 'fulfilled' && gpuData.value.length > 0) {
gpu = gpuData.value.map((gpuInfo: {
deviceName: string;
usage: number;
memoryUsed: number;
memoryTotal: number;
}) => ({
name: gpuInfo.deviceName,
usage: gpuInfo.usage,
memoryUsed: gpuInfo.memoryUsed,
memoryTotal: gpuInfo.memoryTotal,
memoryUsage:
gpuInfo.memoryTotal > 0
? (gpuInfo.memoryUsed / gpuInfo.memoryTotal) * 100
: 0,
}));
} else if (gpuData.status === 'rejected') {
logError('GPU detection failed', gpuData.reason as Error);
}
return {
timestamp,
cpu,
memory,
gpu,
};
}

View file

@ -18,6 +18,8 @@ export function createMainWindow() {
: join(__dirname, '../../assets/icon.png');
mainWindow = new BrowserWindow({
minWidth: 600,
minHeight: 600,
width: windowWidth,
height: windowHeight,
x: Math.floor((size.width - windowWidth) / 2),

View file

@ -6,6 +6,7 @@ import type {
LogsAPI,
SillyTavernAPI,
OpenWebUIAPI,
MonitoringAPI,
} from '@/types/electron';
const koboldAPI: KoboldAPI = {
@ -107,6 +108,17 @@ const openwebUIAPI: OpenWebUIAPI = {
isUvAvailable: () => ipcRenderer.invoke('openwebui:isUvAvailable'),
};
const monitoringAPI: MonitoringAPI = {
start: () => ipcRenderer.invoke('monitoring:start'),
stop: () => ipcRenderer.invoke('monitoring:stop'),
onMetrics: (callback) => {
ipcRenderer.on('system-metrics', (_, metrics) => callback(metrics));
},
removeMetricsListener: () => {
ipcRenderer.removeAllListeners('system-metrics');
},
};
contextBridge.exposeInMainWorld('electronAPI', {
kobold: koboldAPI,
app: appAPI,
@ -114,4 +126,5 @@ contextBridge.exposeInMainWorld('electronAPI', {
logs: logsAPI,
sillytavern: sillyTavernAPI,
openwebui: openwebUIAPI,
monitoring: monitoringAPI,
});

View file

@ -375,7 +375,7 @@ export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
loadConfigFromFile: async (
configFiles: ConfigFile[],
savedConfig: string | null
): Promise<string | null> => {
) => {
let currentSelectedFile = null;
if (savedConfig) {

View file

@ -6,6 +6,7 @@ import type {
} from '@/types/hardware';
import type { BackendOption, BackendSupport } from '@/types';
import type { MantineColorScheme } from '@mantine/core';
import type { SystemMetrics } from '@/main/modules/monitoring';
export interface GitHubAsset {
name: string;
@ -172,6 +173,13 @@ export interface OpenWebUIAPI {
isUvAvailable: () => Promise<boolean>;
}
export interface MonitoringAPI {
start: () => Promise<void>;
stop: () => Promise<void>;
onMetrics: (callback: (metrics: SystemMetrics) => void) => void;
removeMetricsListener: () => void;
}
declare global {
interface Window {
electronAPI: {
@ -181,6 +189,7 @@ declare global {
logs: LogsAPI;
sillytavern: SillyTavernAPI;
openwebui: OpenWebUIAPI;
monitoring: MonitoringAPI;
};
}
}

View file

@ -34,6 +34,11 @@ export interface BasicGPUInfo {
gpuInfo: string[];
}
export interface HardwareDetectionResult {
supported: boolean;
devices: string[];
}
export interface HardwareInfo {
cpu: CPUCapabilities;
gpu: BasicGPUInfo;

View file

@ -1,7 +1,7 @@
import { ASSET_SUFFIXES } from '@/constants';
import { stripAssetExtensions } from '@/utils/version';
export const getAssetDescription = (assetName: string): string => {
export const getAssetDescription = (assetName: string) => {
const name = stripAssetExtensions(assetName).toLowerCase();
if (name.includes(ASSET_SUFFIXES.ROCM)) {
@ -19,7 +19,7 @@ export const getAssetDescription = (assetName: string): string => {
return "Standard build that's ideal for most cases.";
};
export const isAssetStandard = (assetName: string): boolean => {
export const isAssetStandard = (assetName: string) => {
const name = stripAssetExtensions(assetName).toLowerCase();
return (
@ -54,7 +54,7 @@ export const sortDownloadsByType = <T extends { name: string }>(
return a.name.localeCompare(b.name);
});
export const pretifyBinName = (binName: string): string => {
export const pretifyBinName = (binName: string) => {
const cleanName = stripAssetExtensions(binName).toLowerCase();
if (cleanName.includes(ASSET_SUFFIXES.ROCM)) {

View file

@ -1,17 +0,0 @@
import { ROCM } from '@/constants';
export const formatDownloadSize = (size: number, url?: string): string => {
if (!size) return '';
const isApproximateSize = url?.includes(ROCM.LINUX.DOWNLOAD_URL);
return isApproximateSize
? `~${formatFileSizeInMB(size)}`
: formatFileSizeInMB(size);
};
const formatFileSizeInMB = (bytes: number) => {
if (bytes === 0) return '0 MB';
const mb = bytes / (1024 * 1024);
return parseFloat(mb.toFixed(1)) + ' MB';
};

47
src/utils/format.ts Normal file
View file

@ -0,0 +1,47 @@
import { ROCM } from '@/constants';
export const formatDownloadSize = (size: number, url?: string) => {
if (!size) return '';
const isApproximateSize = url?.includes(ROCM.LINUX.DOWNLOAD_URL);
return isApproximateSize
? `~${formatFileSizeInMB(size)}`
: formatFileSizeInMB(size);
};
const formatFileSizeInMB = (bytes: number) => {
if (bytes === 0) return '0 MB';
const mb = bytes / (1024 * 1024);
return parseFloat(mb.toFixed(1)) + ' MB';
};
export const formatBytes = (bytes: number) => {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(1)} ${units[unitIndex]}`;
};
export const formatDeviceName = (deviceName: string) =>
deviceName
.replace(/^AMD\s+/i, '')
.replace(/^NVIDIA\s+/i, '')
.replace(/^Intel\s+/i, '')
.replace(/\s+Graphics/i, '')
.replace(/\s+GPU/i, '')
.replace(/\s+Processor/i, '')
.replace(/\s+CPU/i, '')
.replace(/GeForce\s+/i, '')
.replace(/Radeon\s+/i, '')
.replace(/\s+Series/i, '')
.replace(/\s*[([{].*?[)\]}]\s*/g, '')
.replace(/\s*\d+-core\s*/gi, '')
.replace(/\s+/g, ' ')
.trim();

View file

@ -1,7 +1,7 @@
import { readFile, writeFile, access, mkdir } from 'fs/promises';
import { constants } from 'fs';
export const pathExists = async (path: string): Promise<boolean> => {
export const pathExists = async (path: string) => {
try {
await access(path, constants.F_OK);
return true;
@ -21,15 +21,12 @@ export const readJsonFile = async <T = unknown>(
}
};
export const writeJsonFile = async (
path: string,
data: unknown
): Promise<void> => {
export const writeJsonFile = async (path: string, data: unknown) => {
const content = JSON.stringify(data, null, 2);
await writeFile(path, content, 'utf-8');
};
export const ensureDir = async (path: string): Promise<void> => {
export const ensureDir = async (path: string) => {
try {
await mkdir(path, { recursive: true });
} catch (error) {

View file

@ -1,14 +0,0 @@
export const shortenDeviceName = (deviceName: string): string =>
deviceName
.replace(/^AMD\s+/i, '')
.replace(/^NVIDIA\s+/i, '')
.replace(/^Intel\s+/i, '')
.replace(/\s+Graphics/i, '')
.replace(/\s+GPU/i, '')
.replace(/\s+Processor/i, '')
.replace(/\s+CPU/i, '')
.replace(/GeForce\s+/i, '')
.replace(/Radeon\s+/i, '')
.replace(/\s+Series/i, '')
.replace(/\s+/g, ' ')
.trim();

View file

@ -1,10 +1,4 @@
export interface KoboldConfig {
host: string;
port: number;
isImageMode: boolean;
}
export function parseKoboldConfig(args: string[]): KoboldConfig {
export function parseKoboldConfig(args: string[]) {
let host = 'localhost';
let port = 5001;
let hasSdModel = false;

View file

@ -1,4 +1,4 @@
const logError = (message: string, error: Error): void => {
export const logError = (message: string, error: Error) => {
if (window.electronAPI?.logs?.logError) {
window.electronAPI.logs.logError(message, error);
}
@ -19,7 +19,7 @@ export const safeExecute = async <T>(
export const tryExecute = async (
operation: () => Promise<void>,
errorMessage: string
): Promise<boolean> => {
) => {
try {
await operation();
return true;
@ -28,7 +28,3 @@ export const tryExecute = async (
return false;
}
};
export const error = (message: string, error: Error): void => {
logError(message, error);
};

View file

@ -2,11 +2,11 @@ import { join } from 'path';
import { homedir } from 'os';
import { PRODUCT_NAME, CONFIG_FILE_NAME } from '@/constants';
export function getConfigDir(): string {
export function getConfigDir() {
return join(getConfigDirPath(), CONFIG_FILE_NAME);
}
function getConfigDirPath(): string {
function getConfigDirPath() {
const platform = process.platform;
const home = homedir();

View file

@ -9,8 +9,8 @@ export interface ProcessTerminationOptions {
async function killWindowsProcessTree(
pid: number,
logError?: (message: string, error: Error) => void
): Promise<void> {
return new Promise((resolve) => {
) {
return new Promise<void>((resolve) => {
const taskkill = spawn('taskkill', ['/pid', pid.toString(), '/t', '/f'], {
stdio: 'pipe',
});
@ -35,7 +35,7 @@ async function killWindowsProcessTree(
export async function terminateProcess(
childProcess: ChildProcess,
options: ProcessTerminationOptions = {}
): Promise<void> {
) {
const { timeoutMs = 3000, logError } = options;
if (!childProcess?.pid) {

View file

@ -1,8 +1,7 @@
import { GITHUB_API, ROCM } from '@/constants';
import type { DownloadItem } from '@/types/electron';
import type { GitHubAsset } from '@/types';
export async function getROCmDownload(): Promise<DownloadItem | null> {
export async function getROCmDownload() {
const platform = await window.electronAPI.kobold.getPlatform();
if (platform === 'linux') {
@ -14,7 +13,7 @@ export async function getROCmDownload(): Promise<DownloadItem | null> {
return null;
}
async function getLinuxROCmDownload(): Promise<DownloadItem | null> {
async function getLinuxROCmDownload() {
try {
const response = await fetch(GITHUB_API.LATEST_RELEASE_URL);
if (!response.ok) {
@ -40,7 +39,7 @@ async function getLinuxROCmDownload(): Promise<DownloadItem | null> {
}
}
async function getWindowsROCmDownload(): Promise<DownloadItem | null> {
async function getWindowsROCmDownload() {
try {
const response = await fetch(GITHUB_API.ROCM_LATEST_RELEASE_URL);
if (!response.ok) {

View file

@ -1,9 +1,6 @@
import { error } from '@/utils/logger';
import { logError } from '@/utils/logger';
export const handleTerminalOutput = (
prevContent: string,
newData: string
): string => {
export const handleTerminalOutput = (prevContent: string, newData: string) => {
try {
if (newData.includes('\r')) {
const hasStandaloneCarriageReturns = /\r(?!\n)/g.test(newData);
@ -32,14 +29,14 @@ export const handleTerminalOutput = (
return prevContent + newData;
} catch (err) {
error('Terminal Basic Error', err as Error);
logError('Terminal Basic Error', err as Error);
return prevContent + newData;
}
};
const URL_REGEX = /(https?:\/\/[^\s<>"{}|\\^`[\]]+)/gi;
const linkifyText = (text: string): string =>
const linkifyText = (text: string) =>
text.replace(URL_REGEX, (url) => {
const cleanUrl = url.replace(/[.,;:!?]+$/, '');
const trailingPunctuation = url.slice(cleanUrl.length);
@ -47,7 +44,7 @@ const linkifyText = (text: string): string =>
return `<a href="${cleanUrl}" target="_blank" rel="noopener noreferrer" style="color: #339af0; text-decoration: underline; cursor: pointer;">${cleanUrl}</a>${trailingPunctuation}`;
});
const escapeHtmlExceptLinks = (text: string): string => {
const escapeHtmlExceptLinks = (text: string) => {
const escaped = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
@ -58,7 +55,7 @@ const escapeHtmlExceptLinks = (text: string): string => {
return linkifyText(escaped);
};
export const processTerminalContent = (content: string): string => {
export const processTerminalContent = (content: string) => {
if (!content) return '';
return escapeHtmlExceptLinks(content);

View file

@ -1,4 +1,4 @@
const isValidUrl = (string: string): boolean => {
const isValidUrl = (string: string) => {
try {
const url = new URL(string);
return url.protocol === 'http:' || url.protocol === 'https:';
@ -7,7 +7,7 @@ const isValidUrl = (string: string): boolean => {
}
};
const isValidFilePath = (path: string): boolean => {
const isValidFilePath = (path: string) => {
if (!path.trim()) return false;
const validExtensions = ['.gguf'];
@ -18,9 +18,7 @@ const isValidFilePath = (path: string): boolean => {
return hasValidExtension || path.includes('/') || path.includes('\\');
};
export const getInputValidationState = (
path: string
): 'valid' | 'invalid' | 'neutral' => {
export const getInputValidationState = (path: string) => {
if (!path.trim()) return 'neutral';
if (isValidUrl(path) || isValidFilePath(path)) {

View file

@ -1,8 +1,6 @@
import type { InstalledVersion } from '@/types';
export const getDisplayNameFromPath = (
installedVersion: InstalledVersion
): string => {
export const getDisplayNameFromPath = (installedVersion: InstalledVersion) => {
const pathParts = installedVersion.path.split(/[/\\]/);
const launcherIndex = pathParts.findIndex(
(part) => part === 'koboldcpp-launcher' || part === 'koboldcpp-launcher.exe'
@ -15,16 +13,16 @@ export const getDisplayNameFromPath = (
return installedVersion.filename;
};
export const stripAssetExtensions = (assetName: string): string =>
export const stripAssetExtensions = (assetName: string) =>
assetName.replace(/\.(tar\.gz|zip|exe|dmg|AppImage)$/i, '');
const stripVersionSuffix = (displayName: string): string =>
const stripVersionSuffix = (displayName: string) =>
displayName.replace(
/-(\d+\.\d+(?:\.\d+)?(?:\.[a-zA-Z0-9]+)*(?:-[a-zA-Z0-9]+)*)$/,
''
);
export const compareVersions = (versionA: string, versionB: string): number => {
export const compareVersions = (versionA: string, versionB: string) => {
const cleanVersion = (version: string): string =>
version.replace(/^v/, '').replace(/[^0-9.]/g, '');

View file

@ -1,10 +1,10 @@
import { ZOOM } from '@/constants';
export const zoomLevelToPercentage = (zoomLevel: number): number =>
export const zoomLevelToPercentage = (zoomLevel: number) =>
Math.round(Math.pow(1.2, zoomLevel) * 100);
export const percentageToZoomLevel = (percentage: number): number =>
export const percentageToZoomLevel = (percentage: number) =>
Math.log(percentage / 100) / Math.log(1.2);
export const isValidZoomPercentage = (percentage: number): boolean =>
export const isValidZoomPercentage = (percentage: number) =>
percentage >= ZOOM.MIN_PERCENTAGE && percentage <= ZOOM.MAX_PERCENTAGE;

232
yarn.lock
View file

@ -585,13 +585,13 @@ __metadata:
linkType: hard
"@eslint-community/eslint-utils@npm:^4.7.0, @eslint-community/eslint-utils@npm:^4.8.0":
version: 4.8.0
resolution: "@eslint-community/eslint-utils@npm:4.8.0"
version: 4.9.0
resolution: "@eslint-community/eslint-utils@npm:4.9.0"
dependencies:
eslint-visitor-keys: "npm:^3.4.3"
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
checksum: 10c0/33b93d2a4e9d5fe4c11d02d0fc5ed69e12fcb1e7ca031ded0d6adb24e768c36df77288ed79eecc784f9db34219816247db27688dfe869fb7fbf096840a097d7a
checksum: 10c0/8881e22d519326e7dba85ea915ac7a143367c805e6ba1374c987aa2fbdd09195cc51183d2da72c0e2ff388f84363e1b220fd0d19bef10c272c63455162176817
languageName: node
linkType: hard
@ -864,9 +864,9 @@ __metadata:
languageName: node
linkType: hard
"@mantine/core@npm:^8.3.0":
version: 8.3.0
resolution: "@mantine/core@npm:8.3.0"
"@mantine/core@npm:^8.3.1":
version: 8.3.1
resolution: "@mantine/core@npm:8.3.1"
dependencies:
"@floating-ui/react": "npm:^0.27.16"
clsx: "npm:^2.1.1"
@ -875,19 +875,19 @@ __metadata:
react-textarea-autosize: "npm:8.5.9"
type-fest: "npm:^4.41.0"
peerDependencies:
"@mantine/hooks": 8.3.0
"@mantine/hooks": 8.3.1
react: ^18.x || ^19.x
react-dom: ^18.x || ^19.x
checksum: 10c0/28430c11c726af9e5b992bc195614cc67a8d1a4d25f3ced6753ab0399d67c6bb927c1b5eb4f383f8f75ef09eec3b17998761ab5983c850459d7cf34e75d0688b
checksum: 10c0/6faab4553d35e3f676e852ea9b926e0d3b744eda5b38d450eb3bdb89c92cdd2ce8ae9fd7e4b47c83fc870d215a62c8fa3299d25fcaa83a07f67c782883a8ca30
languageName: node
linkType: hard
"@mantine/hooks@npm:^8.3.0":
version: 8.3.0
resolution: "@mantine/hooks@npm:8.3.0"
"@mantine/hooks@npm:^8.3.1":
version: 8.3.1
resolution: "@mantine/hooks@npm:8.3.1"
peerDependencies:
react: ^18.x || ^19.x
checksum: 10c0/2caaac7654a97424e798d3ae18ee9c70219a62d69dbd21582da90f44b8381674ed496583e4e7ad378a2fd42b7a78fb84534982297918acb24ed674abcb1a312b
checksum: 10c0/d64ea9c848a687668ce34007e3ab226b5c45c52ea1f4bdd721ca17b07d7f7a642b86e23ee65777179d6cd1ae9c3ce56fe43602828f332bfc2cbd59658b1c9047
languageName: node
linkType: hard
@ -974,149 +974,149 @@ __metadata:
languageName: node
linkType: hard
"@rollup/rollup-android-arm-eabi@npm:4.50.0":
version: 4.50.0
resolution: "@rollup/rollup-android-arm-eabi@npm:4.50.0"
"@rollup/rollup-android-arm-eabi@npm:4.50.1":
version: 4.50.1
resolution: "@rollup/rollup-android-arm-eabi@npm:4.50.1"
conditions: os=android & cpu=arm
languageName: node
linkType: hard
"@rollup/rollup-android-arm64@npm:4.50.0":
version: 4.50.0
resolution: "@rollup/rollup-android-arm64@npm:4.50.0"
"@rollup/rollup-android-arm64@npm:4.50.1":
version: 4.50.1
resolution: "@rollup/rollup-android-arm64@npm:4.50.1"
conditions: os=android & cpu=arm64
languageName: node
linkType: hard
"@rollup/rollup-darwin-arm64@npm:4.50.0":
version: 4.50.0
resolution: "@rollup/rollup-darwin-arm64@npm:4.50.0"
"@rollup/rollup-darwin-arm64@npm:4.50.1":
version: 4.50.1
resolution: "@rollup/rollup-darwin-arm64@npm:4.50.1"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@rollup/rollup-darwin-x64@npm:4.50.0":
version: 4.50.0
resolution: "@rollup/rollup-darwin-x64@npm:4.50.0"
"@rollup/rollup-darwin-x64@npm:4.50.1":
version: 4.50.1
resolution: "@rollup/rollup-darwin-x64@npm:4.50.1"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@rollup/rollup-freebsd-arm64@npm:4.50.0":
version: 4.50.0
resolution: "@rollup/rollup-freebsd-arm64@npm:4.50.0"
"@rollup/rollup-freebsd-arm64@npm:4.50.1":
version: 4.50.1
resolution: "@rollup/rollup-freebsd-arm64@npm:4.50.1"
conditions: os=freebsd & cpu=arm64
languageName: node
linkType: hard
"@rollup/rollup-freebsd-x64@npm:4.50.0":
version: 4.50.0
resolution: "@rollup/rollup-freebsd-x64@npm:4.50.0"
"@rollup/rollup-freebsd-x64@npm:4.50.1":
version: 4.50.1
resolution: "@rollup/rollup-freebsd-x64@npm:4.50.1"
conditions: os=freebsd & cpu=x64
languageName: node
linkType: hard
"@rollup/rollup-linux-arm-gnueabihf@npm:4.50.0":
version: 4.50.0
resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.50.0"
"@rollup/rollup-linux-arm-gnueabihf@npm:4.50.1":
version: 4.50.1
resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.50.1"
conditions: os=linux & cpu=arm & libc=glibc
languageName: node
linkType: hard
"@rollup/rollup-linux-arm-musleabihf@npm:4.50.0":
version: 4.50.0
resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.50.0"
"@rollup/rollup-linux-arm-musleabihf@npm:4.50.1":
version: 4.50.1
resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.50.1"
conditions: os=linux & cpu=arm & libc=musl
languageName: node
linkType: hard
"@rollup/rollup-linux-arm64-gnu@npm:4.50.0":
version: 4.50.0
resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.50.0"
"@rollup/rollup-linux-arm64-gnu@npm:4.50.1":
version: 4.50.1
resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.50.1"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@rollup/rollup-linux-arm64-musl@npm:4.50.0":
version: 4.50.0
resolution: "@rollup/rollup-linux-arm64-musl@npm:4.50.0"
"@rollup/rollup-linux-arm64-musl@npm:4.50.1":
version: 4.50.1
resolution: "@rollup/rollup-linux-arm64-musl@npm:4.50.1"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@rollup/rollup-linux-loongarch64-gnu@npm:4.50.0":
version: 4.50.0
resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.50.0"
"@rollup/rollup-linux-loongarch64-gnu@npm:4.50.1":
version: 4.50.1
resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.50.1"
conditions: os=linux & cpu=loong64 & libc=glibc
languageName: node
linkType: hard
"@rollup/rollup-linux-ppc64-gnu@npm:4.50.0":
version: 4.50.0
resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.50.0"
"@rollup/rollup-linux-ppc64-gnu@npm:4.50.1":
version: 4.50.1
resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.50.1"
conditions: os=linux & cpu=ppc64 & libc=glibc
languageName: node
linkType: hard
"@rollup/rollup-linux-riscv64-gnu@npm:4.50.0":
version: 4.50.0
resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.50.0"
"@rollup/rollup-linux-riscv64-gnu@npm:4.50.1":
version: 4.50.1
resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.50.1"
conditions: os=linux & cpu=riscv64 & libc=glibc
languageName: node
linkType: hard
"@rollup/rollup-linux-riscv64-musl@npm:4.50.0":
version: 4.50.0
resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.50.0"
"@rollup/rollup-linux-riscv64-musl@npm:4.50.1":
version: 4.50.1
resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.50.1"
conditions: os=linux & cpu=riscv64 & libc=musl
languageName: node
linkType: hard
"@rollup/rollup-linux-s390x-gnu@npm:4.50.0":
version: 4.50.0
resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.50.0"
"@rollup/rollup-linux-s390x-gnu@npm:4.50.1":
version: 4.50.1
resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.50.1"
conditions: os=linux & cpu=s390x & libc=glibc
languageName: node
linkType: hard
"@rollup/rollup-linux-x64-gnu@npm:4.50.0":
version: 4.50.0
resolution: "@rollup/rollup-linux-x64-gnu@npm:4.50.0"
"@rollup/rollup-linux-x64-gnu@npm:4.50.1":
version: 4.50.1
resolution: "@rollup/rollup-linux-x64-gnu@npm:4.50.1"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@rollup/rollup-linux-x64-musl@npm:4.50.0":
version: 4.50.0
resolution: "@rollup/rollup-linux-x64-musl@npm:4.50.0"
"@rollup/rollup-linux-x64-musl@npm:4.50.1":
version: 4.50.1
resolution: "@rollup/rollup-linux-x64-musl@npm:4.50.1"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@rollup/rollup-openharmony-arm64@npm:4.50.0":
version: 4.50.0
resolution: "@rollup/rollup-openharmony-arm64@npm:4.50.0"
"@rollup/rollup-openharmony-arm64@npm:4.50.1":
version: 4.50.1
resolution: "@rollup/rollup-openharmony-arm64@npm:4.50.1"
conditions: os=openharmony & cpu=arm64
languageName: node
linkType: hard
"@rollup/rollup-win32-arm64-msvc@npm:4.50.0":
version: 4.50.0
resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.50.0"
"@rollup/rollup-win32-arm64-msvc@npm:4.50.1":
version: 4.50.1
resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.50.1"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@rollup/rollup-win32-ia32-msvc@npm:4.50.0":
version: 4.50.0
resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.50.0"
"@rollup/rollup-win32-ia32-msvc@npm:4.50.1":
version: 4.50.1
resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.50.1"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@rollup/rollup-win32-x64-msvc@npm:4.50.0":
version: 4.50.0
resolution: "@rollup/rollup-win32-x64-msvc@npm:4.50.0"
"@rollup/rollup-win32-x64-msvc@npm:4.50.1":
version: 4.50.1
resolution: "@rollup/rollup-win32-x64-msvc@npm:4.50.1"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
@ -1614,9 +1614,9 @@ __metadata:
linkType: hard
"ansi-regex@npm:^6.0.1":
version: 6.2.0
resolution: "ansi-regex@npm:6.2.0"
checksum: 10c0/20a2e55ae9816074a60e6729dbe3daad664cd967fc82acc08b02f5677db84baa688babf940d71f50acbbb184c02459453789705e079f4d521166ae66451de551
version: 6.2.2
resolution: "ansi-regex@npm:6.2.2"
checksum: 10c0/05d4acb1d2f59ab2cf4b794339c7b168890d44dda4bf0ce01152a8da0213aca207802f930442ce8cd22d7a92f44907664aac6508904e75e038fa944d2601b30f
languageName: node
linkType: hard
@ -1630,9 +1630,9 @@ __metadata:
linkType: hard
"ansi-styles@npm:^6.1.0":
version: 6.2.1
resolution: "ansi-styles@npm:6.2.1"
checksum: 10c0/5d1ec38c123984bcedd996eac680d548f31828bd679a66db2bdf11844634dde55fec3efa9c6bb1d89056a5e79c1ac540c4c784d592ea1d25028a92227d2f2d5c
version: 6.2.3
resolution: "ansi-styles@npm:6.2.3"
checksum: 10c0/23b8a4ce14e18fb854693b95351e286b771d23d8844057ed2e7d083cd3e708376c3323707ec6a24365f7d7eda3ca00327fe04092e29e551499ec4c8b7bfac868
languageName: node
linkType: hard
@ -2670,9 +2670,9 @@ __metadata:
linkType: hard
"electron-to-chromium@npm:^1.5.211":
version: 1.5.214
resolution: "electron-to-chromium@npm:1.5.214"
checksum: 10c0/76ca22fd97a2dad84a710915b5984263b31e61c7883cd3ec0c11c0d7beb3fa628780cdfd05a96ec79a904ea1c910cf02c513db60f31b627c96743e50f6b11a2e
version: 1.5.215
resolution: "electron-to-chromium@npm:1.5.215"
checksum: 10c0/3a45976d1193e57284533096b3bbec218a5d4d85af4f7c133522aae35b14bbf22734f48ccc3f0e43a451441ebc375fa2f4350390fd729dcedb97543692133e39
languageName: node
linkType: hard
@ -3627,8 +3627,8 @@ __metadata:
resolution: "gerbil@workspace:."
dependencies:
"@eslint/js": "npm:^9.35.0"
"@mantine/core": "npm:^8.3.0"
"@mantine/hooks": "npm:^8.3.0"
"@mantine/core": "npm:^8.3.1"
"@mantine/hooks": "npm:^8.3.1"
"@types/node": "npm:^24.3.1"
"@types/react": "npm:^19.1.12"
"@types/react-dom": "npm:^19.1.9"
@ -4777,11 +4777,11 @@ __metadata:
linkType: hard
"magic-string@npm:^0.30.17":
version: 0.30.18
resolution: "magic-string@npm:0.30.18"
version: 0.30.19
resolution: "magic-string@npm:0.30.19"
dependencies:
"@jridgewell/sourcemap-codec": "npm:^1.5.5"
checksum: 10c0/80fba01e13ce1f5c474a0498a5aa462fa158eb56567310747089a0033e432d83a2021ee2c109ac116010cd9dcf90a5231d89fbe3858165f73c00a50a74dbefcd
checksum: 10c0/db23fd2e2ee98a1aeb88a4cdb2353137fcf05819b883c856dd79e4c7dfb25151e2a5a4d5dbd88add5e30ed8ae5c51bcf4accbc6becb75249d924ec7b4fbcae27
languageName: node
linkType: hard
@ -6027,30 +6027,30 @@ __metadata:
linkType: hard
"rollup@npm:^4.43.0":
version: 4.50.0
resolution: "rollup@npm:4.50.0"
version: 4.50.1
resolution: "rollup@npm:4.50.1"
dependencies:
"@rollup/rollup-android-arm-eabi": "npm:4.50.0"
"@rollup/rollup-android-arm64": "npm:4.50.0"
"@rollup/rollup-darwin-arm64": "npm:4.50.0"
"@rollup/rollup-darwin-x64": "npm:4.50.0"
"@rollup/rollup-freebsd-arm64": "npm:4.50.0"
"@rollup/rollup-freebsd-x64": "npm:4.50.0"
"@rollup/rollup-linux-arm-gnueabihf": "npm:4.50.0"
"@rollup/rollup-linux-arm-musleabihf": "npm:4.50.0"
"@rollup/rollup-linux-arm64-gnu": "npm:4.50.0"
"@rollup/rollup-linux-arm64-musl": "npm:4.50.0"
"@rollup/rollup-linux-loongarch64-gnu": "npm:4.50.0"
"@rollup/rollup-linux-ppc64-gnu": "npm:4.50.0"
"@rollup/rollup-linux-riscv64-gnu": "npm:4.50.0"
"@rollup/rollup-linux-riscv64-musl": "npm:4.50.0"
"@rollup/rollup-linux-s390x-gnu": "npm:4.50.0"
"@rollup/rollup-linux-x64-gnu": "npm:4.50.0"
"@rollup/rollup-linux-x64-musl": "npm:4.50.0"
"@rollup/rollup-openharmony-arm64": "npm:4.50.0"
"@rollup/rollup-win32-arm64-msvc": "npm:4.50.0"
"@rollup/rollup-win32-ia32-msvc": "npm:4.50.0"
"@rollup/rollup-win32-x64-msvc": "npm:4.50.0"
"@rollup/rollup-android-arm-eabi": "npm:4.50.1"
"@rollup/rollup-android-arm64": "npm:4.50.1"
"@rollup/rollup-darwin-arm64": "npm:4.50.1"
"@rollup/rollup-darwin-x64": "npm:4.50.1"
"@rollup/rollup-freebsd-arm64": "npm:4.50.1"
"@rollup/rollup-freebsd-x64": "npm:4.50.1"
"@rollup/rollup-linux-arm-gnueabihf": "npm:4.50.1"
"@rollup/rollup-linux-arm-musleabihf": "npm:4.50.1"
"@rollup/rollup-linux-arm64-gnu": "npm:4.50.1"
"@rollup/rollup-linux-arm64-musl": "npm:4.50.1"
"@rollup/rollup-linux-loongarch64-gnu": "npm:4.50.1"
"@rollup/rollup-linux-ppc64-gnu": "npm:4.50.1"
"@rollup/rollup-linux-riscv64-gnu": "npm:4.50.1"
"@rollup/rollup-linux-riscv64-musl": "npm:4.50.1"
"@rollup/rollup-linux-s390x-gnu": "npm:4.50.1"
"@rollup/rollup-linux-x64-gnu": "npm:4.50.1"
"@rollup/rollup-linux-x64-musl": "npm:4.50.1"
"@rollup/rollup-openharmony-arm64": "npm:4.50.1"
"@rollup/rollup-win32-arm64-msvc": "npm:4.50.1"
"@rollup/rollup-win32-ia32-msvc": "npm:4.50.1"
"@rollup/rollup-win32-x64-msvc": "npm:4.50.1"
"@types/estree": "npm:1.0.8"
fsevents: "npm:~2.3.2"
dependenciesMeta:
@ -6100,7 +6100,7 @@ __metadata:
optional: true
bin:
rollup: dist/bin/rollup
checksum: 10c0/8a9aa0885b61ee08315cdc097fb9f97b626a0992546a6857d7668f4690a7344d8497fa7504e4dba03acefc77f03d90fec3c11bfa80777177e76ea5d8c18ddc97
checksum: 10c0/2029282826d5fb4e308be261b2c28329a4d2bd34304cc3960da69fd21d5acccd0267d6770b1656ffc8f166203ef7e865b4583d5f842a519c8ef059ac71854205
languageName: node
linkType: hard
@ -6618,11 +6618,11 @@ __metadata:
linkType: hard
"strip-ansi@npm:^7.0.1":
version: 7.1.0
resolution: "strip-ansi@npm:7.1.0"
version: 7.1.2
resolution: "strip-ansi@npm:7.1.2"
dependencies:
ansi-regex: "npm:^6.0.1"
checksum: 10c0/a198c3762e8832505328cbf9e8c8381de14a4fa50a4f9b2160138158ea88c0f5549fb50cb13c651c3088f47e63a108b34622ec18c0499b6c8c3a5ddf6b305ac4
checksum: 10c0/0d6d7a023de33368fd042aab0bf48f4f4077abdfd60e5393e73c7c411e85e1b3a83507c11af2e656188511475776215df9ca589b4da2295c9455cc399ce1858b
languageName: node
linkType: hard