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

This commit is contained in:
lone-cloud 2025-09-18 22:16:51 -07:00
parent 75f44eb7e0
commit ccb230a31d
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
- **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)
- **Automatic updates** - Download and keep your KoboldCpp binary up-to-date effortlessly
- **Smart process management** - Prevents runaway background processes and system resource waste

View file

@ -2,7 +2,7 @@
<html lang="en">
<head>
<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
http-equiv="Content-Security-Policy"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,4 @@
import { useState, useCallback, useEffect } from 'react';
import { tryExecute } from '@/utils/logger';
import { useState, useEffect, useCallback } from 'react';
import { compareVersions } from '@/utils/version';
import { GITHUB_API } from '@/constants';
@ -35,18 +34,16 @@ export const useAppUpdateChecker = () => {
setIsDownloading(true);
await tryExecute(async () => {
const checkResult = await window.electronAPI.updater.checkForUpdates();
if (!checkResult) {
setIsDownloading(false);
return;
}
const checkResult = await window.electronAPI.updater.checkForUpdates();
if (!checkResult) {
setIsDownloading(false);
return;
}
const success = await window.electronAPI.updater.downloadUpdate();
if (success) {
setIsUpdateDownloaded(true);
}
}, 'Failed to download update');
const success = await window.electronAPI.updater.downloadUpdate();
if (success) {
setIsUpdateDownloaded(true);
}
setIsDownloading(false);
}, [canAutoUpdate]);
@ -58,7 +55,7 @@ export const useAppUpdateChecker = () => {
}, [isUpdateDownloaded]);
const checkForAppUpdates = useCallback(async () => {
await tryExecute(async () => {
try {
const currentVersion = await window.electronAPI.app.getVersion();
const response = await fetch(
@ -86,7 +83,12 @@ export const useAppUpdateChecker = () => {
};
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(() => {

View file

@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react';
import { logError, tryExecuteImmediate } from '@/utils/logger';
import { logError } from '@/utils/logger';
import { getROCmDownload } from '@/utils/rocm';
import { GITHUB_API } from '@/constants';
import { filterAssetsByPlatform } from '@/utils/platform';
@ -41,13 +41,15 @@ const loadFromCache = (): CachedReleaseData | null => {
};
const saveToCache = (releases: DownloadItem[]) => {
tryExecuteImmediate(() => {
try {
const data: CachedReleaseData = {
releases,
timestamp: Date.now(),
};
localStorage.setItem(CACHE_KEY, JSON.stringify(data));
}, 'Failed to save releases to cache');
} catch {
void 0;
}
};
const transformReleaseToDownloadItems = (

View file

@ -1,5 +1,4 @@
import { useState, useCallback } from 'react';
import { tryExecute } from '@/utils/logger';
import { useCallback, useState } from 'react';
import type { SdConvDirectMode } from '@/types';
interface UseLaunchLogicProps {
@ -258,32 +257,30 @@ export const useLaunchLogic = ({
onLaunch();
await tryExecute(async () => {
const args: string[] = [
...buildModelArgs(model, sdmodel, launchArgs),
...buildConfigArgs(hasImageModel, launchArgs),
...buildBackendArgs(launchArgs),
];
const args: string[] = [
...buildModelArgs(model, sdmodel, launchArgs),
...buildConfigArgs(hasImageModel, launchArgs),
...buildBackendArgs(launchArgs),
];
if (launchArgs.additionalArguments.trim()) {
const additionalArgs = launchArgs.additionalArguments
.trim()
.split(/\s+/);
args.push(...additionalArgs);
}
if (launchArgs.additionalArguments.trim()) {
const additionalArgs = launchArgs.additionalArguments
.trim()
.split(/\s+/);
args.push(...additionalArgs);
}
const result = await window.electronAPI.kobold.launchKoboldCpp(args);
const result = await window.electronAPI.kobold.launchKoboldCpp(args);
if (result.success) {
onLaunch();
} else {
const errorMessage = result.error || 'Unknown launch error';
window.electronAPI.logs.logError(
'Launch failed:',
new Error(errorMessage)
);
}
}, 'Error launching');
if (result.success) {
onLaunch();
} else {
const errorMessage = result.error || 'Unknown launch error';
window.electronAPI.logs.logError(
'Launch failed:',
new Error(errorMessage)
);
}
setIsLaunching(false);
},

View file

@ -1,5 +1,4 @@
import { useState, useCallback, useEffect } from 'react';
import { tryExecute } from '@/utils/logger';
import {
getDisplayNameFromPath,
compareVersions,
@ -40,13 +39,8 @@ export const useUpdateChecker = () => {
loadDismissedUpdates();
}, []);
const saveDismissedUpdates = useCallback(async (updates: Set<string>) => {
await tryExecute(async () => {
await window.electronAPI.config.set(
'dismissedUpdates',
Array.from(updates)
);
}, 'Failed to save dismissed updates');
const saveDismissedUpdates = useCallback((updates: Set<string>) => {
window.electronAPI.config.set('dismissedUpdates', Array.from(updates));
}, []);
const checkForUpdates = useCallback(async () => {
@ -56,50 +50,46 @@ export const useUpdateChecker = () => {
setIsChecking(true);
await tryExecute(async () => {
const [currentVersion, rocmDownload] = await Promise.all([
window.electronAPI.kobold.getCurrentVersion(),
getROCmDownload(),
]);
const [currentVersion, rocmDownload] = await Promise.all([
window.electronAPI.kobold.getCurrentVersion(),
getROCmDownload(),
]);
if (!currentVersion) {
return;
}
if (!currentVersion) {
return;
if (!currentVersion) {
setIsChecking(false);
return;
}
const availableDownloads: DownloadItem[] = [...releases];
if (rocmDownload) {
availableDownloads.push(rocmDownload);
}
const currentDisplayName = getDisplayNameFromPath(currentVersion);
const matchingDownload = availableDownloads.find(
(download: DownloadItem) => {
const downloadBaseName = stripAssetExtensions(download.name);
return downloadBaseName === currentDisplayName;
}
);
const availableDownloads: DownloadItem[] = [...releases];
if (rocmDownload) {
availableDownloads.push(rocmDownload);
}
if (matchingDownload && matchingDownload.version) {
const hasUpdate =
compareVersions(matchingDownload.version, currentVersion.version) > 0;
const currentDisplayName = getDisplayNameFromPath(currentVersion);
if (hasUpdate) {
const updateKey = `${currentVersion.path}-${matchingDownload.version}`;
const matchingDownload = availableDownloads.find(
(download: DownloadItem) => {
const downloadBaseName = stripAssetExtensions(download.name);
return downloadBaseName === currentDisplayName;
}
);
if (matchingDownload && matchingDownload.version) {
const hasUpdate =
compareVersions(matchingDownload.version, currentVersion.version) > 0;
if (hasUpdate) {
const updateKey = `${currentVersion.path}-${matchingDownload.version}`;
if (!dismissedUpdates.has(updateKey)) {
setUpdateInfo({
currentVersion,
availableUpdate: matchingDownload,
});
setShowUpdateModal(true);
}
if (!dismissedUpdates.has(updateKey)) {
setUpdateInfo({
currentVersion,
availableUpdate: matchingDownload,
});
setShowUpdateModal(true);
}
}
}, 'Failed to check for updates');
}
setIsChecking(false);
}, [dismissedUpdates, dismissedUpdatesLoaded, releases]);

View file

@ -162,7 +162,7 @@ export function setupIPCHandlers() {
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());
@ -265,7 +265,7 @@ export function setupIPCHandlers() {
ipcMain.handle('app:openPerformanceManager', () => openPerformanceManager());
ipcMain.handle('logs:logError', (_, message: string, error?: Error) =>
ipcMain.on('logs:logError', (_, message: string, error?: Error) =>
logError(message, error)
);

View file

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

View file

@ -99,12 +99,12 @@ const appAPI: AppAPI = {
const configAPI: ConfigAPI = {
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 = {
logError: (message, error) =>
ipcRenderer.invoke('logs:logError', message, error),
ipcRenderer.send('logs:logError', message, error),
};
const dependenciesAPI: DependenciesAPI = {

View file

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

View file

@ -15,11 +15,6 @@ declare module '*.jpeg' {
export default src;
}
declare module '*.gif' {
const src: string;
export default src;
}
declare module '*.svg' {
const src: string;
export default src;
@ -34,13 +29,3 @@ declare module '*.mp3' {
const src: string;
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) => {
window.electronAPI.logs.logError(message, error);
};
export const safeExecute = createSafeExecute(logError);
export const tryExecute = createTryExecute(logError);
export const tryExecuteImmediate = createTryExecuteImmediate(logError);
export const safeTryExecute = createSafeTryExecute(logError);
export const safeExecute = async <T>(
operation: () => Promise<T>,
errorMessage: string
) => {
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 {
createSafeExecute,
createTryExecute,
createSafeTryExecute,
} from '@/utils/logger-core';
export const safeExecute = createSafeExecute(logError);
export const tryExecute = createTryExecute(logError);
export const safeTryExecute = createSafeTryExecute(logError);
export const safeExecute = async <T>(
operation: () => Promise<T>,
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,36 +1,30 @@
import { safeTryExecute } from '@/utils/logger';
export const handleTerminalOutput = (prevContent: string, newData: string) => {
const result = safeTryExecute(() => {
if (newData.includes('\r')) {
const hasStandaloneCarriageReturns = /\r(?!\n)/g.test(newData);
if (newData.includes('\r')) {
const hasStandaloneCarriageReturns = /\r(?!\n)/g.test(newData);
if (hasStandaloneCarriageReturns) {
const combined = prevContent + newData;
if (hasStandaloneCarriageReturns) {
const combined = prevContent + newData;
const lines = combined.split(/(\r?\n)/);
const processedLines: string[] = [];
const lines = combined.split(/(\r?\n)/);
const processedLines: string[] = [];
for (let i = 0; i < lines.length; i += 2) {
const line = lines[i] || '';
const lineBreak = lines[i + 1] || '';
for (let i = 0; i < lines.length; i += 2) {
const line = lines[i] || '';
const lineBreak = lines[i + 1] || '';
if (line.includes('\r')) {
const parts = line.split('\r');
processedLines.push(parts[parts.length - 1] + lineBreak);
} else {
processedLines.push(line + lineBreak);
}
if (line.includes('\r')) {
const parts = line.split('\r');
processedLines.push(parts[parts.length - 1] + lineBreak);
} else {
processedLines.push(line + lineBreak);
}
return processedLines.join('').replace(/\r?\n$/, '');
}
return processedLines.join('').replace(/\r?\n$/, '');
}
}
return prevContent + newData;
}, 'Terminal Basic Error');
return result ?? prevContent + newData;
return prevContent + newData;
};
const URL_REGEX = /(https?:\/\/[^\s<>"{}|\\^`[\]]+)/gi;