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;
+}