From 6100a4d0f0400b9fdc784414d3d6b9eeb9a1e5d3 Mon Sep 17 00:00:00 2001 From: Egor Date: Thu, 27 Nov 2025 16:58:50 -0800 Subject: [PATCH] Adding new "Pre-Launch Commands" to the "Advanced" launch tab to allow launching commands before the launch --- package.json | 2 +- src/components/screens/Launch/AdvancedTab.tsx | 57 +++++++- .../Launch/CommandLineArgumentsModal.tsx | 2 +- src/components/screens/Launch/index.tsx | 4 + src/components/settings/AppearanceTab.tsx | 8 +- .../settings/FrontendInterfaceSelector.tsx | 123 +++++++++--------- src/components/settings/GeneralTab.tsx | 4 +- .../settings/TroubleshootingTab.tsx | 4 +- src/hooks/useLaunchConfig.ts | 2 + src/hooks/useLaunchLogic.ts | 10 +- src/main/ipc.ts | 4 +- src/main/modules/koboldcpp/launcher/index.ts | 84 +++++++++++- src/preload/index.ts | 3 +- src/stores/launchConfig.ts | 14 ++ src/types/electron.d.ts | 4 +- 15 files changed, 245 insertions(+), 80 deletions(-) diff --git a/package.json b/package.json index 29fffa3..fbecc34 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "gerbil", "productName": "Gerbil", - "version": "1.12.1", + "version": "1.13.0", "description": "Run Large Language Models locally", "main": "out/main/index.js", "homepage": "./", diff --git a/src/components/screens/Launch/AdvancedTab.tsx b/src/components/screens/Launch/AdvancedTab.tsx index 949346e..ee4d057 100644 --- a/src/components/screens/Launch/AdvancedTab.tsx +++ b/src/components/screens/Launch/AdvancedTab.tsx @@ -6,8 +6,10 @@ import { NumberInput, Button, SimpleGrid, + ActionIcon, } from '@mantine/core'; import { useState, useEffect } from 'react'; +import { Plus, Trash2 } from 'lucide-react'; import { InfoTooltip } from '@/components/InfoTooltip'; import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip'; import { CommandLineArgumentsModal } from '@/components/screens/Launch/CommandLineArgumentsModal'; @@ -16,6 +18,7 @@ import { useLaunchConfig } from '@/hooks/useLaunchConfig'; export const AdvancedTab = () => { const { additionalArguments, + preLaunchCommands, noshift, flashattention, noavx2, @@ -28,6 +31,7 @@ export const AdvancedTab = () => { moecpu, moeexperts, handleAdditionalArgumentsChange, + handlePreLaunchCommandsChange, handleNoshiftChange, handleFlashattentionChange, handleNoavx2Change, @@ -208,7 +212,7 @@ export const AdvancedTab = () => { - Additional arguments + Additional Arguments @@ -229,6 +233,57 @@ export const AdvancedTab = () => { /> +
+ + + Pre-Launch Commands + + + + + {preLaunchCommands.map((command, index) => ( + + { + const newCommands = [...preLaunchCommands]; + newCommands[index] = event.currentTarget.value; + handlePreLaunchCommandsChange(newCommands); + }} + style={{ flex: 1 }} + /> + { + const newCommands = preLaunchCommands.filter( + (_, i) => i !== index + ); + handlePreLaunchCommandsChange( + newCommands.length === 0 ? [''] : newCommands + ); + }} + > + + + + ))} + + +
+ setCommandLineModalOpen(false)} diff --git a/src/components/screens/Launch/CommandLineArgumentsModal.tsx b/src/components/screens/Launch/CommandLineArgumentsModal.tsx index 96951ef..9997e83 100644 --- a/src/components/screens/Launch/CommandLineArgumentsModal.tsx +++ b/src/components/screens/Launch/CommandLineArgumentsModal.tsx @@ -796,7 +796,7 @@ export const CommandLineArgumentsModal = ({ These are additional command line arguments that can be added to the - "Additional Arguments" field. + "Additional Arguments" input field. { contextSize, model, additionalArguments, + preLaunchCommands, port, host, multiuser, @@ -159,6 +160,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => { contextsize: contextSize, model, additionalArguments, + preLaunchCommands: preLaunchCommands.filter((cmd) => cmd.trim() !== ''), port, host, multiuser: multiuser ? 1 : 0, @@ -301,6 +303,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => { usemmap, debugmode, additionalArguments, + preLaunchCommands, sdt5xxl, sdclipl, sdclipg, @@ -338,6 +341,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => { usemmap, debugmode, additionalArguments, + preLaunchCommands, sdt5xxl, sdclipl, sdclipg, diff --git a/src/components/settings/AppearanceTab.tsx b/src/components/settings/AppearanceTab.tsx index 1e56c09..859bfad 100644 --- a/src/components/settings/AppearanceTab.tsx +++ b/src/components/settings/AppearanceTab.tsx @@ -77,11 +77,9 @@ export const AppearanceTab = ({ return ( +
- -
-
- + Theme @@ -138,7 +136,7 @@ export const AppearanceTab = ({ />
- + Zoom Level diff --git a/src/components/settings/FrontendInterfaceSelector.tsx b/src/components/settings/FrontendInterfaceSelector.tsx index 350e4ed..0fa1603 100644 --- a/src/components/settings/FrontendInterfaceSelector.tsx +++ b/src/components/settings/FrontendInterfaceSelector.tsx @@ -223,69 +223,74 @@ export const FrontendInterfaceSelector = ({ return ( <> - - Frontend Interface - +
+ + Frontend Interface + + + {getUnmetRequirements().length === 0 && ( + + Choose which frontend interface to use for interacting with AI + models + + )} + + {getUnmetRequirements().length > 0 && ( + + {getSelectedFrontendConfig()?.label} requires{' '} + {getUnmetRequirements().map((req, index) => ( + + + {req.name} + + {index < getUnmetRequirements().length - 1 ? ', ' : ''} + + ))}{' '} + to be installed on your system + + )} + + ({ - value: config.value, - label: config.label, - disabled: !isFrontendAvailable(config.value), - }))} - leftSection={} - /> - - {renderDisabledFrontendWarnings()} - - - Image Generation Frontend - - - - Choose which frontend to use for image generation specifically - - - } + /> +
); }; diff --git a/src/components/settings/GeneralTab.tsx b/src/components/settings/GeneralTab.tsx index 9938fbc..294558f 100644 --- a/src/components/settings/GeneralTab.tsx +++ b/src/components/settings/GeneralTab.tsx @@ -25,7 +25,7 @@ export const GeneralTab = () => { return (
- + Status Bar @@ -41,7 +41,7 @@ export const GeneralTab = () => {
- + System Tray diff --git a/src/components/settings/TroubleshootingTab.tsx b/src/components/settings/TroubleshootingTab.tsx index 9995018..056f6fc 100644 --- a/src/components/settings/TroubleshootingTab.tsx +++ b/src/components/settings/TroubleshootingTab.tsx @@ -33,7 +33,7 @@ export const TroubleshootingTab = () => { return (
- + Installation Directory @@ -70,7 +70,7 @@ export const TroubleshootingTab = () => {
- + Diagnostics diff --git a/src/hooks/useLaunchConfig.ts b/src/hooks/useLaunchConfig.ts index 1f3b749..25989d4 100644 --- a/src/hooks/useLaunchConfig.ts +++ b/src/hooks/useLaunchConfig.ts @@ -13,6 +13,7 @@ export const useLaunchConfig = () => { contextSize: state.contextSize, model: state.model, additionalArguments: state.additionalArguments, + preLaunchCommands: state.preLaunchCommands, port: state.port, host: state.host, multiuser: state.multiuser, @@ -65,6 +66,7 @@ export const useLaunchConfig = () => { handleQuantmatmulChange: state.setQuantmatmul, handleUsemmapChange: state.setUsemmap, handleDebugmodeChange: state.setDebugmode, + handlePreLaunchCommandsChange: state.setPreLaunchCommands, handleBackendChange: state.setBackend, handleGpuDeviceSelectionChange: state.setGpuDeviceSelection, handleTensorSplitChange: state.setTensorSplit, diff --git a/src/hooks/useLaunchLogic.ts b/src/hooks/useLaunchLogic.ts index a26a06d..bafe504 100644 --- a/src/hooks/useLaunchLogic.ts +++ b/src/hooks/useLaunchLogic.ts @@ -31,6 +31,7 @@ interface LaunchArgs { usemmap: boolean; debugmode: boolean; additionalArguments: string; + preLaunchCommands: string[]; sdt5xxl: string; sdclipl: string; sdclipg: string; @@ -283,7 +284,14 @@ export const useLaunchLogic = ({ args.push(...additionalArgs); } - const result = await window.electronAPI.kobold.launchKoboldCpp(args); + const preLaunchCommands = launchArgs.preLaunchCommands.filter( + (cmd) => cmd.trim() !== '' + ); + + const result = await window.electronAPI.kobold.launchKoboldCpp( + args, + preLaunchCommands + ); if (result.success) { onLaunch(); diff --git a/src/main/ipc.ts b/src/main/ipc.ts index f0baea2..109a1a6 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -135,8 +135,8 @@ export function setupIPCHandlers() { ipcMain.handle('kobold:getPlatform', () => platform); - ipcMain.handle('kobold:launchKoboldCpp', (_, args) => - launchKoboldCppWithCustomFrontends(args) + ipcMain.handle('kobold:launchKoboldCpp', (_, args, preLaunchCommands) => + launchKoboldCppWithCustomFrontends(args, preLaunchCommands) ); ipcMain.handle('kobold:deleteRelease', (_, binaryPath) => diff --git a/src/main/modules/koboldcpp/launcher/index.ts b/src/main/modules/koboldcpp/launcher/index.ts index c5d5864..c8f2e51 100644 --- a/src/main/modules/koboldcpp/launcher/index.ts +++ b/src/main/modules/koboldcpp/launcher/index.ts @@ -1,4 +1,5 @@ import { spawn, ChildProcess } from 'child_process'; +import { platform } from 'process'; import { terminateProcess } from '@/utils/node/process'; import { logError, safeExecute } from '@/utils/node/logging'; @@ -10,6 +11,7 @@ import { getCurrentVersion } from '../version'; import { getCurrentKoboldBinary, get as getConfig, + getInstallDir, } from '@/main/modules/config'; import { startFrontend as startSillyTavernFrontend } from '@/main/modules/sillytavern'; import { startFrontend as startOpenWebUIFrontend } from '@/main/modules/openwebui'; @@ -23,6 +25,70 @@ import type { } from '@/types'; let koboldProcess: ChildProcess | null = null; +const preLaunchProcesses = new Set(); + +function spawnPreLaunchCommands(commands: string[]) { + const installDir = getInstallDir(); + const shell = platform === 'win32' ? 'cmd' : '/bin/sh'; + const shellFlag = platform === 'win32' ? '/c' : '-c'; + + for (const command of commands) { + if (!command.trim()) continue; + + sendKoboldOutput(`[PRE-LAUNCH] Running: ${command}\n`); + + try { + const child = spawn(shell, [shellFlag, command], { + cwd: installDir, + stdio: ['ignore', 'pipe', 'pipe'], + detached: false, + }); + + preLaunchProcesses.add(child); + + child.stdout?.on('data', (data) => { + sendKoboldOutput(`[PRE-LAUNCH] ${data.toString()}`, true); + }); + + child.stderr?.on('data', (data) => { + sendKoboldOutput(`[PRE-LAUNCH] ${data.toString()}`, true); + }); + + child.on('error', (error) => { + sendKoboldOutput( + `[PRE-LAUNCH ERROR] Failed to run "${command}": ${error.message}\n` + ); + preLaunchProcesses.delete(child); + }); + + child.on('exit', (code, signal) => { + preLaunchProcesses.delete(child); + if (code !== 0 && code !== null) { + sendKoboldOutput( + `[PRE-LAUNCH] Command "${command}" exited with code ${code}\n` + ); + } else if (signal) { + sendKoboldOutput( + `[PRE-LAUNCH] Command "${command}" terminated with signal ${signal}\n` + ); + } + }); + } catch (error) { + sendKoboldOutput( + `[PRE-LAUNCH ERROR] Failed to start "${command}": ${error instanceof Error ? error.message : String(error)}\n` + ); + } + } +} + +async function stopPreLaunchProcesses() { + const terminations = Array.from(preLaunchProcesses).map((process) => + terminateProcess(process) + ); + + await Promise.all(terminations); + preLaunchProcesses.clear(); +} async function resolveModelPaths(args: string[]) { const resolvedArgs: string[] = []; @@ -75,13 +141,18 @@ async function resolveModelPaths(args: string[]) { export async function launchKoboldCpp( args: string[] = [], frontendPreference: FrontendPreference = 'koboldcpp', - imageGenerationFrontendPreference?: ImageGenerationFrontendPreference + imageGenerationFrontendPreference?: ImageGenerationFrontendPreference, + preLaunchCommands: string[] = [] ) { try { if (koboldProcess) { await stopKoboldCpp(); } + if (preLaunchCommands.length > 0) { + spawnPreLaunchCommands(preLaunchCommands); + } + const currentVersion = await getCurrentVersion(); if (!currentVersion || !(await pathExists(currentVersion.path))) { const rawPath = getCurrentKoboldBinary(); @@ -213,11 +284,15 @@ export async function launchKoboldCpp( export async function stopKoboldCpp() { abortActiveDownloads(); - await stopProxy(); + stopProxy(); + stopPreLaunchProcesses(); return terminateProcess(koboldProcess); } -export const launchKoboldCppWithCustomFrontends = async (args: string[] = []) => +export const launchKoboldCppWithCustomFrontends = async ( + args: string[] = [], + preLaunchCommands: string[] = [] +) => safeExecute(async () => { const [frontendPreference, imageGenerationFrontendPreference] = (await Promise.all([ @@ -233,7 +308,8 @@ export const launchKoboldCppWithCustomFrontends = async (args: string[] = []) => const result = await launchKoboldCpp( args, frontendPreference, - imageGenerationFrontendPreference + imageGenerationFrontendPreference, + preLaunchCommands ); if ( diff --git a/src/preload/index.ts b/src/preload/index.ts index 58c2b30..370d09a 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -38,7 +38,8 @@ const koboldAPI: KoboldAPI = { ipcRenderer.invoke('kobold:downloadRelease', asset, options), deleteRelease: (binaryPath) => ipcRenderer.invoke('kobold:deleteRelease', binaryPath), - launchKoboldCpp: (args) => ipcRenderer.invoke('kobold:launchKoboldCpp', args), + launchKoboldCpp: (args, preLaunchCommands) => + ipcRenderer.invoke('kobold:launchKoboldCpp', args, preLaunchCommands), getConfigFiles: () => ipcRenderer.invoke('kobold:getConfigFiles'), saveConfigFile: (configName, configData) => ipcRenderer.invoke('kobold:saveConfigFile', configName, configData), diff --git a/src/stores/launchConfig.ts b/src/stores/launchConfig.ts index 65291d4..5376148 100644 --- a/src/stores/launchConfig.ts +++ b/src/stores/launchConfig.ts @@ -9,6 +9,7 @@ interface LaunchConfigState { contextSize: number; model: string; additionalArguments: string; + preLaunchCommands: string[]; port?: number; host: string; multiuser: boolean; @@ -63,6 +64,7 @@ interface LaunchConfigState { setQuantmatmul: (quantmatmul: boolean) => void; setUsemmap: (usemmap: boolean) => void; setDebugmode: (debugmode: boolean) => void; + setPreLaunchCommands: (commands: string[]) => void; setBackend: (backend: string) => void; setGpuDeviceSelection: (selection: string) => void; setTensorSplit: (split: string) => void; @@ -103,6 +105,7 @@ export const useLaunchConfigStore = create((set, get) => ({ contextSize: DEFAULT_CONTEXT_SIZE, model: '', additionalArguments: '', + preLaunchCommands: [''], port: undefined, host: '', multiuser: false, @@ -162,6 +165,7 @@ export const useLaunchConfigStore = create((set, get) => ({ setQuantmatmul: (quantmatmul) => set({ quantmatmul }), setUsemmap: (usemmap) => set({ usemmap }), setDebugmode: (debugmode) => set({ debugmode }), + setPreLaunchCommands: (commands) => set({ preLaunchCommands: commands }), setBackend: (backend) => set({ backend, @@ -225,6 +229,16 @@ export const useLaunchConfigStore = create((set, get) => ({ updates.additionalArguments = ''; } + if (Array.isArray(configData.preLaunchCommands)) { + const filteredCommands = configData.preLaunchCommands.filter( + (cmd): cmd is string => typeof cmd === 'string' + ); + updates.preLaunchCommands = + filteredCommands.length === 0 ? [''] : filteredCommands; + } else { + updates.preLaunchCommands = ['']; + } + if (typeof configData.port === 'number') { updates.port = configData.port; } else { diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 7e7d1d9..65c0ee8 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -116,6 +116,7 @@ export interface KoboldConfig { sdvaecpu?: boolean; sdclipgpu?: boolean; additionalArguments?: string; + preLaunchCommands?: string[]; moecpu?: number; moeexperts?: number; autoGpuLayers?: boolean; @@ -146,7 +147,8 @@ export interface KoboldAPI { binaryPath: string ) => Promise<{ success: boolean; error?: string }>; launchKoboldCpp: ( - args?: string[] + args?: string[], + preLaunchCommands?: string[] ) => Promise<{ success: boolean; pid?: number; error?: string }>; getConfigFiles: () => Promise<{ name: string; path: string; size: number }[]>; saveConfigFile: (