allow gerbil to work fully offline by allowing kcpp binary imports on the initial Download screen, more version -> backend renames

This commit is contained in:
Egor 2025-11-29 17:52:29 -08:00
parent 4b2e9b2ae9
commit 05cb3b8c05
16 changed files with 346 additions and 198 deletions

View file

@ -1,7 +1,7 @@
{
"name": "gerbil",
"productName": "Gerbil",
"version": "1.13.0",
"version": "1.14.0",
"description": "Run Large Language Models locally",
"main": "out/main/index.js",
"homepage": "./",
@ -60,7 +60,7 @@
"eslint-plugin-sonarjs": "^3.0.5",
"globals": "^16.5.0",
"jiti": "^2.6.1",
"prettier": "^3.7.2",
"prettier": "^3.7.3",
"rollup-plugin-visualizer": "^6.0.5",
"typescript": "^5.9.3",
"vite": "^7.2.4"
@ -75,7 +75,7 @@
"@mantine/hooks": "^8.3.9",
"@uiw/react-codemirror": "^4.25.3",
"electron-updater": "^6.6.2",
"execa": "^9.6.0",
"execa": "^9.6.1",
"lucide-react": "^0.555.0",
"mime-types": "^3.0.2",
"react": "^19.2.0",
@ -85,7 +85,7 @@
"winston": "^3.18.3",
"winston-daily-rotate-file": "^5.0.0",
"yauzl": "^3.2.0",
"zustand": "^5.0.8"
"zustand": "^5.0.9"
},
"build": {
"appId": "com.gerbil.app",

View file

@ -12,7 +12,7 @@ import { Download, X, ExternalLink } from 'lucide-react';
import { useState } from 'react';
import type { DownloadItem } from '@/types/electron';
import type { BinaryUpdateInfo } from '@/hooks/useUpdateChecker';
import { useKoboldVersionsStore } from '@/stores/koboldVersions';
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
import { pretifyBinName } from '@/utils/assets';
import { formatDownloadSize } from '@/utils/format';
import { GITHUB_API } from '@/constants';
@ -34,7 +34,7 @@ export const UpdateAvailableModal = ({
updateInfo,
onUpdate,
}: UpdateAvailableModalProps) => {
const { downloading, downloadProgress } = useKoboldVersionsStore();
const { downloading, downloadProgress } = useKoboldBackendsStore();
const currentBackend = updateInfo?.currentBackend;
const availableUpdate = updateInfo?.availableUpdate;
const [isUpdating, setIsUpdating] = useState(false);

View file

@ -15,7 +15,7 @@ import { ErrorBoundary } from '@/components/App/ErrorBoundary';
import { AppRouter } from '@/components/App/Router';
import { NotepadContainer } from '@/components/Notepad/Container';
import { useUpdateChecker } from '@/hooks/useUpdateChecker';
import { useKoboldVersionsStore } from '@/stores/koboldVersions';
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
import { usePreferencesStore } from '@/stores/preferences';
import { useLaunchConfigStore } from '@/stores/launchConfig';
import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants';
@ -81,7 +81,7 @@ export const App = () => {
closeModal,
} = useUpdateChecker();
const { handleDownload, loadingRemote } = useKoboldVersionsStore();
const { handleDownload, loadingRemote } = useKoboldBackendsStore();
const determineScreen = (
currentVersion: unknown,
@ -142,7 +142,7 @@ export const App = () => {
item: download,
isUpdate: true,
wasCurrentBinary: true,
oldVersionPath: currentBackend?.path,
oldBackendPath: currentBackend?.path,
});
closeModal();

View file

@ -13,11 +13,11 @@ import { Download, Trash2 } from 'lucide-react';
import { MouseEvent } from 'react';
import { pretifyBinName, isWindowsROCmBuild } from '@/utils/assets';
import { usePreferencesStore } from '@/stores/preferences';
import { useKoboldVersionsStore } from '@/stores/koboldVersions';
import type { VersionInfo } from '@/types';
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
import type { BackendInfo } from '@/types';
interface DownloadCardProps {
version: VersionInfo;
backend: BackendInfo;
size: string;
description?: string;
disabled?: boolean;
@ -29,7 +29,7 @@ interface DownloadCardProps {
}
export const DownloadCard = ({
version: versionInfo,
backend,
size,
description,
disabled = false,
@ -40,23 +40,23 @@ export const DownloadCard = ({
onDelete,
}: DownloadCardProps) => {
const { resolvedColorScheme: colorScheme } = usePreferencesStore();
const { downloading, downloadProgress } = useKoboldVersionsStore();
const { downloading, downloadProgress } = useKoboldBackendsStore();
const isLoading = downloading === versionInfo.name;
const isLoading = downloading === backend.name;
const currentProgress = isLoading
? Math.min(downloadProgress[versionInfo.name], 100) || 0
? Math.min(downloadProgress[backend.name], 100) || 0
: 0;
const hasVersionMismatch = Boolean(
versionInfo.version &&
versionInfo.actualVersion &&
versionInfo.version !== versionInfo.actualVersion
backend.version &&
backend.actualVersion &&
backend.version !== backend.actualVersion
);
// eslint-disable-next-line sonarjs/cognitive-complexity
const renderActionButtons = () => {
const buttons = [];
if (!versionInfo.isInstalled) {
if (!backend.isInstalled) {
return (
<Button
key="download"
@ -78,7 +78,7 @@ export const DownloadCard = ({
);
}
if (!versionInfo.isCurrent && onMakeCurrent) {
if (!backend.isCurrent && onMakeCurrent) {
buttons.push(
<Button
key="makeCurrent"
@ -92,7 +92,7 @@ export const DownloadCard = ({
);
}
if (versionInfo.hasUpdate && onUpdate) {
if (backend.hasUpdate && onUpdate) {
buttons.push(
<Button
key="update"
@ -110,7 +110,7 @@ export const DownloadCard = ({
)
}
>
{isLoading ? 'Updating...' : `Update to ${versionInfo.newerVersion}`}
{isLoading ? 'Updating...' : `Update to ${backend.newerVersion}`}
</Button>
);
}
@ -138,7 +138,7 @@ export const DownloadCard = ({
);
}
if (onDelete && versionInfo.isInstalled && !versionInfo.isCurrent) {
if (onDelete && backend.isInstalled && !backend.isCurrent) {
buttons.push(
<Button
key="delete"
@ -169,7 +169,7 @@ export const DownloadCard = ({
withBorder
radius="sm"
padding="sm"
{...(versionInfo.isCurrent && {
{...(backend.isCurrent && {
bg: colorScheme === 'dark' ? 'dark.6' : 'gray.0',
bd: `2px solid var(--mantine-color-${colorScheme === 'dark' ? 'blue-4' : 'blue-6'})`,
})}
@ -178,19 +178,19 @@ export const DownloadCard = ({
<div style={{ flex: 1 }}>
<Group gap="xs" align="center" mb="xs">
<Text fw={500} size="sm">
{pretifyBinName(versionInfo.name)}
{pretifyBinName(backend.name)}
</Text>
{versionInfo.isCurrent && (
{backend.isCurrent && (
<Badge variant="light" color="blue" size="sm">
Current
</Badge>
)}
{versionInfo.hasUpdate && (
{backend.hasUpdate && (
<Badge variant="light" color="orange" size="sm">
Update Available
</Badge>
)}
{isWindowsROCmBuild(versionInfo.name) && (
{isWindowsROCmBuild(backend.name) && (
<Badge variant="light" color="yellow" size="sm">
Experimental
</Badge>
@ -202,13 +202,13 @@ export const DownloadCard = ({
</Text>
)}
<Group gap="xs" align="center">
{versionInfo.version && (
{backend.version && (
<Text size="xs" c="dimmed">
Version {versionInfo.version}
{hasVersionMismatch && versionInfo.actualVersion && (
Version {backend.version}
{hasVersionMismatch && backend.actualVersion && (
<span style={{ color: 'var(--mantine-color-red-6)' }}>
{' '}
(actual: {versionInfo.actualVersion})
(actual: {backend.actualVersion})
</span>
)}
</Text>

View file

@ -1,10 +1,18 @@
import { useState, useCallback, useRef, useEffect } from 'react';
import { Card, Text, Title, Loader, Stack, Container } from '@mantine/core';
import {
Card,
Text,
Title,
Loader,
Stack,
Container,
Anchor,
} from '@mantine/core';
import { DownloadCard } from '@/components/DownloadCard';
import { getPlatformDisplayName } from '@/utils/platform';
import { formatDownloadSize } from '@/utils/format';
import { getAssetDescription } from '@/utils/assets';
import { useKoboldVersionsStore } from '@/stores/koboldVersions';
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
import type { DownloadItem } from '@/types/electron';
interface DownloadScreenProps {
@ -19,9 +27,11 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
loadingRemote,
downloading,
handleDownload: handleDownloadFromStore,
} = useKoboldVersionsStore();
} = useKoboldBackendsStore();
const [downloadingAsset, setDownloadingAsset] = useState<string | null>(null);
const [importError, setImportError] = useState<string | null>(null);
const [importing, setImporting] = useState(false);
const downloadingItemRef = useRef<HTMLDivElement>(null);
const loading = loadingPlatform || loadingRemote;
@ -56,7 +66,6 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
return (
<Container size="sm" mt="md">
<Stack gap="xl">
<Card withBorder radius="md" shadow="sm">
<Stack gap="lg">
<Title order={3}>Select a Backend</Title>
@ -81,7 +90,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
ref={isDownloading ? downloadingItemRef : null}
>
<DownloadCard
version={{
backend={{
name: download.name,
version: download.version || '',
size: download.size,
@ -90,14 +99,12 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
downloadUrl: download.url,
hasUpdate: false,
}}
size={formatDownloadSize(
download.size,
download.url
)}
size={formatDownloadSize(download.size, download.url)}
description={getAssetDescription(download.name)}
disabled={
Boolean(downloading) &&
downloadingAsset !== download.name
importing ||
(Boolean(downloading) &&
downloadingAsset !== download.name)
}
onDownload={(e) => {
e.stopPropagation();
@ -109,22 +116,51 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
})}
</Stack>
) : (
<Card withBorder p="md" bg="red.0" c="red.9">
<Stack gap="xs">
<Text fw={500}>No downloads available</Text>
<Text size="sm">
<Text size="sm" c="dimmed" ta="center">
Unable to fetch downloads for your platform (
{getPlatformDisplayName(platform)}). Please check your
internet connection and try again.
{getPlatformDisplayName(platform)}). Check your internet
connection and try again.
</Text>
</Stack>
</Card>
)}
{importError && (
<Text size="sm" c="red" ta="center">
{importError}
</Text>
)}
<Text size="sm" c="dimmed" ta="center">
Already have a backend downloaded?{' '}
<Anchor
component="button"
type="button"
size="sm"
disabled={importing || Boolean(downloading)}
onClick={async () => {
setImportError(null);
setImporting(true);
try {
const result =
await window.electronAPI.kobold.importLocalBackend();
if (result.success) {
onDownloadComplete();
} else if (result.error) {
setImportError(result.error);
}
} finally {
setImporting(false);
}
}}
>
{importing ? 'Importing...' : 'Select a local file'}
</Anchor>
</Text>
</>
)}
</Stack>
</Card>
</Stack>
</Container>
);
};

View file

@ -18,9 +18,9 @@ import {
} from '@/utils/version';
import { formatDownloadSize } from '@/utils/format';
import { useKoboldVersionsStore } from '@/stores/koboldVersions';
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
import type { InstalledBackend, ReleaseWithStatus } from '@/types/electron';
import type { VersionInfo } from '@/types';
import type { BackendInfo } from '@/types';
export const BackendsTab = () => {
const {
@ -31,7 +31,7 @@ export const BackendsTab = () => {
handleDownload: handleDownloadFromStore,
getLatestReleaseWithDownloadStatus,
initialize,
} = useKoboldVersionsStore();
} = useKoboldBackendsStore();
const [installedBackends, setInstalledBackends] = useState<
InstalledBackend[]
@ -83,8 +83,8 @@ export const BackendsTab = () => {
initialize,
]);
const allBackends = useMemo((): VersionInfo[] => {
const backends: VersionInfo[] = [];
const allBackends = useMemo((): BackendInfo[] => {
const backends: BackendInfo[] = [];
const processedInstalled = new Set<string>();
availableDownloads.forEach((download) => {
@ -170,7 +170,7 @@ export const BackendsTab = () => {
}
}, [downloading]);
const handleDownload = async (backend: VersionInfo) => {
const handleDownload = async (backend: BackendInfo) => {
const download = availableDownloads.find((d) => d.name === backend.name);
if (!download) return;
@ -183,7 +183,7 @@ export const BackendsTab = () => {
await loadInstalledBackends();
};
const handleUpdate = async (backend: VersionInfo) => {
const handleUpdate = async (backend: BackendInfo) => {
const download = availableDownloads.find((d) => d.name === backend.name);
if (!download) return;
@ -191,13 +191,13 @@ export const BackendsTab = () => {
item: download,
isUpdate: true,
wasCurrentBinary: backend.isCurrent,
oldVersionPath: backend.installedPath,
oldBackendPath: backend.installedPath,
});
await loadInstalledBackends();
};
const handleRedownload = async (backend: VersionInfo) => {
const handleRedownload = async (backend: BackendInfo) => {
const download = availableDownloads.find((d) => d.name === backend.name);
if (!download) return;
@ -205,13 +205,13 @@ export const BackendsTab = () => {
item: download,
isUpdate: true,
wasCurrentBinary: backend.isCurrent,
oldVersionPath: backend.installedPath,
oldBackendPath: backend.installedPath,
});
await loadInstalledBackends();
};
const handleDelete = async (backend: VersionInfo) => {
const handleDelete = async (backend: BackendInfo) => {
if (!backend.installedPath || backend.isCurrent) return;
const result = await window.electronAPI.kobold.deleteRelease(
@ -222,7 +222,7 @@ export const BackendsTab = () => {
}
};
const makeCurrent = (backend: VersionInfo) => {
const makeCurrent = (backend: BackendInfo) => {
if (!backend.installedPath) return;
const targetBackend = installedBackends.find(
@ -286,7 +286,7 @@ export const BackendsTab = () => {
ref={isDownloading ? downloadingItemRef : null}
>
<DownloadCard
version={backend}
backend={backend}
size={
backend.size
? formatDownloadSize(backend.size, backend.downloadUrl)

View file

@ -4,7 +4,7 @@ import {
compareVersions,
stripAssetExtensions,
} from '@/utils/version';
import { useKoboldVersionsStore } from '@/stores/koboldVersions';
import { useKoboldBackendsStore } from '@/stores/koboldBackends';
import { getROCmDownload } from '@/utils/rocm';
import type { InstalledBackend, DownloadItem } from '@/types/electron';
import type { DismissedUpdate } from '@/types';
@ -23,7 +23,7 @@ export const useUpdateChecker = () => {
);
const { availableDownloads: releases, loadingRemote } =
useKoboldVersionsStore();
useKoboldBackendsStore();
useEffect(() => {
const loadDismissedUpdates = async () => {
const dismissed = (await window.electronAPI.config.get(
@ -75,7 +75,7 @@ export const useUpdateChecker = () => {
if (hasUpdate) {
const isUpdateDismissed = dismissedUpdates.some(
(dismissedUpdate) =>
dismissedUpdate.currentVersionPath === currentBackend.path &&
dismissedUpdate.currentBackendPath === currentBackend.path &&
dismissedUpdate.targetVersion === matchingDownload.version
);
@ -95,7 +95,7 @@ export const useUpdateChecker = () => {
const skipUpdate = useCallback(() => {
if (updateInfo && updateInfo.availableUpdate.version) {
const newDismissedUpdate: DismissedUpdate = {
currentVersionPath: updateInfo.currentBackend.path,
currentBackendPath: updateInfo.currentBackend.path,
targetVersion: updateInfo.availableUpdate.version,
};

View file

@ -6,7 +6,10 @@ import {
stopKoboldCpp,
launchKoboldCppWithCustomFrontends,
} from '@/main/modules/koboldcpp/launcher';
import { downloadRelease } from '@/main/modules/koboldcpp/download';
import {
downloadRelease,
importLocalBackend,
} from '@/main/modules/koboldcpp/download';
import {
getInstalledBackends,
getCurrentBackend,
@ -160,6 +163,8 @@ export function setupIPCHandlers() {
selectModelFile(title)
);
ipcMain.handle('kobold:importLocalBackend', () => importLocalBackend());
ipcMain.handle('kobold:getLocalModels', (_, paramType: string) =>
getLocalModelsForType(
paramType as Parameters<typeof getLocalModelsForType>[0]

View file

@ -13,16 +13,16 @@ import { logError } from '@/utils/node/logging';
import { getLauncherPath } from '@/utils/node/path';
import type { InstalledBackend } from '@/types/electron';
const versionCache = new Map<
const backendVersionCache = new Map<
string,
{ version: string; actualVersion?: string } | null
>();
export function clearVersionCache(path?: string) {
export function clearBackendVersionCache(path?: string) {
if (path) {
versionCache.delete(path);
backendVersionCache.delete(path);
} else {
versionCache.clear();
backendVersionCache.clear();
}
}
@ -161,7 +161,7 @@ export async function deleteRelease(binaryPath: string) {
if (await pathExists(releaseDir)) {
await rm(releaseDir, { recursive: true, force: true });
clearVersionCache(binaryPath);
clearBackendVersionCache(binaryPath);
sendToRenderer('versions-updated');
return { success: true };
@ -179,8 +179,8 @@ export async function getVersionFromBinary(launcherPath: string) {
return null;
}
if (versionCache.has(launcherPath)) {
return versionCache.get(launcherPath);
if (backendVersionCache.has(launcherPath)) {
return backendVersionCache.get(launcherPath);
}
let folderVersion: string | null = null;
@ -224,10 +224,10 @@ export async function getVersionFromBinary(launcherPath: string) {
: undefined,
};
versionCache.set(launcherPath, result);
backendVersionCache.set(launcherPath, result);
return result;
} catch {
versionCache.set(launcherPath, null);
backendVersionCache.set(launcherPath, null);
return null;
}
}

View file

@ -1,8 +1,9 @@
import { createWriteStream } from 'fs';
import { join } from 'path';
import { join, basename } from 'path';
import { platform } from 'process';
import { rm, unlink, rename, mkdir, chmod } from 'fs/promises';
import { rm, unlink, rename, mkdir, chmod, copyFile } from 'fs/promises';
import { execa } from 'execa';
import { dialog } from 'electron';
import {
getInstallDir,
@ -15,7 +16,7 @@ import { pathExists } from '@/utils/node/fs';
import { stripAssetExtensions } from '@/utils/version';
import { getLauncherPath } from '@/utils/node/path';
import type { DownloadReleaseOptions, GitHubAsset } from '@/types/electron';
import { clearVersionCache } from './backend';
import { clearBackendVersionCache, getVersionFromBinary } from './backend';
async function removeDirectoryWithRetry(
dirPath: string,
@ -150,6 +151,59 @@ async function unpackKoboldCpp(packedPath: string, unpackDir: string) {
}
}
interface InstallBackendOptions {
packedFilePath: string;
unpackedDirPath: string;
isUpdate?: boolean;
wasCurrentBinary?: boolean;
oldBackendPath?: string;
skipUnpackError?: boolean;
skipCleanup?: boolean;
}
async function installBackend({
packedFilePath,
unpackedDirPath,
isUpdate = false,
wasCurrentBinary = false,
oldBackendPath,
skipUnpackError = false,
skipCleanup = false,
}: InstallBackendOptions) {
if (!skipCleanup && (await pathExists(unpackedDirPath))) {
await removeDirectoryWithRetry(unpackedDirPath);
}
await mkdir(unpackedDirPath, { recursive: true });
if (skipUnpackError) {
try {
await unpackKoboldCpp(packedFilePath, unpackedDirPath);
} catch {}
} else {
await unpackKoboldCpp(packedFilePath, unpackedDirPath);
}
const launcherPath = await setupLauncher(packedFilePath, unpackedDirPath);
clearBackendVersionCache(launcherPath);
if (oldBackendPath && isUpdate) {
const oldInstallDir = join(oldBackendPath, '..');
if (oldInstallDir !== unpackedDirPath) {
await removeDirectoryWithRetry(oldInstallDir);
}
}
if (!getCurrentKoboldBinary() || (isUpdate && wasCurrentBinary)) {
await setCurrentKoboldBinary(launcherPath);
}
sendToRenderer('versions-updated');
return launcherPath;
}
export async function downloadRelease(
asset: GitHubAsset,
options: DownloadReleaseOptions
@ -162,39 +216,73 @@ export async function downloadRelease(
const unpackedDirPath = join(getInstallDir(), folderName);
try {
if (await pathExists(unpackedDirPath)) {
await removeDirectoryWithRetry(unpackedDirPath);
}
await downloadFile(asset, tempPackedFilePath);
await unpackKoboldCpp(tempPackedFilePath, unpackedDirPath);
const launcherPath = await setupLauncher(
tempPackedFilePath,
unpackedDirPath
);
clearVersionCache(launcherPath);
if (options.oldVersionPath && options.isUpdate) {
const oldInstallDir = join(options.oldVersionPath, '..');
if (oldInstallDir !== unpackedDirPath) {
await removeDirectoryWithRetry(oldInstallDir);
}
}
if (
!getCurrentKoboldBinary() ||
(options.isUpdate && options.wasCurrentBinary)
) {
await setCurrentKoboldBinary(launcherPath);
}
sendToRenderer('versions-updated');
await installBackend({
packedFilePath: tempPackedFilePath,
unpackedDirPath,
isUpdate: options.isUpdate,
wasCurrentBinary: options.wasCurrentBinary,
oldBackendPath: options.oldBackendPath,
});
} catch (error) {
logError('Failed to download or unpack binary:', error as Error);
throw new Error('Failed to download or unpack binary');
}
}
export async function importLocalBackend() {
const result = await dialog.showOpenDialog(getMainWindow(), {
title: 'Select Backend Executable',
filters:
platform === 'win32'
? [
{ name: 'Executable Files', extensions: ['exe'] },
{ name: 'All Files', extensions: ['*'] },
]
: [{ name: 'All Files', extensions: ['*'] }],
properties: ['openFile'],
});
if (result.canceled || result.filePaths.length === 0) {
return { success: false };
}
const selectedPath = result.filePaths[0];
try {
if (platform !== 'win32') {
await chmod(selectedPath, 0o755);
}
const backendVersion = await getVersionFromBinary(selectedPath);
if (!backendVersion || backendVersion.version === 'unknown') {
return {
success: false,
error:
'Invalid backend executable. Could not determine version information.',
};
}
const version = backendVersion.actualVersion || backendVersion.version;
const filename = basename(selectedPath);
const baseFilename = stripAssetExtensions(filename);
const folderName = `${baseFilename}-${version}`;
const installDir = join(getInstallDir(), folderName);
const packedFilePath = join(getInstallDir(), `${filename}.packed`);
await copyFile(selectedPath, packedFilePath);
await installBackend({
packedFilePath,
unpackedDirPath: installDir,
skipUnpackError: true,
});
return { success: true };
} catch (error) {
logError('Failed to import local backend:', error as Error);
return { success: false, error: (error as Error).message };
}
}

View file

@ -18,8 +18,8 @@ import type {
const koboldAPI: KoboldAPI = {
getInstalledBackends: () => ipcRenderer.invoke('kobold:getInstalledBackends'),
getCurrentBackend: () => ipcRenderer.invoke('kobold:getCurrentBackend'),
setCurrentBackend: (version) =>
ipcRenderer.invoke('kobold:setCurrentBackend', version),
setCurrentBackend: (backend) =>
ipcRenderer.invoke('kobold:setCurrentBackend', backend),
getPlatform: () => ipcRenderer.invoke('kobold:getPlatform'),
detectGPU: () => ipcRenderer.invoke('kobold:detectGPU'),
detectCPU: () => ipcRenderer.invoke('kobold:detectCPU'),
@ -53,6 +53,7 @@ const koboldAPI: KoboldAPI = {
ipcRenderer.invoke('kobold:parseConfigFile', filePath),
selectModelFile: (title) =>
ipcRenderer.invoke('kobold:selectModelFile', title),
importLocalBackend: () => ipcRenderer.invoke('kobold:importLocalBackend'),
getLocalModels: (paramType) =>
ipcRenderer.invoke('kobold:getLocalModels', paramType),
analyzeModel: (filePath) =>

View file

@ -16,7 +16,7 @@ interface HandleDownloadParams {
item: DownloadItem;
isUpdate?: boolean;
wasCurrentBinary?: boolean;
oldVersionPath?: string;
oldBackendPath?: string;
}
const transformReleaseToDownloadItems = (
@ -48,7 +48,7 @@ const fetchLatestReleaseFromAPI = async (platform: string) => {
return transformReleaseToDownloadItems(release, platform);
};
interface KoboldVersionsState {
interface KoboldBackendsState {
platform: string;
availableDownloads: DownloadItem[];
loadingPlatform: boolean;
@ -61,7 +61,7 @@ interface KoboldVersionsState {
getLatestReleaseWithDownloadStatus: () => Promise<ReleaseWithStatus | null>;
}
export const useKoboldVersionsStore = create<KoboldVersionsState>(
export const useKoboldBackendsStore = create<KoboldBackendsState>(
(set, get) => ({
platform: '',
availableDownloads: [],
@ -101,7 +101,7 @@ export const useKoboldVersionsStore = create<KoboldVersionsState>(
item,
isUpdate = false,
wasCurrentBinary = false,
oldVersionPath,
oldBackendPath,
} = params;
const { downloading } = get();
@ -127,15 +127,16 @@ export const useKoboldVersionsStore = create<KoboldVersionsState>(
await window.electronAPI.kobold.downloadRelease(asset, {
isUpdate,
wasCurrentBinary,
oldVersionPath,
oldBackendPath,
});
progressCleanup();
set({ downloading: null, downloadProgress: {} });
},
getLatestReleaseWithDownloadStatus: async () =>
safeExecute(async () => {
const [response, installedVersions] = await Promise.all([
const [response, installedBackends] = await Promise.all([
fetch(GITHUB_API.LATEST_RELEASE_URL),
window.electronAPI.kobold.getInstalledBackends(),
]);
@ -147,9 +148,9 @@ export const useKoboldVersionsStore = create<KoboldVersionsState>(
const availableAssets = latestRelease.assets.map(
(asset: GitHubAsset) => {
const installedBackend = installedVersions.find(
(v: InstalledBackend) => {
const pathParts = v.path.split(/[/\\]/);
const installedBackend = installedBackends.find(
(b: InstalledBackend) => {
const pathParts = b.path.split(/[/\\]/);
const launcherIndex = pathParts.findIndex(
(part: string) =>
part === 'koboldcpp-launcher' ||
@ -168,7 +169,7 @@ export const useKoboldVersionsStore = create<KoboldVersionsState>(
return {
asset,
isDownloaded: !!installedBackend,
installedVersion: installedBackend?.version,
installedBackendVersion: installedBackend?.version,
};
}
);
@ -181,4 +182,4 @@ export const useKoboldVersionsStore = create<KoboldVersionsState>(
})
);
useKoboldVersionsStore.getState().initialize();
useKoboldBackendsStore.getState().initialize();

View file

@ -29,7 +29,7 @@ export interface GitHubAsset {
export interface DownloadReleaseOptions {
isUpdate?: boolean;
wasCurrentBinary?: boolean;
oldVersionPath?: string;
oldBackendPath?: string;
}
export interface GitHubRelease {
@ -53,7 +53,7 @@ export interface ReleaseWithStatus {
availableAssets: {
asset: GitHubAsset;
isDownloaded: boolean;
installedVersion?: string;
installedBackendVersion?: string;
}[];
}
@ -162,6 +162,7 @@ export interface KoboldAPI {
setSelectedConfig: (configName: string) => Promise<boolean>;
parseConfigFile: (filePath: string) => Promise<KoboldConfig | null>;
selectModelFile: (title?: string) => Promise<string | null>;
importLocalBackend: () => Promise<{ success: boolean; error?: string }>;
getLocalModels: (paramType: string) => Promise<CachedModel[]>;
analyzeModel: (filePath: string) => Promise<ModelAnalysis>;
calculateOptimalLayers: (

View file

@ -66,7 +66,7 @@ export interface UpdateInfo {
hasUpdate: boolean;
}
export interface VersionInfo {
export interface BackendInfo {
name: string;
version: string;
size?: number;
@ -80,7 +80,7 @@ export interface VersionInfo {
}
export interface DismissedUpdate {
currentVersionPath: string;
currentBackendPath: string;
targetVersion: string;
}

View file

@ -9,14 +9,22 @@ export const getAssetDescription = (assetName: string) => {
}
if (name.endsWith(ASSET_SUFFIXES.OLDPC)) {
return 'Meant for old PCs with outdated CPUs that may not work with the standard build. Does not support modern AVX2 CPU architectures or modern CUDA.';
return 'Meant for old PCs with outdated CPUs that may not work with the standard backend. Does not support modern AVX2 CPU architectures or modern CUDA.';
}
if (name.endsWith(ASSET_SUFFIXES.NOCUDA)) {
return 'Standard build with NVIDIA CUDA support removed for minimal file size.';
return 'Standard backend with NVIDIA CUDA support removed for minimal file size.';
}
return "Standard build that's ideal for most cases.";
if (
name === 'koboldcpp-linux-x64' ||
name === 'koboldcpp-mac-arm64' ||
name === 'koboldcpp'
) {
return "Standard backend that's ideal for most cases.";
}
return 'Custom backend.';
};
export const isWindowsROCmBuild = (assetName: string) => {
@ -78,5 +86,13 @@ export const pretifyBinName = (binName: string) => {
return 'No CUDA';
}
if (
cleanName === 'koboldcpp-linux-x64' ||
cleanName === 'koboldcpp-mac-arm64' ||
cleanName === 'koboldcpp'
) {
return 'Standard';
}
return cleanName;
};

View file

@ -3358,9 +3358,9 @@ __metadata:
languageName: node
linkType: hard
"execa@npm:^9.6.0":
version: 9.6.0
resolution: "execa@npm:9.6.0"
"execa@npm:^9.6.1":
version: 9.6.1
resolution: "execa@npm:9.6.1"
dependencies:
"@sindresorhus/merge-streams": "npm:^4.0.0"
cross-spawn: "npm:^7.0.6"
@ -3374,7 +3374,7 @@ __metadata:
signal-exit: "npm:^4.1.0"
strip-final-newline: "npm:^4.0.0"
yoctocolors: "npm:^2.1.1"
checksum: 10c0/2c44a33142f77d3a6a590a3b769b49b27029a76768593bac1f26fed4dd1330e9c189ee61eba6a8c990fb77e37286c68c7445472ebf24c22b31e9ff320e73d7ac
checksum: 10c0/636b36585306a3c8bc3a9d7b25d2d915fb06d8c9b9b02a804280d62562de3b34535affc1b7702b039320e0953daa6545a073f3c4b63fe974c1fe11336c56b467
languageName: node
linkType: hard
@ -3731,12 +3731,12 @@ __metadata:
eslint-plugin-react: "npm:^7.37.5"
eslint-plugin-react-hooks: "npm:^7.0.1"
eslint-plugin-sonarjs: "npm:^3.0.5"
execa: "npm:^9.6.0"
execa: "npm:^9.6.1"
globals: "npm:^16.5.0"
jiti: "npm:^2.6.1"
lucide-react: "npm:^0.555.0"
mime-types: "npm:^3.0.2"
prettier: "npm:^3.7.2"
prettier: "npm:^3.7.3"
react: "npm:^19.2.0"
react-dom: "npm:^19.2.0"
react-error-boundary: "npm:^6.0.0"
@ -3747,7 +3747,7 @@ __metadata:
winston: "npm:^3.18.3"
winston-daily-rotate-file: "npm:^5.0.0"
yauzl: "npm:^3.2.0"
zustand: "npm:^5.0.8"
zustand: "npm:^5.0.9"
languageName: unknown
linkType: soft
@ -5506,12 +5506,12 @@ __metadata:
languageName: node
linkType: hard
"prettier@npm:^3.7.2":
version: 3.7.2
resolution: "prettier@npm:3.7.2"
"prettier@npm:^3.7.3":
version: 3.7.3
resolution: "prettier@npm:3.7.3"
bin:
prettier: bin/prettier.cjs
checksum: 10c0/df3d658df301face0918f8ecbd4354f32e1151d83a3a4720c7f252342baf631466568f708e0e57beea55bbc56415c40208adc76a91d5f1a88f3e743d0d775dc0
checksum: 10c0/ee86bb06121c74dadc54f30b6f99aff6288966d9b842ce501d6991e20d20c6ce2d45028651b3b0955ca6e5fa89c1bee1e72b6f810243a93cef8bc69737972ef7
languageName: node
linkType: hard
@ -7308,9 +7308,9 @@ __metadata:
languageName: node
linkType: hard
"zustand@npm:^5.0.8":
version: 5.0.8
resolution: "zustand@npm:5.0.8"
"zustand@npm:^5.0.9":
version: 5.0.9
resolution: "zustand@npm:5.0.9"
peerDependencies:
"@types/react": ">=18.0.0"
immer: ">=9.0.6"
@ -7325,6 +7325,6 @@ __metadata:
optional: true
use-sync-external-store:
optional: true
checksum: 10c0/e865a6f7f1c0e03571701db5904151aaa7acefd6f85541a117085e129bf16e8f60b11f8cc82f277472de5c7ad5dcb201e1b0a89035ea0b40c9d9aab3cd24c8ad
checksum: 10c0/552849e4546c7760704d6509a5c412d57c62a1fa9e53169c939ba5e3d75f8cb3df50a64c3a22e6c3f1c8cc00de7543e4edd61ab5ae0c9169ba9a98e28303aba6
languageName: node
linkType: hard