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 { app } from 'electron';
import { WindowManager } from '@/main/managers/WindowManager'; import { getWindowManager } from '@/main/managers/WindowManager';
import { ConfigManager } from '@/main/managers/ConfigManager'; import { getConfigManager } from '@/main/managers/ConfigManager';
import { LogManager } from '@/main/managers/LogManager'; import { getLogManager } from '@/main/managers/LogManager';
import { KoboldCppManager } from '@/main/managers/KoboldCppManager'; import { getKoboldCppManager } from '@/main/managers/KoboldCppManager';
import { SillyTavernManager } from '@/main/managers/SillyTavernManager'; import { getSillyTavernManager } from '@/main/managers/SillyTavernManager';
import { OpenWebUIManager } from '@/main/managers/OpenWebUIManager'; import { getOpenWebUIManager } from '@/main/managers/OpenWebUIManager';
import { HardwareManager } from '@/main/managers/HardwareManager'; import { getHardwareManager } from '@/main/managers/HardwareManager';
import { BinaryManager } from '@/main/managers/BinaryManager'; import { getBinaryManager } from '@/main/managers/BinaryManager';
import { IPCHandlers } from '@/main/ipc'; import { IPCHandlers } from '@/main/ipc';
import { ensureDir } from '@/utils/fs'; import { ensureDir } from '@/utils/fs';
import { getConfigDir } from '@/utils/path'; import { getConfigDir } from '@/utils/path';
export class GerbilApp { 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; private ipcHandlers: IPCHandlers;
constructor() { 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.ipcHandlers = new IPCHandlers(
this.koboldManager, getKoboldCppManager(),
this.configManager, getConfigManager(getConfigDir()),
this.hardwareManager, getHardwareManager(),
this.binaryManager, getBinaryManager(),
this.logManager, getLogManager(),
this.sillyTavernManager, getSillyTavernManager(),
this.openWebUIManager, getOpenWebUIManager(),
this.windowManager getWindowManager()
); );
} }
private async ensureInstallDirectory(): Promise<void> { private async ensureInstallDirectory(): Promise<void> {
const installDir = this.configManager.getInstallDir(); const installDir = getConfigManager().getInstallDir();
await ensureDir(installDir); await ensureDir(installDir);
} }
async initialize(): Promise<void> { async initialize(): Promise<void> {
await app.whenReady(); await app.whenReady();
await this.configManager.initialize(); await getConfigManager().initialize();
await this.ensureInstallDirectory(); await this.ensureInstallDirectory();
this.windowManager.createMainWindow(); getWindowManager().createMainWindow();
this.ipcHandlers.setupHandlers(); this.ipcHandlers.setupHandlers();
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
@ -92,9 +54,9 @@ export class GerbilApp {
try { try {
const cleanupPromises = [ const cleanupPromises = [
this.koboldManager.cleanup(), getKoboldCppManager().cleanup(),
this.sillyTavernManager.cleanup(), getSillyTavernManager().cleanup(),
this.openWebUIManager.cleanup(), getOpenWebUIManager().cleanup(),
]; ];
const timeoutPromise = new Promise<void>((resolve) => { const timeoutPromise = new Promise<void>((resolve) => {
@ -105,10 +67,10 @@ export class GerbilApp {
await Promise.race([Promise.all(cleanupPromises), timeoutPromise]); await Promise.race([Promise.all(cleanupPromises), timeoutPromise]);
} catch (error) { } 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); app.exit(0);
}); });
@ -119,8 +81,8 @@ export class GerbilApp {
}); });
app.on('activate', () => { app.on('activate', () => {
if (!this.windowManager.getMainWindow()) { if (!getWindowManager().getMainWindow()) {
this.windowManager.createMainWindow(); getWindowManager().createMainWindow();
} }
}); });
} }

View file

@ -126,13 +126,16 @@ export class IPCHandlers {
); );
ipcMain.handle('kobold:detectBackendSupport', () => ipcMain.handle('kobold:detectBackendSupport', () =>
this.binaryManager.detectBackendSupport() this.binaryManager.detectBackendSupport(this.koboldManager)
); );
ipcMain.handle( ipcMain.handle(
'kobold:getAvailableBackends', 'kobold:getAvailableBackends',
(_, includeDisabled = false) => (_, includeDisabled = false) =>
this.binaryManager.getAvailableBackends(includeDisabled) this.binaryManager.getAvailableBackends(
this.koboldManager,
includeDisabled
)
); );
ipcMain.handle('kobold:getPlatform', () => process.platform); ipcMain.handle('kobold:getPlatform', () => process.platform);

View file

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