use autoGpuLayers by default and persist its value to config, show un update app icon in appheader, more code cleanup

This commit is contained in:
Egor 2025-08-28 16:52:13 -07:00
parent c00b97a3f6
commit aa554a1b58
16 changed files with 421 additions and 379 deletions

View file

@ -13,7 +13,7 @@ A desktop app for running Large Language Models locally. <!-- markdownlint-disab
- **Smart process management** - Prevents runaway background processes and system resource waste
- **Optimized performance** - Automatically unpacks binaries for faster operation and reduced memory usage
- **Image generation support** - Built-in presets for Flux and Chroma image generation workflows
- **Adaptive theming** - Light, dark, and system theme modes that automatically follow your OS preferences
- **SillyTavern integration** - Seamlessly launch SillyTavern for advanced character interactions (requires [Node.js](https://nodejs.org/))
- **Privacy-focused** - Everything runs locally on your machine, no data sent to external servers
## Installation

View file

@ -10,8 +10,9 @@ import {
Image,
Tooltip,
} from '@mantine/core';
import { Settings, ArrowLeft } from 'lucide-react';
import { Settings, ArrowLeft, CircleFadingArrowUp } from 'lucide-react';
import { soundAssets, playSound, initializeAudio } from '@/utils/sounds';
import { useAppUpdateChecker } from '@/hooks/useAppUpdateChecker';
import iconUrl from '/icon.png';
import { FRONTENDS } from '@/constants';
import type { InterfaceTab, FrontendPreference, Screen } from '@/types';
@ -38,6 +39,7 @@ export const AppHeader = ({
const [logoClickCount, setLogoClickCount] = useState(0);
const [isElephantMode, setIsElephantMode] = useState(false);
const [isMouseSqueaking, setIsMouseSqueaking] = useState(false);
const { hasUpdate, openReleasePage } = useAppUpdateChecker();
const handleLogoClick = async () => {
await initializeAudio();
@ -148,8 +150,25 @@ export const AppHeader = ({
minWidth: '6.25rem',
display: 'flex',
justifyContent: 'flex-end',
gap: '0.5rem',
alignItems: 'center',
}}
>
{hasUpdate && (
<Tooltip label="New release available" position="bottom">
<ActionIcon
variant="light"
color="orange"
size="xl"
onClick={openReleasePage}
aria-label="New release available"
>
<CircleFadingArrowUp
style={{ width: rem(20), height: rem(20) }}
/>
</ActionIcon>
</Tooltip>
)}
<Tooltip label="Settings" position="bottom">
<ActionIcon
variant="subtle"

View file

@ -163,6 +163,7 @@ export const LaunchScreen = ({
};
const buildConfigData = () => ({
autoGpuLayers: autoGpuLayers,
gpulayers: gpuLayers,
contextsize: contextSize,
model: modelPath,

View file

@ -42,6 +42,7 @@ export const VersionsTab = () => {
downloadProgress,
loadRemoteVersions,
handleDownload: sharedHandleDownload,
getLatestReleaseWithDownloadStatus,
} = useKoboldVersions();
const [installedVersions, setInstalledVersions] = useState<
@ -104,8 +105,7 @@ export const VersionsTab = () => {
const loadLatestRelease = useCallback(async () => {
try {
const release =
await window.electronAPI.kobold.getLatestReleaseWithStatus();
const release = await getLatestReleaseWithDownloadStatus();
setLatestRelease(release);
} catch (error) {
window.electronAPI.logs.logError(
@ -113,7 +113,7 @@ export const VersionsTab = () => {
error as Error
);
}
}, []);
}, [getLatestReleaseWithDownloadStatus]);
useEffect(() => {
loadInstalledVersions();

View file

@ -0,0 +1,80 @@
import { useState, useCallback, useEffect } from 'react';
import { compareVersions } from '@/utils/downloadUtils';
import { GITHUB_API } from '@/constants';
interface AppUpdateInfo {
currentVersion: string;
latestVersion: string;
releaseUrl: string;
hasUpdate: boolean;
}
export const useAppUpdateChecker = () => {
const [updateInfo, setUpdateInfo] = useState<AppUpdateInfo | null>(null);
const [isChecking, setIsChecking] = useState(false);
const [lastChecked, setLastChecked] = useState<Date | null>(null);
const checkForAppUpdates = useCallback(async () => {
setIsChecking(true);
try {
const currentVersion = await window.electronAPI.app.getVersion();
const response = await fetch(
`${GITHUB_API.BASE_URL}/repos/${GITHUB_API.FRIENDLY_KOBOLD_REPO}/releases/latest`
);
if (!response.ok) {
throw new Error('Failed to fetch latest release');
}
const release = await response.json();
const latestVersion = release.tag_name?.replace(/^v/, '') || '';
if (!latestVersion) {
throw new Error('Invalid release data');
}
const hasUpdate = compareVersions(latestVersion, currentVersion) > 0;
const updateInfo: AppUpdateInfo = {
currentVersion,
latestVersion,
releaseUrl: release.html_url,
hasUpdate,
};
setUpdateInfo(updateInfo);
setLastChecked(new Date());
return updateInfo;
} catch (error) {
window.electronAPI.logs.logError(
'Failed to check for app updates:',
error as Error
);
return null;
} finally {
setIsChecking(false);
}
}, []);
const openReleasePage = useCallback(() => {
if (updateInfo?.releaseUrl) {
window.electronAPI.app.openExternal(updateInfo.releaseUrl);
}
}, [updateInfo]);
useEffect(() => {
checkForAppUpdates();
}, [checkForAppUpdates]);
return {
updateInfo,
isChecking,
lastChecked,
checkForAppUpdates,
openReleasePage,
hasUpdate: updateInfo?.hasUpdate || false,
};
};

View file

@ -1,5 +1,13 @@
import { useState, useEffect, useCallback } from 'react';
import type { DownloadItem } from '@/types/electron';
import { GITHUB_API, ROCM } from '@/constants';
import { filterAssetsByPlatform } from '@/utils/platform';
import type {
DownloadItem,
GitHubRelease,
ReleaseWithStatus,
GitHubAsset,
InstalledVersion,
} from '@/types/electron';
interface PlatformInfo {
platform: string;
@ -7,6 +15,164 @@ interface PlatformInfo {
hasROCm: boolean;
}
interface CachedReleaseData {
releases: DownloadItem[];
timestamp: number;
}
const CACHE_KEY = 'kobold-releases-cache';
const CACHE_DURATION = 60000;
const loadFromCache = (): CachedReleaseData | null => {
try {
const cached = localStorage.getItem(CACHE_KEY);
if (!cached) return null;
const data: CachedReleaseData = JSON.parse(cached);
const isExpired = Date.now() - data.timestamp > CACHE_DURATION;
return isExpired ? null : data;
} catch {
return null;
}
};
const saveToCache = (releases: DownloadItem[]) => {
try {
const data: CachedReleaseData = {
releases,
timestamp: Date.now(),
};
localStorage.setItem(CACHE_KEY, JSON.stringify(data));
} catch (error) {
window.electronAPI.logs.logError(
'Failed to save releases to cache',
error as Error
);
}
};
const transformReleaseToDownloadItems = (
release: GitHubRelease,
platform: string
): DownloadItem[] => {
const version = release.tag_name?.replace(/^v/, '') || 'unknown';
const platformAssets = filterAssetsByPlatform(release.assets, platform);
return platformAssets.map((asset) => ({
name: asset.name,
url: asset.browser_download_url,
size: asset.size,
version,
type: 'asset' as const,
}));
};
const fetchLatestReleaseFromAPI = async (
platform: string
): Promise<DownloadItem[]> => {
const response = await fetch(GITHUB_API.LATEST_RELEASE_URL);
if (!response.ok) {
if (response.status === 403) {
throw new Error('GitHub API rate limit reached');
}
throw new Error(`HTTP error! status: ${response.status}`);
}
const release: GitHubRelease = await response.json();
return transformReleaseToDownloadItems(release, platform);
};
const getROCmDownload = async (
platform: string
): Promise<DownloadItem | null> => {
if (platform !== 'linux') {
return null;
}
try {
const response = await fetch(GITHUB_API.LATEST_RELEASE_URL);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const latestRelease = await response.json();
const version = latestRelease?.tag_name?.replace(/^v/, '') || 'unknown';
return {
name: ROCM.BINARY_NAME,
url: ROCM.DOWNLOAD_URL,
size: ROCM.SIZE_BYTES_APPROX,
version,
type: 'rocm',
};
} catch (error) {
window.electronAPI.logs.logError(
'Failed to fetch ROCm version info:',
error as Error
);
return {
name: ROCM.BINARY_NAME,
url: ROCM.DOWNLOAD_URL,
size: ROCM.SIZE_BYTES_APPROX,
version: 'unknown',
type: 'rocm',
};
}
};
const getLatestReleaseWithDownloadStatus =
async (): Promise<ReleaseWithStatus | null> => {
try {
const response = await fetch(GITHUB_API.LATEST_RELEASE_URL);
if (!response.ok) return null;
const latestRelease = await response.json();
if (!latestRelease) return null;
const installedVersions =
await window.electronAPI.kobold.getInstalledVersions();
const availableAssets = latestRelease.assets.map((asset: GitHubAsset) => {
const installedVersion = installedVersions.find(
(v: InstalledVersion) => {
const pathParts = v.path.split(/[/\\]/);
const launcherIndex = pathParts.findIndex(
(part) =>
part === 'koboldcpp-launcher' ||
part === 'koboldcpp-launcher.exe'
);
if (launcherIndex > 0) {
const directoryName = pathParts[launcherIndex - 1];
return directoryName === asset.name;
}
return false;
}
);
return {
asset,
isDownloaded: !!installedVersion,
installedVersion: installedVersion?.version,
};
});
return {
release: latestRelease,
availableAssets,
};
} catch (error) {
window.electronAPI.logs.logError(
'Failed to fetch latest release with status:',
error as Error
);
return null;
}
};
interface UseKoboldVersionsReturn {
platformInfo: PlatformInfo;
availableDownloads: DownloadItem[];
@ -15,6 +181,7 @@ interface UseKoboldVersionsReturn {
downloading: string | null;
downloadProgress: Record<string, number>;
loadRemoteVersions: () => Promise<void>;
refresh: () => Promise<void>;
handleDownload: (
type: 'asset' | 'rocm',
item?: DownloadItem,
@ -27,6 +194,8 @@ interface UseKoboldVersionsReturn {
| Record<string, number>
| ((prev: Record<string, number>) => Record<string, number>)
) => void;
getROCmDownload: (platform?: string) => Promise<DownloadItem | null>;
getLatestReleaseWithDownloadStatus: () => Promise<ReleaseWithStatus | null>;
}
export const useKoboldVersions = (): UseKoboldVersionsReturn => {
@ -93,11 +262,25 @@ export const useKoboldVersions = (): UseKoboldVersionsReturn => {
setLoadingRemote(true);
try {
const cached = loadFromCache();
if (cached) {
const rocm = await getROCmDownload(platformInfo.platform);
const allDownloads: DownloadItem[] = [...cached.releases];
if (rocm) {
allDownloads.push(rocm);
}
setAvailableDownloads(allDownloads);
setLoadingRemote(false);
return;
}
const [releases, rocm] = await Promise.all([
window.electronAPI.kobold.getLatestRelease(),
window.electronAPI.kobold.getROCmDownload(),
fetchLatestReleaseFromAPI(platformInfo.platform),
getROCmDownload(platformInfo.platform),
]);
saveToCache(releases);
const allDownloads: DownloadItem[] = [...releases];
if (rocm) {
allDownloads.push(rocm);
@ -109,6 +292,18 @@ export const useKoboldVersions = (): UseKoboldVersionsReturn => {
'Failed to load remote versions:',
error as Error
);
const cached = loadFromCache();
if (cached) {
const rocm = await getROCmDownload(platformInfo.platform).catch(
() => null
);
const allDownloads: DownloadItem[] = [...cached.releases];
if (rocm) {
allDownloads.push(rocm);
}
setAvailableDownloads(allDownloads);
}
} finally {
setLoadingRemote(false);
}
@ -160,6 +355,11 @@ export const useKoboldVersions = (): UseKoboldVersionsReturn => {
[]
);
const refresh = useCallback(async () => {
localStorage.removeItem(CACHE_KEY);
await loadRemoteVersions();
}, [loadRemoteVersions]);
useEffect(() => {
loadPlatformInfo();
}, [loadPlatformInfo]);
@ -195,8 +395,12 @@ export const useKoboldVersions = (): UseKoboldVersionsReturn => {
downloading,
downloadProgress,
loadRemoteVersions,
refresh,
handleDownload,
setDownloading,
setDownloadProgress,
getROCmDownload: (platform?: string) =>
getROCmDownload(platform || platformInfo.platform),
getLatestReleaseWithDownloadStatus,
};
};

View file

@ -1,6 +1,7 @@
import { useState, useCallback, useEffect } from 'react';
import { getDisplayNameFromPath } from '@/utils/versionUtils';
import { compareVersions } from '@/utils/downloadUtils';
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
import type { InstalledVersion, DownloadItem } from '@/types/electron';
interface UpdateInfo {
@ -17,6 +18,8 @@ export const useUpdateChecker = () => {
);
const [dismissedUpdatesLoaded, setDismissedUpdatesLoaded] = useState(false);
const { availableDownloads: releases, getROCmDownload } = useKoboldVersions();
useEffect(() => {
const loadDismissedUpdates = async () => {
try {
@ -54,28 +57,26 @@ export const useUpdateChecker = () => {
}, []);
const checkForUpdates = useCallback(async () => {
if (!dismissedUpdatesLoaded) {
if (!dismissedUpdatesLoaded || releases.length === 0) {
return;
}
setIsChecking(true);
try {
const [currentBinaryPath, installedVersions, releases, rocm] =
const [currentBinaryPath, installedVersionsResult, rocmDownload] =
await Promise.all([
window.electronAPI.config.get(
'currentKoboldBinary'
) as Promise<string>,
window.electronAPI.kobold.getInstalledVersions(),
window.electronAPI.kobold.getLatestRelease(),
window.electronAPI.kobold.getROCmDownload(),
getROCmDownload(),
]);
if (!currentBinaryPath || installedVersions.length === 0) {
if (!currentBinaryPath || installedVersionsResult.length === 0) {
return;
}
const currentVersion = installedVersions.find(
const currentVersion = installedVersionsResult.find(
(v: InstalledVersion) => v.path === currentBinaryPath
);
if (!currentVersion) {
@ -83,8 +84,8 @@ export const useUpdateChecker = () => {
}
const availableDownloads: DownloadItem[] = [...releases];
if (rocm) {
availableDownloads.push(rocm);
if (rocmDownload) {
availableDownloads.push(rocmDownload);
}
const currentDisplayName = getDisplayNameFromPath(currentVersion);
@ -122,7 +123,7 @@ export const useUpdateChecker = () => {
} finally {
setIsChecking(false);
}
}, [dismissedUpdates, dismissedUpdatesLoaded]);
}, [dismissedUpdates, dismissedUpdatesLoaded, releases, getROCmDownload]);
const dismissUpdate = useCallback(async () => {
if (updateInfo) {

View file

@ -7,7 +7,6 @@ import { ConfigManager } from '@/main/managers/ConfigManager';
import { LogManager } from '@/main/managers/LogManager';
import { KoboldCppManager } from '@/main/managers/KoboldCppManager';
import { SillyTavernManager } from '@/main/managers/SillyTavernManager';
import { GitHubService } from '@/main/services/GitHubService';
import { HardwareService } from '@/main/services/HardwareService';
import { BinaryService } from '@/main/services/BinaryService';
import { IPCHandlers } from '@/main/ipc';
@ -16,11 +15,10 @@ import { homedir } from 'os';
export class FriendlyKoboldApp {
private windowManager: WindowManager;
private koboldManager: KoboldCppManager;
private configManager: ConfigManager;
private logManager: LogManager;
private koboldManager: KoboldCppManager;
private sillyTavernManager: SillyTavernManager;
private githubService: GitHubService;
private hardwareService: HardwareService;
private binaryService: BinaryService;
private ipcHandlers: IPCHandlers;
@ -35,12 +33,10 @@ export class FriendlyKoboldApp {
);
this.ensureInstallDirectory();
this.windowManager = new WindowManager();
this.githubService = new GitHubService(this.logManager);
this.hardwareService = new HardwareService(this.logManager);
this.koboldManager = new KoboldCppManager(
this.configManager,
this.githubService,
this.windowManager,
this.logManager
);
@ -59,7 +55,6 @@ export class FriendlyKoboldApp {
this.ipcHandlers = new IPCHandlers(
this.koboldManager,
this.configManager,
this.githubService,
this.hardwareService,
this.binaryService,
this.logManager,

View file

@ -1,9 +1,8 @@
import { ipcMain, shell, app } from 'electron';
import { KoboldCppManager } from '@/main/managers/KoboldCppManager';
import { ConfigManager } from '@/main/managers/ConfigManager';
import { LogManager } from '@/main/managers/LogManager';
import { SillyTavernManager } from '@/main/managers/SillyTavernManager';
import { GitHubService } from '@/main/services/GitHubService';
import type { KoboldCppManager } from '@/main/managers/KoboldCppManager';
import type { ConfigManager } from '@/main/managers/ConfigManager';
import type { LogManager } from '@/main/managers/LogManager';
import type { SillyTavernManager } from '@/main/managers/SillyTavernManager';
import { HardwareService } from '@/main/services/HardwareService';
import { BinaryService } from '@/main/services/BinaryService';
import type { FrontendPreference } from '@/types';
@ -13,14 +12,12 @@ export class IPCHandlers {
private configManager: ConfigManager;
private logManager: LogManager;
private sillyTavernManager: SillyTavernManager;
private githubService: GitHubService;
private hardwareService: HardwareService;
private binaryService: BinaryService;
constructor(
koboldManager: KoboldCppManager,
configManager: ConfigManager,
githubService: GitHubService,
hardwareService: HardwareService,
binaryService: BinaryService,
logManager: LogManager,
@ -30,7 +27,6 @@ export class IPCHandlers {
this.configManager = configManager;
this.logManager = logManager;
this.sillyTavernManager = sillyTavernManager;
this.githubService = githubService;
this.hardwareService = hardwareService;
this.binaryService = binaryService;
}
@ -74,15 +70,6 @@ export class IPCHandlers {
}
setupHandlers() {
ipcMain.handle('kobold:getLatestRelease', () =>
this.githubService.getLatestRelease()
);
ipcMain.handle('kobold:checkForUpdates', async () => {
const latest = await this.githubService.getRawLatestRelease();
return latest;
});
ipcMain.handle('kobold:downloadRelease', async (_event, asset) => {
try {
const mainWindow = this.koboldManager
@ -126,14 +113,6 @@ export class IPCHandlers {
this.configManager.setSelectedConfig(configName)
);
ipcMain.handle('kobold:getCurrentVersion', () =>
this.koboldManager.getCurrentVersion()
);
ipcMain.handle('kobold:getCurrentBinaryInfo', () =>
this.koboldManager.getCurrentBinaryInfo()
);
ipcMain.handle('kobold:setCurrentVersion', (_event, version) =>
this.koboldManager.setCurrentVersion(version)
);
@ -162,10 +141,6 @@ export class IPCHandlers {
this.hardwareService.detectROCm()
);
ipcMain.handle('kobold:detectAllCapabilities', () =>
this.hardwareService.detectAllWithCapabilities()
);
ipcMain.handle('kobold:detectBackendSupport', () =>
this.binaryService.detectBackendSupport()
);
@ -181,10 +156,6 @@ export class IPCHandlers {
arch: process.arch,
}));
ipcMain.handle('kobold:getROCmDownload', () =>
this.koboldManager.getROCmDownload()
);
ipcMain.handle('kobold:downloadROCm', async () => {
try {
const mainWindow = this.koboldManager
@ -201,18 +172,6 @@ export class IPCHandlers {
}
});
ipcMain.handle('kobold:getInstalledVersion', () =>
this.koboldManager.getInstalledVersion()
);
ipcMain.handle('kobold:getVersionFromBinary', (_event, binaryPath) =>
this.koboldManager.getVersionFromBinary(binaryPath)
);
ipcMain.handle('kobold:getLatestReleaseWithStatus', () =>
this.koboldManager.getLatestReleaseWithDownloadStatus()
);
ipcMain.handle('kobold:launchKoboldCpp', (_event, args) =>
this.launchKoboldCppWithCustomFrontends(args)
);

View file

@ -16,19 +16,16 @@ import { dialog } from 'electron';
import { execa } from 'execa';
import { got } from 'got';
import { pipeline } from 'stream/promises';
import { GitHubService } from '@/main/services/GitHubService';
import { ConfigManager } from '@/main/managers/ConfigManager';
import { LogManager } from '@/main/managers/LogManager';
import { WindowManager } from '@/main/managers/WindowManager';
import { ROCM, PRODUCT_NAME } from '@/constants';
import { ROCM, PRODUCT_NAME, GITHUB_API } from '@/constants';
import { stripAssetExtensions } from '@/utils/versionUtils';
import { compareVersions } from '@/utils/downloadUtils';
import type {
DownloadItem,
GitHubAsset,
UpdateInfo,
ReleaseWithStatus,
InstalledVersion,
KoboldConfig,
} from '@/types/electron';
export class KoboldCppManager {
@ -36,18 +33,15 @@ export class KoboldCppManager {
private koboldProcess: ChildProcess | null = null;
private configManager: ConfigManager;
private logManager: LogManager;
private githubService: GitHubService;
private windowManager: WindowManager;
constructor(
configManager: ConfigManager,
githubService: GitHubService,
windowManager: WindowManager,
logManager: LogManager
) {
this.configManager = configManager;
this.logManager = logManager;
this.githubService = githubService;
this.windowManager = windowManager;
this.installDir = this.configManager.getInstallDir() || '';
}
@ -265,12 +259,7 @@ export class KoboldCppManager {
return configFiles.sort((a, b) => a.name.localeCompare(b.name));
}
async parseConfigFile(filePath: string): Promise<{
gpulayers?: number;
contextsize?: number;
model?: string;
[key: string]: unknown;
} | null> {
async parseConfigFile(filePath: string): Promise<KoboldConfig | null> {
try {
if (!existsSync(filePath)) {
return null;
@ -288,33 +277,7 @@ export class KoboldCppManager {
async saveConfigFile(
configName: string,
configData: {
gpulayers?: number;
contextsize?: number;
model?: string;
port?: number;
host?: string;
multiuser?: number;
multiplayer?: boolean;
remotetunnel?: boolean;
nocertify?: boolean;
websearch?: boolean;
noshift?: boolean;
flashattention?: boolean;
noavx2?: boolean;
failsafe?: boolean;
usemmap?: boolean;
usecuda?: boolean;
usevulkan?: boolean;
useclblast?: [number, number] | boolean;
sdmodel?: string;
sdt5xxl?: string;
sdclipl?: string;
sdclipg?: string;
sdphotomaker?: string;
sdvae?: string;
[key: string]: unknown;
}
configData: KoboldConfig
): Promise<boolean> {
try {
if (!this.installDir) {
@ -549,16 +512,35 @@ export class KoboldCppManager {
const platform = process.platform;
if (platform === 'linux') {
const latestRelease = await this.githubService.getRawLatestRelease();
const version = latestRelease?.tag_name?.replace(/^v/, '') || 'unknown';
try {
const response = await fetch(GITHUB_API.LATEST_RELEASE_URL);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return {
name: ROCM.BINARY_NAME,
url: ROCM.DOWNLOAD_URL,
size: ROCM.SIZE_BYTES_APPROX,
version,
type: 'rocm',
};
const latestRelease = await response.json();
const version = latestRelease?.tag_name?.replace(/^v/, '') || 'unknown';
return {
name: ROCM.BINARY_NAME,
url: ROCM.DOWNLOAD_URL,
size: ROCM.SIZE_BYTES_APPROX,
version,
type: 'rocm',
};
} catch (error) {
this.logManager.logError(
'Failed to fetch ROCm version info:',
error as Error
);
return {
name: ROCM.BINARY_NAME,
url: ROCM.DOWNLOAD_URL,
size: ROCM.SIZE_BYTES_APPROX,
version: 'unknown',
type: 'rocm',
};
}
} else if (platform === 'win32') {
return null;
// The launcher doesn't exist in unpacked state yet.
@ -706,73 +688,6 @@ export class KoboldCppManager {
return currentVersion?.version;
}
async checkForUpdates(): Promise<UpdateInfo | null> {
try {
const currentVersion = await this.getCurrentVersion();
if (!currentVersion) {
return null;
}
const latestRelease = await this.githubService.getRawLatestRelease();
if (!latestRelease) {
return null;
}
const latestVersion = latestRelease.tag_name.replace(/^v/, '');
const current = currentVersion.version.replace(/^v/, '');
const hasUpdate = compareVersions(current, latestVersion) < 0;
return {
currentVersion: current,
latestVersion,
releaseInfo: latestRelease,
hasUpdate,
};
} catch {
return null;
}
}
async getLatestReleaseWithDownloadStatus(): Promise<ReleaseWithStatus | null> {
try {
const latestRelease = await this.githubService.getRawLatestRelease();
if (!latestRelease) return null;
const installedVersions = await this.getInstalledVersions();
const availableAssets = latestRelease.assets.map((asset: GitHubAsset) => {
const installedVersion = installedVersions.find((v) => {
const pathParts = v.path.split(/[/\\]/);
const launcherIndex = pathParts.findIndex(
(part) =>
part === 'koboldcpp-launcher' || part === 'koboldcpp-launcher.exe'
);
if (launcherIndex > 0) {
const directoryName = pathParts[launcherIndex - 1];
return directoryName === asset.name;
}
return false;
});
return {
asset,
isDownloaded: !!installedVersion,
installedVersion: installedVersion?.version,
};
});
return {
release: latestRelease,
availableAssets,
};
} catch {
return null;
}
}
async launchKoboldCpp(
args: string[] = []
): Promise<{ success: boolean; pid?: number; error?: string }> {

View file

@ -113,6 +113,13 @@ export class SillyTavernManager {
}
}
private createNpxProcess(args: string[]): ChildProcess {
return spawn('npx', args, {
stdio: ['pipe', 'pipe', 'pipe'],
detached: false,
});
}
private async ensureSillyTavernSettings(): Promise<void> {
const settingsPath = this.getSillyTavernSettingsPath();
@ -132,10 +139,7 @@ export class SillyTavernManager {
);
return new Promise((resolve, reject) => {
const initProcess = spawn('npx', spawnArgs, {
stdio: ['pipe', 'pipe', 'pipe'],
detached: false,
});
const initProcess = this.createNpxProcess(spawnArgs);
let hasResolved = false;
@ -364,10 +368,7 @@ export class SillyTavernManager {
config.port.toString(),
];
this.sillyTavernProcess = spawn('npx', sillyTavernArgs, {
stdio: ['pipe', 'pipe', 'pipe'],
detached: false,
});
this.sillyTavernProcess = this.createNpxProcess(sillyTavernArgs);
if (this.sillyTavernProcess.stdout) {
this.sillyTavernProcess.stdout.on('data', (data: Buffer) => {

View file

@ -1,99 +0,0 @@
import type { GitHubRelease, DownloadItem } from '@/types/electron';
import { LogManager } from '@/main/managers/LogManager';
import { GITHUB_API } from '@/constants';
import { filterAssetsByPlatform } from '@/utils/platform';
export class GitHubService {
private lastApiCall = 0;
private apiCooldown = 60000;
private cachedRelease: GitHubRelease | null = null;
private logManager: LogManager;
constructor(logManager: LogManager) {
this.logManager = logManager;
}
async getLatestRelease(): Promise<DownloadItem[]> {
const now = Date.now();
if (now - this.lastApiCall < this.apiCooldown && this.cachedRelease) {
return this.transformReleaseToDownloadItems(this.cachedRelease);
}
try {
const response = await fetch(GITHUB_API.LATEST_RELEASE_URL);
if (!response.ok) {
if (response.status === 403) {
this.logManager.logError(
'GitHub API rate limit reached, using cached data if available'
);
return this.cachedRelease
? this.transformReleaseToDownloadItems(this.cachedRelease)
: [];
}
throw new Error(`HTTP error! status: ${response.status}`);
}
this.lastApiCall = now;
this.cachedRelease = (await response.json()) as GitHubRelease;
return this.transformReleaseToDownloadItems(this.cachedRelease);
} catch (error) {
this.logManager.logError(
'Error fetching latest release:',
error as Error
);
return this.cachedRelease
? this.transformReleaseToDownloadItems(this.cachedRelease)
: [];
}
}
private transformReleaseToDownloadItems(
release: GitHubRelease
): DownloadItem[] {
const version = release.tag_name?.replace(/^v/, '') || 'unknown';
const platformAssets = filterAssetsByPlatform(
release.assets,
process.platform
);
return platformAssets.map((asset) => ({
name: asset.name,
url: asset.browser_download_url,
size: asset.size,
version,
type: 'asset' as const,
}));
}
async getRawLatestRelease(): Promise<GitHubRelease | null> {
const now = Date.now();
if (now - this.lastApiCall < this.apiCooldown && this.cachedRelease) {
return this.cachedRelease;
}
try {
const response = await fetch(GITHUB_API.LATEST_RELEASE_URL);
if (!response.ok) {
if (response.status === 403) {
this.logManager.logError(
'GitHub API rate limit reached, using cached data if available'
);
return this.cachedRelease;
}
throw new Error(`HTTP error! status: ${response.status}`);
}
this.lastApiCall = now;
this.cachedRelease = (await response.json()) as GitHubRelease;
return this.cachedRelease;
} catch (error) {
this.logManager.logError(
'Error fetching latest release:',
error as Error
);
return this.cachedRelease;
}
}
}

View file

@ -141,16 +141,6 @@ export class HardwareService {
return { cpu, gpu };
}
async detectAllWithCapabilities(): Promise<HardwareInfo> {
const [cpu, gpu, gpuCapabilities] = await Promise.all([
this.detectCPU(),
this.detectGPU(),
this.detectGPUCapabilities(),
]);
return { cpu, gpu, gpuCapabilities };
}
private async detectCUDA(): Promise<{
supported: boolean;
devices: string[];

View file

@ -5,18 +5,14 @@ import type {
ConfigAPI,
LogsAPI,
SillyTavernAPI,
KoboldConfig,
} from '@/types/electron';
const koboldAPI: KoboldAPI = {
getInstalledVersions: () => ipcRenderer.invoke('kobold:getInstalledVersions'),
getCurrentBinaryInfo: () => ipcRenderer.invoke('kobold:getCurrentBinaryInfo'),
setCurrentVersion: (version: string) =>
ipcRenderer.invoke('kobold:setCurrentVersion', version),
getLatestReleaseWithStatus: () =>
ipcRenderer.invoke('kobold:getLatestReleaseWithStatus'),
getROCmDownload: () => ipcRenderer.invoke('kobold:getROCmDownload'),
downloadROCm: () => ipcRenderer.invoke('kobold:downloadROCm'),
getLatestRelease: () => ipcRenderer.invoke('kobold:getLatestRelease'),
getPlatform: () => ipcRenderer.invoke('kobold:getPlatform'),
detectGPU: () => ipcRenderer.invoke('kobold:detectGPU'),
detectCPU: () => ipcRenderer.invoke('kobold:detectCPU'),
@ -35,36 +31,8 @@ const koboldAPI: KoboldAPI = {
launchKoboldCpp: (args?: string[]) =>
ipcRenderer.invoke('kobold:launchKoboldCpp', args),
getConfigFiles: () => ipcRenderer.invoke('kobold:getConfigFiles'),
saveConfigFile: (
configName: string,
configData: {
gpulayers?: number;
contextsize?: number;
model_param?: string;
port?: number;
host?: string;
multiuser?: number;
multiplayer?: boolean;
remotetunnel?: boolean;
nocertify?: boolean;
websearch?: boolean;
noshift?: boolean;
flashattention?: boolean;
noavx2?: boolean;
failsafe?: boolean;
usemmap?: boolean;
usecuda?: boolean;
usevulkan?: boolean;
useclblast?: boolean;
sdmodel?: string;
sdt5xxl?: string;
sdclipl?: string;
sdclipg?: string;
sdphotomaker?: string;
sdvae?: string;
[key: string]: unknown;
}
) => ipcRenderer.invoke('kobold:saveConfigFile', configName, configData),
saveConfigFile: (configName: string, configData: KoboldConfig) =>
ipcRenderer.invoke('kobold:saveConfigFile', configName, configData),
getSelectedConfig: () => ipcRenderer.invoke('kobold:getSelectedConfig'),
setSelectedConfig: (configName: string) =>
ipcRenderer.invoke('kobold:setSelectedConfig', configName),
@ -109,6 +77,7 @@ const koboldAPI: KoboldAPI = {
const appAPI: AppAPI = {
openExternal: (url) => ipcRenderer.invoke('app:openExternal', url),
getVersion: () => ipcRenderer.invoke('app:getVersion'),
};
const configAPI: ConfigAPI = {

View file

@ -87,7 +87,7 @@ interface LaunchConfigState {
export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
gpuLayers: 0,
autoGpuLayers: false,
autoGpuLayers: true,
contextSize: DEFAULT_CONTEXT_SIZE,
modelPath: '',
additionalArguments: '',
@ -163,6 +163,12 @@ export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
if (configData) {
const updates: Partial<LaunchConfigState> = {};
if (typeof configData.autoGpuLayers === 'boolean') {
updates.autoGpuLayers = configData.autoGpuLayers;
} else {
updates.autoGpuLayers = true;
}
if (typeof configData.gpulayers === 'number') {
updates.gpuLayers = configData.gpulayers;
} else {

View file

@ -57,14 +57,47 @@ export interface DownloadItem {
type: 'asset' | 'rocm';
}
export interface KoboldConfig {
gpulayers?: number;
contextsize?: number;
model_param?: string;
port?: number;
host?: string;
multiuser?: number;
multiplayer?: boolean;
remotetunnel?: boolean;
nocertify?: boolean;
websearch?: boolean;
noshift?: boolean;
flashattention?: boolean;
noavx2?: boolean;
failsafe?: boolean;
lowvram?: boolean;
quantmatmul?: boolean;
usemmap?: boolean;
usecuda?: boolean;
usevulkan?: boolean;
useclblast?: boolean | [number, number];
gpuDeviceSelection?: string;
tensorSplit?: string;
gpuPlatform?: number;
sdmodel?: string;
sdt5xxl?: string;
sdclipl?: string;
sdclipg?: string;
sdphotomaker?: string;
sdvae?: string;
sdlora?: string;
sdconvdirect?: string;
additionalArguments?: string;
autoGpuLayers?: boolean;
model?: string;
backend?: string;
}
export interface KoboldAPI {
getInstalledVersions: () => Promise<InstalledVersion[]>;
getCurrentBinaryInfo: () => Promise<{
path: string;
filename: string;
} | null>;
setCurrentVersion: (version: string) => Promise<boolean>;
getLatestRelease: () => Promise<DownloadItem[]>;
getPlatform: () => Promise<PlatformInfo>;
detectGPU: () => Promise<BasicGPUInfo>;
detectCPU: () => Promise<CPUCapabilities>;
@ -90,50 +123,17 @@ export interface KoboldAPI {
path?: string;
error?: string;
}>;
getROCmDownload: () => Promise<DownloadItem | null>;
getLatestReleaseWithStatus: () => Promise<ReleaseWithStatus | null>;
launchKoboldCpp: (
args?: string[]
) => Promise<{ success: boolean; pid?: number; error?: string }>;
getConfigFiles: () => Promise<{ name: string; path: string; size: number }[]>;
saveConfigFile: (
configName: string,
configData: {
gpulayers?: number;
contextsize?: number;
model_param?: string;
port?: number;
host?: string;
multiuser?: number;
multiplayer?: boolean;
remotetunnel?: boolean;
nocertify?: boolean;
websearch?: boolean;
noshift?: boolean;
flashattention?: boolean;
noavx2?: boolean;
failsafe?: boolean;
usemmap?: boolean;
usecuda?: boolean;
usevulkan?: boolean;
useclblast?: boolean;
sdmodel?: string;
sdt5xxl?: string;
sdclipl?: string;
sdclipg?: string;
sdphotomaker?: string;
sdvae?: string;
[key: string]: unknown;
}
configData: KoboldConfig
) => Promise<boolean>;
getSelectedConfig: () => Promise<string | null>;
setSelectedConfig: (configName: string) => Promise<boolean>;
parseConfigFile: (filePath: string) => Promise<{
gpulayers?: number;
contextsize?: number;
model_param?: string;
[key: string]: unknown;
} | null>;
parseConfigFile: (filePath: string) => Promise<KoboldConfig | null>;
selectModelFile: () => Promise<string | null>;
stopKoboldCpp: () => void;
onDownloadProgress: (callback: (progress: number) => void) => void;
@ -145,6 +145,7 @@ export interface KoboldAPI {
export interface AppAPI {
openExternal: (url: string) => Promise<void>;
getVersion: () => Promise<string>;
}
export interface ConfigAPI {