diff --git a/package.json b/package.json index 3ff0aef..02eb663 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.tsx b/src/App.tsx index 7ea62ff..48a1916 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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('terminal'); const [frontendPreference, setFrontendPreference] = useState('koboldcpp'); - const [settingsModalOpen, setSettingsModalOpen] = useState(false); const [ejectConfirmModalOpen, setEjectConfirmModalOpen] = useState(false); const { @@ -146,6 +145,7 @@ export const App = () => { return ( { currentTab={activeInterfaceTab} onTabChange={setActiveInterfaceTab} onEject={handleEject} - onOpenSettings={() => setSettingsModalOpen(true)} frontendPreference={frontendPreference} /> @@ -219,22 +218,15 @@ export const App = () => { } /> - { - setSettingsModalOpen(false); - const preference = await safeExecute( - () => - window.electronAPI.config.get( - 'frontendPreference' - ) as Promise, - 'Failed to load frontend preference:' - ); - setFrontendPreference(preference || 'koboldcpp'); - }} - currentScreen={currentScreen || undefined} - /> + + + + + setEjectConfirmModalOpen(false)} diff --git a/src/components/DownloadCard.tsx b/src/components/DownloadCard.tsx index 50d7588..f69f319 100644 --- a/src/components/DownloadCard.tsx +++ b/src/components/DownloadCard.tsx @@ -48,7 +48,7 @@ export const DownloadCard = ({ onUpdate, }: DownloadCardProps) => { const computedColorScheme = useComputedColorScheme('light', { - getInitialValueInEffect: false, + getInitialValueInEffect: true, }); const isDark = computedColorScheme === 'dark'; const renderActionButtons = () => { diff --git a/src/components/StatusBar.tsx b/src/components/StatusBar.tsx new file mode 100644 index 0000000..9a8b958 --- /dev/null +++ b/src/components/StatusBar.tsx @@ -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( + 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 ( + + + + + + CPU: {currentMetrics.cpu.usage.toFixed(1)}% + + + + + + RAM: {currentMetrics.memory.usage.toFixed(1)}% + + + + {currentMetrics.gpu?.map((gpu, index) => ( + + + + GPU: {gpu.usage.toFixed(1)}% + + + + + + VRAM: {gpu.memoryUsage.toFixed(1)}% + + + + ))} + + + + setSettingsModalOpen(true)} + aria-label="Open settings" + style={{ + borderRadius: '0.25rem', + }} + > + + + + + + { + setSettingsModalOpen(false); + const preference = await safeExecute( + () => + window.electronAPI.config.get( + 'frontendPreference' + ) as Promise, + 'Failed to load frontend preference:' + ); + onFrontendPreferenceChange(preference || 'koboldcpp'); + }} + currentScreen={currentScreen || undefined} + /> + + ); +}; diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx index f2c6773..06ed25f 100644 --- a/src/components/TitleBar.tsx +++ b/src/components/TitleBar.tsx @@ -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 = ({ )} - - - - - - {[ { icon: , diff --git a/src/components/screens/Download.tsx b/src/components/screens/Download.tsx index afb25bc..e6cdf69 100644 --- a/src/components/screens/Download.tsx +++ b/src/components/screens/Download.tsx @@ -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'; diff --git a/src/components/screens/Interface/TerminalTab.tsx b/src/components/screens/Interface/TerminalTab.tsx index 104f7b6..d7d07ad 100644 --- a/src/components/screens/Interface/TerminalTab.tsx +++ b/src/components/screens/Interface/TerminalTab.tsx @@ -30,7 +30,7 @@ export const TerminalTab = forwardRef( ({ onServerReady, frontendPreference = 'koboldcpp' }, ref) => { const { host, port, isImageGenerationMode } = useLaunchConfigStore(); const computedColorScheme = useComputedColorScheme('light', { - getInitialValueInEffect: false, + getInitialValueInEffect: true, }); const [terminalContent, setTerminalContent] = useState(''); const [isUserScrolling, setIsUserScrolling] = useState(false); diff --git a/src/components/screens/Launch/GeneralTab/BackendSelectItem.tsx b/src/components/screens/Launch/GeneralTab/BackendSelectItem.tsx index cfccfc8..2c3f8d0 100644 --- a/src/components/screens/Launch/GeneralTab/BackendSelectItem.tsx +++ b/src/components/screens/Launch/GeneralTab/BackendSelectItem.tsx @@ -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; @@ -9,14 +9,16 @@ export const BackendSelectItem = ({ disabled = false, }: BackendSelectItemProps) => ( - - {label} - {disabled && ( - - (Compatible devices not found) - - )} - + + + {label} + {disabled && ( + + (Compatible devices not found) + + )} + + {devices && devices.length > 0 && ( {devices.slice(0, 2).map((device, index) => ( diff --git a/src/components/screens/Launch/index.tsx b/src/components/screens/Launch/index.tsx index c866194..21e7270 100644 --- a/src/components/screens/Launch/index.tsx +++ b/src/components/screens/Launch/index.tsx @@ -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') ); diff --git a/src/components/settings/AppearanceTab.tsx b/src/components/settings/AppearanceTab.tsx index 3887b7b..e70e59d 100644 --- a/src/components/settings/AppearanceTab.tsx +++ b/src/components/settings/AppearanceTab.tsx @@ -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'; diff --git a/src/components/settings/VersionsTab.tsx b/src/components/settings/VersionsTab.tsx index a3d8803..7a1b4ba 100644 --- a/src/components/settings/VersionsTab.tsx +++ b/src/components/settings/VersionsTab.tsx @@ -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'; diff --git a/src/constants/index.ts b/src/constants/index.ts index 5c1905a..3157b1e 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -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, diff --git a/src/hooks/useAppUpdateChecker.ts b/src/hooks/useAppUpdateChecker.ts index 6f30f55..9b771bb 100644 --- a/src/hooks/useAppUpdateChecker.ts +++ b/src/hooks/useAppUpdateChecker.ts @@ -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); diff --git a/src/hooks/useKoboldVersions.ts b/src/hooks/useKoboldVersions.ts index 846bff0..ad68c41 100644 --- a/src/hooks/useKoboldVersions.ts +++ b/src/hooks/useKoboldVersions.ts @@ -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); diff --git a/src/hooks/useLaunchLogic.ts b/src/hooks/useLaunchLogic.ts index f21a98e..ad6df24 100644 --- a/src/hooks/useLaunchLogic.ts +++ b/src/hooks/useLaunchLogic.ts @@ -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); } diff --git a/src/hooks/useUpdateChecker.ts b/src/hooks/useUpdateChecker.ts index 133796b..5e51cc5 100644 --- a/src/hooks/useUpdateChecker.ts +++ b/src/hooks/useUpdateChecker.ts @@ -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); } diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 85508ac..bf74594 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -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(); + }); } diff --git a/src/main/modules/hardware.ts b/src/main/modules/hardware.ts index 6fab768..7453524 100644 --- a/src/main/modules/hardware.ts +++ b/src/main/modules/hardware.ts @@ -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((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((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((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((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, }); } diff --git a/src/main/modules/monitoring.ts b/src/main/modules/monitoring.ts new file mode 100644 index 0000000..9b396a7 --- /dev/null +++ b/src/main/modules/monitoring.ts @@ -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 | 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 { + 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, + }; +} diff --git a/src/main/modules/window.ts b/src/main/modules/window.ts index a9f1245..3e241bc 100644 --- a/src/main/modules/window.ts +++ b/src/main/modules/window.ts @@ -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), diff --git a/src/preload/index.ts b/src/preload/index.ts index efaf692..aab7f7d 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -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, }); diff --git a/src/stores/launchConfig.ts b/src/stores/launchConfig.ts index b28b289..ab4c95f 100644 --- a/src/stores/launchConfig.ts +++ b/src/stores/launchConfig.ts @@ -375,7 +375,7 @@ export const useLaunchConfigStore = create((set, get) => ({ loadConfigFromFile: async ( configFiles: ConfigFile[], savedConfig: string | null - ): Promise => { + ) => { let currentSelectedFile = null; if (savedConfig) { diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 86e6e0b..b00e294 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -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; } +export interface MonitoringAPI { + start: () => Promise; + stop: () => Promise; + 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; }; } } diff --git a/src/types/hardware.d.ts b/src/types/hardware.d.ts index 3afbd1f..65e89ad 100644 --- a/src/types/hardware.d.ts +++ b/src/types/hardware.d.ts @@ -34,6 +34,11 @@ export interface BasicGPUInfo { gpuInfo: string[]; } +export interface HardwareDetectionResult { + supported: boolean; + devices: string[]; +} + export interface HardwareInfo { cpu: CPUCapabilities; gpu: BasicGPUInfo; diff --git a/src/utils/assets.ts b/src/utils/assets.ts index d5b65d6..cdc8c7c 100644 --- a/src/utils/assets.ts +++ b/src/utils/assets.ts @@ -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 = ( 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)) { diff --git a/src/utils/download.ts b/src/utils/download.ts deleted file mode 100644 index 22182a4..0000000 --- a/src/utils/download.ts +++ /dev/null @@ -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'; -}; diff --git a/src/utils/format.ts b/src/utils/format.ts new file mode 100644 index 0000000..f06fa53 --- /dev/null +++ b/src/utils/format.ts @@ -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(); diff --git a/src/utils/fs.ts b/src/utils/fs.ts index c1197f3..e78a380 100644 --- a/src/utils/fs.ts +++ b/src/utils/fs.ts @@ -1,7 +1,7 @@ import { readFile, writeFile, access, mkdir } from 'fs/promises'; import { constants } from 'fs'; -export const pathExists = async (path: string): Promise => { +export const pathExists = async (path: string) => { try { await access(path, constants.F_OK); return true; @@ -21,15 +21,12 @@ export const readJsonFile = async ( } }; -export const writeJsonFile = async ( - path: string, - data: unknown -): Promise => { +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 => { +export const ensureDir = async (path: string) => { try { await mkdir(path, { recursive: true }); } catch (error) { diff --git a/src/utils/hardware.ts b/src/utils/hardware.ts deleted file mode 100644 index 82a1b7f..0000000 --- a/src/utils/hardware.ts +++ /dev/null @@ -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(); diff --git a/src/utils/kobold.ts b/src/utils/kobold.ts index 937617b..89d8e8d 100644 --- a/src/utils/kobold.ts +++ b/src/utils/kobold.ts @@ -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; diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 5e7e6aa..60312eb 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -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 ( export const tryExecute = async ( operation: () => Promise, errorMessage: string -): Promise => { +) => { 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); -}; diff --git a/src/utils/path.ts b/src/utils/path.ts index d996264..ca0b0b8 100644 --- a/src/utils/path.ts +++ b/src/utils/path.ts @@ -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(); diff --git a/src/utils/process.ts b/src/utils/process.ts index a262828..2ced428 100644 --- a/src/utils/process.ts +++ b/src/utils/process.ts @@ -9,8 +9,8 @@ export interface ProcessTerminationOptions { async function killWindowsProcessTree( pid: number, logError?: (message: string, error: Error) => void -): Promise { - return new Promise((resolve) => { +) { + return new Promise((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 { +) { const { timeoutMs = 3000, logError } = options; if (!childProcess?.pid) { diff --git a/src/utils/rocm.ts b/src/utils/rocm.ts index a944842..a02be32 100644 --- a/src/utils/rocm.ts +++ b/src/utils/rocm.ts @@ -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 { +export async function getROCmDownload() { const platform = await window.electronAPI.kobold.getPlatform(); if (platform === 'linux') { @@ -14,7 +13,7 @@ export async function getROCmDownload(): Promise { return null; } -async function getLinuxROCmDownload(): Promise { +async function getLinuxROCmDownload() { try { const response = await fetch(GITHUB_API.LATEST_RELEASE_URL); if (!response.ok) { @@ -40,7 +39,7 @@ async function getLinuxROCmDownload(): Promise { } } -async function getWindowsROCmDownload(): Promise { +async function getWindowsROCmDownload() { try { const response = await fetch(GITHUB_API.ROCM_LATEST_RELEASE_URL); if (!response.ok) { diff --git a/src/utils/terminal.ts b/src/utils/terminal.ts index 1d0f36d..3e41bf2 100644 --- a/src/utils/terminal.ts +++ b/src/utils/terminal.ts @@ -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 `${cleanUrl}${trailingPunctuation}`; }); -const escapeHtmlExceptLinks = (text: string): string => { +const escapeHtmlExceptLinks = (text: string) => { const escaped = text .replace(/&/g, '&') .replace(/ { return linkifyText(escaped); }; -export const processTerminalContent = (content: string): string => { +export const processTerminalContent = (content: string) => { if (!content) return ''; return escapeHtmlExceptLinks(content); diff --git a/src/utils/validation.ts b/src/utils/validation.ts index a0a7c6c..660c52d 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -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)) { diff --git a/src/utils/version.ts b/src/utils/version.ts index dc59320..14f0e1f 100644 --- a/src/utils/version.ts +++ b/src/utils/version.ts @@ -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, ''); diff --git a/src/utils/zoom.ts b/src/utils/zoom.ts index 87672cd..edc0744 100644 --- a/src/utils/zoom.ts +++ b/src/utils/zoom.ts @@ -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; diff --git a/yarn.lock b/yarn.lock index e53a725..0e7f7cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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