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", "name": "gerbil",
"productName": "Gerbil", "productName": "Gerbil",
"version": "1.1.0", "version": "1.1.1",
"description": "Run Large Language Models locally", "description": "Run Large Language Models locally",
"main": "out/main/index.js", "main": "out/main/index.js",
"homepage": "./", "homepage": "./",
@ -63,8 +63,8 @@
"vite": "^7.1.5" "vite": "^7.1.5"
}, },
"dependencies": { "dependencies": {
"@mantine/core": "^8.3.0", "@mantine/core": "^8.3.1",
"@mantine/hooks": "^8.3.0", "@mantine/hooks": "^8.3.1",
"axios": "^1.11.0", "axios": "^1.11.0",
"execa": "^9.6.0", "execa": "^9.6.0",
"lucide-react": "^0.543.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 { InterfaceScreen } from '@/components/screens/Interface';
import { WelcomeScreen } from '@/components/screens/Welcome'; import { WelcomeScreen } from '@/components/screens/Welcome';
import { UpdateAvailableModal } from '@/components/UpdateAvailableModal'; import { UpdateAvailableModal } from '@/components/UpdateAvailableModal';
import { SettingsModal } from '@/components/settings/SettingsModal';
import { EjectConfirmModal } from '@/components/EjectConfirmModal'; import { EjectConfirmModal } from '@/components/EjectConfirmModal';
import { ScreenTransition } from '@/components/ScreenTransition'; import { ScreenTransition } from '@/components/ScreenTransition';
import { TitleBar } from '@/components/TitleBar'; import { TitleBar } from '@/components/TitleBar';
import { StatusBar } from '@/components/StatusBar';
import { ErrorBoundary } from '@/components/ErrorBoundary'; import { ErrorBoundary } from '@/components/ErrorBoundary';
import { useUpdateChecker } from '@/hooks/useUpdateChecker'; import { useUpdateChecker } from '@/hooks/useUpdateChecker';
import { useKoboldVersions } from '@/hooks/useKoboldVersions'; import { useKoboldVersions } from '@/hooks/useKoboldVersions';
import { safeExecute } from '@/utils/logger'; 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 { DownloadItem } from '@/types/electron';
import type { InterfaceTab, FrontendPreference, Screen } from '@/types'; import type { InterfaceTab, FrontendPreference, Screen } from '@/types';
@ -24,7 +24,6 @@ export const App = () => {
useState<InterfaceTab>('terminal'); useState<InterfaceTab>('terminal');
const [frontendPreference, setFrontendPreference] = const [frontendPreference, setFrontendPreference] =
useState<FrontendPreference>('koboldcpp'); useState<FrontendPreference>('koboldcpp');
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
const [ejectConfirmModalOpen, setEjectConfirmModalOpen] = useState(false); const [ejectConfirmModalOpen, setEjectConfirmModalOpen] = useState(false);
const { const {
@ -146,6 +145,7 @@ export const App = () => {
return ( return (
<AppShell <AppShell
header={{ height: TITLEBAR_HEIGHT }} header={{ height: TITLEBAR_HEIGHT }}
footer={{ height: STATUSBAR_HEIGHT }}
padding={currentScreen === 'interface' ? 0 : 'md'} padding={currentScreen === 'interface' ? 0 : 'md'}
> >
<TitleBar <TitleBar
@ -153,7 +153,6 @@ export const App = () => {
currentTab={activeInterfaceTab} currentTab={activeInterfaceTab}
onTabChange={setActiveInterfaceTab} onTabChange={setActiveInterfaceTab}
onEject={handleEject} onEject={handleEject}
onOpenSettings={() => setSettingsModalOpen(true)}
frontendPreference={frontendPreference} frontendPreference={frontendPreference}
/> />
@ -219,22 +218,15 @@ export const App = () => {
} }
/> />
</AppShell.Main> </AppShell.Main>
<SettingsModal
isOnInterfaceScreen={currentScreen === 'interface'} <AppShell.Footer>
opened={settingsModalOpen} <StatusBar
onClose={async () => { currentScreen={currentScreen}
setSettingsModalOpen(false); frontendPreference={frontendPreference}
const preference = await safeExecute( onFrontendPreferenceChange={setFrontendPreference}
() => />
window.electronAPI.config.get( </AppShell.Footer>
'frontendPreference'
) as Promise<FrontendPreference>,
'Failed to load frontend preference:'
);
setFrontendPreference(preference || 'koboldcpp');
}}
currentScreen={currentScreen || undefined}
/>
<EjectConfirmModal <EjectConfirmModal
opened={ejectConfirmModalOpen} opened={ejectConfirmModalOpen}
onClose={() => setEjectConfirmModalOpen(false)} onClose={() => setEjectConfirmModalOpen(false)}

View file

@ -48,7 +48,7 @@ export const DownloadCard = ({
onUpdate, onUpdate,
}: DownloadCardProps) => { }: DownloadCardProps) => {
const computedColorScheme = useComputedColorScheme('light', { const computedColorScheme = useComputedColorScheme('light', {
getInitialValueInEffect: false, getInitialValueInEffect: true,
}); });
const isDark = computedColorScheme === 'dark'; const isDark = computedColorScheme === 'dark';
const renderActionButtons = () => { 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, useComputedColorScheme,
AppShell, AppShell,
} from '@mantine/core'; } from '@mantine/core';
import { import { Minus, Square, X, Copy, CircleFadingArrowUp } from 'lucide-react';
Minus,
Square,
X,
Settings,
Copy,
CircleFadingArrowUp,
} from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import { soundAssets, playSound, initializeAudio } from '@/utils/sounds'; import { soundAssets, playSound, initializeAudio } from '@/utils/sounds';
import { useAppUpdateChecker } from '@/hooks/useAppUpdateChecker'; import { useAppUpdateChecker } from '@/hooks/useAppUpdateChecker';
@ -28,7 +21,6 @@ interface TitleBarProps {
currentTab: InterfaceTab; currentTab: InterfaceTab;
onTabChange: (tab: InterfaceTab) => void; onTabChange: (tab: InterfaceTab) => void;
onEject: () => void; onEject: () => void;
onOpenSettings: () => void;
frontendPreference: FrontendPreference; frontendPreference: FrontendPreference;
} }
@ -37,11 +29,10 @@ export const TitleBar = ({
currentTab, currentTab,
onTabChange, onTabChange,
onEject, onEject,
onOpenSettings,
frontendPreference, frontendPreference,
}: TitleBarProps) => { }: TitleBarProps) => {
const computedColorScheme = useComputedColorScheme('light', { const computedColorScheme = useComputedColorScheme('light', {
getInitialValueInEffect: false, getInitialValueInEffect: true,
}); });
const { hasUpdate, releaseUrl } = useAppUpdateChecker(); const { hasUpdate, releaseUrl } = useAppUpdateChecker();
const { isImageGenerationMode } = useLaunchConfigStore(); const { isImageGenerationMode } = useLaunchConfigStore();
@ -234,30 +225,6 @@ export const TitleBar = ({
</ActionIcon> </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" />, 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 { Card, Text, Title, Loader, Stack, Container } from '@mantine/core';
import { DownloadCard } from '@/components/DownloadCard'; import { DownloadCard } from '@/components/DownloadCard';
import { getPlatformDisplayName } from '@/utils/platform'; import { getPlatformDisplayName } from '@/utils/platform';
import { formatDownloadSize } from '@/utils/download'; import { formatDownloadSize } from '@/utils/format';
import { getAssetDescription } from '@/utils/assets'; import { getAssetDescription } from '@/utils/assets';
import { useKoboldVersions } from '@/hooks/useKoboldVersions'; import { useKoboldVersions } from '@/hooks/useKoboldVersions';
import { safeExecute } from '@/utils/logger'; import { safeExecute } from '@/utils/logger';

View file

@ -30,7 +30,7 @@ export const TerminalTab = forwardRef<TerminalTabRef, TerminalTabProps>(
({ onServerReady, frontendPreference = 'koboldcpp' }, ref) => { ({ onServerReady, frontendPreference = 'koboldcpp' }, ref) => {
const { host, port, isImageGenerationMode } = useLaunchConfigStore(); const { host, port, isImageGenerationMode } = useLaunchConfigStore();
const computedColorScheme = useComputedColorScheme('light', { const computedColorScheme = useComputedColorScheme('light', {
getInitialValueInEffect: false, getInitialValueInEffect: true,
}); });
const [terminalContent, setTerminalContent] = useState<string>(''); const [terminalContent, setTerminalContent] = useState<string>('');
const [isUserScrolling, setIsUserScrolling] = useState<boolean>(false); 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'; import type { BackendOption } from '@/types';
type BackendSelectItemProps = Omit<BackendOption, 'value'>; type BackendSelectItemProps = Omit<BackendOption, 'value'>;
@ -9,14 +9,16 @@ export const BackendSelectItem = ({
disabled = false, disabled = false,
}: BackendSelectItemProps) => ( }: BackendSelectItemProps) => (
<Group justify="space-between" wrap="nowrap"> <Group justify="space-between" wrap="nowrap">
<Text size="sm" truncate> <Box w={!disabled ? '3rem' : 'auto'}>
{label} <Text size="sm" truncate>
{disabled && ( {label}
<Text component="span" size="xs" ml="xs"> {disabled && (
(Compatible devices not found) <Text component="span" size="xs" ml="xs">
</Text> (Compatible devices not found)
)} </Text>
</Text> )}
</Text>
</Box>
{devices && devices.length > 0 && ( {devices && devices.length > 0 && (
<Group gap={4}> <Group gap={4}>
{devices.slice(0, 2).map((device, index) => ( {devices.slice(0, 2).map((device, index) => (

View file

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

View file

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

View file

@ -17,7 +17,7 @@ import {
compareVersions, compareVersions,
} from '@/utils/version'; } from '@/utils/version';
import { safeExecute } from '@/utils/logger'; import { safeExecute } from '@/utils/logger';
import { formatDownloadSize } from '@/utils/download'; import { formatDownloadSize } from '@/utils/format';
import { useKoboldVersions } from '@/hooks/useKoboldVersions'; import { useKoboldVersions } from '@/hooks/useKoboldVersions';
import type { InstalledVersion, ReleaseWithStatus } from '@/types/electron'; 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 TITLEBAR_HEIGHT = '2.5rem';
export const STATUSBAR_HEIGHT = '1.5rem';
export const MODAL_STYLES_WITH_TITLEBAR = { export const MODAL_STYLES_WITH_TITLEBAR = {
overlay: { overlay: {
top: TITLEBAR_HEIGHT, top: TITLEBAR_HEIGHT,

View file

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

View file

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

View file

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

View file

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

View file

@ -42,6 +42,11 @@ import {
detectBackendSupport, detectBackendSupport,
getAvailableBackends, getAvailableBackends,
} from '@/main/modules/binary'; } from '@/main/modules/binary';
import {
setMainWindow,
startMonitoring,
stopMonitoring,
} from '@/main/modules/monitoring';
import type { FrontendPreference } from '@/types'; import type { FrontendPreference } from '@/types';
async function launchKoboldCppWithCustomFrontends(args: string[] = []) { async function launchKoboldCppWithCustomFrontends(args: string[] = []) {
@ -229,4 +234,16 @@ export function setupIPCHandlers() {
); );
ipcMain.handle('openwebui:isUvAvailable', () => isUvAvailable()); 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 */ /* eslint-disable no-comments/disallowComments */
import si from 'systeminformation'; import si from 'systeminformation';
import { shortenDeviceName } from '@/utils/hardware';
import { logError } from '@/main/modules/logging'; import { logError } from '@/main/modules/logging';
import { terminateProcess } from '@/utils/process'; import { terminateProcess } from '@/utils/process';
import type { import type {
@ -8,8 +7,10 @@ import type {
GPUCapabilities, GPUCapabilities,
BasicGPUInfo, BasicGPUInfo,
GPUMemoryInfo, GPUMemoryInfo,
HardwareDetectionResult,
} from '@/types/hardware'; } from '@/types/hardware';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import { formatDeviceName } from '@/utils/format';
let cpuCapabilitiesCache: CPUCapabilities | null = null; let cpuCapabilitiesCache: CPUCapabilities | null = null;
let basicGPUInfoCache: BasicGPUInfo | null = null; let basicGPUInfoCache: BasicGPUInfo | null = null;
@ -26,7 +27,7 @@ export async function detectCPU() {
const devices: string[] = []; const devices: string[] = [];
if (cpu.brand) { if (cpu.brand) {
devices.push(shortenDeviceName(cpu.brand)); devices.push(formatDeviceName(cpu.brand));
} }
const avx = flags.includes('avx') || flags.includes('AVX'); const avx = flags.includes('avx') || flags.includes('AVX');
@ -65,7 +66,7 @@ export async function detectGPU() {
for (const controller of graphics.controllers) { for (const controller of graphics.controllers) {
if (controller.model) { if (controller.model) {
gpuInfo.push(shortenDeviceName(controller.model)); gpuInfo.push(formatDeviceName(controller.model));
} }
const vendor = controller.vendor?.toLowerCase() || ''; const vendor = controller.vendor?.toLowerCase() || '';
@ -142,10 +143,7 @@ async function detectCUDA() {
output += data.toString(); output += data.toString();
}); });
return new Promise<{ return new Promise<HardwareDetectionResult>((resolve) => {
supported: boolean;
devices: string[];
}>((resolve) => {
nvidia.on('close', (code) => { nvidia.on('close', (code) => {
if (code === 0 && output.trim()) { if (code === 0 && output.trim()) {
const devices = output const devices = output
@ -154,7 +152,7 @@ async function detectCUDA() {
.map((line) => { .map((line) => {
const parts = line.split(','); const parts = line.split(',');
const rawName = parts[0]?.trim() || 'Unknown NVIDIA GPU'; const rawName = parts[0]?.trim() || 'Unknown NVIDIA GPU';
return shortenDeviceName(rawName); return formatDeviceName(rawName);
}) })
.filter(Boolean); .filter(Boolean);
@ -206,10 +204,7 @@ export async function detectROCm() {
output += data.toString(); output += data.toString();
}); });
return new Promise<{ return new Promise<HardwareDetectionResult>((resolve) => {
supported: boolean;
devices: string[];
}>((resolve) => {
// eslint-disable-next-line sonarjs/cognitive-complexity // eslint-disable-next-line sonarjs/cognitive-complexity
rocminfo.on('close', (code) => { rocminfo.on('close', (code) => {
if (code === 0 && output.trim()) { if (code === 0 && output.trim()) {
@ -225,9 +220,9 @@ export async function detectROCm() {
if ( if (
name && name &&
!name.toLowerCase().includes('cpu') && !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') { if (deviceType !== 'CPU') {
devices.push(shortenDeviceName(name)); devices.push(formatDeviceName(name));
} }
} }
} }
@ -301,10 +296,7 @@ async function detectVulkan() {
output += data.toString(); output += data.toString();
}); });
return new Promise<{ return new Promise<HardwareDetectionResult>((resolve) => {
supported: boolean;
devices: string[];
}>((resolve) => {
vulkaninfo.on('close', (code) => { vulkaninfo.on('close', (code) => {
if (code === 0 && output.trim()) { if (code === 0 && output.trim()) {
const devices: string[] = []; const devices: string[] = [];
@ -316,7 +308,7 @@ async function detectVulkan() {
if (parts.length >= 2) { if (parts.length >= 2) {
const name = parts[1]?.trim(); const name = parts[1]?.trim();
if (name) { if (name) {
devices.push(shortenDeviceName(name)); devices.push(formatDeviceName(name));
} }
} }
} }
@ -363,7 +355,7 @@ function parseClInfoOutput(output: string) {
const deviceName = findDeviceNameInClInfo(lines, i); const deviceName = findDeviceNameInClInfo(lines, i);
if (deviceName && currentPlatform) { if (deviceName && currentPlatform) {
const deviceLabel = `${shortenDeviceName(deviceName)} (${currentPlatform})`; const deviceLabel = `${formatDeviceName(deviceName)} (${currentPlatform})`;
devices.push(deviceLabel); devices.push(deviceLabel);
} }
} }
@ -407,10 +399,7 @@ async function detectCLBlast() {
output += data.toString(); output += data.toString();
}); });
return new Promise<{ return new Promise<HardwareDetectionResult>((resolve) => {
supported: boolean;
devices: string[];
}>((resolve) => {
clinfo.on('close', (code) => { clinfo.on('close', (code) => {
if (code === 0 && output.trim()) { if (code === 0 && output.trim()) {
const devices = parseClInfoOutput(output); const devices = parseClInfoOutput(output);
@ -456,7 +445,7 @@ export async function detectGPUMemory() {
} }
memoryInfo.push({ memoryInfo.push({
deviceName: shortenDeviceName(controller.model), deviceName: formatDeviceName(controller.model),
totalMemoryMB: vram, 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'); : join(__dirname, '../../assets/icon.png');
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
minWidth: 600,
minHeight: 600,
width: windowWidth, width: windowWidth,
height: windowHeight, height: windowHeight,
x: Math.floor((size.width - windowWidth) / 2), x: Math.floor((size.width - windowWidth) / 2),

View file

@ -6,6 +6,7 @@ import type {
LogsAPI, LogsAPI,
SillyTavernAPI, SillyTavernAPI,
OpenWebUIAPI, OpenWebUIAPI,
MonitoringAPI,
} from '@/types/electron'; } from '@/types/electron';
const koboldAPI: KoboldAPI = { const koboldAPI: KoboldAPI = {
@ -107,6 +108,17 @@ const openwebUIAPI: OpenWebUIAPI = {
isUvAvailable: () => ipcRenderer.invoke('openwebui:isUvAvailable'), 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', { contextBridge.exposeInMainWorld('electronAPI', {
kobold: koboldAPI, kobold: koboldAPI,
app: appAPI, app: appAPI,
@ -114,4 +126,5 @@ contextBridge.exposeInMainWorld('electronAPI', {
logs: logsAPI, logs: logsAPI,
sillytavern: sillyTavernAPI, sillytavern: sillyTavernAPI,
openwebui: openwebUIAPI, openwebui: openwebUIAPI,
monitoring: monitoringAPI,
}); });

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import { ASSET_SUFFIXES } from '@/constants'; import { ASSET_SUFFIXES } from '@/constants';
import { stripAssetExtensions } from '@/utils/version'; import { stripAssetExtensions } from '@/utils/version';
export const getAssetDescription = (assetName: string): string => { export const getAssetDescription = (assetName: string) => {
const name = stripAssetExtensions(assetName).toLowerCase(); const name = stripAssetExtensions(assetName).toLowerCase();
if (name.includes(ASSET_SUFFIXES.ROCM)) { 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."; 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(); const name = stripAssetExtensions(assetName).toLowerCase();
return ( return (
@ -54,7 +54,7 @@ export const sortDownloadsByType = <T extends { name: string }>(
return a.name.localeCompare(b.name); return a.name.localeCompare(b.name);
}); });
export const pretifyBinName = (binName: string): string => { export const pretifyBinName = (binName: string) => {
const cleanName = stripAssetExtensions(binName).toLowerCase(); const cleanName = stripAssetExtensions(binName).toLowerCase();
if (cleanName.includes(ASSET_SUFFIXES.ROCM)) { 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 { readFile, writeFile, access, mkdir } from 'fs/promises';
import { constants } from 'fs'; import { constants } from 'fs';
export const pathExists = async (path: string): Promise<boolean> => { export const pathExists = async (path: string) => {
try { try {
await access(path, constants.F_OK); await access(path, constants.F_OK);
return true; return true;
@ -21,15 +21,12 @@ export const readJsonFile = async <T = unknown>(
} }
}; };
export const writeJsonFile = async ( export const writeJsonFile = async (path: string, data: unknown) => {
path: string,
data: unknown
): Promise<void> => {
const content = JSON.stringify(data, null, 2); const content = JSON.stringify(data, null, 2);
await writeFile(path, content, 'utf-8'); await writeFile(path, content, 'utf-8');
}; };
export const ensureDir = async (path: string): Promise<void> => { export const ensureDir = async (path: string) => {
try { try {
await mkdir(path, { recursive: true }); await mkdir(path, { recursive: true });
} catch (error) { } 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 { export function parseKoboldConfig(args: string[]) {
host: string;
port: number;
isImageMode: boolean;
}
export function parseKoboldConfig(args: string[]): KoboldConfig {
let host = 'localhost'; let host = 'localhost';
let port = 5001; let port = 5001;
let hasSdModel = false; 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) { if (window.electronAPI?.logs?.logError) {
window.electronAPI.logs.logError(message, error); window.electronAPI.logs.logError(message, error);
} }
@ -19,7 +19,7 @@ export const safeExecute = async <T>(
export const tryExecute = async ( export const tryExecute = async (
operation: () => Promise<void>, operation: () => Promise<void>,
errorMessage: string errorMessage: string
): Promise<boolean> => { ) => {
try { try {
await operation(); await operation();
return true; return true;
@ -28,7 +28,3 @@ export const tryExecute = async (
return false; 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 { homedir } from 'os';
import { PRODUCT_NAME, CONFIG_FILE_NAME } from '@/constants'; import { PRODUCT_NAME, CONFIG_FILE_NAME } from '@/constants';
export function getConfigDir(): string { export function getConfigDir() {
return join(getConfigDirPath(), CONFIG_FILE_NAME); return join(getConfigDirPath(), CONFIG_FILE_NAME);
} }
function getConfigDirPath(): string { function getConfigDirPath() {
const platform = process.platform; const platform = process.platform;
const home = homedir(); const home = homedir();

View file

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

View file

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

View file

@ -1,9 +1,6 @@
import { error } from '@/utils/logger'; import { logError } from '@/utils/logger';
export const handleTerminalOutput = ( export const handleTerminalOutput = (prevContent: string, newData: string) => {
prevContent: string,
newData: string
): string => {
try { try {
if (newData.includes('\r')) { if (newData.includes('\r')) {
const hasStandaloneCarriageReturns = /\r(?!\n)/g.test(newData); const hasStandaloneCarriageReturns = /\r(?!\n)/g.test(newData);
@ -32,14 +29,14 @@ export const handleTerminalOutput = (
return prevContent + newData; return prevContent + newData;
} catch (err) { } catch (err) {
error('Terminal Basic Error', err as Error); logError('Terminal Basic Error', err as Error);
return prevContent + newData; return prevContent + newData;
} }
}; };
const URL_REGEX = /(https?:\/\/[^\s<>"{}|\\^`[\]]+)/gi; const URL_REGEX = /(https?:\/\/[^\s<>"{}|\\^`[\]]+)/gi;
const linkifyText = (text: string): string => const linkifyText = (text: string) =>
text.replace(URL_REGEX, (url) => { text.replace(URL_REGEX, (url) => {
const cleanUrl = url.replace(/[.,;:!?]+$/, ''); const cleanUrl = url.replace(/[.,;:!?]+$/, '');
const trailingPunctuation = url.slice(cleanUrl.length); 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}`; 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 const escaped = text
.replace(/&/g, '&amp;') .replace(/&/g, '&amp;')
.replace(/</g, '&lt;') .replace(/</g, '&lt;')
@ -58,7 +55,7 @@ const escapeHtmlExceptLinks = (text: string): string => {
return linkifyText(escaped); return linkifyText(escaped);
}; };
export const processTerminalContent = (content: string): string => { export const processTerminalContent = (content: string) => {
if (!content) return ''; if (!content) return '';
return escapeHtmlExceptLinks(content); return escapeHtmlExceptLinks(content);

View file

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

View file

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

View file

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

232
yarn.lock
View file

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