upgrade to latest major electron v39 release, new setting to allow running the built-in image gen UI while having a different frontend preference

This commit is contained in:
Egor 2025-10-28 19:12:31 -07:00
parent c2400b9a58
commit 43de016261
14 changed files with 217 additions and 64 deletions

View file

@ -1,7 +1,7 @@
{
"name": "gerbil",
"productName": "Gerbil",
"version": "1.7.6",
"version": "1.7.7",
"description": "Run Large Language Models locally",
"main": "out/main/index.js",
"homepage": "./",
@ -39,7 +39,7 @@
"license": "AGPL-3.0-or-later",
"devDependencies": {
"@eslint/js": "^9.38.0",
"@types/node": "^24.9.1",
"@types/node": "^24.9.2",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@types/yauzl": "^2.10.3",
@ -47,7 +47,7 @@
"@typescript-eslint/parser": "^8.46.2",
"@vitejs/plugin-react": "^5.1.0",
"cross-env": "^10.1.0",
"electron": "^38.4.0",
"electron": "^39.0.0",
"electron-builder": "^26.0.12",
"electron-vite": "^4.0.1",
"eslint": "^9.38.0",

View file

@ -32,19 +32,17 @@ export const TitleBar = ({
onEject,
onTabChange,
}: TitleBarProps) => {
const { resolvedColorScheme: colorScheme, frontendPreference } =
usePreferencesStore();
const {
resolvedColorScheme: colorScheme,
frontendPreference,
imageGenerationFrontendPreference,
} = usePreferencesStore();
const { handleLogoClick, getLogoStyles } = useLogoClickSounds();
const [isMaximized, setIsMaximized] = useState(false);
const [isSelectOpen, setIsSelectOpen] = useState(false);
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
const { isTextMode, isImageGenerationMode } = useLaunchConfigStore();
const interfaceOptions = getAvailableInterfaceOptions({
frontendPreference,
isTextMode,
isImageGenerationMode,
});
const handleTabChange = (value: string | null) => {
if (value === 'eject') {
@ -133,7 +131,12 @@ export const TitleBar = ({
onChange={handleTabChange}
onDropdownOpen={() => setIsSelectOpen(true)}
onDropdownClose={() => setIsSelectOpen(false)}
data={interfaceOptions}
data={getAvailableInterfaceOptions({
frontendPreference,
imageGenerationFrontendPreference,
isTextMode,
isImageGenerationMode,
})}
renderOption={renderOption}
variant="unstyled"
style={{ textAlign: 'center', minWidth: '7.5rem' }}

View file

@ -17,7 +17,8 @@ export const ServerTab = ({
activeTab,
}: ServerTabProps) => {
const { isImageGenerationMode } = useLaunchConfigStore();
const { frontendPreference } = usePreferencesStore();
const { frontendPreference, imageGenerationFrontendPreference } =
usePreferencesStore();
const effectiveImageMode =
activeTab === 'chat-image'
@ -30,10 +31,16 @@ export const ServerTab = ({
() =>
getServerInterfaceInfo({
frontendPreference,
imageGenerationFrontendPreference,
isImageGenerationMode: effectiveImageMode,
serverUrl: serverUrl || '',
}),
[frontendPreference, effectiveImageMode, serverUrl]
[
frontendPreference,
imageGenerationFrontendPreference,
effectiveImageMode,
serverUrl,
]
);
if (!isServerReady || !serverUrl) {

View file

@ -26,9 +26,13 @@ export interface TerminalTabRef {
export const TerminalTab = forwardRef<TerminalTabRef, TerminalTabProps>(
({ onServerReady }, ref) => {
const { host, port, isImageGenerationMode } = useLaunchConfigStore();
const { frontendPreference, resolvedColorScheme: colorScheme } =
usePreferencesStore();
const { host, port, isImageGenerationMode, isTextMode } =
useLaunchConfigStore();
const {
frontendPreference,
imageGenerationFrontendPreference,
resolvedColorScheme: colorScheme,
} = usePreferencesStore();
const [terminalContent, setTerminalContent] = useState('');
const [isUserScrolling, setIsUserScrolling] = useState(false);
const [shouldAutoScroll, setShouldAutoScroll] = useState(true);
@ -79,16 +83,19 @@ export const TerminalTab = forwardRef<TerminalTabRef, TerminalTabProps>(
const serverHost = host || 'localhost';
const serverPort = port || 5001;
const effectiveFrontend = isTextMode
? frontendPreference
: imageGenerationFrontendPreference === 'builtin'
? 'koboldcpp'
: frontendPreference;
let signalToCheck: string = SERVER_READY_SIGNALS.KOBOLDCPP;
if (frontendPreference === 'sillytavern') {
if (effectiveFrontend === 'sillytavern') {
signalToCheck = SERVER_READY_SIGNALS.SILLYTAVERN;
} else if (frontendPreference === 'openwebui') {
} else if (effectiveFrontend === 'openwebui') {
signalToCheck = SERVER_READY_SIGNALS.OPENWEBUI;
} else if (
frontendPreference === 'comfyui' &&
isImageGenerationMode
) {
} else if (effectiveFrontend === 'comfyui') {
signalToCheck = SERVER_READY_SIGNALS.COMFYUI;
}
@ -106,7 +113,15 @@ export const TerminalTab = forwardRef<TerminalTabRef, TerminalTabProps>(
);
return cleanup;
}, [onServerReady, host, port, frontendPreference, isImageGenerationMode]);
}, [
onServerReady,
host,
port,
frontendPreference,
imageGenerationFrontendPreference,
isImageGenerationMode,
isTextMode,
]);
const scrollToBottom = () => {
if (viewportRef.current) {

View file

@ -23,16 +23,23 @@ export const InterfaceScreen = ({
const terminalTabRef = useRef<TerminalTabRef>(null);
const { isTextMode, isImageGenerationMode } = useLaunchConfigStore();
const { frontendPreference } = usePreferencesStore();
const { frontendPreference, imageGenerationFrontendPreference } =
usePreferencesStore();
const defaultInterfaceTab = useMemo(
() =>
getDefaultInterfaceTab({
frontendPreference,
imageGenerationFrontendPreference,
isTextMode,
isImageGenerationMode,
}),
[frontendPreference, isTextMode, isImageGenerationMode]
[
frontendPreference,
imageGenerationFrontendPreference,
isTextMode,
isImageGenerationMode,
]
);
const handleServerReady = useCallback(

View file

@ -1,8 +1,11 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import { Text, Box, Anchor, rem } from '@mantine/core';
import { Monitor } from 'lucide-react';
import { Monitor, Image } from 'lucide-react';
import { usePreferencesStore } from '@/stores/preferences';
import type { FrontendPreference } from '@/types';
import type {
FrontendPreference,
ImageGenerationFrontendPreference,
} from '@/types';
import { FRONTENDS } from '@/constants';
import { Select } from '@/components/Select';
@ -27,7 +30,12 @@ interface FrontendInterfaceSelectorProps {
export const FrontendInterfaceSelector = ({
isOnInterfaceScreen = false,
}: FrontendInterfaceSelectorProps) => {
const { frontendPreference, setFrontendPreference } = usePreferencesStore();
const {
frontendPreference,
setFrontendPreference,
imageGenerationFrontendPreference,
setImageGenerationFrontendPreference,
} = usePreferencesStore();
const [frontendRequirements, setFrontendRequirements] = useState<
Map<string, boolean>
@ -132,6 +140,12 @@ export const FrontendInterfaceSelector = ({
setFrontendPreference(value as FrontendPreference);
};
const handleImageGenerationFrontendChange = (value: string | null) => {
setImageGenerationFrontendPreference(
value as ImageGenerationFrontendPreference
);
};
const renderDisabledFrontendWarnings = () => {
const disabledFrontends = frontendConfigs.filter(
(config) => !isFrontendAvailable(config.value)
@ -266,6 +280,25 @@ export const FrontendInterfaceSelector = ({
/>
{renderDisabledFrontendWarnings()}
<Text fw={500} mb="sm" mt="xl">
Image Generation Frontend
</Text>
<Text size="sm" c="dimmed" mb="md">
Choose which frontend to use for image generation specifically
</Text>
<Select
value={imageGenerationFrontendPreference}
onChange={handleImageGenerationFrontendChange}
disabled={isOnInterfaceScreen}
data={[
{ value: 'match', label: 'Match Frontend' },
{ value: 'builtin', label: 'Built-in' },
]}
leftSection={<Image style={{ width: rem(16), height: rem(16) }} />}
/>
</>
);
};

View file

@ -28,7 +28,7 @@ const checkModelWarnings = (
if (showModelPriorityWarning) {
warnings.push({
type: 'warning',
type: 'info',
message:
'Both text and image generation models are selected. This may load both models into VRAM simultaneously, which requires significant memory (typically 16GB+ VRAM recommended). Ensure your system has sufficient VRAM to avoid crashes or poor performance.',
});

View file

@ -6,7 +6,11 @@ import { join } from 'path';
import { platform } from 'process';
import { nativeTheme } from 'electron';
import { PRODUCT_NAME } from '@/constants';
import type { FrontendPreference, DismissedUpdate } from '@/types';
import type {
FrontendPreference,
DismissedUpdate,
ImageGenerationFrontendPreference,
} from '@/types';
import type { MantineColorScheme } from '@mantine/core';
import type { SavedNotepadState } from '@/types/electron';
@ -23,6 +27,7 @@ interface AppConfig {
currentKoboldBinary?: string;
selectedConfig?: string;
frontendPreference?: FrontendPreference;
imageGenerationFrontendPreference?: ImageGenerationFrontendPreference;
colorScheme?: MantineColorScheme;
windowBounds?: WindowBounds;
hasSeenWelcome?: boolean;

View file

@ -18,7 +18,10 @@ import { getCurrentKoboldBinary, get as getConfig } from '../config';
import { startFrontend as startSillyTavernFrontend } from '@/main/modules/sillytavern';
import { startFrontend as startOpenWebUIFrontend } from '@/main/modules/openwebui';
import { startFrontend as startComfyUIFrontend } from '@/main/modules/comfyui';
import type { FrontendPreference } from '@/types';
import type {
FrontendPreference,
ImageGenerationFrontendPreference,
} from '@/types';
let koboldProcess: ChildProcess | null = null;
@ -219,9 +222,20 @@ export const launchKoboldCppWithCustomFrontends = async (args: string[] = []) =>
'frontendPreference'
)) as FrontendPreference;
const imageGenerationFrontendPreference = (await getConfig(
'imageGenerationFrontendPreference'
)) as ImageGenerationFrontendPreference | undefined;
const result = await launchKoboldCpp(args, frontendPreference);
const { isImageMode } = parseKoboldConfig(args);
const { isImageMode, isTextMode } = parseKoboldConfig(args);
if (
frontendPreference === 'koboldcpp' ||
(!isTextMode && imageGenerationFrontendPreference === 'builtin')
) {
return result;
}
if (frontendPreference === 'sillytavern') {
startSillyTavernFrontend(args);

View file

@ -1,16 +1,23 @@
import { create } from 'zustand';
import type { MantineColorScheme } from '@mantine/core';
import type { FrontendPreference } from '@/types';
import type {
FrontendPreference,
ImageGenerationFrontendPreference,
} from '@/types';
type ResolvedColorScheme = 'light' | 'dark';
interface PreferencesStore {
frontendPreference: FrontendPreference;
imageGenerationFrontendPreference: ImageGenerationFrontendPreference;
rawColorScheme: MantineColorScheme;
resolvedColorScheme: ResolvedColorScheme;
systemMonitoringEnabled: boolean;
setFrontendPreference: (preference: FrontendPreference) => void;
setImageGenerationFrontendPreference: (
preference: ImageGenerationFrontendPreference
) => void;
setColorScheme: (scheme: MantineColorScheme) => Promise<void>;
setSystemMonitoringEnabled: (enabled: boolean) => void;
loadPreferences: () => Promise<void>;
@ -37,6 +44,7 @@ mediaQuery.addEventListener('change', () => {
export const usePreferencesStore = create<PreferencesStore>((set) => ({
frontendPreference: 'koboldcpp',
imageGenerationFrontendPreference: 'match',
rawColorScheme: 'auto',
resolvedColorScheme: 'light',
systemMonitoringEnabled: true,
@ -46,6 +54,16 @@ export const usePreferencesStore = create<PreferencesStore>((set) => ({
window.electronAPI.config.set('frontendPreference', preference);
},
setImageGenerationFrontendPreference: (
preference: ImageGenerationFrontendPreference
) => {
set({ imageGenerationFrontendPreference: preference });
window.electronAPI.config.set(
'imageGenerationFrontendPreference',
preference
);
},
setSystemMonitoringEnabled: (enabled: boolean) => {
set({ systemMonitoringEnabled: enabled });
window.electronAPI.config.set('systemMonitoringEnabled', enabled);
@ -60,18 +78,23 @@ export const usePreferencesStore = create<PreferencesStore>((set) => ({
},
loadPreferences: async () => {
const [frontendPref, colorScheme, systemMonitoring] = await Promise.all([
window.electronAPI.config.get(
'frontendPreference'
) as Promise<FrontendPreference>,
window.electronAPI.app.getColorScheme(),
window.electronAPI.config.get(
'systemMonitoringEnabled'
) as Promise<boolean>,
]);
const [frontendPref, imageGenFrontendPref, colorScheme, systemMonitoring] =
await Promise.all([
window.electronAPI.config.get(
'frontendPreference'
) as Promise<FrontendPreference>,
window.electronAPI.config.get(
'imageGenerationFrontendPreference'
) as Promise<ImageGenerationFrontendPreference>,
window.electronAPI.app.getColorScheme(),
window.electronAPI.config.get(
'systemMonitoringEnabled'
) as Promise<boolean>,
]);
set({
frontendPreference: frontendPref || 'koboldcpp',
imageGenerationFrontendPreference: imageGenFrontendPref || 'match',
rawColorScheme: colorScheme || 'auto',
resolvedColorScheme: resolveColorScheme(colorScheme || 'auto'),
systemMonitoringEnabled: systemMonitoring ?? true,

View file

@ -18,6 +18,8 @@ export type FrontendPreference =
| 'openwebui'
| 'comfyui';
export type ImageGenerationFrontendPreference = 'match' | 'builtin';
export type Screen = 'welcome' | 'download' | 'launch' | 'interface';
export interface GitHubAsset {

View file

@ -1,4 +1,8 @@
import type { FrontendPreference, InterfaceTab } from '@/types';
import type {
FrontendPreference,
InterfaceTab,
ImageGenerationFrontendPreference,
} from '@/types';
import { FRONTENDS, SILLYTAVERN, OPENWEBUI, COMFYUI } from '@/constants';
export interface InterfaceOption {
@ -8,22 +12,32 @@ export interface InterfaceOption {
export interface InterfaceSelectionParams {
frontendPreference: FrontendPreference;
imageGenerationFrontendPreference?: ImageGenerationFrontendPreference;
isTextMode: boolean;
isImageGenerationMode: boolean;
}
export function getAvailableInterfaceOptions({
frontendPreference,
imageGenerationFrontendPreference = 'match',
isTextMode,
isImageGenerationMode,
}: InterfaceSelectionParams) {
const chatItems: InterfaceOption[] = [];
const effectiveImageFrontend =
imageGenerationFrontendPreference === 'builtin'
? 'koboldcpp'
: frontendPreference;
if (
frontendPreference === 'sillytavern' ||
frontendPreference === 'openwebui'
) {
if (isTextMode || isImageGenerationMode) {
if (
isTextMode ||
(isImageGenerationMode && effectiveImageFrontend === frontendPreference)
) {
const label =
frontendPreference === 'sillytavern'
? FRONTENDS.SILLYTAVERN
@ -34,23 +48,25 @@ export function getAvailableInterfaceOptions({
label,
});
}
} else {
} else if (frontendPreference === 'koboldcpp') {
if (isTextMode) {
chatItems.push({
value: 'chat-text',
label: FRONTENDS.KOBOLDAI_LITE,
});
}
}
if (isImageGenerationMode) {
const imageLabel =
frontendPreference === 'comfyui'
? FRONTENDS.COMFYUI
: FRONTENDS.STABLE_UI;
if (isImageGenerationMode) {
if (effectiveImageFrontend === 'comfyui') {
chatItems.push({
value: 'chat-image',
label: imageLabel,
label: FRONTENDS.COMFYUI,
});
} else if (effectiveImageFrontend === 'koboldcpp') {
chatItems.push({
value: 'chat-image',
label: FRONTENDS.STABLE_UI,
});
}
}
@ -64,17 +80,29 @@ export function getAvailableInterfaceOptions({
export function getDefaultInterfaceTab({
frontendPreference,
imageGenerationFrontendPreference = 'match',
isTextMode,
isImageGenerationMode,
}: InterfaceSelectionParams) {
const effectiveImageFrontend =
imageGenerationFrontendPreference === 'builtin'
? 'koboldcpp'
: frontendPreference;
if (
frontendPreference === 'sillytavern' ||
frontendPreference === 'openwebui'
) {
if (
isImageGenerationMode &&
effectiveImageFrontend !== frontendPreference
) {
return 'chat-image';
}
return 'chat-text';
}
if (frontendPreference === 'comfyui' && isImageGenerationMode) {
if (effectiveImageFrontend === 'comfyui' && isImageGenerationMode) {
return 'chat-image';
}
@ -96,13 +124,25 @@ export interface ServerInterfaceInfo {
export function getServerInterfaceInfo({
frontendPreference,
imageGenerationFrontendPreference = 'match',
isImageGenerationMode,
serverUrl,
}: {
frontendPreference: FrontendPreference;
imageGenerationFrontendPreference?: ImageGenerationFrontendPreference;
isImageGenerationMode: boolean;
serverUrl: string;
}) {
if (
isImageGenerationMode &&
imageGenerationFrontendPreference === 'builtin'
) {
return {
url: `${serverUrl}/sdui`,
title: FRONTENDS.STABLE_UI,
};
}
if (frontendPreference === 'sillytavern') {
return {
url: SILLYTAVERN.PROXY_URL,

View file

@ -2,6 +2,7 @@ export function parseKoboldConfig(args: string[]) {
let host = 'localhost';
let port = 5001;
let hasSdModel = false;
let hasTextModel = false;
for (let i = 0; i < args.length - 1; i++) {
if (args[i] === '--hostname' || args[i] === '--host') {
@ -13,10 +14,13 @@ export function parseKoboldConfig(args: string[]) {
}
} else if (args[i] === '--sdmodel') {
hasSdModel = true;
} else if (args[i] === '--model') {
hasTextModel = true;
}
}
const isImageMode = hasSdModel;
const isTextMode = hasTextModel;
return { host, port, isImageMode };
return { host, port, isImageMode, isTextMode };
}

View file

@ -1394,12 +1394,12 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:*, @types/node@npm:^24.9.1":
version: 24.9.1
resolution: "@types/node@npm:24.9.1"
"@types/node@npm:*, @types/node@npm:^24.9.2":
version: 24.9.2
resolution: "@types/node@npm:24.9.2"
dependencies:
undici-types: "npm:~7.16.0"
checksum: 10c0/c52f8168080ef9a7c3dc23d8ac6061fab5371aad89231a0f6f4c075869bc3de7e89b075b1f3e3171d9e5143d0dda1807c3dab8e32eac6d68f02e7480e7e78576
checksum: 10c0/7905d43f65cee72ef475fe76316e10bbf6ac5d08a7f0f6c38f2b6285d7ca3009e8fcafc8f8a1d2bf3f55889c9c278dbb203a9081fd0cf2d6d62161703924c6fa
languageName: node
linkType: hard
@ -2822,16 +2822,16 @@ __metadata:
languageName: node
linkType: hard
"electron@npm:^38.4.0":
version: 38.4.0
resolution: "electron@npm:38.4.0"
"electron@npm:^39.0.0":
version: 39.0.0
resolution: "electron@npm:39.0.0"
dependencies:
"@electron/get": "npm:^2.0.0"
"@types/node": "npm:^22.7.7"
extract-zip: "npm:^2.0.1"
bin:
electron: cli.js
checksum: 10c0/3458409151d12f1fcd5e95374aa36e0d2f4aa0d3421c9f57dc521c606070294f33b24a681b3f93b49b02f4a3a07eb0070100ebda51b1198efd4b49dbf1260713
checksum: 10c0/7d2272fe6d479edd6544a010008382f3acb21b6206a2585f67df25c43274a3c905affb98dac2e7ea367b51653e8697738c83409f051293545f69d69305d2892e
languageName: node
linkType: hard
@ -3771,7 +3771,7 @@ __metadata:
"@huggingface/gguf": "npm:^0.3.2"
"@mantine/core": "npm:^8.3.5"
"@mantine/hooks": "npm:^8.3.5"
"@types/node": "npm:^24.9.1"
"@types/node": "npm:^24.9.2"
"@types/react": "npm:^19.2.2"
"@types/react-dom": "npm:^19.2.2"
"@types/yauzl": "npm:^2.10.3"
@ -3780,7 +3780,7 @@ __metadata:
"@uiw/react-codemirror": "npm:^4.25.2"
"@vitejs/plugin-react": "npm:^5.1.0"
cross-env: "npm:^10.1.0"
electron: "npm:^38.4.0"
electron: "npm:^39.0.0"
electron-builder: "npm:^26.0.12"
electron-updater: "npm:^6.6.2"
electron-vite: "npm:^4.0.1"