much better backend support logic and configuration, new advanced tab configs, LoRa support for image gen

This commit is contained in:
lone-cloud 2025-08-16 16:53:36 -07:00
parent 13f864b3b9
commit 617f13f4ca
29 changed files with 1004 additions and 546 deletions

2
.github/RELEASE.md vendored
View file

@ -54,7 +54,7 @@ If the build fails:
To customize the app icon: To customize the app icon:
1. Replace `assets/icon_512.png` with your custom 512x512 PNG icon 1. Replace `assets/icon.png` with your custom 512x512 PNG icon
2. Electron Builder automatically converts this single PNG to the appropriate format for each platform: 2. Electron Builder automatically converts this single PNG to the appropriate format for each platform:
- macOS: Converts to `.icns` format - macOS: Converts to `.icns` format
- Windows: Converts to `.ico` format - Windows: Converts to `.ico` format

View file

@ -26,14 +26,8 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: yarn run: yarn
- name: Run type check - name: Run checks (lint, type check, spell check)
run: yarn compile run: yarn check-all
- name: Run linter
run: yarn lint
- name: Run spell check
run: yarn spell-check
- name: Test build - name: Test build
run: yarn build:electron run: yarn build

View file

@ -42,7 +42,7 @@ jobs:
run: yarn lint run: yarn lint
- name: Build Electron app - name: Build Electron app
run: yarn build run: yarn package
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2
.npmrc
View file

@ -1,2 +0,0 @@
# Yarn configuration for Electron compatibility
nodeLinker: node-modules

View file

@ -1,34 +0,0 @@
# VS Code Spell Checker Settings
Add this to your VS Code workspace settings (`.vscode/settings.json`) for the best spell checking experience:
```json
{
"cSpell.enabled": true,
"cSpell.showCommandsInEditorContextMenu": true,
"cSpell.showStatus": true,
"cSpell.diagnosticLevel": "Warning",
"cSpell.checkLimit": 500,
"cSpell.numSuggestions": 8,
"cSpell.suggestionMenuType": "quickPick",
"cSpell.allowCompoundWords": true,
"cSpell.enableFiletypes": [
"javascript",
"typescript",
"javascriptreact",
"typescriptreact",
"markdown",
"json",
"css",
"html"
],
"cSpell.ignorePaths": [
"node_modules/**",
"dist/**",
"dist-electron/**",
"**/*.min.js",
"**/*.min.css",
"package-lock.json"
]
}
```

View file

