diff --git a/assets/icon.ico b/assets/icon.ico new file mode 100644 index 0000000..395297b Binary files /dev/null and b/assets/icon.ico differ diff --git a/package.json b/package.json index 74ed78e..f48fe3b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "gerbil", "productName": "Gerbil", - "version": "0.9.8", + "version": "0.9.9", "description": "Run Large Language Models locally", "main": "out/main/index.js", "homepage": "./", @@ -125,6 +125,7 @@ }, "win": { "compression": "normal", + "icon": "assets/icon.ico", "target": [ { "target": "nsis", @@ -154,6 +155,7 @@ "linux": { "compression": "store", "category": "Utility", + "icon": "src/assets/icon.png", "desktop": { "entry": { "Name": "Gerbil", diff --git a/src/components/settings/AppearanceTab.tsx b/src/components/settings/AppearanceTab.tsx index 74530c1..97585a5 100644 --- a/src/components/settings/AppearanceTab.tsx +++ b/src/components/settings/AppearanceTab.tsx @@ -132,40 +132,38 @@ export const AppearanceTab = () => { Zoom Level - - Adjust the zoom level of the application interface - + + + Adjust the zoom level of the application interface + + + handleZoomPercentageChange(event.currentTarget.value) + } + onBlur={(event) => { + const value = event.currentTarget.value; + const numValue = Number(value); + if (isNaN(numValue) || !isValidZoomPercentage(numValue)) { + setZoomPercentage(zoomLevelToPercentage(zoomLevel).toString()); + } + }} + rightSection={ + + % + + } + size="sm" + w={80} + styles={{ + input: { + textAlign: 'center', + }, + }} + /> +
- - - handleZoomPercentageChange(event.currentTarget.value) - } - onBlur={(event) => { - const value = event.currentTarget.value; - const numValue = Number(value); - if (isNaN(numValue) || !isValidZoomPercentage(numValue)) { - setZoomPercentage( - zoomLevelToPercentage(zoomLevel).toString() - ); - } - }} - rightSection={ - - % - - } - size="sm" - w={80} - styles={{ - input: { - textAlign: 'center', - }, - }} - /> - { try { await execa(packedPath, ['--unpack', unpackDir], { - timeout: 30000, + timeout: 60000, stdio: ['ignore', 'pipe', 'pipe'], }); } catch (error) { @@ -501,7 +501,7 @@ export class KoboldCppManager { } const result = await execa(launcherPath, ['--version'], { - timeout: 10000, + timeout: 30000, stdio: ['ignore', 'pipe', 'pipe'], }); @@ -674,14 +674,6 @@ export class KoboldCppManager { }>((resolve, reject) => { readyResolve = resolve; _readyReject = reject; - - setTimeout(() => { - if (!isReady) { - reject( - new Error('Timeout waiting for KoboldCpp ready signal (30s)') - ); - } - }, 30000); }); child.stdout?.on('data', (data) => { diff --git a/src/main/managers/OpenWebUIManager.ts b/src/main/managers/OpenWebUIManager.ts index 056f632..2d28812 100644 --- a/src/main/managers/OpenWebUIManager.ts +++ b/src/main/managers/OpenWebUIManager.ts @@ -1,8 +1,6 @@ import { spawn } from 'child_process'; import type { ChildProcess } from 'child_process'; import { join } from 'path'; -import { homedir } from 'os'; -import { access } from 'fs/promises'; import { LogManager } from './LogManager'; import { WindowManager } from './WindowManager'; @@ -55,16 +53,11 @@ export class OpenWebUIManager { > { const env = { ...process.env }; - const cargoPath = join(homedir(), '.cargo', 'bin'); - - try { - await access(cargoPath); - env.PATH = - process.platform === 'win32' - ? `${cargoPath};${env.PATH}` - : `${cargoPath}:${env.PATH}`; - } catch { - return env; + if (process.platform === 'win32') { + env.PYTHONIOENCODING = 'utf-8'; + env.PYTHONLEGACYWINDOWSSTDIO = '1'; + env.PYTHONUTF8 = '1'; + env.CHCP = '65001'; } return env; @@ -73,13 +66,17 @@ export class OpenWebUIManager { async isUvAvailable(): Promise { try { const env = await this.getUvEnvironment(); - const testProcess = spawn('uv', ['--version'], { stdio: 'pipe', env }); + const testProcess = spawn('uv', ['--version'], { + stdio: 'pipe', + env, + shell: true, + }); return new Promise((resolve) => { const timeout = setTimeout(() => { testProcess.kill(); resolve(false); - }, 5000); + }, 10000); testProcess.on('exit', (code) => { clearTimeout(timeout); @@ -101,10 +98,18 @@ export class OpenWebUIManager { env?: Record ): Promise { const uvEnv = await this.getUvEnvironment(); + const mergedEnv = { ...uvEnv, ...env }; + + if (process.platform === 'win32') { + mergedEnv.PYTHONIOENCODING = 'utf-8'; + mergedEnv.PYTHONUTF8 = '1'; + } + return spawn('uvx', args, { stdio: ['pipe', 'pipe', 'pipe'], detached: false, - env: { ...uvEnv, ...env }, + env: mergedEnv, + shell: true, }); } @@ -113,14 +118,24 @@ export class OpenWebUIManager { 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!'); - resolve(); + try { + const output = data.toString('utf8'); + if (output.includes(SERVER_READY_SIGNALS.OPENWEBUI)) { + this.windowManager.sendKoboldOutput('Open WebUI is now running!'); + resolve(); - if (this.openWebUIProcess?.stdout) { - this.openWebUIProcess.stdout.removeListener('data', checkForOutput); + if (this.openWebUIProcess?.stdout) { + this.openWebUIProcess.stdout.removeListener( + 'data', + checkForOutput + ); + } } + } catch (error) { + this.logManager.logError( + 'Error checking OpenWebUI output:', + error as Error + ); } }; @@ -182,13 +197,29 @@ export class OpenWebUIManager { if (this.openWebUIProcess.stdout) { this.openWebUIProcess.stdout.on('data', (data: Buffer) => { - this.windowManager.sendKoboldOutput(data.toString(), true); + try { + const output = data.toString('utf8'); + this.windowManager.sendKoboldOutput(output, true); + } catch (error) { + this.logManager.logError( + 'Error processing stdout data:', + error as Error + ); + } }); } if (this.openWebUIProcess.stderr) { this.openWebUIProcess.stderr.on('data', (data: Buffer) => { - this.windowManager.sendKoboldOutput(data.toString(), true); + try { + const output = data.toString('utf8'); + this.windowManager.sendKoboldOutput(output, true); + } catch (error) { + this.logManager.logError( + 'Error processing stderr data:', + error as Error + ); + } }); } diff --git a/src/main/managers/SillyTavernManager.ts b/src/main/managers/SillyTavernManager.ts index 39b2bf8..c9b2e63 100644 --- a/src/main/managers/SillyTavernManager.ts +++ b/src/main/managers/SillyTavernManager.ts @@ -2,7 +2,6 @@ import { spawn } from 'child_process'; import { createServer, request, type Server } from 'http'; import { homedir } from 'os'; import { join } from 'path'; -import { access, readdir } from 'fs/promises'; import type { ChildProcess } from 'child_process'; import { LogManager } from './LogManager'; @@ -87,94 +86,12 @@ export class SillyTavernManager { return join(dataRoot, 'default-user', 'settings.json'); } - private async tryAddPathToEnv( - env: Record, - path: string - ): Promise { - const pathSeparator = process.platform === 'win32' ? ';' : ':'; - if (!env.PATH?.includes(path)) { - env.PATH = `${path}${pathSeparator}${env.PATH}`; - return true; - } - return false; - } - - private async tryVersionManagerPath( - basePath: string, - env: Record - ): Promise { - try { - await access(basePath); - const versions = await readdir(basePath); - if (versions.length > 0) { - const latestVersion = versions.sort().pop(); - if (latestVersion) { - const binSubPath = basePath.includes('fnm') - ? join('installation', 'bin') - : 'bin'; - const nodeBinPath = join(basePath, latestVersion, binSubPath); - - try { - await access(nodeBinPath); - return this.tryAddPathToEnv(env, nodeBinPath); - } catch { - return false; - } - } - } - } catch { - return false; - } - return false; - } - - private async getNodeEnvironment(): Promise< - Record - > { - const env = { ...process.env }; - - const versionManagerPaths = [ - join(homedir(), '.local', 'share', 'fnm', 'node-versions'), - join(homedir(), '.nvm', 'versions', 'node'), - join(homedir(), '.volta', 'tools', 'image', 'node'), - join(homedir(), '.asdf', 'installs', 'nodejs'), - ]; - - const systemPaths: string[] = []; - if (process.platform === 'darwin') { - systemPaths.push('/opt/homebrew/bin', '/usr/local/bin'); - } - if (process.platform === 'win32') { - versionManagerPaths.push( - join(homedir(), 'AppData', 'Local', 'fnm', 'node-versions'), - join(homedir(), 'AppData', 'Roaming', 'nvm') - ); - } - - for (const systemPath of systemPaths) { - try { - await access(systemPath); - if (await this.tryAddPathToEnv(env, systemPath)) { - return env; - } - } catch { - continue; - } - } - - for (const versionPath of versionManagerPaths) { - if (await this.tryVersionManagerPath(versionPath, env)) { - return env; - } - } - - return env; - } - async isNpxAvailable(): Promise { try { - const env = await this.getNodeEnvironment(); - const testProcess = spawn('npx', ['--version'], { stdio: 'pipe', env }); + const testProcess = spawn('npx', ['--version'], { + stdio: 'pipe', + shell: true, + }); return new Promise((resolve) => { const timeout = setTimeout(() => { @@ -198,11 +115,10 @@ export class SillyTavernManager { } private async createNpxProcess(args: string[]): Promise { - const env = await this.getNodeEnvironment(); return spawn('npx', args, { stdio: ['pipe', 'pipe', 'pipe'], detached: false, - env, + shell: true, }); } diff --git a/src/main/managers/WindowManager.ts b/src/main/managers/WindowManager.ts index 7e82857..ca772c5 100644 --- a/src/main/managers/WindowManager.ts +++ b/src/main/managers/WindowManager.ts @@ -72,6 +72,10 @@ export class WindowManager { this.mainWindow = null; }); + if (!this.isDevelopment()) { + Menu.setApplicationMenu(null); + } + this.mainWindow.webContents.on('will-navigate', (event, navigationUrl) => { const url = new URL(navigationUrl); if (