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",
"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",

View file

@ -132,12 +132,10 @@ export const AppearanceTab = () => {
<Text fw={500} mb="sm">
Zoom Level
</Text>
<Text size="sm" c="dimmed" mb="md">
<Group justify="space-between" align="center" mb="md">
<Text size="sm" c="dimmed">
Adjust the zoom level of the application interface
</Text>
<Group gap="md" align="flex-end">
<div style={{ flex: 1 }}>
<Group justify="space-between" align="center" mb="xs">
<TextInput
value={zoomPercentage}
onChange={(event) =>
@ -147,9 +145,7 @@ export const AppearanceTab = () => {
const value = event.currentTarget.value;
const numValue = Number(value);
if (isNaN(numValue) || !isValidZoomPercentage(numValue)) {
setZoomPercentage(
zoomLevelToPercentage(zoomLevel).toString()
);
setZoomPercentage(zoomLevelToPercentage(zoomLevel).toString());
}
}}
rightSection={
@ -166,6 +162,8 @@ export const AppearanceTab = () => {
}}
/>
</Group>
<Group gap="md" align="flex-end">
<div style={{ flex: 1 }}>
<Slider
value={zoomLevel}
onChange={handleZoomChange}

View file

@ -239,7 +239,7 @@ export class KoboldCppManager {
): Promise<void> {
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) => {

View file

@ -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<boolean> {
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<boolean>((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<string, string>
): Promise<ChildProcess> {
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,15 +118,25 @@ export class OpenWebUIManager {
return new Promise((resolve, reject) => {
const checkForOutput = (data: Buffer) => {
const output = data.toString();
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);
this.openWebUIProcess.stdout.removeListener(
'data',
checkForOutput
);
}
}
} catch (error) {
this.logManager.logError(
'Error checking OpenWebUI output:',
error as Error
);
}
};
if (this.openWebUIProcess?.stdout) {
@ -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
);
}
});
}

View file

@ -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<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> {
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<boolean>((resolve) => {
const timeout = setTimeout(() => {
@ -198,11 +115,10 @@ export class SillyTavernManager {
}
private async createNpxProcess(args: string[]): Promise<ChildProcess> {
const env = await this.getNodeEnvironment();
return spawn('npx', args, {
stdio: ['pipe', 'pipe', 'pipe'],
detached: false,
env,
shell: true,
});
}

View file

@ -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 (