From e56b581109eab1e6cdccfebcec88d42e464916dd Mon Sep 17 00:00:00 2001 From: Egor Date: Mon, 8 Sep 2025 14:24:49 -0700 Subject: [PATCH] WIP - figuring out more consistent .embd copying for portable binaries, starting to move away from dep injection to pure functional --- assets/kcpp_sdui.embd | 524 ++++++++++++++++++++++++ src/main/gui.ts | 90 ++-- src/main/ipc.ts | 7 +- src/main/managers/BinaryManager.ts | 52 ++- src/main/managers/ConfigManager.ts | 24 +- src/main/managers/HardwareManager.ts | 22 +- src/main/managers/KoboldCppManager.ts | 116 +++--- src/main/managers/LogManager.ts | 9 + src/main/managers/OpenWebUIManager.ts | 72 ++-- src/main/managers/SillyTavernManager.ts | 88 ++-- src/main/managers/WindowManager.ts | 32 +- 11 files changed, 767 insertions(+), 269 deletions(-) create mode 100644 assets/kcpp_sdui.embd diff --git a/assets/kcpp_sdui.embd b/assets/kcpp_sdui.embd new file mode 100644 index 0000000..8799aa4 --- /dev/null +++ b/assets/kcpp_sdui.embd @@ -0,0 +1,524 @@ + + + + + + + Stable UI for KoboldCpp + + + + +
+ + + + diff --git a/src/main/gui.ts b/src/main/gui.ts index 362c6ee..8091c55 100644 --- a/src/main/gui.ts +++ b/src/main/gui.ts @@ -1,82 +1,44 @@ import { app } from 'electron'; -import { WindowManager } from '@/main/managers/WindowManager'; -import { ConfigManager } from '@/main/managers/ConfigManager'; -import { LogManager } from '@/main/managers/LogManager'; -import { KoboldCppManager } from '@/main/managers/KoboldCppManager'; -import { SillyTavernManager } from '@/main/managers/SillyTavernManager'; -import { OpenWebUIManager } from '@/main/managers/OpenWebUIManager'; -import { HardwareManager } from '@/main/managers/HardwareManager'; -import { BinaryManager } from '@/main/managers/BinaryManager'; +import { getWindowManager } from '@/main/managers/WindowManager'; +import { getConfigManager } from '@/main/managers/ConfigManager'; +import { getLogManager } from '@/main/managers/LogManager'; +import { getKoboldCppManager } from '@/main/managers/KoboldCppManager'; +import { getSillyTavernManager } from '@/main/managers/SillyTavernManager'; +import { getOpenWebUIManager } from '@/main/managers/OpenWebUIManager'; +import { getHardwareManager } from '@/main/managers/HardwareManager'; +import { getBinaryManager } from '@/main/managers/BinaryManager'; import { IPCHandlers } from '@/main/ipc'; import { ensureDir } from '@/utils/fs'; import { getConfigDir } from '@/utils/path'; export class GerbilApp { - private windowManager: WindowManager; - private koboldManager: KoboldCppManager; - private configManager: ConfigManager; - private logManager: LogManager; - private sillyTavernManager: SillyTavernManager; - private openWebUIManager: OpenWebUIManager; - private hardwareManager: HardwareManager; - private binaryManager: BinaryManager; private ipcHandlers: IPCHandlers; constructor() { - this.logManager = new LogManager(); - this.logManager.setupGlobalErrorHandlers(); - - this.configManager = new ConfigManager(getConfigDir(), this.logManager); - this.windowManager = new WindowManager(); - this.hardwareManager = new HardwareManager(this.logManager); - - this.koboldManager = new KoboldCppManager( - this.configManager, - this.windowManager, - this.logManager - ); - - this.binaryManager = new BinaryManager( - this.logManager, - this.koboldManager, - this.hardwareManager - ); - - this.sillyTavernManager = new SillyTavernManager( - this.logManager, - this.windowManager - ); - - this.openWebUIManager = new OpenWebUIManager( - this.configManager, - this.logManager, - this.windowManager - ); - this.ipcHandlers = new IPCHandlers( - this.koboldManager, - this.configManager, - this.hardwareManager, - this.binaryManager, - this.logManager, - this.sillyTavernManager, - this.openWebUIManager, - this.windowManager + getKoboldCppManager(), + getConfigManager(getConfigDir()), + getHardwareManager(), + getBinaryManager(), + getLogManager(), + getSillyTavernManager(), + getOpenWebUIManager(), + getWindowManager() ); } private async ensureInstallDirectory(): Promise { - const installDir = this.configManager.getInstallDir(); + const installDir = getConfigManager().getInstallDir(); await ensureDir(installDir); } async initialize(): Promise { await app.whenReady(); - await this.configManager.initialize(); + await getConfigManager().initialize(); await this.ensureInstallDirectory(); - this.windowManager.createMainWindow(); + getWindowManager().createMainWindow(); this.ipcHandlers.setupHandlers(); app.on('window-all-closed', () => { @@ -92,9 +54,9 @@ export class GerbilApp { try { const cleanupPromises = [ - this.koboldManager.cleanup(), - this.sillyTavernManager.cleanup(), - this.openWebUIManager.cleanup(), + getKoboldCppManager().cleanup(), + getSillyTavernManager().cleanup(), + getOpenWebUIManager().cleanup(), ]; const timeoutPromise = new Promise((resolve) => { @@ -105,10 +67,10 @@ export class GerbilApp { await Promise.race([Promise.all(cleanupPromises), timeoutPromise]); } catch (error) { - this.logManager.logError('Error during cleanup:', error as Error); + getLogManager().logError('Error during cleanup:', error as Error); } - this.windowManager.cleanup(); + getWindowManager().cleanup(); app.exit(0); }); @@ -119,8 +81,8 @@ export class GerbilApp { }); app.on('activate', () => { - if (!this.windowManager.getMainWindow()) { - this.windowManager.createMainWindow(); + if (!getWindowManager().getMainWindow()) { + getWindowManager().createMainWindow(); } }); } diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 02f1d8d..ce656ef 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -126,13 +126,16 @@ export class IPCHandlers { ); ipcMain.handle('kobold:detectBackendSupport', () => - this.binaryManager.detectBackendSupport() + this.binaryManager.detectBackendSupport(this.koboldManager) ); ipcMain.handle( 'kobold:getAvailableBackends', (_, includeDisabled = false) => - this.binaryManager.getAvailableBackends(includeDisabled) + this.binaryManager.getAvailableBackends( + this.koboldManager, + includeDisabled + ) ); ipcMain.handle('kobold:getPlatform', () => process.platform); diff --git a/src/main/managers/BinaryManager.ts b/src/main/managers/BinaryManager.ts index 73bbbd0..e6e3a42 100644 --- a/src/main/managers/BinaryManager.ts +++ b/src/main/managers/BinaryManager.ts @@ -1,26 +1,13 @@ import { join, dirname } from 'path'; import { pathExists } from '@/utils/fs'; -import { LogManager } from '@/main/managers/LogManager'; -import type { KoboldCppManager } from '@/main/managers/KoboldCppManager'; -import type { HardwareManager } from '@/main/managers/HardwareManager'; +import { getLogManager } from '@/main/managers/LogManager'; +import { getHardwareManager } from '@/main/managers/HardwareManager'; import type { BackendOption, BackendSupport } from '@/types'; +import type { KoboldCppManager } from '@/main/managers/KoboldCppManager'; export class BinaryManager { private backendSupportCache = new Map(); private availableBackendsCache = new Map(); - private logManager: LogManager; - private koboldManager: KoboldCppManager; - private hardwareManager: HardwareManager; - - constructor( - logManager: LogManager, - koboldManager: KoboldCppManager, - hardwareManager: HardwareManager - ) { - this.logManager = logManager; - this.koboldManager = koboldManager; - this.hardwareManager = hardwareManager; - } private async detectBackendSupportFromPath( koboldBinaryPath: string @@ -68,7 +55,7 @@ export class BinaryManager { support.failsafe = await hasKoboldCppLib('koboldcpp_failsafe'); support.cuda = await hasKoboldCppLib('koboldcpp_cublas'); } catch (error) { - this.logManager.logError( + getLogManager().logError( 'Error detecting backend support:', error as Error ); @@ -78,9 +65,11 @@ export class BinaryManager { return support; } - async detectBackendSupport(): Promise { + async detectBackendSupport( + koboldManager: KoboldCppManager + ): Promise { try { - const currentBinaryInfo = await this.koboldManager.getCurrentBinaryInfo(); + const currentBinaryInfo = await koboldManager.getCurrentBinaryInfo(); if (!currentBinaryInfo?.path) { return null; @@ -88,7 +77,7 @@ export class BinaryManager { return this.detectBackendSupportFromPath(currentBinaryInfo.path); } catch (error) { - this.logManager.logError( + getLogManager().logError( 'Error detecting current binary backend support:', error as Error ); @@ -98,16 +87,16 @@ export class BinaryManager { // eslint-disable-next-line sonarjs/cognitive-complexity async getAvailableBackends( + koboldManager: KoboldCppManager, includeDisabled = false ): Promise { try { + const hardwareManager = getHardwareManager(); const [currentBinaryInfo, hardwareCapabilities, cpuCapabilities] = await Promise.all([ - this.koboldManager.getCurrentBinaryInfo(), - this.hardwareManager.detectGPUCapabilities(), - includeDisabled - ? this.hardwareManager.detectCPU() - : Promise.resolve(null), + koboldManager.getCurrentBinaryInfo(), + hardwareManager.detectGPUCapabilities(), + includeDisabled ? hardwareManager.detectCPU() : Promise.resolve(null), ]); if (!currentBinaryInfo?.path) { @@ -120,7 +109,7 @@ export class BinaryManager { return this.availableBackendsCache.get(cacheKey)!; } - const backendSupport = await this.detectBackendSupport(); + const backendSupport = await this.detectBackendSupport(koboldManager); if (!backendSupport) { return []; @@ -193,7 +182,7 @@ export class BinaryManager { this.availableBackendsCache.set(cacheKey, backends); return backends; } catch (error) { - this.logManager.logError( + getLogManager().logError( 'Failed to get available backends:', error as Error ); @@ -206,3 +195,12 @@ export class BinaryManager { this.availableBackendsCache.clear(); } } + +let binaryManagerInstance: BinaryManager; + +export function getBinaryManager(): BinaryManager { + if (!binaryManagerInstance) { + binaryManagerInstance = new BinaryManager(); + } + return binaryManagerInstance; +} diff --git a/src/main/managers/ConfigManager.ts b/src/main/managers/ConfigManager.ts index c9fa5fe..3b7e7be 100644 --- a/src/main/managers/ConfigManager.ts +++ b/src/main/managers/ConfigManager.ts @@ -1,4 +1,4 @@ -import { LogManager } from '@/main/managers/LogManager'; +import { getLogManager } from '@/main/managers/LogManager'; import { readJsonFile, writeJsonFile } from '@/utils/fs'; import type { FrontendPreference } from '@/types'; import { homedir } from 'os'; @@ -15,14 +15,14 @@ interface AppConfig { [key: string]: ConfigValue; } +let configManagerInstance: ConfigManager | null = null; + export class ConfigManager { private config: AppConfig = {}; private configPath: string; - private logManager: LogManager; - constructor(configPath: string, logManager: LogManager) { + constructor(configPath: string) { this.configPath = configPath; - this.logManager = logManager; } async initialize(): Promise { @@ -34,7 +34,7 @@ export class ConfigManager { const config = await readJsonFile(this.configPath); return config || {}; } catch (error) { - this.logManager.logError('Error loading config:', error as Error); + getLogManager().logError('Error loading config:', error as Error); return {}; } } @@ -43,7 +43,7 @@ export class ConfigManager { try { await writeJsonFile(this.configPath, this.config); } catch (error) { - this.logManager.logError('Error saving config:', error as Error); + getLogManager().logError('Error saving config:', error as Error); } } @@ -98,3 +98,15 @@ export class ConfigManager { await this.saveConfig(); } } + +export const getConfigManager = (configPath?: string): ConfigManager => { + if (!configManagerInstance) { + if (!configPath) { + throw new Error( + 'ConfigManager not initialized. Provide configPath on first call.' + ); + } + configManagerInstance = new ConfigManager(configPath); + } + return configManagerInstance; +}; diff --git a/src/main/managers/HardwareManager.ts b/src/main/managers/HardwareManager.ts index 0314671..b291ad4 100644 --- a/src/main/managers/HardwareManager.ts +++ b/src/main/managers/HardwareManager.ts @@ -1,7 +1,7 @@ /* eslint-disable no-comments/disallowComments */ import si from 'systeminformation'; import { shortenDeviceName } from '@/utils/hardware'; -import { LogManager } from '@/main/managers/LogManager'; +import { getLogManager } from '@/main/managers/LogManager'; import { terminateProcess } from '@/utils/process'; import type { CPUCapabilities, @@ -12,16 +12,13 @@ import type { } from '@/types/hardware'; import { spawn } from 'child_process'; +let hardwareManagerInstance: HardwareManager | null = null; + export class HardwareManager { private cpuCapabilitiesCache: CPUCapabilities | null = null; private basicGPUInfoCache: BasicGPUInfo | null = null; private gpuCapabilitiesCache: GPUCapabilities | null = null; private gpuMemoryInfoCache: GPUMemoryInfo[] | null = null; - private logManager: LogManager; - - constructor(logManager: LogManager) { - this.logManager = logManager; - } async detectCPU(): Promise { if (this.cpuCapabilitiesCache) { @@ -47,7 +44,7 @@ export class HardwareManager { return this.cpuCapabilitiesCache; } catch (error) { - this.logManager.logError('CPU detection failed:', error as Error); + getLogManager().logError('CPU detection failed:', error as Error); const fallbackCapabilities = { avx: false, avx2: false, @@ -107,7 +104,7 @@ export class HardwareManager { return this.basicGPUInfoCache; } catch (error) { - this.logManager.logError('GPU detection failed:', error as Error); + getLogManager().logError('GPU detection failed:', error as Error); const fallbackGPUInfo = { hasAMD: false, hasNVIDIA: false, @@ -482,10 +479,17 @@ export class HardwareManager { this.gpuMemoryInfoCache = memoryInfo; } catch (error) { - this.logManager.logError('GPU memory detection failed:', error as Error); + getLogManager().logError('GPU memory detection failed:', error as Error); this.gpuMemoryInfoCache = []; } return this.gpuMemoryInfoCache; } } + +export const getHardwareManager = (): HardwareManager => { + if (!hardwareManagerInstance) { + hardwareManagerInstance = new HardwareManager(); + } + return hardwareManagerInstance; +}; diff --git a/src/main/managers/KoboldCppManager.ts b/src/main/managers/KoboldCppManager.ts index d44ce4d..c2466a5 100644 --- a/src/main/managers/KoboldCppManager.ts +++ b/src/main/managers/KoboldCppManager.ts @@ -18,9 +18,9 @@ import axios from 'axios'; import { execa } from 'execa'; import { terminateProcess } from '@/utils/process'; -import { ConfigManager } from '@/main/managers/ConfigManager'; -import { LogManager } from '@/main/managers/LogManager'; -import { WindowManager } from '@/main/managers/WindowManager'; +import { getConfigManager } from '@/main/managers/ConfigManager'; +import { getLogManager } from '@/main/managers/LogManager'; +import { getWindowManager } from '@/main/managers/WindowManager'; import { PRODUCT_NAME, SERVER_READY_SIGNALS } from '@/constants'; import { KLITE_CSS_OVERRIDE, @@ -39,19 +39,6 @@ import type { FrontendPreference } from '@/types'; export class KoboldCppManager { private koboldProcess: ChildProcess | null = null; - private configManager: ConfigManager; - private logManager: LogManager; - private windowManager: WindowManager; - - constructor( - configManager: ConfigManager, - windowManager: WindowManager, - logManager: LogManager - ) { - this.configManager = configManager; - this.logManager = logManager; - this.windowManager = windowManager; - } private async removeDirectoryWithRetry( dirPath: string, @@ -72,7 +59,7 @@ export class KoboldCppManager { } if (isPermissionError && process.platform === 'win32') { - this.windowManager.sendKoboldOutput( + getWindowManager().sendKoboldOutput( `Attempt ${attempt}/${maxRetries} failed (file in use), retrying in ${delayMs}ms...` ); await new Promise((resolve) => setTimeout(resolve, delayMs)); @@ -94,7 +81,7 @@ export class KoboldCppManager { try { if (this.koboldProcess && !this.koboldProcess.killed) { - this.windowManager.sendKoboldOutput( + getWindowManager().sendKoboldOutput( 'Stopping process before update...' ); await this.cleanup(); @@ -103,7 +90,7 @@ export class KoboldCppManager { await this.removeDirectoryWithRetry(unpackedDirPath); } catch (error) { - this.logManager.logError( + getLogManager().logError( 'Failed to remove existing directory for update:', error as Error ); @@ -136,7 +123,7 @@ export class KoboldCppManager { downloadedBytes += chunk.length; if (totalBytes > 0) { const progress = (downloadedBytes / totalBytes) * 100; - const mainWindow = this.windowManager.getMainWindow(); + const mainWindow = getWindowManager().getMainWindow(); if (mainWindow) { mainWindow.webContents.send('download-progress', progress); } @@ -151,7 +138,7 @@ export class KoboldCppManager { try { await chmod(tempPackedFilePath, 0o755); } catch (error) { - this.logManager.logError( + getLogManager().logError( 'Failed to make binary executable:', error as Error ); @@ -182,7 +169,7 @@ export class KoboldCppManager { await rename(tempPackedFilePath, newLauncherPath); launcherPath = newLauncherPath; } catch (error) { - this.logManager.logError( + getLogManager().logError( 'Failed to rename binary as launcher:', error as Error ); @@ -192,7 +179,7 @@ export class KoboldCppManager { try { await unlink(tempPackedFilePath); } catch (error) { - this.logManager.logError( + getLogManager().logError( 'Failed to cleanup packed file:', error as Error ); @@ -208,7 +195,7 @@ export class KoboldCppManager { async downloadRelease(asset: GitHubAsset): Promise { const tempPackedFilePath = join( - this.configManager.getInstallDir(), + getConfigManager().getInstallDir(), `${asset.name}.packed` ); const baseFilename = stripAssetExtensions(asset.name); @@ -216,7 +203,7 @@ export class KoboldCppManager { ? `${baseFilename}-${asset.version}` : baseFilename; const unpackedDirPath = join( - this.configManager.getInstallDir(), + getConfigManager().getInstallDir(), folderName ); @@ -233,15 +220,15 @@ export class KoboldCppManager { unpackedDirPath ); - const currentBinary = this.configManager.getCurrentKoboldBinary(); + const currentBinary = getConfigManager().getCurrentKoboldBinary(); if (!currentBinary || (asset.isUpdate && asset.wasCurrentBinary)) { - await this.configManager.setCurrentKoboldBinary(launcherPath); + await getConfigManager().setCurrentKoboldBinary(launcherPath); } - this.windowManager.sendToRenderer('versions-updated'); + getWindowManager().sendToRenderer('versions-updated'); return launcherPath; } catch (error) { - this.logManager.logError( + getLogManager().logError( 'Failed to download or unpack binary:', error as Error ); @@ -318,7 +305,7 @@ export class KoboldCppManager { await writeFile(kliteEmbdPath, patchedContent, 'utf8'); } } catch (error) { - this.logManager.logError('Failed to patch klite.embd:', error as Error); + getLogManager().logError('Failed to patch klite.embd:', error as Error); } } @@ -338,7 +325,7 @@ export class KoboldCppManager { } } } catch (error) { - this.logManager.logError( + getLogManager().logError( 'Failed to patch kcpp_sdui.embd:', error as Error ); @@ -361,7 +348,7 @@ export class KoboldCppManager { async getInstalledVersions(): Promise { try { - const installDir = this.configManager.getInstallDir(); + const installDir = getConfigManager().getInstallDir(); if (!(await pathExists(installDir))) { return []; } @@ -401,7 +388,7 @@ export class KoboldCppManager { size: launcher.size, } as InstalledVersion; } catch (error) { - this.logManager.logError( + getLogManager().logError( `Could not detect version for ${launcher.filename}:`, error as Error ); @@ -414,7 +401,7 @@ export class KoboldCppManager { (version): version is InstalledVersion => version !== null ); } catch (error) { - this.logManager.logError( + getLogManager().logError( 'Error scanning install directory:', error as Error ); @@ -428,7 +415,7 @@ export class KoboldCppManager { const configFiles: { name: string; path: string; size: number }[] = []; try { - const installDir = this.configManager.getInstallDir(); + const installDir = getConfigManager().getInstallDir(); if (await pathExists(installDir)) { const files = await readdir(installDir); @@ -451,7 +438,7 @@ export class KoboldCppManager { } } } catch (error) { - this.logManager.logError( + getLogManager().logError( 'Error scanning for config files:', error as Error ); @@ -469,7 +456,7 @@ export class KoboldCppManager { const config = await readJsonFile(filePath); return config as KoboldConfig; } catch (error) { - this.logManager.logError('Error parsing config file:', error as Error); + getLogManager().logError('Error parsing config file:', error as Error); return null; } } @@ -479,19 +466,19 @@ export class KoboldCppManager { configData: KoboldConfig ): Promise { try { - const installDir = this.configManager.getInstallDir(); + const installDir = getConfigManager().getInstallDir(); const configPath = join(installDir, configFileName); await writeJsonFile(configPath, configData); return true; } catch (error) { - this.logManager.logError('Error saving config file:', error as Error); + getLogManager().logError('Error saving config file:', error as Error); return false; } } async selectModelFile(title = 'Select Model File'): Promise { try { - const mainWindow = this.windowManager.getMainWindow(); + const mainWindow = getWindowManager().getMainWindow(); if (!mainWindow) { return null; } @@ -514,13 +501,13 @@ export class KoboldCppManager { return result.filePaths[0]; } catch (error) { - this.logManager.logError('Error selecting model file:', error as Error); + getLogManager().logError('Error selecting model file:', error as Error); return null; } } async getCurrentVersion(): Promise { - const currentBinaryPath = this.configManager.getCurrentKoboldBinary(); + const currentBinaryPath = getConfigManager().getCurrentKoboldBinary(); const versions = await this.getInstalledVersions(); if (currentBinaryPath && (await pathExists(currentBinaryPath))) { @@ -532,12 +519,12 @@ export class KoboldCppManager { const firstVersion = versions[0]; if (firstVersion) { - await this.configManager.setCurrentKoboldBinary(firstVersion.path); + await getConfigManager().setCurrentKoboldBinary(firstVersion.path); return firstVersion; } if (currentBinaryPath) { - await this.configManager.setCurrentKoboldBinary(''); + await getConfigManager().setCurrentKoboldBinary(''); } return null; @@ -565,9 +552,9 @@ export class KoboldCppManager { async setCurrentVersion(binaryPath: string): Promise { if (await pathExists(binaryPath)) { - await this.configManager.setCurrentKoboldBinary(binaryPath); + await getConfigManager().setCurrentKoboldBinary(binaryPath); - this.windowManager.sendToRenderer('versions-updated'); + getWindowManager().sendToRenderer('versions-updated'); return true; } @@ -626,14 +613,14 @@ export class KoboldCppManager { const result = await dialog.showOpenDialog({ properties: ['openDirectory', 'createDirectory'], title: `Select the ${PRODUCT_NAME} Installation Directory`, - defaultPath: this.configManager.getInstallDir(), + defaultPath: getConfigManager().getInstallDir(), buttonLabel: 'Select Directory', }); if (!result.canceled && result.filePaths.length > 0) { - await this.configManager.setInstallDir(result.filePaths[0]); + await getConfigManager().setInstallDir(result.filePaths[0]); - this.windowManager.sendToRenderer( + getWindowManager().sendToRenderer( 'install-dir-changed', result.filePaths[0] ); @@ -655,12 +642,12 @@ export class KoboldCppManager { const currentVersion = await this.getCurrentVersion(); if (!currentVersion || !(await pathExists(currentVersion.path))) { - const rawPath = this.configManager.getCurrentKoboldBinary(); + const rawPath = getConfigManager().getCurrentKoboldBinary(); const error = currentVersion ? `Binary file does not exist at path: ${currentVersion.path}` : 'No version configured'; - this.logManager.logError( + getLogManager().logError( `Launch failed: ${error}. Raw config path: "${rawPath}", Current version: ${JSON.stringify(currentVersion)}` ); @@ -698,7 +685,7 @@ export class KoboldCppManager { const commandLine = `$ ${currentVersion.path} ${finalArgs.join(' ')}`; - this.windowManager.sendKoboldOutput(commandLine); + getWindowManager().sendKoboldOutput(commandLine); let readyResolve: | ((value: { success: boolean; pid?: number; error?: string }) => void) @@ -717,7 +704,7 @@ export class KoboldCppManager { child.stdout?.on('data', (data) => { const output = data.toString(); - this.windowManager.sendKoboldOutput(output, true); + getWindowManager().sendKoboldOutput(output, true); if (!isReady && output.includes(SERVER_READY_SIGNALS.KOBOLDCPP)) { isReady = true; @@ -727,7 +714,7 @@ export class KoboldCppManager { child.stderr?.on('data', (data) => { const output = data.toString(); - this.windowManager.sendKoboldOutput(output, true); + getWindowManager().sendKoboldOutput(output, true); if (!isReady && output.includes(SERVER_READY_SIGNALS.KOBOLDCPP)) { isReady = true; @@ -743,7 +730,7 @@ export class KoboldCppManager { : code && (code > 1 || code < 0) ? `\n[ERROR] Process exited with code ${code}` : `\n[INFO] Process exited with code ${code}`; - this.windowManager.sendKoboldOutput(displayMessage); + getWindowManager().sendKoboldOutput(displayMessage); this.koboldProcess = null; if (!isReady) { @@ -756,9 +743,9 @@ export class KoboldCppManager { }); child.on('error', (error) => { - this.logManager.logError(`Process error: ${error.message}`, error); + getLogManager().logError(`Process error: ${error.message}`, error); - this.windowManager.sendKoboldOutput( + getWindowManager().sendKoboldOutput( `\n[ERROR] Process error: ${error.message}\n` ); this.koboldProcess = null; @@ -771,7 +758,7 @@ export class KoboldCppManager { return readyPromise; } catch (error) { const errorMessage = (error as Error).message; - this.logManager.logError( + getLogManager().logError( `Failed to launch: ${errorMessage}`, error as Error ); @@ -783,7 +770,7 @@ export class KoboldCppManager { if (this.koboldProcess) { await terminateProcess(this.koboldProcess, { timeoutMs: 5000, - logError: (message, error) => this.logManager.logError(message, error), + logError: (message, error) => getLogManager().logError(message, error), }); this.koboldProcess = null; } @@ -792,9 +779,18 @@ export class KoboldCppManager { async cleanup(): Promise { if (this.koboldProcess) { await terminateProcess(this.koboldProcess, { - logError: (message, error) => this.logManager.logError(message, error), + logError: (message, error) => getLogManager().logError(message, error), }); this.koboldProcess = null; } } } + +let koboldCppManagerInstance: KoboldCppManager; + +export function getKoboldCppManager(): KoboldCppManager { + if (!koboldCppManagerInstance) { + koboldCppManagerInstance = new KoboldCppManager(); + } + return koboldCppManagerInstance; +} diff --git a/src/main/managers/LogManager.ts b/src/main/managers/LogManager.ts index a4190c5..942858a 100644 --- a/src/main/managers/LogManager.ts +++ b/src/main/managers/LogManager.ts @@ -3,6 +3,8 @@ import { join } from 'path'; import { createLogger, format, type Logger } from 'winston'; import DailyRotateFile from 'winston-daily-rotate-file'; +let logManagerInstance: LogManager | null = null; + export class LogManager { private logger: Logger; @@ -86,3 +88,10 @@ export class LogManager { return join(app.getPath('userData'), 'logs'); } } + +export const getLogManager = (): LogManager => { + if (!logManagerInstance) { + logManagerInstance = new LogManager(); + } + return logManagerInstance; +}; diff --git a/src/main/managers/OpenWebUIManager.ts b/src/main/managers/OpenWebUIManager.ts index 05daf8e..76cfc84 100644 --- a/src/main/managers/OpenWebUIManager.ts +++ b/src/main/managers/OpenWebUIManager.ts @@ -4,18 +4,15 @@ import { join } from 'path'; import { homedir } from 'os'; import { access } from 'fs/promises'; -import { LogManager } from './LogManager'; -import { WindowManager } from './WindowManager'; -import { ConfigManager } from './ConfigManager'; +import { getLogManager } from './LogManager'; +import { getWindowManager } from './WindowManager'; +import { getConfigManager } from './ConfigManager'; import { OPENWEBUI, SERVER_READY_SIGNALS } from '@/constants'; import { terminateProcess } from '@/utils/process'; import { parseKoboldConfig } from '@/utils/kobold'; export class OpenWebUIManager { private openWebUIProcess: ChildProcess | null = null; - private logManager: LogManager; - private windowManager: WindowManager; - private configManager: ConfigManager; private static readonly OPENWEBUI_BASE_ARGS = [ '--python', '3.11', @@ -23,15 +20,7 @@ export class OpenWebUIManager { 'serve', ]; - constructor( - configManager: ConfigManager, - logManager: LogManager, - windowManager: WindowManager - ) { - this.configManager = configManager; - this.logManager = logManager; - this.windowManager = windowManager; - + constructor() { process.on('SIGINT', () => { this.cleanup().catch(() => { void 0; @@ -126,13 +115,13 @@ export class OpenWebUIManager { } private async waitForOpenWebUIToStart(): Promise { - this.windowManager.sendKoboldOutput('Waiting for Open WebUI to start...'); + getWindowManager().sendKoboldOutput('Waiting for Open WebUI to start...'); return new Promise((resolve, reject) => { const checkForOutput = (data: Buffer) => { const output = data.toString(); if (output.includes(SERVER_READY_SIGNALS.OPENWEBUI)) { - this.windowManager.sendKoboldOutput('Open WebUI is now running!'); + getWindowManager().sendKoboldOutput('Open WebUI is now running!'); resolve(); if (this.openWebUIProcess?.stdout) { @@ -167,11 +156,11 @@ export class OpenWebUIManager { await this.stopFrontend(); - this.windowManager.sendKoboldOutput( + getWindowManager().sendKoboldOutput( `Preparing Open WebUI to connect at ${koboldHost}:${koboldPort}...` ); - this.windowManager.sendKoboldOutput( + getWindowManager().sendKoboldOutput( `Starting ${config.name} frontend on port ${config.port}...` ); @@ -183,9 +172,9 @@ export class OpenWebUIManager { config.port.toString(), ]; - this.windowManager.sendKoboldOutput('Starting Open WebUI with uv...'); + getWindowManager().sendKoboldOutput('Starting Open WebUI with uv...'); - const installDir = this.configManager.getInstallDir(); + const installDir = getConfigManager().getInstallDir(); const openWebUIDataDir = join(installDir, 'openwebui-data'); this.openWebUIProcess = await this.createUvProcess(openWebUIArgs, { @@ -199,9 +188,9 @@ export class OpenWebUIManager { this.openWebUIProcess.stdout.on('data', (data: Buffer) => { try { const output = data.toString('utf8'); - this.windowManager.sendKoboldOutput(output, true); + getWindowManager().sendKoboldOutput(output, true); } catch (error) { - this.logManager.logError( + getLogManager().logError( 'Error processing stdout data:', error as Error ); @@ -213,9 +202,9 @@ export class OpenWebUIManager { this.openWebUIProcess.stderr.on('data', (data: Buffer) => { try { const output = data.toString('utf8'); - this.windowManager.sendKoboldOutput(output, true); + getWindowManager().sendKoboldOutput(output, true); } catch (error) { - this.logManager.logError( + getLogManager().logError( 'Error processing stderr data:', error as Error ); @@ -229,14 +218,14 @@ export class OpenWebUIManager { const message = signal ? `Open WebUI terminated with signal ${signal}` : `Open WebUI exited with code ${code}`; - this.windowManager.sendKoboldOutput(message); + getWindowManager().sendKoboldOutput(message); this.openWebUIProcess = null; } ); this.openWebUIProcess.on('error', (error) => { - this.logManager.logError('Open WebUI process error:', error); - this.windowManager.sendKoboldOutput( + getLogManager().logError('Open WebUI process error:', error); + getWindowManager().sendKoboldOutput( `Open WebUI error: ${error.message}` ); this.openWebUIProcess = null; @@ -244,21 +233,21 @@ export class OpenWebUIManager { await this.waitForOpenWebUIToStart(); - this.windowManager.sendKoboldOutput( + getWindowManager().sendKoboldOutput( `Open WebUI is ready and auto-configured!` ); - this.windowManager.sendKoboldOutput( + getWindowManager().sendKoboldOutput( `Access Open WebUI at: http://localhost:${config.port}` ); - this.windowManager.sendKoboldOutput( + getWindowManager().sendKoboldOutput( `Connection: ${koboldUrl}/v1 (auto-configured)` ); } catch (error) { - this.logManager.logError( + getLogManager().logError( 'Failed to start Open WebUI frontend:', error as Error ); - this.windowManager.sendKoboldOutput( + getWindowManager().sendKoboldOutput( `Failed to start Open WebUI: ${(error as Error).message}` ); this.openWebUIProcess = null; @@ -268,16 +257,16 @@ export class OpenWebUIManager { async stopFrontend(): Promise { if (this.openWebUIProcess) { - this.windowManager.sendKoboldOutput('Stopping Open WebUI...'); + getWindowManager().sendKoboldOutput('Stopping Open WebUI...'); try { await terminateProcess(this.openWebUIProcess, { logError: (message, error) => - this.logManager.logError(message, error), + getLogManager().logError(message, error), }); - this.windowManager.sendKoboldOutput('Open WebUI stopped'); + getWindowManager().sendKoboldOutput('Open WebUI stopped'); } catch (error) { - this.logManager.logError('Error stopping Open WebUI:', error as Error); + getLogManager().logError('Error stopping Open WebUI:', error as Error); } this.openWebUIProcess = null; @@ -288,3 +277,12 @@ export class OpenWebUIManager { await this.stopFrontend(); } } + +let openWebUIManagerInstance: OpenWebUIManager; + +export function getOpenWebUIManager(): OpenWebUIManager { + if (!openWebUIManagerInstance) { + openWebUIManagerInstance = new OpenWebUIManager(); + } + return openWebUIManagerInstance; +} diff --git a/src/main/managers/SillyTavernManager.ts b/src/main/managers/SillyTavernManager.ts index 8254353..7dac52d 100644 --- a/src/main/managers/SillyTavernManager.ts +++ b/src/main/managers/SillyTavernManager.ts @@ -5,8 +5,8 @@ import { join } from 'path'; import { access, readdir } from 'fs/promises'; import type { ChildProcess } from 'child_process'; -import { LogManager } from './LogManager'; -import { WindowManager } from './WindowManager'; +import { getLogManager } from './LogManager'; +import { getWindowManager } from './WindowManager'; import { SILLYTAVERN, SERVER_READY_SIGNALS } from '@/constants'; import { terminateProcess } from '@/utils/process'; import { pathExists, readJsonFile, writeJsonFile } from '@/utils/fs'; @@ -15,8 +15,6 @@ import { parseKoboldConfig } from '@/utils/kobold'; export class SillyTavernManager { private sillyTavernProcess: ChildProcess | null = null; private proxyServer: Server | null = null; - private logManager: LogManager; - private windowManager: WindowManager; private static readonly SILLYTAVERN_BASE_ARGS = [ 'sillytavern', '--global', @@ -26,10 +24,7 @@ export class SillyTavernManager { '--disableCsrf', ]; - constructor(logManager: LogManager, windowManager: WindowManager) { - this.logManager = logManager; - this.windowManager = windowManager; - + constructor() { process.on('SIGINT', () => { this.cleanup().catch(() => { void 0; @@ -206,13 +201,13 @@ export class SillyTavernManager { const settingsPath = await this.getSillyTavernSettingsPath(); if (await pathExists(settingsPath)) { - this.windowManager.sendKoboldOutput( + getWindowManager().sendKoboldOutput( `SillyTavern settings found at ${settingsPath}` ); return; } - this.windowManager.sendKoboldOutput( + getWindowManager().sendKoboldOutput( 'SillyTavern settings not found, starting SillyTavern briefly to generate config...' ); @@ -224,7 +219,7 @@ export class SillyTavernManager { let hasResolved = false; initProcess.on('exit', (code: number | null, signal: string | null) => { - this.windowManager.sendKoboldOutput( + getWindowManager().sendKoboldOutput( signal ? `SillyTavern init process terminated with signal ${signal}` : `SillyTavern init process exited with code ${code}` @@ -239,14 +234,14 @@ export class SillyTavernManager { ? 'SillyTavern failed to install due to EBUSY error (resource busy or locked). This is a critical error.' : `SillyTavern initialization failed with exit code ${code}`; - this.logManager.logError( + getLogManager().logError( 'SillyTavern initialization failed:', new Error(errorMsg) ); - this.windowManager.sendKoboldOutput(`CRITICAL ERROR: ${errorMsg}`); + getWindowManager().sendKoboldOutput(`CRITICAL ERROR: ${errorMsg}`); reject(new Error(errorMsg)); } else { - this.windowManager.sendKoboldOutput( + getWindowManager().sendKoboldOutput( 'SillyTavern settings should now be generated' ); resolve(); @@ -257,11 +252,11 @@ export class SillyTavernManager { initProcess.on('error', (error) => { if (!hasResolved) { hasResolved = true; - this.logManager.logError( + getLogManager().logError( 'Failed to initialize SillyTavern settings:', error ); - this.windowManager.sendKoboldOutput( + getWindowManager().sendKoboldOutput( `SillyTavern initialization error: ${error.message}` ); reject(error); @@ -279,10 +274,10 @@ export class SillyTavernManager { await terminateProcess(initProcess, { logError: (message, error) => - this.logManager.logError(message, error), + getLogManager().logError(message, error), }); - this.windowManager.sendKoboldOutput( + getWindowManager().sendKoboldOutput( 'SillyTavern settings should now be generated' ); @@ -295,7 +290,7 @@ export class SillyTavernManager { if (initProcess.stderr) { initProcess.stderr.on('data', (data: Buffer) => { - this.windowManager.sendKoboldOutput(data.toString().trim()); + getWindowManager().sendKoboldOutput(data.toString().trim()); }); } }); @@ -316,12 +311,12 @@ export class SillyTavernManager { await readJsonFile>(configPath); if (existingSettings) { settings = existingSettings; - this.windowManager.sendKoboldOutput( + getWindowManager().sendKoboldOutput( `Loaded existing SillyTavern settings` ); } } catch { - this.windowManager.sendKoboldOutput( + getWindowManager().sendKoboldOutput( `Could not read existing settings, creating new ones` ); } @@ -334,7 +329,7 @@ export class SillyTavernManager { powerUser.auto_connect = true; if (isImageMode) { - this.windowManager.sendKoboldOutput( + getWindowManager().sendKoboldOutput( `Image generation mode detected. Please configure SillyTavern manually:\n` + `1. Open SillyTavern and navigate to Settings (top-right gear icon)\n` + `2. Go to 'Extensions' tab and enable 'Image Generation'\n` + @@ -358,33 +353,33 @@ export class SillyTavernManager { settings.main_api = 'textgenerationwebui'; textgenSettings.type = 'koboldcpp'; - this.windowManager.sendKoboldOutput( + getWindowManager().sendKoboldOutput( `Configured SillyTavern for text generation at ${koboldUrl}` ); await writeJsonFile(configPath, settings); - this.windowManager.sendKoboldOutput( + getWindowManager().sendKoboldOutput( `SillyTavern configuration updated successfully!` ); } catch (error) { - this.logManager.logError( + getLogManager().logError( 'Failed to setup SillyTavern config:', error as Error ); - this.windowManager.sendKoboldOutput( + getWindowManager().sendKoboldOutput( `Failed to configure SillyTavern: ${error instanceof Error ? error.message : String(error)}` ); } } private async waitForSillyTavernToStart(_port: number): Promise { - this.windowManager.sendKoboldOutput('Waiting for SillyTavern to start...'); + getWindowManager().sendKoboldOutput('Waiting for SillyTavern to start...'); return new Promise((resolve, reject) => { const checkForOutput = (data: Buffer) => { if (data.toString().includes(SERVER_READY_SIGNALS.SILLYTAVERN)) { - this.windowManager.sendKoboldOutput('SillyTavern is now running!'); + getWindowManager().sendKoboldOutput('SillyTavern is now running!'); resolve(); if (this.sillyTavernProcess?.stdout) { @@ -422,7 +417,7 @@ export class SillyTavernManager { }); proxyReq.on('error', (err) => { - this.logManager.logError('Proxy request error:', err); + getLogManager().logError('Proxy request error:', err); res.writeHead(500); res.end('Proxy error'); }); @@ -431,13 +426,13 @@ export class SillyTavernManager { }); this.proxyServer.listen(proxyPort, () => { - this.windowManager.sendKoboldOutput( + getWindowManager().sendKoboldOutput( `Proxy server started on port ${proxyPort}, forwarding to SillyTavern on port ${targetPort}` ); }); this.proxyServer.on('error', (err) => { - this.logManager.logError('Proxy server error:', err); + getLogManager().logError('Proxy server error:', err); }); } @@ -456,14 +451,14 @@ export class SillyTavernManager { await this.stopFrontend(); - this.windowManager.sendKoboldOutput( + getWindowManager().sendKoboldOutput( `Preparing SillyTavern to connect at ${koboldHost}:${koboldPort}...` ); await this.ensureSillyTavernSettings(); await this.setupSillyTavernConfig(koboldHost, koboldPort, isImageMode); - this.windowManager.sendKoboldOutput( + getWindowManager().sendKoboldOutput( `Starting ${config.name} frontend on port ${config.port}...` ); @@ -473,7 +468,7 @@ export class SillyTavernManager { config.port.toString(), ]; - this.windowManager.sendKoboldOutput( + getWindowManager().sendKoboldOutput( 'Final port check before starting SillyTavern...' ); @@ -481,13 +476,13 @@ export class SillyTavernManager { if (this.sillyTavernProcess.stdout) { this.sillyTavernProcess.stdout.on('data', (data: Buffer) => { - this.windowManager.sendKoboldOutput(data.toString(), true); + getWindowManager().sendKoboldOutput(data.toString(), true); }); } if (this.sillyTavernProcess.stderr) { this.sillyTavernProcess.stderr.on('data', (data: Buffer) => { - this.windowManager.sendKoboldOutput(data.toString(), true); + getWindowManager().sendKoboldOutput(data.toString(), true); }); } @@ -497,14 +492,14 @@ export class SillyTavernManager { const message = signal ? `SillyTavern terminated with signal ${signal}` : `SillyTavern exited with code ${code}`; - this.windowManager.sendKoboldOutput(message); + getWindowManager().sendKoboldOutput(message); this.sillyTavernProcess = null; } ); this.sillyTavernProcess.on('error', (error) => { - this.logManager.logError('SillyTavern process error:', error); - this.windowManager.sendKoboldOutput( + getLogManager().logError('SillyTavern process error:', error); + getWindowManager().sendKoboldOutput( `SillyTavern error: ${error.message}` ); @@ -514,7 +509,7 @@ export class SillyTavernManager { await this.waitForSillyTavernToStart(config.port); this.createProxyServer(config.port, config.proxyPort); } catch (error) { - this.logManager.logError( + getLogManager().logError( `Failed to start SillyTavern: ${error instanceof Error ? error.message : String(error)}`, error as Error ); @@ -529,7 +524,7 @@ export class SillyTavernManager { promises.push( terminateProcess(this.sillyTavernProcess, { logError: (message, error) => - this.logManager.logError(message, error), + getLogManager().logError(message, error), }).then(() => { this.sillyTavernProcess = null; }) @@ -555,7 +550,7 @@ export class SillyTavernManager { try { await this.stopFrontend(); } catch (error) { - this.logManager.logError( + getLogManager().logError( 'Error during SillyTavernManager cleanup:', error as Error ); @@ -563,3 +558,12 @@ export class SillyTavernManager { } } } + +let sillyTavernManagerInstance: SillyTavernManager; + +export function getSillyTavernManager(): SillyTavernManager { + if (!sillyTavernManagerInstance) { + sillyTavernManagerInstance = new SillyTavernManager(); + } + return sillyTavernManagerInstance; +} diff --git a/src/main/managers/WindowManager.ts b/src/main/managers/WindowManager.ts index 28c4584..821a0ce 100644 --- a/src/main/managers/WindowManager.ts +++ b/src/main/managers/WindowManager.ts @@ -1,17 +1,8 @@ -import { - BrowserWindow, - app, - shell, - nativeImage, - screen, - Menu, - clipboard, -} from 'electron'; +import { BrowserWindow, app, shell, screen, Menu, clipboard } from 'electron'; import { join } from 'path'; import { stripVTControlCharacters } from 'util'; import { PRODUCT_NAME } from '../../constants'; import type { IPCChannel, IPCChannelPayloads } from '@/types/ipc'; -import { getAssetPath } from '@/utils/path'; export class WindowManager { private mainWindow: BrowserWindow | null = null; @@ -20,14 +11,7 @@ export class WindowManager { return process.env.NODE_ENV === 'development' || !app.isPackaged; } - private getIconPath(): string { - return getAssetPath('icon.png'); - } - createMainWindow(): BrowserWindow { - const iconPath = this.getIconPath(); - const iconImage = nativeImage.createFromPath(iconPath); - const { workAreaSize } = screen.getPrimaryDisplay(); const windowHeight = Math.floor(workAreaSize.height * 0.86); @@ -35,7 +19,6 @@ export class WindowManager { width: 1000, height: windowHeight, frame: false, - icon: iconImage, title: PRODUCT_NAME, show: false, backgroundColor: '#ffffff', @@ -49,10 +32,6 @@ export class WindowManager { }, }); - if (process.platform === 'linux') { - this.mainWindow.setIcon(iconImage); - } - this.mainWindow.once('ready-to-show', () => { this.mainWindow?.show(); }); @@ -223,3 +202,12 @@ export class WindowManager { } } } + +let windowManagerInstance: WindowManager; + +export function getWindowManager(): WindowManager { + if (!windowManagerInstance) { + windowManagerInstance = new WindowManager(); + } + return windowManagerInstance; +}