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:
lone-cloud 2025-08-28 16:52:13 -07:00
parent 4eaa7ad106
commit 2ecbef9108
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 - **Smart process management** - Prevents runaway background processes and system resource waste
- **Optimized performance** - Automatically unpacks binaries for faster operation and reduced memory usage - **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 - **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 - **Privacy-focused** - Everything runs locally on your machine, no data sent to external servers
## Installation ## Installation

View file

@ -10,8 +10,9 @@ import {
Image, Image,
Tooltip, Tooltip,
} from '@mantine/core'; } from '@mantine/core';
import { Settings, ArrowLeft } from 'lucide-react'; import { Settings, ArrowLeft, CircleFadingArrowUp } from 'lucide-react';
import { soundAssets, playSound, initializeAudio } from '@/utils/sounds'; import { soundAssets, playSound, initializeAudio } from '@/utils/sounds';
import { useAppUpdateChecker } from '@/hooks/useAppUpdateChecker';
import iconUrl from '/icon.png'; import iconUrl from '/icon.png';
import { FRONTENDS } from '@/constants'; import { FRONTENDS } from '@/constants';
import type { InterfaceTab, FrontendPreference, Screen } from '@/types'; import type { InterfaceTab, FrontendPreference, Screen } from '@/types';
@ -38,6 +39,7 @@ export const AppHeader = ({
const [logoClickCount, setLogoClickCount] = useState(0); const [logoClickCount, setLogoClickCount] = useState(0);
const [isElephantMode, setIsElephantMode] = useState(false); const [isElephantMode, setIsElephantMode] = useState(false);
const [isMouseSqueaking, setIsMouseSqueaking] = useState(false); const [isMouseSqueaking, setIsMouseSqueaking] = useState(false);
const { hasUpdate, openReleasePage } = useAppUpdateChecker();
const handleLogoClick = async () => { const handleLogoClick = async () => {
await initializeAudio(); await initializeAudio();
@ -148,8 +150,25 @@ export const AppHeader = ({
minWidth: '6.25rem', minWidth: '6.25rem',
display: 'flex', display: 'flex',
justifyContent: 'flex-end', 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"> <Tooltip label="Settings" position="bottom">
<ActionIcon <ActionIcon
variant="subtle" variant="subtle"

View file

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

View file

@ -42,6 +42,7 @@ export const VersionsTab = () => {
downloadProgress, downloadProgress,
loadRemoteVersions, loadRemoteVersions,
handleDownload: sharedHandleDownload, handleDownload: sharedHandleDownload,
getLatestReleaseWithDownloadStatus,
} = useKoboldVersions(); } = useKoboldVersions();
const [installedVersions, setInstalledVersions] = useState< const [installedVersions, setInstalledVersions] = useState<
@ -104,8 +105,7 @@ export const VersionsTab = () => {
const loadLatestRelease = useCallback(async () => { const loadLatestRelease = useCallback(async () => {
try { try {
const release = const release = await getLatestReleaseWithDownloadStatus();
await window.electronAPI.kobold.getLatestReleaseWithStatus();
setLatestRelease(release); setLatestRelease(release);
} catch (error) { } catch (error) {
window.electronAPI.logs.logError( window.electronAPI.logs.logError(
@ -113,7 +113,7 @@ export const VersionsTab = () => {
error as Error error as Error
); );
} }
}, []); }, [getLatestReleaseWithDownloadStatus]);
useEffect(() => { useEffect(() => {
loadInstalledVersions(); 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 { 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 { interface PlatformInfo {
platform: string; platform: string;
@ -7,6 +15,164 @@ interface PlatformInfo {
hasROCm: boolean; 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 { interface UseKoboldVersionsReturn {
platformInfo: PlatformInfo; platformInfo: PlatformInfo;
availableDownloads: DownloadItem[]; availableDownloads: DownloadItem[];
@ -15,6 +181,7 @@ interface UseKoboldVersionsReturn {
downloading: string | null; downloading: string | null;
downloadProgress: Record<string, number>; downloadProgress: Record<string, number>;
loadRemoteVersions: () => Promise<void>; loadRemoteVersions: () => Promise<void>;
refresh: () => Promise<void>;
handleDownload: ( handleDownload: (
type: 'asset' | 'rocm', type: 'asset' | 'rocm',
item?: DownloadItem, item?: DownloadItem,
@ -27,6 +194,8 @@ interface UseKoboldVersionsReturn {
| Record<string, number> | Record<string, number>
| ((prev: Record<string, number>) => Record<string, number>) | ((prev: Record<string, number>) => Record<string, number>)
) => void; ) => void;
getROCmDownload: (platform?: string) => Promise<DownloadItem | null>;
getLatestReleaseWithDownloadStatus: () => Promise<ReleaseWithStatus | null>;
} }
export const useKoboldVersions = (): UseKoboldVersionsReturn => { export const useKoboldVersions = (): UseKoboldVersionsReturn => {
@ -93,11 +262,25 @@ export const useKoboldVersions = (): UseKoboldVersionsReturn => {
setLoadingRemote(true); setLoadingRemote(true);
try { 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([ const [releases, rocm] = await Promise.all([
window.electronAPI.kobold.getLatestRelease(), fetchLatestReleaseFromAPI(platformInfo.platform),
window.electronAPI.kobold.getROCmDownload(), getROCmDownload(platformInfo.platform),
]); ]);
saveToCache(releases);
const allDownloads: DownloadItem[] = [...releases]; const allDownloads: DownloadItem[] = [...releases];
if (rocm) { if (rocm) {
allDownloads.push(rocm); allDownloads.push(rocm);
@ -109,6 +292,18 @@ export const useKoboldVersions = (): UseKoboldVersionsReturn => {
'Failed to load remote versions:', 'Failed to load remote versions:',
error as Error 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 { } finally {
setLoadingRemote(false); setLoadingRemote(false);
} }
@ -160,6 +355,11 @@ export const useKoboldVersions = (): UseKoboldVersionsReturn => {
[] []
); );
const refresh = useCallback(async () => {
localStorage.removeItem(CACHE_KEY);
await loadRemoteVersions();
}, [loadRemoteVersions]);
useEffect(() => { useEffect(() => {
loadPlatformInfo(); loadPlatformInfo();
}, [loadPlatformInfo]); }, [loadPlatformInfo]);
@ -195,8 +395,12 @@ export const useKoboldVersions = (): UseKoboldVersionsReturn => {
downloading, downloading,
downloadProgress, downloadProgress,
loadRemoteVersions, loadRemoteVersions,
refresh,
handleDownload, handleDownload,
setDownloading, setDownloading,
setDownloadProgress, setDownloadProgress,
getROCmDownload: (platform?: string) =>
getROCmDownload(platform || platformInfo.platform),
getLatestReleaseWithDownloadStatus,
}; };
}; };

View file

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

View file

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

View file

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

View file

@ -16,19 +16,16 @@ import { dialog } from 'electron';
import { execa } from 'execa'; import { execa } from 'execa';
import { got } from 'got'; import { got } from 'got';
import { pipeline } from 'stream/promises'; import { pipeline } from 'stream/promises';
import { GitHubService } from '@/main/services/GitHubService';
import { ConfigManager } from '@/main/managers/ConfigManager'; import { ConfigManager } from '@/main/managers/ConfigManager';
import { LogManager } from '@/main/managers/LogManager'; import { LogManager } from '@/main/managers/LogManager';
import { WindowManager } from '@/main/managers/WindowManager'; 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 { stripAssetExtensions } from '@/utils/versionUtils';
import { compareVersions } from '@/utils/downloadUtils';
import type { import type {
DownloadItem, DownloadItem,
GitHubAsset, GitHubAsset,
UpdateInfo,
ReleaseWithStatus,
InstalledVersion, InstalledVersion,
KoboldConfig,
} from '@/types/electron'; } from '@/types/electron';
export class KoboldCppManager { export class KoboldCppManager {
@ -36,18 +33,15 @@ export class KoboldCppManager {
private koboldProcess: ChildProcess | null = null; private koboldProcess: ChildProcess | null = null;
private configManager: ConfigManager; private configManager: ConfigManager;
private logManager: LogManager; private logManager: LogManager;
private githubService: GitHubService;
private windowManager: WindowManager; private windowManager: WindowManager;
constructor( constructor(
configManager: ConfigManager, configManager: ConfigManager,
githubService: GitHubService,
windowManager: WindowManager, windowManager: WindowManager,
logManager: LogManager logManager: LogManager
) { ) {
this.configManager = configManager; this.configManager = configManager;
this.logManager = logManager; this.logManager = logManager;
this.githubService = githubService;
this.windowManager = windowManager; this.windowManager = windowManager;
this.installDir = this.configManager.getInstallDir() || ''; this.installDir = this.configManager.getInstallDir() || '';
} }
@ -265,12 +259,7 @@ export class KoboldCppManager {
return configFiles.sort((a, b) => a.name.localeCompare(b.name)); return configFiles.sort((a, b) => a.name.localeCompare(b.name));
} }
async parseConfigFile(filePath: string): Promise<{ async parseConfigFile(filePath: string): Promise<KoboldConfig | null> {
gpulayers?: number;
contextsize?: number;
model?: string;
[key: string]: unknown;
} | null> {
try { try {
if (!existsSync(filePath)) { if (!existsSync(filePath)) {
return null; return null;
@ -288,33 +277,7 @@ export class KoboldCppManager {
async saveConfigFile( async saveConfigFile(
configName: string, configName: string,
configData: { configData: KoboldConfig
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;
}
): Promise<boolean> { ): Promise<boolean> {
try { try {
if (!this.installDir) { if (!this.installDir) {
@ -549,16 +512,35 @@ export class KoboldCppManager {
const platform = process.platform; const platform = process.platform;
if (platform === 'linux') { if (platform === 'linux') {
const latestRelease = await this.githubService.getRawLatestRelease(); try {
const version = latestRelease?.tag_name?.replace(/^v/, '') || 'unknown'; const response = await fetch(GITHUB_API.LATEST_RELEASE_URL);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return { const latestRelease = await response.json();
name: ROCM.BINARY_NAME, const version = latestRelease?.tag_name?.replace(/^v/, '') || 'unknown';
url: ROCM.DOWNLOAD_URL,
size: ROCM.SIZE_BYTES_APPROX, return {
version, name: ROCM.BINARY_NAME,
type: 'rocm', 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') { } else if (platform === 'win32') {
return null; return null;
// The launcher doesn't exist in unpacked state yet. // The launcher doesn't exist in unpacked state yet.
@ -706,73 +688,6 @@ export class KoboldCppManager {
return currentVersion?.version; 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( async launchKoboldCpp(
args: string[] = [] args: string[] = []
): Promise<{ success: boolean; pid?: number; error?: 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> { private async ensureSillyTavernSettings(): Promise<void> {
const settingsPath = this.getSillyTavernSettingsPath(); const settingsPath = this.getSillyTavernSettingsPath();
@ -132,10 +139,7 @@ export class SillyTavernManager {
); );
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const initProcess = spawn('npx', spawnArgs, { const initProcess = this.createNpxProcess(spawnArgs);
stdio: ['pipe', 'pipe', 'pipe'],
detached: false,
});
let hasResolved = false; let hasResolved = false;
@ -364,10 +368,7 @@ export class SillyTavernManager {
config.port.toString(), config.port.toString(),
]; ];
this.sillyTavernProcess = spawn('npx', sillyTavernArgs, { this.sillyTavernProcess = this.createNpxProcess(sillyTavernArgs);
stdio: ['pipe', 'pipe', 'pipe'],
detached: false,
});
if (this.sillyTavernProcess.stdout) { if (this.sillyTavernProcess.stdout) {
this.sillyTavernProcess.stdout.on('data', (data: Buffer) => { 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 }; 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<{ private async detectCUDA(): Promise<{
supported: boolean; supported: boolean;
devices: string[]; devices: string[];

View file

@ -5,18 +5,14 @@ import type {
ConfigAPI, ConfigAPI,
LogsAPI, LogsAPI,
SillyTavernAPI, SillyTavernAPI,
KoboldConfig,
} from '@/types/electron'; } from '@/types/electron';
const koboldAPI: KoboldAPI = { const koboldAPI: KoboldAPI = {
getInstalledVersions: () => ipcRenderer.invoke('kobold:getInstalledVersions'), getInstalledVersions: () => ipcRenderer.invoke('kobold:getInstalledVersions'),
getCurrentBinaryInfo: () => ipcRenderer.invoke('kobold:getCurrentBinaryInfo'),
setCurrentVersion: (version: string) => setCurrentVersion: (version: string) =>
ipcRenderer.invoke('kobold:setCurrentVersion', version), ipcRenderer.invoke('kobold:setCurrentVersion', version),
getLatestReleaseWithStatus: () =>
ipcRenderer.invoke('kobold:getLatestReleaseWithStatus'),
getROCmDownload: () => ipcRenderer.invoke('kobold:getROCmDownload'),
downloadROCm: () => ipcRenderer.invoke('kobold:downloadROCm'), downloadROCm: () => ipcRenderer.invoke('kobold:downloadROCm'),
getLatestRelease: () => ipcRenderer.invoke('kobold:getLatestRelease'),
getPlatform: () => ipcRenderer.invoke('kobold:getPlatform'), getPlatform: () => ipcRenderer.invoke('kobold:getPlatform'),
detectGPU: () => ipcRenderer.invoke('kobold:detectGPU'), detectGPU: () => ipcRenderer.invoke('kobold:detectGPU'),
detectCPU: () => ipcRenderer.invoke('kobold:detectCPU'), detectCPU: () => ipcRenderer.invoke('kobold:detectCPU'),
@ -35,36 +31,8 @@ const koboldAPI: KoboldAPI = {
launchKoboldCpp: (args?: string[]) => launchKoboldCpp: (args?: string[]) =>
ipcRenderer.invoke('kobold:launchKoboldCpp', args), ipcRenderer.invoke('kobold:launchKoboldCpp', args),
getConfigFiles: () => ipcRenderer.invoke('kobold:getConfigFiles'), getConfigFiles: () => ipcRenderer.invoke('kobold:getConfigFiles'),
saveConfigFile: ( saveConfigFile: (configName: string, configData: KoboldConfig) =>
configName: string, ipcRenderer.invoke('kobold:saveConfigFile', configName, configData),
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),
getSelectedConfig: () => ipcRenderer.invoke('kobold:getSelectedConfig'), getSelectedConfig: () => ipcRenderer.invoke('kobold:getSelectedConfig'),
setSelectedConfig: (configName: string) => setSelectedConfig: (configName: string) =>
ipcRenderer.invoke('kobold:setSelectedConfig', configName), ipcRenderer.invoke('kobold:setSelectedConfig', configName),
@ -109,6 +77,7 @@ const koboldAPI: KoboldAPI = {
const appAPI: AppAPI = { const appAPI: AppAPI = {
openExternal: (url) => ipcRenderer.invoke('app:openExternal', url), openExternal: (url) => ipcRenderer.invoke('app:openExternal', url),
getVersion: () => ipcRenderer.invoke('app:getVersion'),
}; };
const configAPI: ConfigAPI = { const configAPI: ConfigAPI = {

View file

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

View file

@ -57,14 +57,47 @@ export interface DownloadItem {
type: 'asset' | 'rocm'; 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 { export interface KoboldAPI {
getInstalledVersions: () => Promise<InstalledVersion[]>; getInstalledVersions: () => Promise<InstalledVersion[]>;
getCurrentBinaryInfo: () => Promise<{
path: string;
filename: string;
} | null>;
setCurrentVersion: (version: string) => Promise<boolean>; setCurrentVersion: (version: string) => Promise<boolean>;
getLatestRelease: () => Promise<DownloadItem[]>;
getPlatform: () => Promise<PlatformInfo>; getPlatform: () => Promise<PlatformInfo>;
detectGPU: () => Promise<BasicGPUInfo>; detectGPU: () => Promise<BasicGPUInfo>;
detectCPU: () => Promise<CPUCapabilities>; detectCPU: () => Promise<CPUCapabilities>;
@ -90,50 +123,17 @@ export interface KoboldAPI {
path?: string; path?: string;
error?: string; error?: string;
}>; }>;
getROCmDownload: () => Promise<DownloadItem | null>;
getLatestReleaseWithStatus: () => Promise<ReleaseWithStatus | null>;
launchKoboldCpp: ( launchKoboldCpp: (
args?: string[] args?: string[]
) => Promise<{ success: boolean; pid?: number; error?: string }>; ) => Promise<{ success: boolean; pid?: number; error?: string }>;
getConfigFiles: () => Promise<{ name: string; path: string; size: number }[]>; getConfigFiles: () => Promise<{ name: string; path: string; size: number }[]>;
saveConfigFile: ( saveConfigFile: (
configName: string, configName: string,
configData: { configData: 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;
usemmap?: boolean;
usecuda?: boolean;
usevulkan?: boolean;
useclblast?: boolean;
sdmodel?: string;
sdt5xxl?: string;
sdclipl?: string;
sdclipg?: string;
sdphotomaker?: string;
sdvae?: string;
[key: string]: unknown;
}
) => Promise<boolean>; ) => Promise<boolean>;
getSelectedConfig: () => Promise<string | null>; getSelectedConfig: () => Promise<string | null>;
setSelectedConfig: (configName: string) => Promise<boolean>; setSelectedConfig: (configName: string) => Promise<boolean>;
parseConfigFile: (filePath: string) => Promise<{ parseConfigFile: (filePath: string) => Promise<KoboldConfig | null>;
gpulayers?: number;
contextsize?: number;
model_param?: string;
[key: string]: unknown;
} | null>;
selectModelFile: () => Promise<string | null>; selectModelFile: () => Promise<string | null>;
stopKoboldCpp: () => void; stopKoboldCpp: () => void;
onDownloadProgress: (callback: (progress: number) => void) => void; onDownloadProgress: (callback: (progress: number) => void) => void;
@ -145,6 +145,7 @@ export interface KoboldAPI {
export interface AppAPI { export interface AppAPI {
openExternal: (url: string) => Promise<void>; openExternal: (url: string) => Promise<void>;
getVersion: () => Promise<string>;
} }
export interface ConfigAPI { export interface ConfigAPI {