diff --git a/README.md b/README.md
index 58d9a54..f28575c 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/index.html b/index.html
index 5e63e71..f8c7657 100644
--- a/index.html
+++ b/index.html
@@ -2,7 +2,7 @@
-
+
{
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) {
diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx
index 26678bc..0b660b9 100644
--- a/src/components/TitleBar.tsx
+++ b/src/components/TitleBar.tsx
@@ -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 (
@@ -175,19 +160,22 @@ export const TitleBar = ({
{[
{
icon: ,
- onClick: handleMinimize,
+ onClick: () => window.electronAPI.app.minimizeWindow(),
color: undefined,
label: 'Minimize window',
},
{
icon: isMaximized ? : ,
- onClick: handleMaximize,
+ onClick: () => {
+ window.electronAPI.app.maximizeWindow();
+ setIsMaximized(!isMaximized);
+ },
color: undefined,
label: isMaximized ? 'Restore window' : 'Maximize window',
},
{
icon: ,
- 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}
diff --git a/src/components/settings/AboutTab.tsx b/src/components/settings/AboutTab.tsx
index 10ec1f5..95a0d11 100644
--- a/src/components/settings/AboutTab.tsx
+++ b/src/components/settings/AboutTab.tsx
@@ -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 },
diff --git a/src/components/settings/GeneralTab.tsx b/src/components/settings/GeneralTab.tsx
index 1e73634..81034d5 100644
--- a/src/components/settings/GeneralTab.tsx
+++ b/src/components/settings/GeneralTab.tsx
@@ -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,
diff --git a/src/hooks/useAppUpdateChecker.ts b/src/hooks/useAppUpdateChecker.ts
index c094d17..a1b0992 100644
--- a/src/hooks/useAppUpdateChecker.ts
+++ b/src/hooks/useAppUpdateChecker.ts
@@ -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(() => {
diff --git a/src/hooks/useKoboldVersions.ts b/src/hooks/useKoboldVersions.ts
index 7154758..b119c6d 100644
--- a/src/hooks/useKoboldVersions.ts
+++ b/src/hooks/useKoboldVersions.ts
@@ -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 = (
diff --git a/src/hooks/useLaunchLogic.ts b/src/hooks/useLaunchLogic.ts
index b776334..4746d7c 100644
--- a/src/hooks/useLaunchLogic.ts
+++ b/src/hooks/useLaunchLogic.ts
@@ -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);
},
diff --git a/src/hooks/useUpdateChecker.ts b/src/hooks/useUpdateChecker.ts
index f82b24c..24a27a1 100644
--- a/src/hooks/useUpdateChecker.ts
+++ b/src/hooks/useUpdateChecker.ts
@@ -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) => {
- await tryExecute(async () => {
- await window.electronAPI.config.set(
- 'dismissedUpdates',
- Array.from(updates)
- );
- }, 'Failed to save dismissed updates');
+ const saveDismissedUpdates = useCallback((updates: Set) => {
+ 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]);
diff --git a/src/main/ipc.ts b/src/main/ipc.ts
index aa8215b..3ca252f 100644
--- a/src/main/ipc.ts
+++ b/src/main/ipc.ts
@@ -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)
);
diff --git a/src/main/modules/openwebui.ts b/src/main/modules/openwebui.ts
index 923f1a7..d261f91 100644
--- a/src/main/modules/openwebui.ts
+++ b/src/main/modules/openwebui.ts
@@ -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);
});
}
diff --git a/src/preload/index.ts b/src/preload/index.ts
index ab63c90..fd94680 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -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 = {
diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts
index 6ddb112..70bdeaa 100644
--- a/src/types/electron.d.ts
+++ b/src/types/electron.d.ts
@@ -175,11 +175,11 @@ export interface AppAPI {
export interface ConfigAPI {
get: (key: string) => Promise;
- set: (key: string, value: unknown) => Promise;
+ set: (key: string, value: unknown) => void;
}
export interface LogsAPI {
- logError: (message: string, error?: Error) => Promise;
+ logError: (message: string, error?: Error) => void;
}
export interface DependenciesAPI {
diff --git a/src/types/vite-env.d.ts b/src/types/vite-env.d.ts
index 5d46fa0..77fd8a1 100644
--- a/src/types/vite-env.d.ts
+++ b/src/types/vite-env.d.ts
@@ -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;
-}
diff --git a/src/utils/logger-core.ts b/src/utils/logger-core.ts
deleted file mode 100644
index a6ff39f..0000000
--- a/src/utils/logger-core.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-type LoggerFunction = (message: string, error: Error) => void;
-
-export const createSafeExecute =
- (logger: LoggerFunction) =>
- async (operation: () => Promise, errorMessage: string) => {
- try {
- return operation();
- } catch (error) {
- logger(errorMessage, error as Error);
- return null;
- }
- };
-
-export const createTryExecute =
- (logger: LoggerFunction) =>
- async (operation: () => Promise, errorMessage: string) => {
- try {
- await operation();
- return true;
- } catch (error) {
- logger(errorMessage, error as Error);
- return false;
- }
- };
-
-export const createSafeTryExecute =
- (logger: LoggerFunction) =>
- (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;
- }
- };
diff --git a/src/utils/logger.ts b/src/utils/logger.ts
index 81669a7..ba5b883 100644
--- a/src/utils/logger.ts
+++ b/src/utils/logger.ts
@@ -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 (
+ operation: () => Promise,
+ errorMessage: string
+) => {
+ try {
+ return operation();
+ } catch (error) {
+ logError(errorMessage, error as Error);
+ return null;
+ }
+};
diff --git a/src/utils/node/logger.ts b/src/utils/node/logger.ts
index f6acec5..a4fa28c 100644
--- a/src/utils/node/logger.ts
+++ b/src/utils/node/logger.ts
@@ -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 (
+ operation: () => Promise,
+ errorMessage: string
+) => {
+ try {
+ return operation();
+ } catch (error) {
+ logError(errorMessage, error as Error);
+ return null;
+ }
+};
+
+export const tryExecute = async (
+ operation: () => Promise,
+ errorMessage: string
+) => {
+ try {
+ await operation();
+ return true;
+ } catch (error) {
+ logError(errorMessage, error as Error);
+ return false;
+ }
+};
diff --git a/src/utils/terminal.ts b/src/utils/terminal.ts
index 6c74b63..8b6e556 100644
--- a/src/utils/terminal.ts
+++ b/src/utils/terminal.ts
@@ -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;