@ -9,6 +9,7 @@ A koboldcpp manager.
- better surface the ROCm-specific builds of koboldcpp from YellowRoseCx and from [koboldai.org](https://koboldai.org/cpplinuxrocm) - better surface the ROCm-specific builds of koboldcpp from YellowRoseCx and from [koboldai.org](https://koboldai.org/cpplinuxrocm)
- manage the koboldcpp binary to prevent it from running in the background indefinitely - manage the koboldcpp binary to prevent it from running in the background indefinitely
- automatically unpack all downloaded koboldcpp binaries for significantly faster operation and reduced RAM+HDD utilization (up to ~4GB less RAM usage for ROCm) - automatically unpack all downloaded koboldcpp binaries for significantly faster operation and reduced RAM+HDD utilization (up to ~4GB less RAM usage for ROCm)
- adding presets for a basic flux or chroma image generation setup
### Prerequisites ### Prerequisites

View file

@ -2,7 +2,7 @@
Name=FriendlyKobold Name=FriendlyKobold
Comment=A modern Electron shell for KoboldCpp Comment=A modern Electron shell for KoboldCpp
Exec=friendly-kobold %U Exec=friendly-kobold %U
Icon=/home/eggy/Projects/friendly-kobold/assets/icon_512.png Icon=/home/eggy/Projects/friendly-kobold/assets/icon.png
Type=Application Type=Application
Categories=Development;TextEditor; Categories=Development;TextEditor;
StartupWMClass=FriendlyKobold StartupWMClass=FriendlyKobold

Binary file not shown.

Before

Width:  |  Height:  |  Size: 429 B

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

View file

@ -18,10 +18,12 @@
"babel", "babel",
"basename", "basename",
"bgcolor", "bgcolor",
"BLAS",
"browserWindow", "browserWindow",
"bundler", "bundler",
"bundling", "bundling",
"can", "can",
"ckpt",
"classList", "classList",
"className", "className",
"clblast", "clblast",
@ -43,6 +45,7 @@
"createElement", "createElement",
"createRef", "createRef",
"css", "css",
"cublas",
"cuda", "cuda",
"dataset", "dataset",
"deps", "deps",
@ -60,6 +63,7 @@
"filenames", "filenames",
"filepath", "filepath",
"filepaths", "filepaths",
"finetuned",
"flashattention", "flashattention",
"Flashattention", "Flashattention",
"flexbox", "flexbox",
@ -79,9 +83,11 @@
"GGUF", "GGUF",
"gif", "gif",
"gitignore", "gitignore",
"gpuid",
"gpulayers", "gpulayers",
"gridcol", "gridcol",
"gridrow", "gridrow",
"hipblas",
"hostname", "hostname",
"html", "html",
"http", "http",
@ -112,11 +118,14 @@
"lineheight", "lineheight",
"linted", "linted",
"localhost", "localhost",
"lowvram",
"Lowvram",
"lscpu", "lscpu",
"machdep", "machdep",
"mainWindow", "mainWindow",
"minified", "minified",
"minify", "minify",
"mmq",
"moz", "moz",
"multiplayer", "multiplayer",
"multiuser", "multiuser",
@ -128,6 +137,7 @@
"nocuda", "nocuda",
"nodeIntegration", "nodeIntegration",
"noheader", "noheader",
"nommq",
"noshift", "noshift",
"Noshift", "Noshift",
"nsis", "nsis",
@ -153,6 +163,8 @@
"preventDefault", "preventDefault",
"PYTHONDONTWRITEBYTECODE", "PYTHONDONTWRITEBYTECODE",
"PYTHONUNBUFFERED", "PYTHONUNBUFFERED",
"quantmatmul",
"Quantmatmul",
"querySelector", "querySelector",
"querySelectorAll", "querySelectorAll",
"radeon", "radeon",
@ -175,6 +187,8 @@
"sdapi", "sdapi",
"sdclipg", "sdclipg",
"sdclipl", "sdclipl",
"sdlora",
"Sdlora",
"sdmodel", "sdmodel",
"sdphotomaker", "sdphotomaker",
"sdt5xxl", "sdt5xxl",
@ -216,6 +230,7 @@
"ttf", "ttf",
"typeof", "typeof",
"typescript", "typescript",
"unquantized",
"uri", "uri",
"uris", "uris",
"url", "url",
@ -237,6 +252,7 @@
"vite", "vite",
"vitejs", "vitejs",
"vitest", "vitest",
"vram",
"vulkan", "vulkan",
"vulkaninfo", "vulkaninfo",
"wayland", "wayland",

View file

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta <meta
http-equiv="Content-Security-Policy" http-equiv="Content-Security-Policy"
content="default-src 'self' 'unsafe-inline'; connect-src 'self' http://localhost:*; script-src 'self' 'unsafe-inline'; frame-src 'self' http://localhost:*;" content="default-src 'self'; connect-src 'self' http: https:; script-src 'self'; style-src 'self' 'unsafe-inline' data:; frame-src 'self' http: https:;"
/> />
<title>Friendly Kobold</title> <title>Friendly Kobold</title>
</head> </head>

View file

@ -15,16 +15,12 @@
}, },
"scripts": { "scripts": {
"dev": "electron-vite dev", "dev": "electron-vite dev",
"build": "electron-vite build && electron-builder", "build": "electron-vite build",
"build:analyze": "cross-env ANALYZE=true electron-vite build && yarn exec open-cli dist/stats.html", "package": "electron-vite build && electron-builder",
"build:electron": "electron-vite build", "analyze": "cross-env ANALYZE=true electron-vite build && yarn dlx open-cli dist/stats.html",
"analyze": "yarn run build:analyze",
"analyze:server": "cross-env ANALYZE=server electron-vite build && yarn exec open-cli dist/stats.html",
"preview": "electron-vite preview", "preview": "electron-vite preview",
"preb": "electron-vite build",
"start": "electron .", "start": "electron .",
"electron": "wait-on tcp:5173 && electron .", "electron": "wait-on tcp:5173 && electron .",
"dist": "electron-vite build && electron-builder",
"format": "prettier --write . --ignore-path .gitignore", "format": "prettier --write . --ignore-path .gitignore",
"lint": "eslint .", "lint": "eslint .",
"lint:fix": "eslint . --fix", "lint:fix": "eslint . --fix",
@ -97,7 +93,8 @@
"appId": "com.friendly-kobold.app", "appId": "com.friendly-kobold.app",
"productName": "Friendly Kobold", "productName": "Friendly Kobold",
"compression": "maximum", "compression": "maximum",
"icon": "assets/icon_512.png", "icon": "assets/icon.png",
"publish": null,
"directories": { "directories": {
"output": "release" "output": "release"
}, },

View file

@ -9,9 +9,11 @@ export const useLaunchConfig = () => {
const [gpuLayers, setGpuLayers] = useState<number>(0); const [gpuLayers, setGpuLayers] = useState<number>(0);
const [autoGpuLayers, setAutoGpuLayers] = useState<boolean>(false); const [autoGpuLayers, setAutoGpuLayers] = useState<boolean>(false);
const [contextSize, setContextSize] = useState<number>(2048); const [contextSize, setContextSize] = useState<number>(2048);
const [modelPath, setModelPath] = useState<string>(''); const [modelPath, setModelPath] = useState<string>(
'https://huggingface.co/MaziyarPanahi/gemma-3-4b-it-GGUF/resolve/main/gemma-3-4b-it.Q8_0.gguf?download=true'
);
const [additionalArguments, setAdditionalArguments] = useState<string>(''); const [additionalArguments, setAdditionalArguments] = useState<string>('');
const [port, setPort] = useState<number>(5001); const [port, setPort] = useState<number | undefined>(undefined);
const [host, setHost] = useState<string>('localhost'); const [host, setHost] = useState<string>('localhost');
const [multiuser, setMultiuser] = useState<boolean>(false); const [multiuser, setMultiuser] = useState<boolean>(false);
const [multiplayer, setMultiplayer] = useState<boolean>(false); const [multiplayer, setMultiplayer] = useState<boolean>(false);
@ -22,20 +24,18 @@ export const useLaunchConfig = () => {
const [flashattention, setFlashattention] = useState<boolean>(false); const [flashattention, setFlashattention] = useState<boolean>(false);
const [noavx2, setNoavx2] = useState<boolean>(false); const [noavx2, setNoavx2] = useState<boolean>(false);
const [failsafe, setFailsafe] = useState<boolean>(false); const [failsafe, setFailsafe] = useState<boolean>(false);
const [lowvram, setLowvram] = useState<boolean>(false);
const [quantmatmul, setQuantmatmul] = useState<boolean>(false);
const [backend, setBackend] = useState<string>('cpu'); const [backend, setBackend] = useState<string>('cpu');
const [gpuDevice, setGpuDevice] = useState<number>(0);
const [sdmodel, setSdmodel] = useState<string>(''); const [sdmodel, setSdmodel] = useState<string>('');
const [sdt5xxl, setSdt5xxl] = useState<string>( const [sdt5xxl, setSdt5xxl] = useState<string>('');
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/t5xxl_fp8_e4m3fn.safetensors?download=true' const [sdclipl, setSdclipl] = useState<string>('');
);
const [sdclipl, setSdclipl] = useState<string>(
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/clip_l.safetensors?download=true'
);
const [sdclipg, setSdclipg] = useState<string>(''); const [sdclipg, setSdclipg] = useState<string>('');
const [sdphotomaker, setSdphotomaker] = useState<string>(''); const [sdphotomaker, setSdphotomaker] = useState<string>('');
const [sdvae, setSdvae] = useState<string>( const [sdvae, setSdvae] = useState<string>('');
'https://huggingface.co/camenduru/FLUX.1-dev/resolve/main/ae.safetensors?download=true' const [sdlora, setSdlora] = useState<string>('');
);
// eslint-disable-next-line sonarjs/cognitive-complexity // eslint-disable-next-line sonarjs/cognitive-complexity
const parseAndApplyConfigFile = useCallback(async (configPath: string) => { const parseAndApplyConfigFile = useCallback(async (configPath: string) => {
@ -62,7 +62,7 @@ export const useLaunchConfig = () => {
if (typeof configData.port === 'number') { if (typeof configData.port === 'number') {
setPort(configData.port); setPort(configData.port);
} else { } else {
setPort(5001); setPort(undefined);
} }
if (typeof configData.host === 'string') { if (typeof configData.host === 'string') {
@ -125,9 +125,27 @@ export const useLaunchConfig = () => {
setFailsafe(false); setFailsafe(false);
} }
if (typeof configData.lowvram === 'boolean') {
setLowvram(configData.lowvram);
}
if (typeof configData.quantmatmul === 'boolean') {
setQuantmatmul(configData.quantmatmul);
}
if (configData.usecuda !== null && configData.usecuda !== undefined) { if (configData.usecuda !== null && configData.usecuda !== undefined) {
const gpuInfo = await window.electronAPI.kobold.detectGPU(); const gpuInfo = await window.electronAPI.kobold.detectGPU();
setBackend(gpuInfo.hasNVIDIA ? 'cuda' : 'rocm'); setBackend(gpuInfo.hasNVIDIA ? 'cuda' : 'rocm');
if (
Array.isArray(configData.usecuda) &&
configData.usecuda.length >= 3
) {
const [vramMode, deviceId, mmqMode] = configData.usecuda;
setLowvram(vramMode === 'lowvram');
setGpuDevice(parseInt(deviceId, 10) || 0);
setQuantmatmul(mmqMode === 'mmq');
}
} else if ( } else if (
configData.usevulkan !== null && configData.usevulkan !== null &&
configData.usevulkan !== undefined configData.usevulkan !== undefined
@ -165,11 +183,15 @@ export const useLaunchConfig = () => {
if (typeof configData.sdvae === 'string') { if (typeof configData.sdvae === 'string') {
setSdvae(configData.sdvae); setSdvae(configData.sdvae);
} }
if (typeof configData.sdlora === 'string') {
setSdlora(configData.sdlora);
}
} else { } else {
const cpuCapabilities = await window.electronAPI.kobold.detectCPU(); const cpuCapabilities = await window.electronAPI.kobold.detectCPU();
setGpuLayers(0); setGpuLayers(0);
setContextSize(2048); setContextSize(2048);
setPort(5001); setPort(undefined);
setHost('localhost'); setHost('localhost');
setMultiuser(false); setMultiuser(false);
setMultiplayer(false); setMultiplayer(false);
@ -203,7 +225,7 @@ export const useLaunchConfig = () => {
setModelPath(''); setModelPath('');
setGpuLayers(0); setGpuLayers(0);
setContextSize(2048); setContextSize(2048);
setPort(5001); setPort(undefined);
setHost('localhost'); setHost('localhost');
setMultiuser(false); setMultiuser(false);
setMultiplayer(false); setMultiplayer(false);
@ -292,7 +314,7 @@ export const useLaunchConfig = () => {
setAutoGpuLayers(checked); setAutoGpuLayers(checked);
}, []); }, []);
const handlePortChange = useCallback((value: number) => { const handlePortChange = useCallback((value: number | undefined) => {
setPort(value); setPort(value);
}, []); }, []);
@ -336,10 +358,22 @@ export const useLaunchConfig = () => {
setFailsafe(checked); setFailsafe(checked);
}, []); }, []);
const handleLowvramChange = useCallback((checked: boolean) => {
setLowvram(checked);
}, []);
const handleQuantmatmulChange = useCallback((checked: boolean) => {
setQuantmatmul(checked);
}, []);
const handleBackendChange = useCallback((backend: string) => { const handleBackendChange = useCallback((backend: string) => {
setBackend(backend); setBackend(backend);
}, []); }, []);
const handleGpuDeviceChange = useCallback((device: number) => {
setGpuDevice(device);
}, []);
const handleSdmodelChange = useCallback((path: string) => { const handleSdmodelChange = useCallback((path: string) => {
setSdmodel(path); setSdmodel(path);
}, []); }, []);
@ -406,6 +440,17 @@ export const useLaunchConfig = () => {
} }
}, []); }, []);
const handleSdloraChange = useCallback((path: string) => {
setSdlora(path);
}, []);
const handleSelectSdloraFile = useCallback(async () => {
const filePath = await window.electronAPI.kobold.selectModelFile();
if (filePath) {
setSdlora(filePath);
}
}, []);
const applyImageModelPreset = useCallback((preset: ImageModelPreset) => { const applyImageModelPreset = useCallback((preset: ImageModelPreset) => {
setSdmodel(preset.sdmodel); setSdmodel(preset.sdmodel);
setSdt5xxl(preset.sdt5xxl); setSdt5xxl(preset.sdt5xxl);
@ -442,13 +487,17 @@ export const useLaunchConfig = () => {
flashattention, flashattention,
noavx2, noavx2,
failsafe, failsafe,
lowvram,
quantmatmul,
backend, backend,
gpuDevice,
sdmodel, sdmodel,
sdt5xxl, sdt5xxl,
sdclipl, sdclipl,
sdclipg, sdclipg,
sdphotomaker, sdphotomaker,
sdvae, sdvae,
sdlora,
parseAndApplyConfigFile, parseAndApplyConfigFile,
loadSavedSettings, loadSavedSettings,
@ -470,7 +519,10 @@ export const useLaunchConfig = () => {
handleFlashattentionChange, handleFlashattentionChange,
handleNoavx2Change, handleNoavx2Change,
handleFailsafeChange, handleFailsafeChange,
handleLowvramChange,
handleQuantmatmulChange,
handleBackendChange, handleBackendChange,
handleGpuDeviceChange,
handleSdmodelChange, handleSdmodelChange,
handleSelectSdmodelFile, handleSelectSdmodelFile,
handleSdt5xxlChange, handleSdt5xxlChange,
@ -483,6 +535,8 @@ export const useLaunchConfig = () => {
handleSelectSdphotomakerFile, handleSelectSdphotomakerFile,
handleSdvaeChange, handleSdvaeChange,
handleSelectSdvaeFile, handleSelectSdvaeFile,
handleSdloraChange,
handleSelectSdloraFile,
applyImageModelPreset, applyImageModelPreset,
handleApplyPreset, handleApplyPreset,
}; };

View file

@ -8,6 +8,7 @@ import { ConfigManager } from '@/main/managers/ConfigManager';
import { KoboldCppManager } from '@/main/managers/KoboldCppManager'; import { KoboldCppManager } from '@/main/managers/KoboldCppManager';
import { GitHubService } from '@/main/services/GitHubService'; import { GitHubService } from '@/main/services/GitHubService';
import { HardwareService } from '@/main/services/HardwareService'; import { HardwareService } from '@/main/services/HardwareService';
import { BinaryService } from '@/main/services/BinaryService';
import { IPCHandlers } from '@/main/utils/IPCHandlers'; import { IPCHandlers } from '@/main/utils/IPCHandlers';
import { APP_NAME, CONFIG_FILE_NAME } from '@/constants'; import { APP_NAME, CONFIG_FILE_NAME } from '@/constants';
@ -17,6 +18,7 @@ class FriendlyKoboldApp {
private koboldManager: KoboldCppManager; private koboldManager: KoboldCppManager;
private githubService: GitHubService; private githubService: GitHubService;
private hardwareService: HardwareService; private hardwareService: HardwareService;
private binaryService: BinaryService;
private ipcHandlers: IPCHandlers; private ipcHandlers: IPCHandlers;
constructor() { constructor() {
@ -26,6 +28,7 @@ class FriendlyKoboldApp {
this.windowManager = new WindowManager(this.configManager); this.windowManager = new WindowManager(this.configManager);
this.githubService = new GitHubService(); this.githubService = new GitHubService();
this.hardwareService = new HardwareService(); this.hardwareService = new HardwareService();
this.binaryService = new BinaryService();
this.koboldManager = new KoboldCppManager( this.koboldManager = new KoboldCppManager(
this.configManager, this.configManager,
this.githubService, this.githubService,
@ -35,7 +38,8 @@ class FriendlyKoboldApp {
this.koboldManager, this.koboldManager,
this.configManager, this.configManager,
this.githubService, this.githubService,
this.hardwareService this.hardwareService,
this.binaryService
); );
} }

View file

@ -0,0 +1,124 @@
import { existsSync } from 'fs';
import { join, dirname } from 'path';
export interface BackendSupport {
rocm: boolean;
vulkan: boolean;
clblast: boolean;
noavx2: boolean;
failsafe: boolean;
cuda: boolean;
}
export class BinaryService {
private backendSupportCache = new Map<string, BackendSupport>();
detectBackendSupport(koboldBinaryPath: string): BackendSupport {
if (this.backendSupportCache.has(koboldBinaryPath)) {
return this.backendSupportCache.get(koboldBinaryPath)!;
}
const support: BackendSupport = {
rocm: false,
vulkan: false,
clblast: false,
noavx2: false,
failsafe: false,
cuda: false,
};
try {
const binaryDir = dirname(koboldBinaryPath);
const internalDir = join(binaryDir, '_internal');
if (!existsSync(internalDir)) {
console.warn(
'_internal directory not found, cannot detect backend support'
);
this.backendSupportCache.set(koboldBinaryPath, support);
return support;
}
const platform = process.platform;
const isDynamicLib = (name: string) => {
if (platform === 'win32') {
return existsSync(join(internalDir, `${name}.dll`));
} else {
return existsSync(join(internalDir, `${name}.so`));
}
};
support.rocm = isDynamicLib('koboldcpp_hipblas');
support.vulkan = isDynamicLib('koboldcpp_vulkan');
support.clblast = isDynamicLib('koboldcpp_clblast');
support.noavx2 = isDynamicLib('koboldcpp_noavx2');
support.failsafe = isDynamicLib('koboldcpp_failsafe');
support.cuda = isDynamicLib('koboldcpp_cublas');
} catch (error) {
console.error('Error detecting backend support:', error);
}
this.backendSupportCache.set(koboldBinaryPath, support);
return support;
}
getAvailableBackends(
koboldBinaryPath: string,
hardwareCapabilities?: {
cuda: { supported: boolean; devices: string[] };
rocm: { supported: boolean; devices: string[] };
vulkan: { supported: boolean; devices: string[] };
clblast: { supported: boolean; devices: string[] };
}
): Array<{ value: string; label: string; devices?: string[] }> {
const backendSupport = this.detectBackendSupport(koboldBinaryPath);
const backends: Array<{
value: string;
label: string;
devices?: string[];
}> = [];
if (backendSupport.cuda && hardwareCapabilities?.cuda.supported) {
backends.push({
value: 'cuda',
label: 'CUDA',
devices: hardwareCapabilities.cuda.devices,
});
}
if (backendSupport.rocm && hardwareCapabilities?.rocm.supported) {
backends.push({
value: 'rocm',
label: 'ROCm',
devices: hardwareCapabilities.rocm.devices,
});
}
if (backendSupport.vulkan && hardwareCapabilities?.vulkan.supported) {
backends.push({
value: 'vulkan',
label: 'Vulkan',
devices: hardwareCapabilities.vulkan.devices,
});
}
if (backendSupport.clblast && hardwareCapabilities?.clblast.supported) {
backends.push({
value: 'clblast',
label: 'CLBlast',
devices: hardwareCapabilities.clblast.devices,
});
}
backends.push({
value: 'cpu',
label: 'CPU',
});
return backends;
}
clearCache(): void {
this.backendSupportCache.clear();
}
}

View file

@ -20,15 +20,9 @@ export class HardwareService {
try { try {
const [cpu, flags] = await Promise.all([si.cpu(), si.cpuFlags()]); const [cpu, flags] = await Promise.all([si.cpu(), si.cpuFlags()]);
const cpuInfo: string[] = []; const devices: string[] = [];
if (cpu.brand) { if (cpu.brand) {
cpuInfo.push(cpu.brand); devices.push(cpu.brand);
}
if (cpu.cores) {
cpuInfo.push(`${cpu.cores} cores`);
}
if (cpu.speed) {
cpuInfo.push(`${cpu.speed}GHz`);
} }
const avx = flags.includes('avx') || flags.includes('AVX'); const avx = flags.includes('avx') || flags.includes('AVX');
@ -37,7 +31,7 @@ export class HardwareService {
this.cpuCapabilitiesCache = { this.cpuCapabilitiesCache = {
avx, avx,
avx2, avx2,
cpuInfo: cpuInfo.length > 0 ? cpuInfo : ['CPU information unavailable'], devices,
}; };
return this.cpuCapabilitiesCache; return this.cpuCapabilitiesCache;
@ -46,7 +40,7 @@ export class HardwareService {
const fallbackCapabilities = { const fallbackCapabilities = {
avx: false, avx: false,
avx2: false, avx2: false,
cpuInfo: ['CPU detection failed'], devices: [],
}; };
this.cpuCapabilitiesCache = fallbackCapabilities; this.cpuCapabilitiesCache = fallbackCapabilities;
return fallbackCapabilities; return fallbackCapabilities;

View file

@ -4,23 +4,28 @@ import { KoboldCppManager } from '@/main/managers/KoboldCppManager';
import { ConfigManager } from '@/main/managers/ConfigManager'; import { ConfigManager } from '@/main/managers/ConfigManager';
import { GitHubService } from '@/main/services/GitHubService'; import { GitHubService } from '@/main/services/GitHubService';
import { HardwareService } from '@/main/services/HardwareService'; import { HardwareService } from '@/main/services/HardwareService';
import { BinaryService } from '@/main/services/BinaryService';
import type { GPUCapabilities } from '@/types/hardware';
export class IPCHandlers { export class IPCHandlers {
private koboldManager: KoboldCppManager; private koboldManager: KoboldCppManager;
private configManager: ConfigManager; private configManager: ConfigManager;
private githubService: GitHubService; private githubService: GitHubService;
private hardwareService: HardwareService; private hardwareService: HardwareService;
private binaryService: BinaryService;
constructor( constructor(
koboldManager: KoboldCppManager, koboldManager: KoboldCppManager,
configManager: ConfigManager, configManager: ConfigManager,
githubService: GitHubService, githubService: GitHubService,
hardwareService: HardwareService hardwareService: HardwareService,
binaryService: BinaryService
) { ) {
this.koboldManager = koboldManager; this.koboldManager = koboldManager;
this.configManager = configManager; this.configManager = configManager;
this.githubService = githubService; this.githubService = githubService;
this.hardwareService = hardwareService; this.hardwareService = hardwareService;
this.binaryService = binaryService;
} }
setupHandlers() { setupHandlers() {
@ -132,6 +137,23 @@ export class IPCHandlers {
this.hardwareService.detectAllWithCapabilities() this.hardwareService.detectAllWithCapabilities()
); );
ipcMain.handle('kobold:detectBackendSupport', (_, binaryPath: string) =>
this.binaryService.detectBackendSupport(binaryPath)
);
ipcMain.handle(
'kobold:getAvailableBackends',
(_, binaryPath: string, hardwareCapabilities: GPUCapabilities) =>
this.binaryService.getAvailableBackends(
binaryPath,
hardwareCapabilities
)
);
ipcMain.handle('kobold:clearBinaryCache', () =>
this.binaryService.clearCache()
);
ipcMain.handle('kobold:getPlatform', () => ({ ipcMain.handle('kobold:getPlatform', () => ({
platform: process.platform, platform: process.platform,
arch: process.arch, arch: process.arch,

View file

@ -5,6 +5,7 @@ import type {
ConfigAPI, ConfigAPI,
UpdateInfo, UpdateInfo,
} from '@/types/electron'; } from '@/types/electron';
import type { GPUCapabilities } from '@/types/hardware';
const koboldAPI: KoboldAPI = { const koboldAPI: KoboldAPI = {
getInstalledVersion: () => ipcRenderer.invoke('kobold:getInstalledVersion'), getInstalledVersion: () => ipcRenderer.invoke('kobold:getInstalledVersion'),
@ -30,6 +31,18 @@ const koboldAPI: KoboldAPI = {
detectHardware: () => ipcRenderer.invoke('kobold:detectHardware'), detectHardware: () => ipcRenderer.invoke('kobold:detectHardware'),
detectAllCapabilities: () => detectAllCapabilities: () =>
ipcRenderer.invoke('kobold:detectAllCapabilities'), ipcRenderer.invoke('kobold:detectAllCapabilities'),
detectBackendSupport: (binaryPath: string) =>
ipcRenderer.invoke('kobold:detectBackendSupport', binaryPath),
getAvailableBackends: (
binaryPath: string,
hardwareCapabilities: GPUCapabilities
) =>
ipcRenderer.invoke(
'kobold:getAvailableBackends',
binaryPath,
hardwareCapabilities
),
clearBinaryCache: () => ipcRenderer.invoke('kobold:clearBinaryCache'),
getCurrentInstallDir: () => ipcRenderer.invoke('kobold:getCurrentInstallDir'), getCurrentInstallDir: () => ipcRenderer.invoke('kobold:getCurrentInstallDir'),
selectInstallDirectory: () => selectInstallDirectory: () =>
ipcRenderer.invoke('kobold:selectInstallDirectory'), ipcRenderer.invoke('kobold:selectInstallDirectory'),

View file

@ -1,4 +1,5 @@
import { Stack, Text, Group, TextInput, Checkbox } from '@mantine/core'; import { Stack, Text, Group, TextInput, Checkbox } from '@mantine/core';
import { useState, useEffect } from 'react';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
interface AdvancedTabProps { interface AdvancedTabProps {
@ -7,11 +8,16 @@ interface AdvancedTabProps {
flashattention: boolean; flashattention: boolean;
noavx2: boolean; noavx2: boolean;
failsafe: boolean; failsafe: boolean;
lowvram: boolean;
quantmatmul: boolean;
backend: string;
onAdditionalArgumentsChange: (args: string) => void; onAdditionalArgumentsChange: (args: string) => void;
onNoshiftChange: (noshift: boolean) => void; onNoshiftChange: (noshift: boolean) => void;
onFlashattentionChange: (flashattention: boolean) => void; onFlashattentionChange: (flashattention: boolean) => void;
onNoavx2Change: (noavx2: boolean) => void; onNoavx2Change: (noavx2: boolean) => void;
onFailsafeChange: (failsafe: boolean) => void; onFailsafeChange: (failsafe: boolean) => void;
onLowvramChange: (lowvram: boolean) => void;
onQuantmatmulChange: (quantmatmul: boolean) => void;
} }
export const AdvancedTab = ({ export const AdvancedTab = ({
@ -20,14 +26,56 @@ export const AdvancedTab = ({
flashattention, flashattention,
noavx2, noavx2,
failsafe, failsafe,
lowvram,
quantmatmul,
backend,
onAdditionalArgumentsChange, onAdditionalArgumentsChange,
onNoshiftChange, onNoshiftChange,
onFlashattentionChange, onFlashattentionChange,
onNoavx2Change, onNoavx2Change,
onFailsafeChange, onFailsafeChange,
}: AdvancedTabProps) => ( onLowvramChange,
onQuantmatmulChange,
}: AdvancedTabProps) => {
const [backendSupport, setBackendSupport] = useState<{
noavx2: boolean;
failsafe: boolean;
} | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const detectBackendSupport = async () => {
try {
const currentBinaryInfo =
await window.electronAPI.kobold.getCurrentBinaryInfo();
if (currentBinaryInfo?.path) {
const support = await window.electronAPI.kobold.detectBackendSupport(
currentBinaryInfo.path
);
setBackendSupport({
noavx2: support.noavx2,
failsafe: support.failsafe,
});
}
} catch (error) {
console.warn('Failed to detect backend support:', error);
setBackendSupport({ noavx2: false, failsafe: false });
} finally {
setIsLoading(false);
}
};
void detectBackendSupport();
}, []);
return (
<Stack gap="lg"> <Stack gap="lg">
<div> <div>
<Group gap="xs" align="center" mb="md">
<Text size="sm" fw={600}>
Performance Options
</Text>
</Group>
<Stack gap="md"> <Stack gap="md">
<Group gap="lg" align="flex-start" wrap="nowrap"> <Group gap="lg" align="flex-start" wrap="nowrap">
<div style={{ minWidth: '200px' }}> <div style={{ minWidth: '200px' }}>
@ -57,6 +105,47 @@ export const AdvancedTab = ({
</div> </div>
</Group> </Group>
{(backend === 'cuda' || backend === 'rocm') && (
<Group gap="lg" align="flex-start" wrap="nowrap">
<div style={{ minWidth: '200px' }}>
<Group gap="xs" align="center">
<Checkbox
checked={lowvram}
onChange={(event) =>
onLowvramChange(event.currentTarget.checked)
}
label="Low VRAM"
/>
<InfoTooltip
label="Avoid offloading KV Cache or scratch buffers to VRAM.&#10;Allows more layers to fit, but may result in a speed loss."
/>
</Group>
</div>
<div style={{ minWidth: '200px' }}>
<Group gap="xs" align="center">
<Checkbox
checked={quantmatmul}
onChange={(event) =>
onQuantmatmulChange(event.currentTarget.checked)
}
label="QuantMatMul"
/>
<InfoTooltip label="Enable MMQ mode to use finetuned kernels instead of default CuBLAS/HipBLAS for prompt processing." />
</Group>
</div>
</Group>
)}
</Stack>
</div>
<div>
<Group gap="xs" align="center" mb="md">
<Text size="sm" fw={600}>
Hardware Compatibility
</Text>
</Group>
<Stack gap="md">
<Group gap="lg" align="flex-start" wrap="nowrap"> <Group gap="lg" align="flex-start" wrap="nowrap">
<div style={{ minWidth: '200px' }}> <div style={{ minWidth: '200px' }}>
<Group gap="xs" align="center"> <Group gap="xs" align="center">
@ -66,8 +155,15 @@ export const AdvancedTab = ({
onNoavx2Change(event.currentTarget.checked) onNoavx2Change(event.currentTarget.checked)
} }
label="Disable AVX2" label="Disable AVX2"
disabled={isLoading || !backendSupport?.noavx2}
/>
<InfoTooltip
label={
!backendSupport?.noavx2 && !isLoading
? 'This binary does not support the no-AVX2 mode.'
: 'Do not use AVX2 instructions, a slower compatibility mode for older devices.'
}
/> />
<InfoTooltip label="Do not use AVX2 instructions, a slower compatibility mode for older devices." />
</Group> </Group>
</div> </div>
@ -79,13 +175,21 @@ export const AdvancedTab = ({
onFailsafeChange(event.currentTarget.checked) onFailsafeChange(event.currentTarget.checked)
} }
label="Failsafe" label="Failsafe"
disabled={isLoading || !backendSupport?.failsafe}
/>
<InfoTooltip
label={
!backendSupport?.failsafe && !isLoading
? 'This binary does not support failsafe mode.'
: 'Use failsafe mode, extremely slow CPU only compatibility mode that should work on all devices. Can be combined with useclblast if your device supports OpenCL.'
}
/> />
<InfoTooltip label="Use failsafe mode, extremely slow CPU only compatibility mode that should work on all devices. Can be combined with useclblast if your device supports OpenCL." />
</Group> </Group>
</div> </div>
</Group> </Group>
</Stack> </Stack>
</div> </div>
<div> <div>
<Group gap="xs" align="center" mb="xs"> <Group gap="xs" align="center" mb="xs">
<Text size="sm" fw={500}> <Text size="sm" fw={500}>
@ -103,3 +207,4 @@ export const AdvancedTab = ({
</div> </div>
</Stack> </Stack>
); );
};

View file

@ -11,11 +11,12 @@ import {
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { AlertTriangle } from 'lucide-react'; import { AlertTriangle } from 'lucide-react';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
import { isNoCudaBinary, isRocmBinary } from '@/utils/binaryUtils';
interface BackendSelectorProps { interface BackendSelectorProps {
backend: string; backend: string;
onBackendChange: (backend: string) => void; onBackendChange: (backend: string) => void;
gpuDevice?: number;
onGpuDeviceChange?: (device: number) => void;
noavx2?: boolean; noavx2?: boolean;
failsafe?: boolean; failsafe?: boolean;
} }
@ -23,6 +24,8 @@ interface BackendSelectorProps {
export const BackendSelector = ({ export const BackendSelector = ({
backend, backend,
onBackendChange, onBackendChange,
gpuDevice = 0,
onGpuDeviceChange,
noavx2 = false, noavx2 = false,
failsafe = false, failsafe = false,
}: BackendSelectorProps) => { }: BackendSelectorProps) => {
@ -55,52 +58,22 @@ export const BackendSelector = ({
avx2: cpuCapabilitiesResult.avx2, avx2: cpuCapabilitiesResult.avx2,
}); });
const backends: Array<{ let backends: Array<{
value: string; value: string;
label: string; label: string;
devices?: string[]; devices?: string[];
}> = []; }> = [];
if (currentBinaryInfo?.filename) { if (currentBinaryInfo?.path) {
const filename = currentBinaryInfo.filename; backends = await window.electronAPI.kobold.getAvailableBackends(
currentBinaryInfo.path,
gpuCapabilities
);
if (!isNoCudaBinary(filename) && gpuCapabilities.cuda.supported) { const cpuBackend = backends.find((b) => b.value === 'cpu');
backends.push({ if (cpuBackend) {
value: 'cuda', cpuBackend.devices = cpuCapabilitiesResult.devices;
label: 'CUDA',
devices: gpuCapabilities.cuda.devices,
});
} }
if (isRocmBinary(filename) && gpuCapabilities.rocm.supported) {
backends.push({
value: 'rocm',
label: 'ROCm',
devices: gpuCapabilities.rocm.devices,
});
}
if (gpuCapabilities.vulkan.supported) {
backends.push({
value: 'vulkan',
label: 'Vulkan',
devices: gpuCapabilities.vulkan.devices,
});
}
if (gpuCapabilities.clblast.supported) {
backends.push({
value: 'clblast',
label: 'CLBlast',
devices: gpuCapabilities.clblast.devices,
});
}
backends.push({
value: 'cpu',
label: 'CPU',
devices: cpuCapabilitiesResult.cpuInfo,
});
} }
setAvailableBackends(backends); setAvailableBackends(backends);
@ -174,18 +147,48 @@ export const BackendSelector = ({
: 'Select backend' : 'Select backend'
} }
value={backend} value={backend}
onChange={(value) => value && onBackendChange(value)} onChange={(value) => {
if (value) {
onBackendChange(value);
}
}}
data={availableBackends.map((b) => ({ data={availableBackends.map((b) => ({
value: b.value, value: b.value,
label: b.label, label: b.label,
}))} }))}
disabled={isLoadingBackends || availableBackends.length === 0} disabled={isLoadingBackends || availableBackends.length === 0}
/> />
{(backend === 'cuda' || backend === 'rocm') &&
onGpuDeviceChange &&
availableBackends.find((b) => b.value === backend)?.devices &&
availableBackends.find((b) => b.value === backend)!.devices!.length >
1 && (
<Select
label="GPU Device"
placeholder="Select GPU device"
value={gpuDevice.toString()}
onChange={(value) =>
value && onGpuDeviceChange(parseInt(value, 10))
}
data={availableBackends
.find((b) => b.value === backend)!
.devices!.map((device, index) => ({
value: index.toString(),
label: `GPU ${index}: ${device}`,
}))}
mt="xs"
/>
)}
{backend && {backend &&
availableBackends.find((b) => b.value === backend)?.devices && ( availableBackends.find((b) => b.value === backend)?.devices && (
<Group gap="xs" mt="xs"> <Group gap="xs" mt="xs">
<Text size="xs" c="dimmed"> <Text size="xs" c="dimmed">
Devices: {availableBackends.find((b) => b.value === backend)?.devices
?.length === 1
? 'Device:'
: 'Devices:'}
</Text> </Text>
{availableBackends {availableBackends
.find((b) => b.value === backend) .find((b) => b.value === backend)

View file

@ -18,6 +18,7 @@ interface GeneralTabProps {
autoGpuLayers: boolean; autoGpuLayers: boolean;
contextSize: number; contextSize: number;
backend: string; backend: string;
gpuDevice?: number;
noavx2: boolean; noavx2: boolean;
failsafe: boolean; failsafe: boolean;
onModelPathChange: (path: string) => void; onModelPathChange: (path: string) => void;
@ -26,6 +27,7 @@ interface GeneralTabProps {
onAutoGpuLayersChange: (auto: boolean) => void; onAutoGpuLayersChange: (auto: boolean) => void;
onContextSizeChange: (size: number) => void; onContextSizeChange: (size: number) => void;
onBackendChange: (backend: string) => void; onBackendChange: (backend: string) => void;
onGpuDeviceChange?: (device: number) => void;
} }
export const GeneralTab = ({ export const GeneralTab = ({
@ -34,6 +36,7 @@ export const GeneralTab = ({
autoGpuLayers, autoGpuLayers,
contextSize, contextSize,
backend, backend,
gpuDevice,
noavx2, noavx2,
failsafe, failsafe,
onModelPathChange, onModelPathChange,
@ -42,6 +45,7 @@ export const GeneralTab = ({
onAutoGpuLayersChange, onAutoGpuLayersChange,
onContextSizeChange, onContextSizeChange,
onBackendChange, onBackendChange,
onGpuDeviceChange,
}: GeneralTabProps) => { }: GeneralTabProps) => {
const validationState = getInputValidationState(modelPath); const validationState = getInputValidationState(modelPath);
@ -68,6 +72,15 @@ export const GeneralTab = ({
return ( return (
<Stack gap="lg"> <Stack gap="lg">
<BackendSelector
backend={backend}
onBackendChange={onBackendChange}
gpuDevice={gpuDevice}
onGpuDeviceChange={onGpuDeviceChange}
noavx2={noavx2}
failsafe={failsafe}
/>
<div> <div>
<Text size="sm" fw={500} mb="xs"> <Text size="sm" fw={500} mb="xs">
Text Model File Text Model File
@ -105,13 +118,6 @@ export const GeneralTab = ({
</Group> </Group>
</div> </div>
<BackendSelector
backend={backend}
onBackendChange={onBackendChange}
noavx2={noavx2}
failsafe={failsafe}
/>
<div> <div>
<Group justify="space-between" align="center" mb="xs"> <Group justify="space-between" align="center" mb="xs">
<Group gap="xs" align="center"> <Group gap="xs" align="center">

View file

@ -19,6 +19,7 @@ interface ImageGenerationTabProps {
sdclipg: string; sdclipg: string;
sdphotomaker: string; sdphotomaker: string;
sdvae: string; sdvae: string;
sdlora: string;
textModelPath?: string; textModelPath?: string;
onSdmodelChange: (path: string) => void; onSdmodelChange: (path: string) => void;
onSelectSdmodelFile: () => void; onSelectSdmodelFile: () => void;
@ -32,6 +33,8 @@ interface ImageGenerationTabProps {
onSelectSdphotomakerFile: () => void; onSelectSdphotomakerFile: () => void;
onSdvaeChange: (path: string) => void; onSdvaeChange: (path: string) => void;
onSelectSdvaeFile: () => void; onSelectSdvaeFile: () => void;
onSdloraChange: (path: string) => void;
onSelectSdloraFile: () => void;
onApplyPreset: (presetName: string) => void; onApplyPreset: (presetName: string) => void;
} }
@ -125,6 +128,7 @@ export const ImageGenerationTab = ({
sdclipg, sdclipg,
sdphotomaker, sdphotomaker,
sdvae, sdvae,
sdlora,
textModelPath, textModelPath,
onSdmodelChange, onSdmodelChange,
onSelectSdmodelFile, onSelectSdmodelFile,
@ -138,6 +142,8 @@ export const ImageGenerationTab = ({
onSelectSdphotomakerFile, onSelectSdphotomakerFile,
onSdvaeChange, onSdvaeChange,
onSelectSdvaeFile, onSelectSdvaeFile,
onSdloraChange,
onSelectSdloraFile,
onApplyPreset, onApplyPreset,
}: ImageGenerationTabProps) => { }: ImageGenerationTabProps) => {
const hasTextModel = textModelPath?.trim() !== ''; const hasTextModel = textModelPath?.trim() !== '';
@ -228,6 +234,15 @@ export const ImageGenerationTab = ({
onChange={onSdvaeChange} onChange={onSdvaeChange}
onSelectFile={onSelectSdvaeFile} onSelectFile={onSelectSdvaeFile}
/> />
<ModelField
label="Image LoRa"
value={sdlora}
placeholder="Select a LoRa file or enter a direct URL"
tooltip="LoRa (Low-Rank Adaptation) file for customizing image generation. Select a .safetensors or .gguf LoRa file to be loaded. Should be unquantized."
onChange={onSdloraChange}
onSelectFile={onSelectSdloraFile}
/>
</Stack> </Stack>
); );
}; };

View file

@ -1,15 +1,16 @@
import { Stack, Text, TextInput, Group, Checkbox } from '@mantine/core'; import { Stack, Text, TextInput, Group, Checkbox } from '@mantine/core';
import { useState, useEffect } from 'react';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
interface NetworkTabProps { interface NetworkTabProps {
port: number; port: number | undefined;
host: string; host: string;
multiuser: boolean; multiuser: boolean;
multiplayer: boolean; multiplayer: boolean;
remotetunnel: boolean; remotetunnel: boolean;
nocertify: boolean; nocertify: boolean;
websearch: boolean; websearch: boolean;
onPortChange: (port: number) => void; onPortChange: (port: number | undefined) => void;
onHostChange: (host: string) => void; onHostChange: (host: string) => void;
onMultiuserChange: (multiuser: boolean) => void; onMultiuserChange: (multiuser: boolean) => void;
onMultiplayerChange: (multiplayer: boolean) => void; onMultiplayerChange: (multiplayer: boolean) => void;
@ -33,7 +34,14 @@ export const NetworkTab = ({
onRemotetunnelChange, onRemotetunnelChange,
onNocertifyChange, onNocertifyChange,
onWebsearchChange, onWebsearchChange,
}: NetworkTabProps) => ( }: NetworkTabProps) => {
const [portInput, setPortInput] = useState(port?.toString() ?? '');
useEffect(() => {
setPortInput(port?.toString() ?? '');
}, [port]);
return (
<Stack gap="lg"> <Stack gap="lg">
<Group gap="lg" align="flex-start"> <Group gap="lg" align="flex-start">
<div> <div>
@ -56,14 +64,31 @@ export const NetworkTab = ({
<Text size="sm" fw={500}> <Text size="sm" fw={500}>
Port Port
</Text> </Text>
<InfoTooltip label="The port number on which KoboldCpp will listen for connections. Default is 5001." /> <InfoTooltip label="The port number on which KoboldCpp will listen for connections. Leave empty to use default port 5001." />
</Group> </Group>
<TextInput <TextInput
placeholder="5001" placeholder="5001"
value={port.toString()} value={portInput}
onChange={(event) => onChange={(event) => {
onPortChange(Number(event.currentTarget.value) || 5001) const value = event.currentTarget.value;
setPortInput(value);
if (value === '') {
return;
} }
const numValue = Number(value);
if (!isNaN(numValue) && numValue >= 1 && numValue <= 65535) {
onPortChange(numValue);
}
}}
onBlur={(event) => {
const value = event.currentTarget.value;
if (value === '') {
onPortChange(undefined);
setPortInput('');
}
}}
type="number" type="number"
min={1} min={1}
max={65535} max={65535}
@ -133,7 +158,9 @@ export const NetworkTab = ({
<Group gap="xs" align="center"> <Group gap="xs" align="center">
<Checkbox <Checkbox
checked={websearch} checked={websearch}
onChange={(event) => onWebsearchChange(event.currentTarget.checked)} onChange={(event) =>
onWebsearchChange(event.currentTarget.checked)
}
label="Enable WebSearch" label="Enable WebSearch"
/> />
<InfoTooltip label="Enable the local search engine proxy so Web Searches can be done." /> <InfoTooltip label="Enable the local search engine proxy so Web Searches can be done." />
@ -142,3 +169,4 @@ export const NetworkTab = ({
</div> </div>
</Stack> </Stack>
); );
};

View file

@ -1,42 +0,0 @@
import { Modal, Stack, TextInput, Group, Button } from '@mantine/core';
import { Save } from 'lucide-react';
interface SaveConfigModalProps {
opened: boolean;
onClose: () => void;
configName: string;
onConfigNameChange: (name: string) => void;
onSave: () => void;
}
export const SaveConfigModal = ({
opened,
onClose,
configName,
onConfigNameChange,
onSave,
}: SaveConfigModalProps) => (
<Modal opened={opened} onClose={onClose} title="Save Configuration" size="sm">
<Stack gap="md">
<TextInput
label="Configuration Name"
placeholder="Enter a name for this configuration"
value={configName}
onChange={(event) => onConfigNameChange(event.currentTarget.value)}
data-autofocus
/>
<Group justify="flex-end" gap="sm">
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<Button
disabled={!configName.trim()}
leftSection={<Save size={16} />}
onClick={onSave}
>
Save Configuration
</Button>
</Group>
</Stack>
</Modal>
);

View file

@ -7,15 +7,24 @@ import {
Title, Title,
Group, Group,
Button, Button,
Select,
Modal,
TextInput,
Badge,
} from '@mantine/core'; } from '@mantine/core';
import { useState, useEffect, useCallback } from 'react'; import {
useState,
useEffect,
useCallback,
forwardRef,
type ComponentPropsWithoutRef,
} from 'react';
import { Save, File, Plus } from 'lucide-react';
import { useLaunchConfig } from '@/hooks/useLaunchConfig'; import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import { ConfigurationManager } from '@/screens/Launch/ConfigurationManager';
import { GeneralTab } from '@/screens/Launch/GeneralTab'; import { GeneralTab } from '@/screens/Launch/GeneralTab';
import { AdvancedTab } from '@/screens/Launch/AdvancedTab'; import { AdvancedTab } from '@/screens/Launch/AdvancedTab';
import { NetworkTab } from '@/screens/Launch/NetworkTab'; import { NetworkTab } from '@/screens/Launch/NetworkTab';
import { ImageGenerationTab } from '@/screens/Launch/ImageGenerationTab'; import { ImageGenerationTab } from '@/screens/Launch/ImageGenerationTab';
import { SaveConfigModal } from '@/screens/Launch/SaveConfigModal';
import type { ConfigFile } from '@/types'; import type { ConfigFile } from '@/types';
interface LaunchScreenProps { interface LaunchScreenProps {
@ -23,6 +32,39 @@ interface LaunchScreenProps {
onLaunchModeChange?: (isImageMode: boolean) => void; onLaunchModeChange?: (isImageMode: boolean) => void;
} }
interface SelectItemProps extends ComponentPropsWithoutRef<'div'> {
label: string;
extension: string;
}
const getBadgeColor = (extension: string) => {
switch (extension.toLowerCase()) {
case '.kcpps':
return 'blue';
case '.kcppt':
return 'green';
default:
return 'gray';
}
};
const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
({ label, extension, ...others }, ref) => (
<div ref={ref} {...others}>
<Group justify="space-between" wrap="nowrap">
<Text size="sm" truncate>
{label}
</Text>
<Badge size="xs" variant="light" color={getBadgeColor(extension)}>
{extension}
</Badge>
</Group>
</div>
)
);
SelectItem.displayName = 'SelectItem';
export const LaunchScreen = ({ export const LaunchScreen = ({
onLaunch, onLaunch,
onLaunchModeChange, onLaunchModeChange,
@ -32,7 +74,7 @@ export const LaunchScreen = ({
const [, setInstallDir] = useState<string>(''); const [, setInstallDir] = useState<string>('');
const [isLaunching, setIsLaunching] = useState(false); const [isLaunching, setIsLaunching] = useState(false);
const [activeTab, setActiveTab] = useState<string | null>('general'); const [activeTab, setActiveTab] = useState<string | null>('general');
const [saveModalOpened, setSaveModalOpened] = useState(false); const [saveAsModalOpened, setSaveAsModalOpened] = useState(false);
const [newConfigName, setNewConfigName] = useState(''); const [newConfigName, setNewConfigName] = useState('');
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const { const {
@ -52,13 +94,17 @@ export const LaunchScreen = ({
flashattention, flashattention,
noavx2, noavx2,
failsafe, failsafe,
lowvram,
quantmatmul,
backend, backend,
gpuDevice,
sdmodel, sdmodel,
sdt5xxl, sdt5xxl,
sdclipl, sdclipl,
sdclipg, sdclipg,
sdphotomaker, sdphotomaker,
sdvae, sdvae,
sdlora,
parseAndApplyConfigFile, parseAndApplyConfigFile,
loadSavedSettings, loadSavedSettings,
loadConfigFromFile, loadConfigFromFile,
@ -79,7 +125,10 @@ export const LaunchScreen = ({
handleFlashattentionChange, handleFlashattentionChange,
handleNoavx2Change, handleNoavx2Change,
handleFailsafeChange, handleFailsafeChange,
handleLowvramChange,
handleQuantmatmulChange,
handleBackendChange, handleBackendChange,
handleGpuDeviceChange,
handleSdmodelChange, handleSdmodelChange,
handleSelectSdmodelFile, handleSelectSdmodelFile,
handleSdt5xxlChange, handleSdt5xxlChange,
@ -92,6 +141,8 @@ export const LaunchScreen = ({
handleSelectSdphotomakerFile, handleSelectSdphotomakerFile,
handleSdvaeChange, handleSdvaeChange,
handleSelectSdvaeFile, handleSelectSdvaeFile,
handleSdloraChange,
handleSelectSdloraFile,
handleApplyPreset, handleApplyPreset,
} = useLaunchConfig(); } = useLaunchConfig();
@ -156,7 +207,7 @@ export const LaunchScreen = ({
setHasUnsavedChanges(true); setHasUnsavedChanges(true);
}; };
const handlePortChangeWithTracking = (port: number) => { const handlePortChangeWithTracking = (port: number | undefined) => {
handlePortChange(port); handlePortChange(port);
setHasUnsavedChanges(true); setHasUnsavedChanges(true);
}; };
@ -186,6 +237,16 @@ export const LaunchScreen = ({
setHasUnsavedChanges(true); setHasUnsavedChanges(true);
}; };
const handleLowvramChangeWithTracking = (lowvram: boolean) => {
handleLowvramChange(lowvram);
setHasUnsavedChanges(true);
};
const handleQuantmatmulChangeWithTracking = (quantmatmul: boolean) => {
handleQuantmatmulChange(quantmatmul);
setHasUnsavedChanges(true);
};
const handleMultiuserChangeWithTracking = (multiuser: boolean) => { const handleMultiuserChangeWithTracking = (multiuser: boolean) => {
handleMultiuserChange(multiuser); handleMultiuserChange(multiuser);
setHasUnsavedChanges(true); setHasUnsavedChanges(true);
@ -246,6 +307,11 @@ export const LaunchScreen = ({
setHasUnsavedChanges(true); setHasUnsavedChanges(true);
}; };
const handleSdloraChangeWithTracking = (path: string) => {
handleSdloraChange(path);
setHasUnsavedChanges(true);
};
useEffect(() => { useEffect(() => {
void loadConfigFiles(); void loadConfigFiles();
@ -302,6 +368,10 @@ export const LaunchScreen = ({
if (sdvae.trim()) { if (sdvae.trim()) {
args.push('--sdvae', sdvae); args.push('--sdvae', sdvae);
} }
if (sdlora.trim()) {
args.push('--sdlora', sdlora);
}
} else { } else {
args.push('--model', modelPath); args.push('--model', modelPath);
} }
@ -316,8 +386,9 @@ export const LaunchScreen = ({
args.push('--contextsize', contextSize.toString()); args.push('--contextsize', contextSize.toString());
} }
if (port !== 5001) { const actualPort = port ?? 5001;
args.push('--port', port.toString()); if (port !== undefined) {
args.push('--port', actualPort.toString());
} }
if (host !== 'localhost') { if (host !== 'localhost') {
@ -353,10 +424,14 @@ export const LaunchScreen = ({
} }
if (backend && backend !== 'cpu') { if (backend && backend !== 'cpu') {
if (backend === 'cuda') { if (backend === 'cuda' || backend === 'rocm') {
args.push('--usecuda'); const cudaArgs = ['--usecuda'];
} else if (backend === 'rocm') {
args.push('--usecuda'); cudaArgs.push(lowvram ? 'lowvram' : 'normal');
cudaArgs.push(gpuDevice.toString());
cudaArgs.push(quantmatmul ? 'mmq' : 'nommq');
args.push(...cudaArgs);
} else if (backend === 'vulkan') { } else if (backend === 'vulkan') {
args.push('--usevulkan'); args.push('--usevulkan');
} else if (backend === 'clblast') { } else if (backend === 'clblast') {
@ -394,23 +469,11 @@ export const LaunchScreen = ({
return ( return (
<Container size="sm"> <Container size="sm">
<Stack gap="md">
<Card withBorder radius="md" shadow="sm" p="lg">
<Stack gap="lg"> <Stack gap="lg">
<Card withBorder radius="md" shadow="sm">
<Group justify="space-between" align="center"> <Group justify="space-between" align="center">
<div>
<Title order={3}>Launch Configuration</Title> <Title order={3}>Launch Configuration</Title>
<Text size="sm" c="dimmed">
{selectedFile
? `Using: ${selectedFile}`
: 'No configuration file selected'}
{hasUnsavedChanges && (
<Text span c="orange">
{' '}
Unsaved changes
</Text>
)}
</Text>
</div>
<Button <Button
radius="md" radius="md"
disabled={(!modelPath && !sdmodel) || isLaunching} disabled={(!modelPath && !sdmodel) || isLaunching}
@ -418,23 +481,104 @@ export const LaunchScreen = ({
loading={isLaunching} loading={isLaunching}
size="lg" size="lg"
variant="filled" variant="filled"
color="blue"
style={{
fontWeight: 600,
fontSize: '16px',
padding: '12px 28px',
minWidth: '120px',
textTransform: 'uppercase',
letterSpacing: '0.5px',
}}
> >
{isLaunching ? 'Launching...' : 'Launch'} {isLaunching ? 'Launching...' : 'Launch'}
</Button> </Button>
</Group> </Group>
</Card>
<Card withBorder radius="md"> <Stack gap="xs">
<ConfigurationManager <Text fw={500} size="sm">
configFiles={configFiles} Configuration File
selectedFile={selectedFile} </Text>
onFileSelection={handleFileSelection} {configFiles.length === 0 ? (
onSaveAsNew={() => setSaveModalOpened(true)} <Text c="dimmed" size="sm">
onUpdateCurrent={() => {}} No configuration files found in the installation directory.
</Text>
) : (
(() => {
const selectData = configFiles.map((file) => {
const extension = file.name.split('.').pop() || '';
const nameWithoutExtension = file.name.replace(
`.${extension}`,
''
);
return {
value: file.name,
label: nameWithoutExtension,
extension: `.${extension}`,
};
});
return (
<Group gap="xs" align="flex-end">
<div style={{ flex: 1 }}>
<Select
placeholder="Select a configuration file"
value={selectedFile}
onChange={(value: string | null) =>
value && handleFileSelection(value)
}
data={selectData}
leftSection={<File size={16} />}
searchable
clearable={false}
renderOption={({ option }) => {
const dataItem = selectData.find(
(item) => item.value === option.value
);
const extension = dataItem?.extension || '';
return (
<SelectItem
label={option.label}
extension={extension}
/> />
</Card> );
}}
/>
</div>
<Button
variant="light"
leftSection={<Plus size={14} />}
size="sm"
onClick={() => {
setSelectedFile(null);
setHasUnsavedChanges(true);
}}
>
New
</Button>
<Button
variant="outline"
leftSection={<Save size={14} />}
size="sm"
disabled={!hasUnsavedChanges}
onClick={() => {
if (selectedFile) {
setHasUnsavedChanges(false);
} else {
setSaveAsModalOpened(true);
}
}}
>
Save
</Button>
</Group>
);
})()
)}
</Stack>
<Card withBorder radius="md">
<Tabs value={activeTab} onChange={setActiveTab}> <Tabs value={activeTab} onChange={setActiveTab}>
<Tabs.List> <Tabs.List>
<Tabs.Tab value="general">General</Tabs.Tab> <Tabs.Tab value="general">General</Tabs.Tab>
@ -451,14 +595,18 @@ export const LaunchScreen = ({
autoGpuLayers={autoGpuLayers} autoGpuLayers={autoGpuLayers}
contextSize={contextSize} contextSize={contextSize}
backend={backend} backend={backend}
gpuDevice={gpuDevice}
noavx2={noavx2} noavx2={noavx2}
failsafe={failsafe} failsafe={failsafe}
onModelPathChange={handleModelPathChangeWithTracking} onModelPathChange={handleModelPathChangeWithTracking}
onSelectModelFile={handleSelectModelFile} onSelectModelFile={handleSelectModelFile}
onGpuLayersChange={handleGpuLayersChangeWithTracking} onGpuLayersChange={handleGpuLayersChangeWithTracking}
onAutoGpuLayersChange={handleAutoGpuLayersChangeWithTracking} onAutoGpuLayersChange={
handleAutoGpuLayersChangeWithTracking
}
onContextSizeChange={handleContextSizeChangeWithTracking} onContextSizeChange={handleContextSizeChangeWithTracking}
onBackendChange={handleBackendChangeWithTracking} onBackendChange={handleBackendChangeWithTracking}
onGpuDeviceChange={handleGpuDeviceChange}
/> />
</Tabs.Panel> </Tabs.Panel>
@ -469,6 +617,9 @@ export const LaunchScreen = ({
flashattention={flashattention} flashattention={flashattention}
noavx2={noavx2} noavx2={noavx2}
failsafe={failsafe} failsafe={failsafe}
lowvram={lowvram}
quantmatmul={quantmatmul}
backend={backend}
onAdditionalArgumentsChange={ onAdditionalArgumentsChange={
handleAdditionalArgumentsChangeWithTracking handleAdditionalArgumentsChangeWithTracking
} }
@ -478,6 +629,8 @@ export const LaunchScreen = ({
} }
onNoavx2Change={handleNoavx2ChangeWithTracking} onNoavx2Change={handleNoavx2ChangeWithTracking}
onFailsafeChange={handleFailsafeChangeWithTracking} onFailsafeChange={handleFailsafeChangeWithTracking}
onLowvramChange={handleLowvramChangeWithTracking}
onQuantmatmulChange={handleQuantmatmulChangeWithTracking}
/> />
</Tabs.Panel> </Tabs.Panel>
@ -508,6 +661,7 @@ export const LaunchScreen = ({
sdclipg={sdclipg} sdclipg={sdclipg}
sdphotomaker={sdphotomaker} sdphotomaker={sdphotomaker}
sdvae={sdvae} sdvae={sdvae}
sdlora={sdlora}
textModelPath={modelPath} textModelPath={modelPath}
onSdmodelChange={handleSdmodelChangeWithTracking} onSdmodelChange={handleSdmodelChangeWithTracking}
onSelectSdmodelFile={handleSelectSdmodelFile} onSelectSdmodelFile={handleSelectSdmodelFile}
@ -521,23 +675,51 @@ export const LaunchScreen = ({
onSelectSdphotomakerFile={handleSelectSdphotomakerFile} onSelectSdphotomakerFile={handleSelectSdphotomakerFile}
onSdvaeChange={handleSdvaeChangeWithTracking} onSdvaeChange={handleSdvaeChangeWithTracking}
onSelectSdvaeFile={handleSelectSdvaeFile} onSelectSdvaeFile={handleSelectSdvaeFile}
onSdloraChange={handleSdloraChangeWithTracking}
onSelectSdloraFile={handleSelectSdloraFile}
onApplyPreset={handleApplyPreset} onApplyPreset={handleApplyPreset}
/> />
</Tabs.Panel> </Tabs.Panel>
</div> </div>
</Tabs> </Tabs>
</Stack>
</Card> </Card>
<SaveConfigModal <Modal
opened={saveModalOpened} opened={saveAsModalOpened}
onClose={() => setSaveModalOpened(false)} onClose={() => setSaveAsModalOpened(false)}
configName={newConfigName} title="Save Configuration As..."
onConfigNameChange={setNewConfigName} size="sm"
onSave={() => { >
setSaveModalOpened(false); <Stack gap="md">
setNewConfigName(''); <TextInput
}} label="Configuration Name"
placeholder="Enter a name for this configuration"
value={newConfigName}
onChange={(event) => setNewConfigName(event.currentTarget.value)}
data-autofocus
/> />
<Group justify="flex-end" gap="sm">
<Button
variant="outline"
onClick={() => setSaveAsModalOpened(false)}
>
Cancel
</Button>
<Button
disabled={!newConfigName.trim()}
leftSection={<Save size={16} />}
onClick={() => {
setSaveAsModalOpened(false);
setNewConfigName('');
setHasUnsavedChanges(false);
}}
>
Save As New
</Button>
</Group>
</Stack>
</Modal>
</Stack> </Stack>
</Container> </Container>
); );

View file

@ -72,6 +72,19 @@ export interface KoboldAPI {
detectROCm: () => Promise<{ supported: boolean; devices: string[] }>; detectROCm: () => Promise<{ supported: boolean; devices: string[] }>;
detectHardware: () => Promise<HardwareInfo>; detectHardware: () => Promise<HardwareInfo>;
detectAllCapabilities: () => Promise<HardwareInfo>; detectAllCapabilities: () => Promise<HardwareInfo>;
detectBackendSupport: (binaryPath: string) => Promise<{
rocm: boolean;
vulkan: boolean;
clblast: boolean;
noavx2: boolean;
failsafe: boolean;
cuda: boolean;
}>;
getAvailableBackends: (
binaryPath: string,
hardwareCapabilities: GPUCapabilities
) => Promise<Array<{ value: string; label: string; devices?: string[] }>>;
clearBinaryCache: () => Promise<void>;
getCurrentInstallDir: () => Promise<string>; getCurrentInstallDir: () => Promise<string>;
selectInstallDirectory: () => Promise<string | null>; selectInstallDirectory: () => Promise<string | null>;
downloadRelease: ( downloadRelease: (

View file

@ -1,7 +1,7 @@
export interface CPUCapabilities { export interface CPUCapabilities {
avx: boolean; avx: boolean;
avx2: boolean; avx2: boolean;
cpuInfo: string[]; devices: string[];
} }
export interface GPUCapabilities { export interface GPUCapabilities {

View file

@ -1,35 +0,0 @@
export const removeBinaryExtension = (filename: string): string => {
if (filename.endsWith('.exe')) {
return filename.slice(0, -4);
}
return filename;
};
export const getBinaryBaseName = (filename: string): string => {
const baseName = filename.split(/[/\\]/).pop() || filename;
return removeBinaryExtension(baseName).toLowerCase();
};
export const isOldPcBinary = (filename: string): boolean => {
const baseName = getBinaryBaseName(filename);
return baseName.endsWith('oldpc');
};
export const isNoCudaBinary = (filename: string): boolean => {
const baseName = getBinaryBaseName(filename);
return baseName.endsWith('nocuda');
};
export const isRocmBinary = (filename: string): boolean => {
const baseName = getBinaryBaseName(filename);
return baseName.endsWith('rocm');
};
export const getBinaryType = (
filename: string
): 'oldpc' | 'nocuda' | 'rocm' | 'standard' => {
if (isOldPcBinary(filename)) return 'oldpc';
if (isNoCudaBinary(filename)) return 'nocuda';
if (isRocmBinary(filename)) return 'rocm';
return 'standard';
};