fix linux + windows app icons, don't allow default app menu shortcuts (ex page rrefresh) outside of dev, less timeouts for kcpp in case it needs to download

This commit is contained in:
Egor 2025-09-03 02:19:13 -07:00
parent ce5dc0fce8
commit 9a20f0d1b0
7 changed files with 98 additions and 155 deletions

BIN
assets/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

View file

@ -1,7 +1,7 @@
{ {
"name": "gerbil", "name": "gerbil",
"productName": "Gerbil", "productName": "Gerbil",
"version": "0.9.8", "version": "0.9.9",
"description": "Run Large Language Models locally", "description": "Run Large Language Models locally",
"main": "out/main/index.js", "main": "out/main/index.js",
"homepage": "./", "homepage": "./",
@ -125,6 +125,7 @@
}, },
"win": { "win": {
"compression": "normal", "compression": "normal",
"icon": "assets/icon.ico",
"target": [ "target": [
{ {
"target": "nsis", "target": "nsis",
@ -154,6 +155,7 @@
"linux": { "linux": {
"compression": "store", "compression": "store",
"category": "Utility", "category": "Utility",
"icon": "src/assets/icon.png",
"desktop": { "desktop": {
"entry": { "entry": {
"Name": "Gerbil", "Name": "Gerbil",

View file

@ -132,40 +132,38 @@ export const AppearanceTab = () => {
<Text fw={500} mb="sm"> <Text fw={500} mb="sm">
Zoom Level Zoom Level
</Text> </Text>
<Text size="sm" c="dimmed" mb="md"> <Group justify="space-between" align="center" mb="md">
Adjust the zoom level of the application interface <Text size="sm" c="dimmed">
</Text> Adjust the zoom level of the application interface
</Text>
<TextInput
value={zoomPercentage}
onChange={(event) =>
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={
<Text size="sm" c="dimmed">
%
</Text>
}
size="sm"
w={80}
styles={{
input: {
textAlign: 'center',
},
}}
/>
</Group>
<Group gap="md" align="flex-end"> <Group gap="md" align="flex-end">
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<Group justify="space-between" align="center" mb="xs">
<TextInput
value={zoomPercentage}
onChange={(event) =>
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={
<Text size="sm" c="dimmed">
%
</Text>
}
size="sm"
w={80}
styles={{
input: {
textAlign: 'center',
},
}}
/>
</Group>
<Slider <Slider
value={zoomLevel} value={zoomLevel}
onChange={handleZoomChange} onChange={handleZoomChange}

View file

@ -239,7 +239,7 @@ export class KoboldCppManager {
): Promise<void> { ): Promise<void> {
try { try {
await execa(packedPath, ['--unpack', unpackDir], { await execa(packedPath, ['--unpack', unpackDir], {
timeout: 30000, timeout: 60000,
stdio: ['ignore', 'pipe', 'pipe'], stdio: ['ignore', 'pipe', 'pipe'],
}); });
} catch (error) { } catch (error) {
@ -501,7 +501,7 @@ export class KoboldCppManager {
} }
const result = await execa(launcherPath, ['--version'], { const result = await execa(launcherPath, ['--version'], {
timeout: 10000, timeout: 30000,
stdio: ['ignore', 'pipe', 'pipe'], stdio: ['ignore', 'pipe', 'pipe'],
}); });
@ -674,14 +674,6 @@ export class KoboldCppManager {
}>((resolve, reject) => { }>((resolve, reject) => {
readyResolve = resolve; readyResolve = resolve;
_readyReject = reject; _readyReject = reject;
setTimeout(() => {
if (!isReady) {
reject(
new Error('Timeout waiting for KoboldCpp ready signal (30s)')
);
}
}, 30000);
}); });
child.stdout?.on('data', (data) => { child.stdout?.on('data', (data) => {

View file

@ -1,8 +1,6 @@
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import type { ChildProcess } from 'child_process'; import type { ChildProcess } from 'child_process';
import { join } from 'path'; import { join } from 'path';
import { homedir } from 'os';
import { access } from 'fs/promises';
import { LogManager } from './LogManager'; import { LogManager } from './LogManager';
import { WindowManager } from './WindowManager'; import { WindowManager } from './WindowManager';
@ -55,16 +53,11 @@ export class OpenWebUIManager {
> { > {
const env = { ...process.env }; const env = { ...process.env };
const cargoPath = join(homedir(), '.cargo', 'bin'); if (process.platform === 'win32') {
env.PYTHONIOENCODING = 'utf-8';
try { env.PYTHONLEGACYWINDOWSSTDIO = '1';
await access(cargoPath); env.PYTHONUTF8 = '1';
env.PATH = env.CHCP = '65001';
process.platform === 'win32'
? `${cargoPath};${env.PATH}`
: `${cargoPath}:${env.PATH}`;
} catch {
return env;
} }
return env; return env;
@ -73,13 +66,17 @@ export class OpenWebUIManager {
async isUvAvailable(): Promise<boolean> { async isUvAvailable(): Promise<boolean> {
try { try {
const env = await this.getUvEnvironment(); 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<boolean>((resolve) => { return new Promise<boolean>((resolve) => {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
testProcess.kill(); testProcess.kill();
resolve(false); resolve(false);
}, 5000); }, 10000);
testProcess.on('exit', (code) => { testProcess.on('exit', (code) => {
clearTimeout(timeout); clearTimeout(timeout);
@ -101,10 +98,18 @@ export class OpenWebUIManager {
env?: Record<string, string> env?: Record<string, string>
): Promise<ChildProcess> { ): Promise<ChildProcess> {
const uvEnv = await this.getUvEnvironment(); const uvEnv = await this.getUvEnvironment();
const mergedEnv = { ...uvEnv, ...env };
if (process.platform === 'win32') {
mergedEnv.PYTHONIOENCODING = 'utf-8';
mergedEnv.PYTHONUTF8 = '1';
}
return spawn('uvx', args, { return spawn('uvx', args, {
stdio: ['pipe', 'pipe', 'pipe'], stdio: ['pipe', 'pipe', 'pipe'],
detached: false, detached: false,
env: { ...uvEnv, ...env }, env: mergedEnv,
shell: true,
}); });
} }
@ -113,14 +118,24 @@ export class OpenWebUIManager {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const checkForOutput = (data: Buffer) => { const checkForOutput = (data: Buffer) => {
const output = data.toString(); try {
if (output.includes(SERVER_READY_SIGNALS.OPENWEBUI)) { const output = data.toString('utf8');
this.windowManager.sendKoboldOutput('Open WebUI is now running!'); if (output.includes(SERVER_READY_SIGNALS.OPENWEBUI)) {
resolve(); this.windowManager.sendKoboldOutput('Open WebUI is now running!');
resolve();
if (this.openWebUIProcess?.stdout) { if (this.openWebUIProcess?.stdout) {
this.openWebUIProcess.stdout.removeListener('data', checkForOutput); 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) { if (this.openWebUIProcess.stdout) {
this.openWebUIProcess.stdout.on('data', (data: Buffer) => { 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) { if (this.openWebUIProcess.stderr) {
this.openWebUIProcess.stderr.on('data', (data: Buffer) => { 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
);
}
}); });
} }

View file

@ -2,7 +2,6 @@ import { spawn } from 'child_process';
import { createServer, request, type Server } from 'http'; import { createServer, request, type Server } from 'http';
import { homedir } from 'os'; import { homedir } from 'os';
import { join } from 'path'; import { join } from 'path';
import { access, readdir } from 'fs/promises';
import type { ChildProcess } from 'child_process'; import type { ChildProcess } from 'child_process';
import { LogManager } from './LogManager'; import { LogManager } from './LogManager';
@ -87,94 +86,12 @@ export class SillyTavernManager {
return join(dataRoot, 'default-user', 'settings.json'); return join(dataRoot, 'default-user', 'settings.json');
} }
private async tryAddPathToEnv(
env: Record<string, string | undefined>,
path: string
): Promise<boolean> {
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<string, string | undefined>
): Promise<boolean> {
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<string, string | undefined>
> {
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<boolean> { async isNpxAvailable(): Promise<boolean> {
try { try {
const env = await this.getNodeEnvironment(); const testProcess = spawn('npx', ['--version'], {
const testProcess = spawn('npx', ['--version'], { stdio: 'pipe', env }); stdio: 'pipe',
shell: true,
});
return new Promise<boolean>((resolve) => { return new Promise<boolean>((resolve) => {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
@ -198,11 +115,10 @@ export class SillyTavernManager {
} }
private async createNpxProcess(args: string[]): Promise<ChildProcess> { private async createNpxProcess(args: string[]): Promise<ChildProcess> {
const env = await this.getNodeEnvironment();
return spawn('npx', args, { return spawn('npx', args, {
stdio: ['pipe', 'pipe', 'pipe'], stdio: ['pipe', 'pipe', 'pipe'],
detached: false, detached: false,
env, shell: true,
}); });
} }

View file

@ -72,6 +72,10 @@ export class WindowManager {
this.mainWindow = null; this.mainWindow = null;
}); });
if (!this.isDevelopment()) {
Menu.setApplicationMenu(null);
}
this.mainWindow.webContents.on('will-navigate', (event, navigationUrl) => { this.mainWindow.webContents.on('will-navigate', (event, navigationUrl) => {
const url = new URL(navigationUrl); const url = new URL(navigationUrl);
if ( if (