more excessive try/catch clean up, never wait for config set and logError, better titlebar color in dark mode

This commit is contained in:
Egor 2025-09-18 22:16:51 -07:00
parent fa06a3fbe1
commit e8d5c59a52
20 changed files with 170 additions and 249 deletions

View file

@ -8,7 +8,7 @@ A desktop app to easily run Large Language Models locally.
## Core Features ## Core Features
- **Run LLMs locally** powered by a highly modified fork of [llama.cpp](https://github.com/ggml-org/llama.cpp) - **Run LLMs locally** powered by [KoboldCpp](https://github.com/LostRuins/koboldcpp) which itself is a highly modified fork of [llama.cpp](https://github.com/ggml-org/llama.cpp)
- **Cross-platform desktop app** - Native support for Windows, macOS, and Linux (including Wayland) - **Cross-platform desktop app** - Native support for Windows, macOS, and Linux (including Wayland)
- **Automatic updates** - Download and keep your KoboldCpp binary up-to-date effortlessly - **Automatic updates** - Download and keep your KoboldCpp binary up-to-date effortlessly
- **Smart process management** - Prevents runaway background processes and system resource waste - **Smart process management** - Prevents runaway background processes and system resource waste

View file

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/png" href="/icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta <meta
http-equiv="Content-Security-Policy" http-equiv="Content-Security-Policy"

View file

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

View file

@ -111,15 +111,15 @@ export const App = () => {
handleBackToLaunch(); handleBackToLaunch();
}; };
const handleEjectConfirm = async (skipConfirmation: boolean) => { const handleEjectConfirm = (skipConfirmation: boolean) => {
if (skipConfirmation) { if (skipConfirmation) {
await window.electronAPI.config.set('skipEjectConfirmation', true); window.electronAPI.config.set('skipEjectConfirmation', true);
} }
performEject(); performEject();
}; };
const handleWelcomeComplete = async () => { const handleWelcomeComplete = async () => {
await window.electronAPI.config.set('hasSeenWelcome', true); window.electronAPI.config.set('hasSeenWelcome', true);
const versions = await window.electronAPI.kobold.getInstalledVersions(); const versions = await window.electronAPI.kobold.getInstalledVersions();
if (versions.length > 0) { if (versions.length > 0) {

View file

@ -38,19 +38,6 @@ export const TitleBar = ({
const [isSelectOpen, setIsSelectOpen] = useState(false); const [isSelectOpen, setIsSelectOpen] = useState(false);
const [settingsModalOpen, setSettingsModalOpen] = useState(false); const [settingsModalOpen, setSettingsModalOpen] = useState(false);
const handleMinimize = () => {
window.electronAPI.app.minimizeWindow();
};
const handleMaximize = async () => {
await window.electronAPI.app.maximizeWindow();
setIsMaximized(!isMaximized);
};
const handleClose = () => {
window.electronAPI.app.closeWindow();
};
return ( return (
<AppShell.Header <AppShell.Header
style={{ display: 'flex', flexDirection: 'column', border: 'none' }} style={{ display: 'flex', flexDirection: 'column', border: 'none' }}
@ -64,7 +51,7 @@ export const TitleBar = ({
justifyContent: 'space-between', justifyContent: 'space-between',
backgroundColor: backgroundColor:
colorScheme === 'dark' colorScheme === 'dark'
? 'var(--mantine-color-dark-8)' ? 'var(--mantine-color-dark-6)'
: 'var(--mantine-color-gray-1)', : 'var(--mantine-color-gray-1)',
border: '1px solid var(--mantine-color-default-border)', border: '1px solid var(--mantine-color-default-border)',
WebkitAppRegion: isSelectOpen ? 'no-drag' : 'drag', WebkitAppRegion: isSelectOpen ? 'no-drag' : 'drag',
@ -154,8 +141,6 @@ export const TitleBar = ({
aria-label="Open settings" aria-label="Open settings"
tabIndex={-1} tabIndex={-1}
style={{ style={{
borderRadius: 0,
margin: '2px 1px 1px',
outline: 'none', outline: 'none',
}} }}
> >
@ -175,19 +160,22 @@ export const TitleBar = ({
{[ {[
{ {
icon: <Minus size="1rem" />, icon: <Minus size="1rem" />,
onClick: handleMinimize, onClick: () => window.electronAPI.app.minimizeWindow(),
color: undefined, color: undefined,
label: 'Minimize window', label: 'Minimize window',
}, },
{ {
icon: isMaximized ? <Copy size="1rem" /> : <Square size="1rem" />, icon: isMaximized ? <Copy size="1rem" /> : <Square size="1rem" />,
onClick: handleMaximize, onClick: () => {
window.electronAPI.app.maximizeWindow();
setIsMaximized(!isMaximized);
},
color: undefined, color: undefined,
label: isMaximized ? 'Restore window' : 'Maximize window', label: isMaximized ? 'Restore window' : 'Maximize window',
}, },
{ {
icon: <X size="1.25rem" />, icon: <X size="1.25rem" />,
onClick: handleClose, onClick: () => window.electronAPI.app.closeWindow(),
color: 'red' as const, color: 'red' as const,
label: 'Close window', label: 'Close window',
}, },
@ -200,10 +188,6 @@ export const TitleBar = ({
color={button.color} color={button.color}
aria-label={button.label} aria-label={button.label}
tabIndex={-1} tabIndex={-1}
style={{
borderRadius: 0,
margin: '2px 1px 1px',
}}
> >
{button.icon} {button.icon}
</ActionIcon> </ActionIcon>

View file

@ -44,7 +44,7 @@ export const AboutTab = () => {
{ {
label: PRODUCT_NAME, label: PRODUCT_NAME,
value: versionInfo.isAUR value: versionInfo.isAUR
? `${versionInfo.appVersion} (AUR Package)` ? `${versionInfo.appVersion} (AUR)`
: versionInfo.appVersion, : versionInfo.appVersion,
}, },
{ label: 'Electron', value: versionInfo.electronVersion }, { label: 'Electron', value: versionInfo.electronVersion },

View file

@ -1,5 +1,4 @@
import { useState, useEffect, useCallback, useMemo } from 'react'; import { useState, useEffect, useCallback, useMemo } from 'react';
import { tryExecute } from '@/utils/logger';
import { import {
Stack, Stack,
Text, Text,
@ -112,10 +111,7 @@ export const GeneralTab = ({
(config) => config.value === FrontendPreference (config) => config.value === FrontendPreference
); );
if (currentFrontendConfig && !requirementResults.get(FrontendPreference)) { if (currentFrontendConfig && !requirementResults.get(FrontendPreference)) {
await tryExecute( window.electronAPI.config.set('frontendPreference', 'koboldcpp');
() => window.electronAPI.config.set('frontendPreference', 'koboldcpp'),
'Failed to reset frontend preference:'
);
setFrontendPreference('koboldcpp'); setFrontendPreference('koboldcpp');
} }
}, [frontendConfigs, FrontendPreference]); }, [frontendConfigs, FrontendPreference]);
@ -129,6 +125,13 @@ export const GeneralTab = ({
]); ]);
}; };
initialize(); initialize();
const handleFocus = () => {
checkAllFrontendRequirements();
};
window.addEventListener('focus', handleFocus);
return () => window.removeEventListener('focus', handleFocus);
}, [checkAllFrontendRequirements]); }, [checkAllFrontendRequirements]);
const getSelectedFrontendConfig = () => const getSelectedFrontendConfig = () =>
@ -191,13 +194,10 @@ export const GeneralTab = ({
) )
return; return;
const success = await tryExecute( await checkAllFrontendRequirements();
() => window.electronAPI.config.set('frontendPreference', value),
'Failed to save frontend preference:' window.electronAPI.config.set('frontendPreference', value);
);
if (success) {
setFrontendPreference(value as FrontendPreference); setFrontendPreference(value as FrontendPreference);
}
}; };
return ( return (
@ -266,6 +266,7 @@ export const GeneralTab = ({
value={FrontendPreference} value={FrontendPreference}
onChange={handleFrontendPreferenceChange} onChange={handleFrontendPreferenceChange}
disabled={isOnInterfaceScreen} disabled={isOnInterfaceScreen}
onClick={() => checkAllFrontendRequirements()}
data={frontendConfigs.map((config) => ({ data={frontendConfigs.map((config) => ({
value: config.value, value: config.value,
label: config.label, label: config.label,

View file

@ -1,5 +1,4 @@
import { useState, useCallback, useEffect } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { tryExecute } from '@/utils/logger';
import { compareVersions } from '@/utils/version'; import { compareVersions } from '@/utils/version';
import { GITHUB_API } from '@/constants'; import { GITHUB_API } from '@/constants';
@ -35,7 +34,6 @@ export const useAppUpdateChecker = () => {
setIsDownloading(true); setIsDownloading(true);
await tryExecute(async () => {
const checkResult = await window.electronAPI.updater.checkForUpdates(); const checkResult = await window.electronAPI.updater.checkForUpdates();
if (!checkResult) { if (!checkResult) {
setIsDownloading(false); setIsDownloading(false);
@ -46,7 +44,6 @@ export const useAppUpdateChecker = () => {
if (success) { if (success) {
setIsUpdateDownloaded(true); setIsUpdateDownloaded(true);
} }
}, 'Failed to download update');
setIsDownloading(false); setIsDownloading(false);
}, [canAutoUpdate]); }, [canAutoUpdate]);
@ -58,7 +55,7 @@ export const useAppUpdateChecker = () => {
}, [isUpdateDownloaded]); }, [isUpdateDownloaded]);
const checkForAppUpdates = useCallback(async () => { const checkForAppUpdates = useCallback(async () => {
await tryExecute(async () => { try {
const currentVersion = await window.electronAPI.app.getVersion(); const currentVersion = await window.electronAPI.app.getVersion();
const response = await fetch( const response = await fetch(
@ -86,7 +83,12 @@ export const useAppUpdateChecker = () => {
}; };
setUpdateInfo(updateInfo); setUpdateInfo(updateInfo);
}, 'Failed to check for app updates'); } catch (error) {
window.electronAPI.logs.logError(
'Failed to check for app updates',
error instanceof Error ? error : undefined
);
}
}, []); }, []);
useEffect(() => { useEffect(() => {

View file

@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { logError, tryExecuteImmediate } 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';
@ -41,13 +41,15 @@ const loadFromCache = (): CachedReleaseData | null => {
}; };
const saveToCache = (releases: DownloadItem[]) => { const saveToCache = (releases: DownloadItem[]) => {
tryExecuteImmediate(() => { try {
const data: CachedReleaseData = { const data: CachedReleaseData = {
releases, releases,
timestamp: Date.now(), timestamp: Date.now(),
}; };
localStorage.setItem(CACHE_KEY, JSON.stringify(data)); localStorage.setItem(CACHE_KEY, JSON.stringify(data));
}, 'Failed to save releases to cache'); } catch {
void 0;
}
}; };
const transformReleaseToDownloadItems = ( const transformReleaseToDownloadItems = (

View file

@ -1,5 +1,4 @@
import { useState, useCallback } from 'react'; import { useCallback, useState } from 'react';
import { tryExecute } from '@/utils/logger';
import type { SdConvDirectMode } from '@/types'; import type { SdConvDirectMode } from '@/types';
interface UseLaunchLogicProps { interface UseLaunchLogicProps {
@ -258,7 +257,6 @@ export const useLaunchLogic = ({
onLaunch(); onLaunch();
await tryExecute(async () => {
const args: string[] = [ const args: string[] = [
...buildModelArgs(model, sdmodel, launchArgs), ...buildModelArgs(model, sdmodel, launchArgs),
...buildConfigArgs(hasImageModel, launchArgs), ...buildConfigArgs(hasImageModel, launchArgs),
@ -283,7 +281,6 @@ export const useLaunchLogic = ({
new Error(errorMessage) new Error(errorMessage)
); );
} }
}, 'Error launching');
setIsLaunching(false); setIsLaunching(false);
}, },

View file

@ -1,5 +1,4 @@
import { useState, useCallback, useEffect } from 'react'; import { useState, useCallback, useEffect } from 'react';
import { tryExecute } from '@/utils/logger';
import { import {
getDisplayNameFromPath, getDisplayNameFromPath,
compareVersions, compareVersions,
@ -40,13 +39,8 @@ export const useUpdateChecker = () => {
loadDismissedUpdates(); loadDismissedUpdates();
}, []); }, []);
const saveDismissedUpdates = useCallback(async (updates: Set<string>) => { const saveDismissedUpdates = useCallback((updates: Set<string>) => {
await tryExecute(async () => { window.electronAPI.config.set('dismissedUpdates', Array.from(updates));
await window.electronAPI.config.set(
'dismissedUpdates',
Array.from(updates)
);
}, 'Failed to save dismissed updates');
}, []); }, []);
const checkForUpdates = useCallback(async () => { const checkForUpdates = useCallback(async () => {
@ -56,16 +50,13 @@ export const useUpdateChecker = () => {
setIsChecking(true); setIsChecking(true);
await tryExecute(async () => {
const [currentVersion, rocmDownload] = await Promise.all([ const [currentVersion, rocmDownload] = await Promise.all([
window.electronAPI.kobold.getCurrentVersion(), window.electronAPI.kobold.getCurrentVersion(),
getROCmDownload(), getROCmDownload(),
]); ]);
if (!currentVersion) { if (!currentVersion) {
return; setIsChecking(false);
}
if (!currentVersion) {
return; return;
} }
@ -99,7 +90,6 @@ export const useUpdateChecker = () => {
} }
} }
} }
}, 'Failed to check for updates');
setIsChecking(false); setIsChecking(false);
}, [dismissedUpdates, dismissedUpdatesLoaded, releases]); }, [dismissedUpdates, dismissedUpdatesLoaded, releases]);

View file

@ -162,7 +162,7 @@ export function setupIPCHandlers() {
ipcMain.handle('config:get', (_, key) => getConfig(key)); ipcMain.handle('config:get', (_, key) => getConfig(key));
ipcMain.handle('config:set', (_, key, value) => setConfig(key, value)); ipcMain.on('config:set', (_, key, value) => setConfig(key, value));
ipcMain.handle('app:getVersion', () => getAppVersion()); ipcMain.handle('app:getVersion', () => getAppVersion());
@ -265,7 +265,7 @@ export function setupIPCHandlers() {
ipcMain.handle('app:openPerformanceManager', () => openPerformanceManager()); ipcMain.handle('app:openPerformanceManager', () => openPerformanceManager());
ipcMain.handle('logs:logError', (_, message: string, error?: Error) => ipcMain.on('logs:logError', (_, message: string, error?: Error) =>
logError(message, error) logError(message, error)
); );

View file

@ -4,7 +4,7 @@ import { join } from 'path';
import { on } from 'process'; import { on } from 'process';
import { logError } from './logging'; import { logError } from './logging';
import { safeTryExecute, tryExecute } from '@/utils/node/logger'; import { tryExecute } from '@/utils/node/logger';
import { sendKoboldOutput } from './window'; import { sendKoboldOutput } from './window';
import { getInstallDir } from './config'; import { getInstallDir } from './config';
import { OPENWEBUI, SERVER_READY_SIGNALS } from '@/constants'; import { OPENWEBUI, SERVER_READY_SIGNALS } from '@/constants';
@ -115,19 +115,15 @@ export async function startFrontend(args: string[]) {
if (openWebUIProcess.stdout) { if (openWebUIProcess.stdout) {
openWebUIProcess.stdout.on('data', (data: Buffer) => { openWebUIProcess.stdout.on('data', (data: Buffer) => {
safeTryExecute(() => {
const output = data.toString('utf8'); const output = data.toString('utf8');
sendKoboldOutput(output, true); sendKoboldOutput(output, true);
}, 'Error processing stdout data');
}); });
} }
if (openWebUIProcess.stderr) { if (openWebUIProcess.stderr) {
openWebUIProcess.stderr.on('data', (data: Buffer) => { openWebUIProcess.stderr.on('data', (data: Buffer) => {
safeTryExecute(() => {
const output = data.toString('utf8'); const output = data.toString('utf8');
sendKoboldOutput(output, true); sendKoboldOutput(output, true);
}, 'Error processing stderr data');
}); });
} }

View file

@ -99,12 +99,12 @@ const appAPI: AppAPI = {
const configAPI: ConfigAPI = { const configAPI: ConfigAPI = {
get: (key) => ipcRenderer.invoke('config:get', key), get: (key) => ipcRenderer.invoke('config:get', key),
set: (key, value) => ipcRenderer.invoke('config:set', key, value), set: (key, value) => ipcRenderer.send('config:set', key, value),
}; };
const logsAPI: LogsAPI = { const logsAPI: LogsAPI = {
logError: (message, error) => logError: (message, error) =>
ipcRenderer.invoke('logs:logError', message, error), ipcRenderer.send('logs:logError', message, error),
}; };
const dependenciesAPI: DependenciesAPI = { const dependenciesAPI: DependenciesAPI = {

View file

@ -175,11 +175,11 @@ export interface AppAPI {
export interface ConfigAPI { export interface ConfigAPI {
get: (key: string) => Promise<unknown>; get: (key: string) => Promise<unknown>;
set: (key: string, value: unknown) => Promise<void>; set: (key: string, value: unknown) => void;
} }
export interface LogsAPI { export interface LogsAPI {
logError: (message: string, error?: Error) => Promise<void>; logError: (message: string, error?: Error) => void;
} }
export interface DependenciesAPI { export interface DependenciesAPI {

View file

@ -15,11 +15,6 @@ declare module '*.jpeg' {
export default src; export default src;
} }
declare module '*.gif' {
const src: string;
export default src;
}
declare module '*.svg' { declare module '*.svg' {
const src: string; const src: string;
export default src; export default src;
@ -34,13 +29,3 @@ declare module '*.mp3' {
const src: string; const src: string;
export default src; export default src;
} }
declare module '*.wav' {
const src: string;
export default src;
}
declare module '*.ogg' {
const src: string;
export default src;
}

View file

@ -1,46 +0,0 @@
type LoggerFunction = (message: string, error: Error) => void;
export const createSafeExecute =
(logger: LoggerFunction) =>
async <T>(operation: () => Promise<T>, errorMessage: string) => {
try {
return operation();
} catch (error) {
logger(errorMessage, error as Error);
return null;
}
};
export const createTryExecute =
(logger: LoggerFunction) =>
async (operation: () => Promise<void>, errorMessage: string) => {
try {
await operation();
return true;
} catch (error) {
logger(errorMessage, error as Error);
return false;
}
};
export const createSafeTryExecute =
(logger: LoggerFunction) =>
<T>(operation: () => T, errorMessage: string) => {
try {
return operation();
} catch (error) {
logger(errorMessage, error as Error);
return null;
}
};
export const createTryExecuteImmediate =
(logger: LoggerFunction) => (operation: () => void, errorMessage: string) => {
try {
operation();
return true;
} catch (error) {
logger(errorMessage, error as Error);
return false;
}
};

View file

@ -1,15 +1,15 @@
import {
createSafeExecute,
createTryExecute,
createTryExecuteImmediate,
createSafeTryExecute,
} from '@/utils/logger-core';
export const logError = (message: string, error: Error) => { export const logError = (message: string, error: Error) => {
window.electronAPI.logs.logError(message, error); window.electronAPI.logs.logError(message, error);
}; };
export const safeExecute = createSafeExecute(logError); export const safeExecute = async <T>(
export const tryExecute = createTryExecute(logError); operation: () => Promise<T>,
export const tryExecuteImmediate = createTryExecuteImmediate(logError); errorMessage: string
export const safeTryExecute = createSafeTryExecute(logError); ) => {
try {
return operation();
} catch (error) {
logError(errorMessage, error as Error);
return null;
}
};

View file

@ -1,10 +1,26 @@
import { logError } from '@/main/modules/logging'; import { logError } from '@/main/modules/logging';
import {
createSafeExecute,
createTryExecute,
createSafeTryExecute,
} from '@/utils/logger-core';
export const safeExecute = createSafeExecute(logError); export const safeExecute = async <T>(
export const tryExecute = createTryExecute(logError); operation: () => Promise<T>,
export const safeTryExecute = createSafeTryExecute(logError); errorMessage: string
) => {
try {
return operation();
} catch (error) {
logError(errorMessage, error as Error);
return null;
}
};
export const tryExecute = async (
operation: () => Promise<void>,
errorMessage: string
) => {
try {
await operation();
return true;
} catch (error) {
logError(errorMessage, error as Error);
return false;
}
};

View file

@ -1,7 +1,4 @@
import { safeTryExecute } from '@/utils/logger';
export const handleTerminalOutput = (prevContent: string, newData: string) => { export const handleTerminalOutput = (prevContent: string, newData: string) => {
const result = safeTryExecute(() => {
if (newData.includes('\r')) { if (newData.includes('\r')) {
const hasStandaloneCarriageReturns = /\r(?!\n)/g.test(newData); const hasStandaloneCarriageReturns = /\r(?!\n)/g.test(newData);
@ -28,9 +25,6 @@ export const handleTerminalOutput = (prevContent: string, newData: string) => {
} }
return prevContent + newData; return prevContent + newData;
}, 'Terminal Basic Error');
return result ?? prevContent + newData;
}; };
const URL_REGEX = /(https?:\/\/[^\s<>"{}|\\^`[\]]+)/gi; const URL_REGEX = /(https?:\/\/[^\s<>"{}|\\^`[\]]+)/gi;