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;