WIP - figuring out more consistent .embd copying for portable binaries, starting to move away from dep injection to pure functional

This commit is contained in:
Egor 2025-09-08 14:24:49 -07:00
parent 804472ac78
commit e56b581109
11 changed files with 767 additions and 269 deletions

524
assets/kcpp_sdui.embd Normal file

File diff suppressed because one or more lines are too long

View file

@ -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<void> {
const installDir = this.configManager.getInstallDir();
const installDir = getConfigManager().getInstallDir();
await ensureDir(installDir);
}
async initialize(): Promise<void> {
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<void>((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();
}
});
}

View file

@ -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);

View file

@ -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<string, BackendSupport>();
private availableBackendsCache = new Map<string, BackendOption[]>();
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<BackendSupport | null> {
async detectBackendSupport(
koboldManager: KoboldCppManager
): Promise<BackendSupport | null> {
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<BackendOption[]> {
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;
}

View file

@ -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<void> {
@ -34,7 +34,7 @@ export class ConfigManager {
const config = await readJsonFile<AppConfig>(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;
};

View file

@ -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<CPUCapabilities> {
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;
};

View file

@ -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<string> {
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<InstalledVersion[]> {
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<boolean> {
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<string | null> {
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<InstalledVersion | null> {
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<boolean> {
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<void> {
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;
}

View file

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

View file

@ -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<void> {
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<void> {
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;
}

View file

@ -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<Record<string, unknown>>(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<void> {
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;
}

View file

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