unpack the binary before working with it, bug fixes, 2 more advanced configuration items

This commit is contained in:
Egor 2025-08-15 02:29:34 -07:00
parent 01058a8d02
commit 21b6f87a53
21 changed files with 964 additions and 443 deletions

View file

@ -8,6 +8,7 @@ A koboldcpp manager.
- download and keep up-to-date your [koboldcpp](https://github.com/LostRuins/koboldcpp/releases) binary - download and keep up-to-date your [koboldcpp](https://github.com/LostRuins/koboldcpp/releases) binary
- better surface the ROCm-specific builds of koboldcpp from YellowRoseCx and from [koboldai.org](https://koboldai.org/cpplinuxrocm) - better surface the ROCm-specific builds of koboldcpp from YellowRoseCx and from [koboldai.org](https://koboldai.org/cpplinuxrocm)
- manage the koboldcpp binary to prevent it from running in the background indefinitely - manage the koboldcpp binary to prevent it from running in the background indefinitely
- automatically unpack all downloaded koboldcpp binaries for significantly faster operations
### Prerequisites ### Prerequisites

View file

@ -22,10 +22,10 @@
"bundler", "bundler",
"bundling", "bundling",
"can", "can",
"clblast",
"clinfo",
"classList", "classList",
"className", "className",
"clblast",
"clinfo",
"cloneNode", "cloneNode",
"codeinterface", "codeinterface",
"componentDidCatch", "componentDidCatch",
@ -60,11 +60,11 @@
"filenames", "filenames",
"filepath", "filepath",
"filepaths", "filepaths",
"flashattention",
"Flashattention",
"flexbox", "flexbox",
"flexdir", "flexdir",
"flexwrap", "flexwrap",
"flashattention",
"Flashattention",
"fontsize", "fontsize",
"fontweight", "fontweight",
"forwardRef", "forwardRef",
@ -118,11 +118,12 @@
"minified", "minified",
"minify", "minify",
"moz", "moz",
"multiuser",
"multiplayer", "multiplayer",
"multiuser",
"namespace", "namespace",
"namespaces", "namespaces",
"newpackagename", "newpackagename",
"noavx",
"nocertify", "nocertify",
"nocuda", "nocuda",
"nodeIntegration", "nodeIntegration",
@ -150,6 +151,8 @@
"preload", "preload",
"prettierrc", "prettierrc",
"preventDefault", "preventDefault",
"PYTHONDONTWRITEBYTECODE",
"PYTHONUNBUFFERED",
"querySelector", "querySelector",
"querySelectorAll", "querySelectorAll",
"radeon", "radeon",
@ -208,14 +211,14 @@
"url", "url",
"urls", "urls",
"useCallback", "useCallback",
"useclblast",
"useContext", "useContext",
"usecuda",
"useEffect", "useEffect",
"useMemo", "useMemo",
"useReducer", "useReducer",
"useRef", "useRef",
"useState", "useState",
"useclblast",
"usecuda",
"usevulkan", "usevulkan",
"util", "util",
"utils", "utils",
@ -231,8 +234,8 @@
"webkit", "webkit",
"webp", "webp",
"webpack", "webpack",
"webSecurity",
"websearch", "websearch",
"webSecurity",
"whitespace", "whitespace",
"wmic", "wmic",
"woff", "woff",

8
package-lock.json generated
View file

@ -29,7 +29,7 @@
"@vitejs/plugin-react": "^5.0.0", "@vitejs/plugin-react": "^5.0.0",
"cross-env": "^10.0.0", "cross-env": "^10.0.0",
"cspell": "^9.2.0", "cspell": "^9.2.0",
"electron": "^37.2.6", "electron": "^37.3.0",
"electron-builder": "^26.0.12", "electron-builder": "^26.0.12",
"electron-vite": "^4.0.0", "electron-vite": "^4.0.0",
"eslint": "^9.33.0", "eslint": "^9.33.0",
@ -5607,9 +5607,9 @@
} }
}, },
"node_modules/electron": { "node_modules/electron": {
"version": "37.2.6", "version": "37.3.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-37.2.6.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-37.3.0.tgz",
"integrity": "sha512-Ns6xyxE+hIK5UlujtRlw7w4e2Ju/ImCWXf1Q/PoOhc0N3/6SN6YW7+ujCarsHbxWnolbW+1RlkHtdklUJpjbPA==", "integrity": "sha512-cPOPUD26DwCh+PZ9q+gMyVBvdBN75SnekI6u5zcOeoLVIXQpzrCm1ewz9BcrkWkVW7oOtfQAEo1G1SffvXrSSw==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",

View file

@ -57,7 +57,7 @@
"@vitejs/plugin-react": "^5.0.0", "@vitejs/plugin-react": "^5.0.0",
"cross-env": "^10.0.0", "cross-env": "^10.0.0",
"cspell": "^9.2.0", "cspell": "^9.2.0",
"electron": "^37.2.6", "electron": "^37.3.0",
"electron-builder": "^26.0.12", "electron-builder": "^26.0.12",
"electron-vite": "^4.0.0", "electron-vite": "^4.0.0",
"eslint": "^9.33.0", "eslint": "^9.33.0",

View file

@ -11,6 +11,7 @@ import {
Text, Text,
Button, Button,
Select, Select,
Badge,
useMantineColorScheme, useMantineColorScheme,
} from '@mantine/core'; } from '@mantine/core';
import { Settings, ArrowLeft } from 'lucide-react'; import { Settings, ArrowLeft } from 'lucide-react';
@ -20,7 +21,7 @@ import { InterfaceScreen } from '@/components/screens/InterfaceScreen';
import { UpdateDialog } from '@/components/UpdateDialog'; import { UpdateDialog } from '@/components/UpdateDialog';
import { SettingsModal } from '@/components/SettingsModal'; import { SettingsModal } from '@/components/SettingsModal';
import { ScreenTransition } from '@/components/ScreenTransition'; import { ScreenTransition } from '@/components/ScreenTransition';
import type { UpdateInfo } from '@/types'; import type { UpdateInfo, InstalledVersion } from '@/types';
type Screen = 'download' | 'launch' | 'interface'; type Screen = 'download' | 'launch' | 'interface';
@ -33,24 +34,64 @@ export const App = () => {
const [activeInterfaceTab, setActiveInterfaceTab] = useState<string | null>( const [activeInterfaceTab, setActiveInterfaceTab] = useState<string | null>(
'terminal' 'terminal'
); );
const [currentVersion, setCurrentVersion] = useState<InstalledVersion | null>(
null
);
const [installedVersions, setInstalledVersions] = useState<
InstalledVersion[]
>([]);
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const getDisplayNameFromPath = (
installedVersion: InstalledVersion
): string => {
const pathParts = installedVersion.path.split(/[/\\]/);
const launcherIndex = pathParts.findIndex(
(part) =>
part === 'koboldcpp-launcher' ||
part === 'koboldcpp.exe' ||
part === 'koboldcpp'
);
if (launcherIndex > 0) {
return pathParts[launcherIndex - 1];
}
return installedVersion.filename;
};
useEffect(() => { useEffect(() => {
const checkInstallation = async () => { const checkInstallation = async () => {
try { try {
const startTime = Date.now(); const [versions, currentBinaryPath] = await Promise.all([
const installedVersions = window.electronAPI.kobold.getInstalledVersions(),
await window.electronAPI.kobold.getInstalledVersions(false); window.electronAPI.config.get(
'currentKoboldBinary'
) as Promise<string>,
]);
const elapsed = Date.now() - startTime; setInstalledVersions(versions);
const minDelay = 500;
if (elapsed < minDelay) { if (versions.length > 0) {
await new Promise((resolve) => let current = null;
setTimeout(resolve, minDelay - elapsed) if (currentBinaryPath) {
current = versions.find((v) => v.path === currentBinaryPath);
}
if (!current) {
current = versions[0];
if (current) {
await window.electronAPI.config.set(
'currentKoboldBinary',
current.path
); );
} }
}
setCurrentVersion(current);
setCurrentScreen('launch');
} else {
setCurrentScreen('download');
}
setCurrentScreen(installedVersions.length > 0 ? 'launch' : 'download');
setHasInitialized(true); setHasInitialized(true);
} catch (error) { } catch (error) {
console.error('Error checking installation:', error); console.error('Error checking installation:', error);
@ -70,18 +111,73 @@ export const App = () => {
checkInstallation(); checkInstallation();
}); });
const cleanupVersionsListener = window.electronAPI.kobold.onVersionsUpdated(
() => {
checkInstallation();
}
);
return () => { return () => {
window.electronAPI.kobold.removeAllListeners('update-available'); window.electronAPI.kobold.removeAllListeners('update-available');
cleanupInstallDirListener(); cleanupInstallDirListener();
cleanupVersionsListener();
}; };
}, []); }, []);
const handleDownloadComplete = () => { const handleDownloadComplete = async () => {
try {
const [versions, currentBinaryPath] = await Promise.all([
window.electronAPI.kobold.getInstalledVersions(),
window.electronAPI.config.get('currentKoboldBinary') as Promise<string>,
]);
setInstalledVersions(versions);
if (versions.length > 0) {
let current = null;
if (currentBinaryPath) {
current = versions.find((v) => v.path === currentBinaryPath);
}
if (!current) {
current = versions[0];
if (current) {
await window.electronAPI.config.set(
'currentKoboldBinary',
current.path
);
}
}
setCurrentVersion(current);
}
} catch (error) {
console.error('Error refreshing versions after download:', error);
}
setTimeout(() => { setTimeout(() => {
setCurrentScreen('launch'); setCurrentScreen('launch');
}, 100); }, 100);
}; };
const handleVersionChange = async (versionPath: string | null) => {
if (!versionPath) return;
try {
const success =
await window.electronAPI.kobold.setCurrentVersion(versionPath);
if (success) {
await window.electronAPI.config.set('currentKoboldBinary', versionPath);
const newCurrent = installedVersions.find(
(v) => v.path === versionPath
);
if (newCurrent) {
setCurrentVersion(newCurrent);
}
}
} catch (error) {
console.error('Failed to change version:', error);
}
};
const handleLaunch = () => { const handleLaunch = () => {
setCurrentScreen('interface'); setCurrentScreen('interface');
}; };
@ -170,6 +266,49 @@ export const App = () => {
/> />
)} )}
{currentScreen === 'launch' && currentVersion && (
<Group gap="xs" align="center">
<Select
value={currentVersion.path}
onChange={handleVersionChange}
data={installedVersions.map((version) => ({
value: version.path,
label: getDisplayNameFromPath(version),
}))}
placeholder="Select version"
variant="unstyled"
styles={{
input: {
minWidth: '225px',
textAlign: 'center',
border: `1px solid var(--mantine-color-${colorScheme === 'dark' ? 'dark-4' : 'gray-4'})`,
borderRadius: 'var(--mantine-radius-sm)',
backgroundColor:
colorScheme === 'dark'
? 'var(--mantine-color-dark-6)'
: 'var(--mantine-color-gray-0)',
fontWeight: 500,
fontSize: '14px',
padding: '6px 12px',
transition: 'all 200ms ease',
'&:hover': {
backgroundColor:
colorScheme === 'dark'
? 'var(--mantine-color-dark-5)'
: 'var(--mantine-color-gray-1)',
},
},
dropdown: {
minWidth: '200px',
},
}}
/>
<Badge variant="light" color="blue" size="sm">
v{currentVersion.version}
</Badge>
</Group>
)}
<div <div
style={{ style={{
minWidth: '100px', minWidth: '100px',
@ -206,7 +345,7 @@ export const App = () => {
<Stack align="center" gap="lg"> <Stack align="center" gap="lg">
<Loader size="xl" type="dots" /> <Loader size="xl" type="dots" />
<Text c="dimmed" size="lg"> <Text c="dimmed" size="lg">
Initializing... Loading...
</Text> </Text>
</Stack> </Stack>
</Center> </Center>
@ -249,6 +388,7 @@ export const App = () => {
<SettingsModal <SettingsModal
opened={settingsOpened} opened={settingsOpened}
onClose={() => setSettingsOpened(false)} onClose={() => setSettingsOpened(false)}
currentScreen={currentScreen || undefined}
/> />
</AppShell> </AppShell>
</> </>

View file

@ -0,0 +1,143 @@
import {
Card,
Stack,
Group,
Text,
Badge,
Button,
Loader,
Progress,
rem,
} from '@mantine/core';
import { Download } from 'lucide-react';
import { MouseEvent } from 'react';
interface DownloadCardProps {
name: string;
size: string;
version?: string;
description?: string;
isRecommended?: boolean;
isCurrent?: boolean;
isInstalled?: boolean;
isDownloading?: boolean;
downloadProgress?: number;
disabled?: boolean;
onDownload?: (e: MouseEvent<HTMLButtonElement>) => void;
onMakeCurrent?: () => void;
}
export const DownloadCard = ({
name,
size,
version,
description,
isRecommended = false,
isCurrent = false,
isInstalled = false,
isDownloading = false,
downloadProgress = 0,
disabled = false,
onDownload,
onMakeCurrent,
}: DownloadCardProps) => {
const renderActionButton = () => {
if (!isInstalled && onDownload) {
return (
<Button
variant="filled"
size="xs"
onClick={onDownload}
loading={isDownloading}
disabled={disabled}
leftSection={
isDownloading ? (
<Loader size="1rem" />
) : (
<Download style={{ width: rem(14), height: rem(14) }} />
)
}
>
{isDownloading ? 'Downloading...' : 'Download'}
</Button>
);
}
if (isInstalled && !isCurrent && onMakeCurrent) {
return (
<Button variant="light" size="xs" onClick={onMakeCurrent}>
Make Current
</Button>
);
}
return null;
};
return (
<Card
withBorder
radius="sm"
padding="sm"
bd={isCurrent ? '2px solid var(--mantine-color-blue-filled)' : undefined}
bg={isCurrent ? 'var(--mantine-color-blue-light)' : undefined}
>
<Group justify="space-between" align="center">
<div style={{ flex: 1 }}>
<Group gap="xs" align="center" mb="xs">
<Text fw={500} size="sm">
{name}
</Text>
{isCurrent && (
<Badge variant="light" color="blue" size="sm">
Current
</Badge>
)}
{isRecommended && (
<Badge variant="light" color="blue" size="sm">
Recommended
</Badge>
)}
</Group>
{description && (
<Text size="xs" c="dimmed" mb="xs">
{description}
</Text>
)}
<Group gap="xs" align="center">
{version && (
<Text size="xs" c="dimmed">
Version {version}
</Text>
)}
{size && (
<>
<Text size="xs" c="dimmed">
</Text>
<Badge variant="light" color="gray" size="xs">
{size}
</Badge>
</>
)}
</Group>
</div>
{renderActionButton()}
</Group>
{isDownloading && downloadProgress !== undefined && (
<Stack gap="xs" mt="sm">
<Progress
value={Math.min(downloadProgress, 100)}
color="blue"
radius="xl"
/>
<Text size="xs" c="dimmed" ta="center">
{Math.min(downloadProgress, 100).toFixed(1)}% complete
</Text>
</Stack>
)}
</Card>
);
};

View file

@ -1,92 +0,0 @@
import {
Card,
Stack,
Group,
Text,
Badge,
Button,
Loader,
Progress,
} from '@mantine/core';
import { Download } from 'lucide-react';
import { MouseEvent } from 'react';
interface DownloadOptionCardProps {
name: string;
description: string;
size: string;
isSelected: boolean;
isRecommended: boolean;
isDownloading: boolean;
downloadProgress?: number;
onClick: () => void;
onDownload: (e: MouseEvent<HTMLButtonElement>) => void;
}
export const DownloadOptionCard = ({
name,
description,
size,
isSelected,
isRecommended,
isDownloading,
downloadProgress = 0,
onClick,
onDownload,
}: DownloadOptionCardProps) => (
<Card
withBorder
radius="md"
style={{
cursor: 'pointer',
}}
bd={isSelected ? '2px solid var(--mantine-color-blue-filled)' : undefined}
bg={isSelected ? 'var(--mantine-color-blue-light)' : undefined}
onClick={onClick}
>
<Stack gap="xs" style={{ flex: 1 }}>
<Group justify="space-between" align="center">
<Group gap="xs">
<Text fw={500}>{name}</Text>
{isRecommended && (
<Badge variant="light" color="blue" size="sm">
Recommended
</Badge>
)}
</Group>
<Badge variant="light" color="gray" size="sm">
{size}
</Badge>
</Group>
<Text size="sm" c="dimmed">
{description}
</Text>
{isSelected && (
<Stack gap="sm" pt="sm">
<Button
onClick={onDownload}
disabled={isDownloading}
leftSection={
isDownloading ? <Loader size="1rem" /> : <Download size="1rem" />
}
size="sm"
radius="md"
fullWidth
>
{isDownloading ? 'Downloading...' : 'Download'}
</Button>
{isDownloading && (
<Stack gap="xs">
<Progress value={downloadProgress} color="blue" radius="xl" />
<Text size="xs" c="dimmed" ta="center">
{downloadProgress.toFixed(1)}% complete
</Text>
</Stack>
)}
</Stack>
)}
</Stack>
</Card>
);

View file

@ -8,11 +8,24 @@ import { AppearanceTab } from './settings/AppearanceTab';
interface SettingsModalProps { interface SettingsModalProps {
opened: boolean; opened: boolean;
onClose: () => void; onClose: () => void;
currentScreen?: 'download' | 'launch' | 'interface';
} }
export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => { export const SettingsModal = ({
opened,
onClose,
currentScreen,
}: SettingsModalProps) => {
const [activeTab, setActiveTab] = useState('general'); const [activeTab, setActiveTab] = useState('general');
const showVersionsTab = currentScreen !== 'download';
useEffect(() => {
if (!showVersionsTab && activeTab === 'versions') {
setActiveTab('general');
}
}, [showVersionsTab, activeTab]);
useEffect(() => { useEffect(() => {
if (opened) { if (opened) {
const originalOverflow = document.body.style.overflow; const originalOverflow = document.body.style.overflow;
@ -39,7 +52,7 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
<Text fw={500}>Settings</Text> <Text fw={500}>Settings</Text>
</Group> </Group>
} }
size="lg" size="xl"
centered centered
lockScroll={false} lockScroll={false}
styles={{ styles={{
@ -81,6 +94,7 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
> >
General General
</Tabs.Tab> </Tabs.Tab>
{showVersionsTab && (
<Tabs.Tab <Tabs.Tab
value="versions" value="versions"
leftSection={ leftSection={
@ -89,6 +103,7 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
> >
Versions Versions
</Tabs.Tab> </Tabs.Tab>
)}
<Tabs.Tab <Tabs.Tab
value="appearance" value="appearance"
leftSection={ leftSection={
@ -103,9 +118,11 @@ export const SettingsModal = ({ opened, onClose }: SettingsModalProps) => {
<GeneralTab /> <GeneralTab />
</Tabs.Panel> </Tabs.Panel>
{showVersionsTab && (
<Tabs.Panel value="versions"> <Tabs.Panel value="versions">
<VersionsTab /> <VersionsTab />
</Tabs.Panel> </Tabs.Panel>
)}
<Tabs.Panel value="appearance"> <Tabs.Panel value="appearance">
<AppearanceTab /> <AppearanceTab />

View file

@ -6,10 +6,14 @@ interface AdvancedTabProps {
serverOnly: boolean; serverOnly: boolean;
noshift: boolean; noshift: boolean;
flashattention: boolean; flashattention: boolean;
noavx2: boolean;
failsafe: boolean;
onAdditionalArgumentsChange: (args: string) => void; onAdditionalArgumentsChange: (args: string) => void;
onServerOnlyChange: (serverOnly: boolean) => void; onServerOnlyChange: (serverOnly: boolean) => void;
onNoshiftChange: (noshift: boolean) => void; onNoshiftChange: (noshift: boolean) => void;
onFlashattentionChange: (flashattention: boolean) => void; onFlashattentionChange: (flashattention: boolean) => void;
onNoavx2Change: (noavx2: boolean) => void;
onFailsafeChange: (failsafe: boolean) => void;
} }
export const AdvancedTab = ({ export const AdvancedTab = ({
@ -17,10 +21,14 @@ export const AdvancedTab = ({
serverOnly, serverOnly,
noshift, noshift,
flashattention, flashattention,
noavx2,
failsafe,
onAdditionalArgumentsChange, onAdditionalArgumentsChange,
onServerOnlyChange, onServerOnlyChange,
onNoshiftChange, onNoshiftChange,
onFlashattentionChange, onFlashattentionChange,
onNoavx2Change,
onFailsafeChange,
}: AdvancedTabProps) => ( }: AdvancedTabProps) => (
<Stack gap="lg"> <Stack gap="lg">
<div> <div>
@ -40,28 +48,37 @@ export const AdvancedTab = ({
</div> </div>
<div> <div>
<Stack gap="md">
<Group gap="lg" align="flex-start" wrap="nowrap">
<div style={{ minWidth: '200px' }}>
<Group gap="xs" align="center"> <Group gap="xs" align="center">
<Checkbox <Checkbox
checked={serverOnly} checked={serverOnly}
onChange={(event) => onServerOnlyChange(event.currentTarget.checked)} onChange={(event) =>
onServerOnlyChange(event.currentTarget.checked)
}
label="Server-only mode" label="Server-only mode"
/> />
<InfoTooltip label="In server-only mode, the KoboldAI Lite web UI won't be displayed. Use this if you'll be using your own frontend." /> <InfoTooltip label="In server-only mode, the KoboldAI Lite web UI won't be displayed. Use this if you'll be using your own frontend." />
</Group> </Group>
</div> </div>
<div> <div style={{ minWidth: '200px' }}>
<Group gap="xs" align="center"> <Group gap="xs" align="center">
<Checkbox <Checkbox
checked={!noshift} checked={!noshift}
onChange={(event) => onNoshiftChange(!event.currentTarget.checked)} onChange={(event) =>
onNoshiftChange(!event.currentTarget.checked)
}
label="Use ContextShift" label="Use ContextShift"
/> />
<InfoTooltip label="Use Context Shifting to reduce reprocessing. Recommended" /> <InfoTooltip label="Use Context Shifting to reduce reprocessing. Recommended" />
</Group> </Group>
</div> </div>
</Group>
<div> <Group gap="lg" align="flex-start" wrap="nowrap">
<div style={{ minWidth: '200px' }}>
<Group gap="xs" align="center"> <Group gap="xs" align="center">
<Checkbox <Checkbox
checked={flashattention} checked={flashattention}
@ -73,5 +90,30 @@ export const AdvancedTab = ({
<InfoTooltip label="Enable flash attention for GGUF models." /> <InfoTooltip label="Enable flash attention for GGUF models." />
</Group> </Group>
</div> </div>
<div style={{ minWidth: '200px' }}>
<Group gap="xs" align="center">
<Checkbox
checked={noavx2}
onChange={(event) =>
onNoavx2Change(event.currentTarget.checked)
}
label="Disable AVX2"
/>
<InfoTooltip label="Do not use AVX2 instructions, a slower compatibility mode for older devices." />
</Group>
</div>
</Group>
<Group gap="xs" align="center">
<Checkbox
checked={failsafe}
onChange={(event) => onFailsafeChange(event.currentTarget.checked)}
label="Failsafe"
/>
<InfoTooltip label="Use failsafe mode, extremely slow CPU only compatibility mode that should work on all devices. Can be combined with useclblast if your device supports OpenCL." />
</Group>
</Stack>
</div>
</Stack> </Stack>
); );

View file

@ -9,16 +9,13 @@ import {
Select, Select,
Badge, Badge,
Card, Card,
useMantineColorScheme,
} from '@mantine/core'; } from '@mantine/core';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { File, Search } from 'lucide-react'; import { File, Search } from 'lucide-react';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
import { getInputValidationState } from '@/utils/validation'; import { getInputValidationState } from '@/utils/validation';
import { import { isNoCudaBinary, isRocmBinary } from '@/utils/binaryUtils';
isOldPcBinary,
isNoCudaBinary,
isRocmBinary,
} from '@/utils/binaryUtils';
interface GeneralTabProps { interface GeneralTabProps {
modelPath: string; modelPath: string;
@ -47,6 +44,9 @@ export const GeneralTab = ({
onContextSizeChange, onContextSizeChange,
onBackendChange, onBackendChange,
}: GeneralTabProps) => { }: GeneralTabProps) => {
const { colorScheme } = useMantineColorScheme();
const isDark = colorScheme === 'dark';
const [availableBackends, setAvailableBackends] = useState< const [availableBackends, setAvailableBackends] = useState<
Array<{ value: string; label: string; devices?: string[] }> Array<{ value: string; label: string; devices?: string[] }>
>([]); >([]);
@ -57,9 +57,9 @@ export const GeneralTab = ({
setIsLoadingBackends(true); setIsLoadingBackends(true);
try { try {
const [currentVersion, cpuCapabilities, gpuCapabilities] = const [currentBinaryInfo, _cpuCapabilities, gpuCapabilities] =
await Promise.all([ await Promise.all([
window.electronAPI.kobold.getCurrentVersion(), window.electronAPI.kobold.getCurrentBinaryInfo(),
window.electronAPI.kobold.detectCPU(), window.electronAPI.kobold.detectCPU(),
window.electronAPI.kobold.detectGPUCapabilities(), window.electronAPI.kobold.detectGPUCapabilities(),
]); ]);
@ -70,12 +70,10 @@ export const GeneralTab = ({
devices?: string[]; devices?: string[];
}> = []; }> = [];
if (currentVersion?.filename) { if (currentBinaryInfo?.filename) {
const filename = currentVersion.filename; const filename = currentBinaryInfo.filename;
if (!isOldPcBinary(filename) && cpuCapabilities.avx2) {
backends.push({ value: 'cpu', label: 'CPU' }); backends.push({ value: 'cpu', label: 'CPU' });
}
if (!isNoCudaBinary(filename) && gpuCapabilities.cuda.supported) { if (!isNoCudaBinary(filename) && gpuCapabilities.cuda.supported) {
backends.push({ backends.push({
@ -110,8 +108,6 @@ export const GeneralTab = ({
} }
} }
backends.push({ value: 'failsafe', label: 'Failsafe' });
setAvailableBackends(backends); setAvailableBackends(backends);
if ( if (
@ -231,8 +227,8 @@ export const GeneralTab = ({
</Group> </Group>
)} )}
{backend === 'cpu' && ( {backend === 'cpu' && (
<Card withBorder p="sm" mt="xs" bg="blue.0"> <Card withBorder p="sm" mt="xs" bg={isDark ? 'blue.9' : 'blue.0'}>
<Text size="sm" c="blue.8"> <Text size="sm" c={isDark ? 'blue.2' : 'blue.8'}>
<Text component="span" fw={600}> <Text component="span" fw={600}>
Performance Note: Performance Note:
</Text>{' '} </Text>{' '}

View file

@ -75,7 +75,7 @@ export const NetworkTab = ({
<div> <div>
<Stack gap="md"> <Stack gap="md">
<Group gap="lg" align="flex-start" wrap="nowrap"> <Group gap="lg" align="flex-start" wrap="nowrap">
<div style={{ minWidth: '250px' }}> <div style={{ minWidth: '200px' }}>
<Group gap="xs" align="center"> <Group gap="xs" align="center">
<Checkbox <Checkbox
checked={multiuser} checked={multiuser}
@ -88,7 +88,7 @@ export const NetworkTab = ({
</Group> </Group>
</div> </div>
<div style={{ minWidth: '250px' }}> <div style={{ minWidth: '200px' }}>
<Group gap="xs" align="center"> <Group gap="xs" align="center">
<Checkbox <Checkbox
checked={multiplayer} checked={multiplayer}
@ -103,7 +103,7 @@ export const NetworkTab = ({
</Group> </Group>
<Group gap="lg" align="flex-start" wrap="nowrap"> <Group gap="lg" align="flex-start" wrap="nowrap">
<div style={{ minWidth: '250px' }}> <div style={{ minWidth: '200px' }}>
<Group gap="xs" align="center"> <Group gap="xs" align="center">
<Checkbox <Checkbox
checked={remotetunnel} checked={remotetunnel}
@ -116,7 +116,7 @@ export const NetworkTab = ({
</Group> </Group>
</div> </div>
<div style={{ minWidth: '250px' }}> <div style={{ minWidth: '200px' }}>
<Group gap="xs" align="center"> <Group gap="xs" align="center">
<Checkbox <Checkbox
checked={nocertify} checked={nocertify}

View file

@ -9,16 +9,16 @@ import {
Badge, Badge,
Tooltip, Tooltip,
} from '@mantine/core'; } from '@mantine/core';
import { DownloadOptionCard } from '@/components/DownloadOptionCard'; import { DownloadCard } from '@/components/DownloadCard';
import { import {
getPlatformDisplayName, getPlatformDisplayName,
filterAssetsByPlatform, filterAssetsByPlatform,
} from '@/utils/platform'; } from '@/utils/platform';
import { formatFileSize } from '@/utils/fileSize'; import { formatFileSize } from '@/utils/fileSize';
import { import {
getAssetDescription,
isAssetRecommended, isAssetRecommended,
sortAssetsByRecommendation, sortAssetsByRecommendation,
getAssetDescription,
} from '@/utils/assets'; } from '@/utils/assets';
import { ROCM } from '@/constants'; import { ROCM } from '@/constants';
import type { GitHubAsset, GitHubRelease } from '@/types'; import type { GitHubAsset, GitHubRelease } from '@/types';
@ -32,8 +32,6 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
null null
); );
const [filteredAssets, setFilteredAssets] = useState<GitHubAsset[]>([]); const [filteredAssets, setFilteredAssets] = useState<GitHubAsset[]>([]);
const [selectedAsset, setSelectedAsset] = useState<GitHubAsset | null>(null);
const [selectedROCm, setSelectedROCm] = useState<boolean>(false);
const [userPlatform, setUserPlatform] = useState<string>(''); const [userPlatform, setUserPlatform] = useState<string>('');
const [hasAMDGPU, setHasAMDGPU] = useState<boolean>(false); const [hasAMDGPU, setHasAMDGPU] = useState<boolean>(false);
const [hasROCm, setHasROCm] = useState<boolean>(false); const [hasROCm, setHasROCm] = useState<boolean>(false);
@ -43,6 +41,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
const [downloadingType, setDownloadingType] = useState< const [downloadingType, setDownloadingType] = useState<
'asset' | 'rocm' | null 'asset' | 'rocm' | null
>(null); >(null);
const [downloadingAsset, setDownloadingAsset] = useState<string | null>(null);
const [rocmDownload, setRocmDownload] = useState<{ const [rocmDownload, setRocmDownload] = useState<{
name: string; name: string;
url: string; url: string;
@ -112,54 +111,51 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const handleDownload = async (type: 'asset' | 'rocm' = 'asset') => { const handleDownload = useCallback(
if (type === 'asset' && !selectedAsset) return; async (type: 'asset' | 'rocm', asset?: GitHubAsset) => {
if (type === 'asset' && !asset) return;
setDownloading(true);
setDownloadingType(type);
setDownloadingAsset(type === 'asset' ? asset!.name : null);
setDownloadProgress(0);
try { try {
setDownloading(true); await (type === 'rocm'
setDownloadProgress(0); ? window.electronAPI.kobold.downloadROCm()
setDownloadingType(type); : await window.electronAPI.kobold.downloadRelease(asset!));
const result =
type === 'rocm'
? await window.electronAPI.kobold.downloadROCm()
: await window.electronAPI.kobold.downloadRelease(selectedAsset!);
if (result.success) {
onDownloadComplete(); onDownloadComplete();
} else {
console.error( setTimeout(() => {
'Download Failed',
result.error || `${type === 'rocm' ? 'ROCm' : ''} Download failed`
);
}
} catch (err) {
console.error(`${type === 'rocm' ? 'ROCm' : ''} Download error:`, err);
} finally {
setDownloading(false); setDownloading(false);
setDownloadProgress(0);
setDownloadingType(null); setDownloadingType(null);
setDownloadingAsset(null);
setDownloadProgress(0);
}, 200);
} catch (error) {
console.error(`Failed to download ${type}:`, error);
setDownloading(false);
setDownloadingType(null);
setDownloadingAsset(null);
setDownloadProgress(0);
} }
}; },
[onDownloadComplete]
);
const renderROCmCard = () => { const renderROCmCard = () => {
if (!rocmDownload) return null; if (!rocmDownload) return null;
return ( return (
<DownloadOptionCard <DownloadCard
name={ROCM.BINARY_NAME} name={ROCM.BINARY_NAME}
description={getAssetDescription(ROCM.BINARY_NAME)}
size={formatFileSize(ROCM.SIZE_BYTES)} size={formatFileSize(ROCM.SIZE_BYTES)}
isSelected={selectedROCm} description={getAssetDescription(ROCM.BINARY_NAME)}
isRecommended={isAssetRecommended(ROCM.BINARY_NAME, hasAMDGPU)} isRecommended={isAssetRecommended(ROCM.BINARY_NAME, hasAMDGPU)}
isDownloading={downloading && downloadingType === 'rocm'} isDownloading={downloading && downloadingType === 'rocm'}
downloadProgress={downloadingType === 'rocm' ? downloadProgress : 0} downloadProgress={downloadingType === 'rocm' ? downloadProgress : 0}
onClick={() => { disabled={downloading && downloadingType !== 'rocm'}
if (!selectedROCm) {
setSelectedROCm(true);
setSelectedAsset(null);
}
}}
onDownload={(e) => { onDownload={(e) => {
e.stopPropagation(); e.stopPropagation();
handleDownload('rocm'); handleDownload('rocm');
@ -178,7 +174,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
{loading ? ( {loading ? (
<Stack align="center" gap="md" py="xl"> <Stack align="center" gap="md" py="xl">
<Loader color="blue" /> <Loader color="blue" />
<Text c="dimmed">Loading latest release...</Text> <Text c="dimmed">Preparing download options...</Text>
</Stack> </Stack>
) : ( ) : (
<> <>
@ -253,12 +249,11 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
filteredAssets, filteredAssets,
hasAMDGPU hasAMDGPU
).map((asset) => ( ).map((asset) => (
<DownloadOptionCard <DownloadCard
key={asset.name} key={asset.name}
name={asset.name} name={asset.name}
description={getAssetDescription(asset.name)}
size={formatFileSize(asset.size)} size={formatFileSize(asset.size)}
isSelected={selectedAsset?.name === asset.name} description={getAssetDescription(asset.name)}
isRecommended={isAssetRecommended( isRecommended={isAssetRecommended(
asset.name, asset.name,
hasAMDGPU hasAMDGPU
@ -266,24 +261,21 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
isDownloading={ isDownloading={
downloading && downloading &&
downloadingType === 'asset' && downloadingType === 'asset' &&
selectedAsset?.name === asset.name downloadingAsset === asset.name
} }
downloadProgress={ downloadProgress={
downloading && downloading &&
downloadingType === 'asset' && downloadingType === 'asset' &&
selectedAsset?.name === asset.name downloadingAsset === asset.name
? downloadProgress ? downloadProgress
: 0 : 0
} }
onClick={() => { disabled={
if (selectedAsset?.name !== asset.name) { downloading && downloadingAsset !== asset.name
setSelectedAsset(asset);
setSelectedROCm(false);
} }
}}
onDownload={(e) => { onDownload={(e) => {
e.stopPropagation(); e.stopPropagation();
handleDownload(); handleDownload('asset', asset);
}} }}
/> />
))} ))}

View file

@ -46,6 +46,8 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
websearch, websearch,
noshift, noshift,
flashattention, flashattention,
noavx2,
failsafe,
backend, backend,
parseAndApplyConfigFile, parseAndApplyConfigFile,
loadSavedSettings, loadSavedSettings,
@ -66,6 +68,8 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
handleWebsearchChange, handleWebsearchChange,
handleNoshiftChange, handleNoshiftChange,
handleFlashattentionChange, handleFlashattentionChange,
handleNoavx2Change,
handleFailsafeChange,
handleBackendChange, handleBackendChange,
} = useLaunchConfig(); } = useLaunchConfig();
@ -155,6 +159,16 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
setHasUnsavedChanges(true); setHasUnsavedChanges(true);
}; };
const handleNoavx2ChangeWithTracking = (noavx2: boolean) => {
handleNoavx2Change(noavx2);
setHasUnsavedChanges(true);
};
const handleFailsafeChangeWithTracking = (failsafe: boolean) => {
handleFailsafeChange(failsafe);
setHasUnsavedChanges(true);
};
const handleMultiuserChangeWithTracking = (multiuser: boolean) => { const handleMultiuserChangeWithTracking = (multiuser: boolean) => {
handleMultiuserChange(multiuser); handleMultiuserChange(multiuser);
setHasUnsavedChanges(true); setHasUnsavedChanges(true);
@ -199,7 +213,6 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
return cleanup; return cleanup;
}, [loadConfigFiles]); }, [loadConfigFiles]);
// eslint-disable-next-line sonarjs/cognitive-complexity
const handleLaunch = async () => { const handleLaunch = async () => {
if (isLaunching || !modelPath) { if (isLaunching || !modelPath) {
return; return;
@ -271,8 +284,6 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
args.push('--usevulkan'); args.push('--usevulkan');
} else if (backend === 'clblast') { } else if (backend === 'clblast') {
args.push('--useclblast'); args.push('--useclblast');
} else if (backend === 'failsafe') {
args.push('--failsafe');
} }
} }
@ -375,6 +386,8 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
serverOnly={serverOnly} serverOnly={serverOnly}
noshift={noshift} noshift={noshift}
flashattention={flashattention} flashattention={flashattention}
noavx2={noavx2}
failsafe={failsafe}
onAdditionalArgumentsChange={ onAdditionalArgumentsChange={
handleAdditionalArgumentsChangeWithTracking handleAdditionalArgumentsChangeWithTracking
} }
@ -383,6 +396,8 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
onFlashattentionChange={ onFlashattentionChange={
handleFlashattentionChangeWithTracking handleFlashattentionChangeWithTracking
} }
onNoavx2Change={handleNoavx2ChangeWithTracking}
onFailsafeChange={handleFailsafeChangeWithTracking}
/> />
</Tabs.Panel> </Tabs.Panel>

View file

@ -5,14 +5,14 @@ import {
Group, Group,
Button, Button,
Card, Card,
Badge,
Progress,
Loader, Loader,
rem, rem,
Center, Center,
} from '@mantine/core'; } from '@mantine/core';
import { RotateCcw, Download } from 'lucide-react'; import { RotateCcw } from 'lucide-react';
import { DownloadCard } from '@/components/DownloadCard';
import { isAssetCompatibleWithPlatform } from '@/utils/platform'; import { isAssetCompatibleWithPlatform } from '@/utils/platform';
import { getAssetDescription } from '@/utils/assets';
import type { import type {
InstalledVersion, InstalledVersion,
GitHubAsset, GitHubAsset,
@ -27,6 +27,7 @@ interface VersionInfo {
isCurrent: boolean; isCurrent: boolean;
downloadUrl?: string; downloadUrl?: string;
installedPath?: string; installedPath?: string;
isROCm?: boolean;
} }
export const VersionsTab = () => { export const VersionsTab = () => {
@ -37,6 +38,12 @@ export const VersionsTab = () => {
null null
); );
const [availableAssets, setAvailableAssets] = useState<GitHubAsset[]>([]); const [availableAssets, setAvailableAssets] = useState<GitHubAsset[]>([]);
const [rocmDownload, setRocmDownload] = useState<{
name: string;
url: string;
size: number;
version?: string;
} | null>(null);
const [latestRelease, setLatestRelease] = useState<GitHubRelease | null>( const [latestRelease, setLatestRelease] = useState<GitHubRelease | null>(
null null
); );
@ -60,14 +67,28 @@ export const VersionsTab = () => {
setInstalledVersions(versions); setInstalledVersions(versions);
if (currentBinaryPath) { if (currentBinaryPath && versions.length > 0) {
const current = versions.find( const current = versions.find((v) => v.path === currentBinaryPath);
(v) => if (current) {
v.path === currentBinaryPath || v.filename === currentBinaryPath setCurrentVersion(current);
);
setCurrentVersion(current || null);
} else { } else {
setCurrentVersion(versions[0] || null); setCurrentVersion(versions[0]);
await window.electronAPI.config.set(
'currentKoboldBinary',
versions[0].path
);
}
} else if (versions.length > 0) {
setCurrentVersion(versions[0]);
await window.electronAPI.config.set(
'currentKoboldBinary',
versions[0].path
);
} else {
setCurrentVersion(null);
if (currentBinaryPath) {
await window.electronAPI.config.set('currentKoboldBinary', '');
}
} }
} catch (error) { } catch (error) {
console.error('Failed to load installed versions:', error); console.error('Failed to load installed versions:', error);
@ -81,7 +102,10 @@ export const VersionsTab = () => {
setLoadingRemote(true); setLoadingRemote(true);
try { try {
const release = await window.electronAPI.kobold.getLatestRelease(); const [release, rocm] = await Promise.all([
window.electronAPI.kobold.getLatestRelease(),
window.electronAPI.kobold.getROCmDownload(),
]);
if (release) { if (release) {
setLatestRelease(release); setLatestRelease(release);
@ -90,6 +114,8 @@ export const VersionsTab = () => {
); );
setAvailableAssets(compatibleAssets); setAvailableAssets(compatibleAssets);
} }
setRocmDownload(rocm);
} catch (error) { } catch (error) {
console.error('Failed to load remote versions:', error); console.error('Failed to load remote versions:', error);
} finally { } finally {
@ -134,13 +160,32 @@ export const VersionsTab = () => {
}; };
}, [downloading]); }, [downloading]);
const getDisplayNameFromPath = (
installedVersion: InstalledVersion
): string => {
const pathParts = installedVersion.path.split(/[/\\]/);
const launcherIndex = pathParts.findIndex(
(part) =>
part === 'koboldcpp-launcher' ||
part === 'koboldcpp.exe' ||
part === 'koboldcpp'
);
if (launcherIndex > 0) {
return pathParts[launcherIndex - 1];
}
return installedVersion.filename;
};
const getAllVersions = (): VersionInfo[] => { const getAllVersions = (): VersionInfo[] => {
const versions: VersionInfo[] = []; const versions: VersionInfo[] = [];
availableAssets.forEach((asset) => { availableAssets.forEach((asset) => {
const installedVersion = installedVersions.find( const installedVersion = installedVersions.find((v) => {
(v) => v.filename === asset.name const displayName = getDisplayNameFromPath(v);
); return displayName === asset.name;
});
const isCurrent = Boolean( const isCurrent = Boolean(
installedVersion && installedVersion &&
@ -154,31 +199,63 @@ export const VersionsTab = () => {
installedVersion?.version || installedVersion?.version ||
latestRelease?.tag_name.replace(/^v/, '') || latestRelease?.tag_name.replace(/^v/, '') ||
'unknown', 'unknown',
size: installedVersion?.size || asset.size, size: installedVersion ? undefined : asset.size,
isInstalled: Boolean(installedVersion), isInstalled: Boolean(installedVersion),
isCurrent, isCurrent,
downloadUrl: asset.browser_download_url, downloadUrl: asset.browser_download_url,
installedPath: installedVersion?.path, installedPath: installedVersion?.path,
isROCm: false,
}); });
}); });
installedVersions.forEach((installed) => { if (rocmDownload) {
const existsInRemote = versions.some( const installedVersion = installedVersions.find((v) => {
(v) => v.name === installed.filename const displayName = getDisplayNameFromPath(v);
return displayName === rocmDownload.name;
});
const isCurrent = Boolean(
installedVersion &&
currentVersion &&
currentVersion.path === installedVersion.path
); );
const existsInVersions = versions.some(
(v) => v.name === rocmDownload.name
);
if (!existsInVersions) {
versions.push({
name: rocmDownload.name,
version:
installedVersion?.version || rocmDownload.version || 'unknown',
size: installedVersion ? undefined : rocmDownload.size,
isInstalled: Boolean(installedVersion),
isCurrent,
downloadUrl: rocmDownload.url,
installedPath: installedVersion?.path,
isROCm: true,
});
}
}
installedVersions.forEach((installed) => {
const displayName = getDisplayNameFromPath(installed);
const existsInRemote = versions.some((v) => v.name === displayName);
if (!existsInRemote) { if (!existsInRemote) {
const isCurrent = Boolean( const isCurrent = Boolean(
currentVersion && currentVersion.path === installed.path currentVersion && currentVersion.path === installed.path
); );
versions.push({ versions.push({
name: installed.filename, name: displayName,
version: installed.version, version: installed.version,
size: installed.size, size: undefined,
isInstalled: true, isInstalled: true,
isCurrent, isCurrent,
installedPath: installed.path, installedPath: installed.path,
isROCm: false,
}); });
} }
}); });
@ -186,12 +263,22 @@ export const VersionsTab = () => {
return versions; return versions;
}; };
const handleDownload = async (asset: GitHubAsset) => { const handleDownload = async (version: VersionInfo) => {
setDownloading(asset.name); setDownloading(version.name);
setDownloadProgress((prev) => ({ ...prev, [asset.name]: 0 })); setDownloadProgress((prev) => ({ ...prev, [version.name]: 0 }));
try { try {
const result = await window.electronAPI.kobold.downloadRelease(asset); let result;
if (version.isROCm) {
result = await window.electronAPI.kobold.downloadROCm();
} else {
const asset = availableAssets.find((a) => a.name === version.name);
if (!asset) {
throw new Error('Asset not found');
}
result = await window.electronAPI.kobold.downloadRelease(asset);
}
if (result.success) { if (result.success) {
await loadInstalledVersions(); await loadInstalledVersions();
@ -202,7 +289,7 @@ export const VersionsTab = () => {
setDownloading(null); setDownloading(null);
setDownloadProgress((prev) => { setDownloadProgress((prev) => {
const newProgress = { ...prev }; const newProgress = { ...prev };
delete newProgress[asset.name]; delete newProgress[version.name];
return newProgress; return newProgress;
}); });
} }
@ -275,92 +362,37 @@ export const VersionsTab = () => {
<Stack gap="xs"> <Stack gap="xs">
{getAllVersions().map((version, index) => { {getAllVersions().map((version, index) => {
const isDownloading = downloading === version.name; const isDownloading = downloading === version.name;
const asset = availableAssets.find((a) => a.name === version.name);
return ( return (
<Card <DownloadCard
key={`${version.name}-${version.version}-${index}`} key={`${version.name}-${version.version}-${index}`}
withBorder name={version.name}
radius="sm" size={
padding="sm" version.size
bd={ ? `${(version.size / 1024 / 1024).toFixed(1)} MB`
version.isCurrent : ''
? '2px solid var(--mantine-color-blue-filled)'
: undefined
} }
bg={ version={version.version}
version.isCurrent description={getAssetDescription(version.name)}
? 'var(--mantine-color-blue-light)' isCurrent={version.isCurrent}
: undefined isInstalled={version.isInstalled}
} isDownloading={isDownloading}
> downloadProgress={downloadProgress[version.name]}
<Group justify="space-between" align="center">
<div style={{ flex: 1 }}>
<Group gap="xs" align="center" mb="xs">
<Text fw={500} size="sm">
{version.name}
</Text>
{version.isCurrent && (
<Badge variant="light" color="blue" size="sm">
Current
</Badge>
)}
</Group>
<Text size="xs" c="dimmed">
Version {version.version}
{version.size && (
<> {(version.size / 1024 / 1024).toFixed(1)} MB</>
)}
</Text>
</div>
{!version.isInstalled && asset && (
<Button
variant="filled"
size="xs"
onClick={(e) => {
e.stopPropagation();
handleDownload(asset);
}}
loading={isDownloading}
disabled={downloading !== null} disabled={downloading !== null}
leftSection={ onDownload={
isDownloading ? ( !version.isInstalled
<Loader size="1rem" /> ? (e) => {
) : ( e.stopPropagation();
<Download style={{ width: rem(14), height: rem(14) }} /> handleDownload(version);
) }
: undefined
}
onMakeCurrent={
version.isInstalled && !version.isCurrent
? () => makeCurrent(version)
: undefined
} }
>
{isDownloading ? 'Downloading...' : 'Download'}
</Button>
)}
{version.isInstalled && !version.isCurrent && (
<Button
variant="light"
size="xs"
onClick={() => makeCurrent(version)}
>
Make Current
</Button>
)}
</Group>
{isDownloading &&
downloadProgress[version.name] !== undefined && (
<Stack gap="xs" mt="sm">
<Progress
value={downloadProgress[version.name]}
color="blue"
radius="xl"
/> />
<Text size="xs" c="dimmed" ta="center">
{downloadProgress[version.name].toFixed(1)}% complete
</Text>
</Stack>
)}
</Card>
); );
})} })}

View file

@ -17,7 +17,9 @@ export const useLaunchConfig = () => {
const [websearch, setWebsearch] = useState<boolean>(false); const [websearch, setWebsearch] = useState<boolean>(false);
const [noshift, setNoshift] = useState<boolean>(false); const [noshift, setNoshift] = useState<boolean>(false);
const [flashattention, setFlashattention] = useState<boolean>(false); const [flashattention, setFlashattention] = useState<boolean>(false);
const [backend, setBackend] = useState<string>(''); const [noavx2, setNoavx2] = useState<boolean>(false);
const [failsafe, setFailsafe] = useState<boolean>(false);
const [backend, setBackend] = useState<string>('cpu');
// eslint-disable-next-line sonarjs/cognitive-complexity // eslint-disable-next-line sonarjs/cognitive-complexity
const parseAndApplyConfigFile = useCallback(async (configPath: string) => { const parseAndApplyConfigFile = useCallback(async (configPath: string) => {
@ -94,10 +96,33 @@ export const useLaunchConfig = () => {
setFlashattention(false); setFlashattention(false);
} }
if (typeof configData.backend === 'string') { if (typeof configData.noavx2 === 'boolean') {
setBackend(configData.backend); setNoavx2(configData.noavx2);
} else { } else {
setBackend(''); setNoavx2(false);
}
if (typeof configData.failsafe === 'boolean') {
setFailsafe(configData.failsafe);
} else {
setFailsafe(false);
}
if (configData.usecuda !== null && configData.usecuda !== undefined) {
const gpuInfo = await window.electronAPI.kobold.detectGPU();
setBackend(gpuInfo.hasNVIDIA ? 'cuda' : 'rocm');
} else if (
configData.usevulkan !== null &&
configData.usevulkan !== undefined
) {
setBackend('vulkan');
} else if (
configData.useclblast !== null &&
configData.useclblast !== undefined
) {
setBackend('clblast');
} else {
setBackend('cpu');
} }
} else { } else {
setGpuLayers(0); setGpuLayers(0);
@ -111,7 +136,9 @@ export const useLaunchConfig = () => {
setWebsearch(false); setWebsearch(false);
setNoshift(false); setNoshift(false);
setFlashattention(false); setFlashattention(false);
setBackend(''); setNoavx2(false);
setFailsafe(false);
setBackend('cpu');
} }
}, []); }, []);
@ -131,7 +158,9 @@ export const useLaunchConfig = () => {
setWebsearch(false); setWebsearch(false);
setNoshift(false); setNoshift(false);
setFlashattention(false); setFlashattention(false);
setBackend(''); setNoavx2(false);
setFailsafe(false);
setBackend('cpu');
}, []); }, []);
const loadConfigFromFile = useCallback( const loadConfigFromFile = useCallback(
@ -237,6 +266,14 @@ export const useLaunchConfig = () => {
setFlashattention(checked); setFlashattention(checked);
}, []); }, []);
const handleNoavx2Change = useCallback((checked: boolean) => {
setNoavx2(checked);
}, []);
const handleFailsafeChange = useCallback((checked: boolean) => {
setFailsafe(checked);
}, []);
const handleBackendChange = useCallback((backend: string) => { const handleBackendChange = useCallback((backend: string) => {
setBackend(backend); setBackend(backend);
}, []); }, []);
@ -257,6 +294,8 @@ export const useLaunchConfig = () => {
websearch, websearch,
noshift, noshift,
flashattention, flashattention,
noavx2,
failsafe,
backend, backend,
parseAndApplyConfigFile, parseAndApplyConfigFile,
@ -278,6 +317,8 @@ export const useLaunchConfig = () => {
handleWebsearchChange, handleWebsearchChange,
handleNoshiftChange, handleNoshiftChange,
handleFlashattentionChange, handleFlashattentionChange,
handleNoavx2Change,
handleFailsafeChange,
handleBackendChange, handleBackendChange,
}; };
}; };

View file

@ -95,13 +95,29 @@ class FriendlyKoboldApp {
app.on('before-quit', async (event) => { app.on('before-quit', async (event) => {
event.preventDefault(); event.preventDefault();
await this.koboldManager.cleanup(); try {
const cleanupPromise = this.koboldManager.cleanup();
const timeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, 10000);
});
await Promise.race([cleanupPromise, timeoutPromise]);
} catch (error) {
console.error('Error during KoboldCpp cleanup:', error);
}
this.windowManager.cleanup(); this.windowManager.cleanup();
app.exit(0); app.exit(0);
}); });
app.on('will-quit', async (event) => {
event.preventDefault();
app.exit(0);
});
app.on('activate', () => { app.on('activate', () => {
if (!this.windowManager.getMainWindow()) { if (!this.windowManager.getMainWindow()) {
this.windowManager.createMainWindow(); this.windowManager.createMainWindow();

View file

@ -7,6 +7,7 @@ import {
createWriteStream, createWriteStream,
chmodSync, chmodSync,
readFileSync, readFileSync,
unlinkSync,
} from 'fs'; } from 'fs';
import { dialog } from 'electron'; import { dialog } from 'electron';
import { GitHubService } from '@/main/services/GitHubService'; import { GitHubService } from '@/main/services/GitHubService';
@ -74,7 +75,9 @@ export class KoboldCppManager {
asset: GitHubAsset, asset: GitHubAsset,
onProgress?: (progress: number) => void onProgress?: (progress: number) => void
): Promise<string> { ): Promise<string> {
const filePath = join(this.installDir, asset.name); const tempPackedFilePath = join(this.installDir, `${asset.name}.packed`);
const baseFilename = asset.name.replace(/\.exe$/, '');
const unpackedDirPath = join(this.installDir, baseFilename);
const response = await fetch(asset.browser_download_url); const response = await fetch(asset.browser_download_url);
@ -90,7 +93,7 @@ export class KoboldCppManager {
throw new Error('Failed to get response reader'); throw new Error('Failed to get response reader');
} }
const writer = createWriteStream(filePath); const writer = createWriteStream(tempPackedFilePath);
try { try {
while (true) { while (true) {
@ -119,77 +122,151 @@ export class KoboldCppManager {
if (process.platform !== 'win32') { if (process.platform !== 'win32') {
try { try {
chmodSync(filePath, 0o755); chmodSync(tempPackedFilePath, 0o755);
} catch (error) { } catch (error) {
console.warn('Failed to make binary executable:', error); console.error('Failed to make binary executable:', error);
} }
} }
try {
await this.unpackKoboldCpp(tempPackedFilePath, unpackedDirPath);
try {
unlinkSync(tempPackedFilePath);
} catch (error) {
console.warn('Failed to cleanup packed file:', error);
}
const launcherPath = this.getLauncherPath(unpackedDirPath);
if (launcherPath && existsSync(launcherPath)) {
const currentBinary = this.configManager.getCurrentKoboldBinary(); const currentBinary = this.configManager.getCurrentKoboldBinary();
if (!currentBinary) { if (!currentBinary) {
this.configManager.setCurrentKoboldBinary(filePath); this.configManager.setCurrentKoboldBinary(launcherPath);
} }
return filePath; const mainWindow = this.windowManager.getMainWindow();
if (mainWindow) {
mainWindow.webContents.send('versions-updated');
} }
async getInstalledVersions( return launcherPath;
includeVersions = true } else {
): Promise<InstalledVersion[]> { throw new Error('Failed to find koboldcpp-launcher after unpacking');
}
} catch (error) {
console.error('Failed to unpack KoboldCpp:', error);
throw error;
}
}
private async unpackKoboldCpp(
packedPath: string,
unpackDir: string
): Promise<void> {
return new Promise((resolve, reject) => {
const unpackProcess = spawn(packedPath, ['--unpack', unpackDir], {
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 30000,
});
let output = '';
let errorOutput = '';
unpackProcess.stdout?.on('data', (data) => {
output += data.toString();
});
unpackProcess.stderr?.on('data', (data) => {
errorOutput += data.toString();
});
unpackProcess.on('close', (code) => {
if (code === 0) {
resolve();
} else {
reject(
new Error(
`Unpack failed with code ${code}: ${errorOutput || output}`
)
);
}
});
unpackProcess.on('error', (error) => {
reject(error);
});
setTimeout(() => {
try {
unpackProcess.kill('SIGTERM');
} catch {
void 0;
}
reject(new Error('Unpack process timed out'));
}, 30000);
});
}
private getLauncherPath(unpackedDir: string): string | null {
const extensions =
process.platform === 'win32' ? ['.exe', ''] : ['', '.exe'];
for (const ext of extensions) {
const launcherPath = join(unpackedDir, `koboldcpp-launcher${ext}`);
if (existsSync(launcherPath)) {
return launcherPath;
}
}
return null;
}
async getInstalledVersions(): Promise<InstalledVersion[]> {
try { try {
if (!existsSync(this.installDir)) { if (!existsSync(this.installDir)) {
return []; return [];
} }
const files = readdirSync(this.installDir); const items = readdirSync(this.installDir);
const koboldFiles = files.filter((file) => { const launchers: Array<{ path: string; filename: string; size: number }> =
const filePath = join(this.installDir, file); [];
try {
const stats = statSync(filePath);
if (stats.isFile() && file.startsWith('koboldcpp')) { for (const item of items) {
if (process.platform !== 'win32') { const itemPath = join(this.installDir, item);
const isExecutable = (stats.mode & parseInt('111', 8)) !== 0; const stats = statSync(itemPath);
return isExecutable;
} else {
return true;
}
}
return false;
} catch {
return false;
}
});
if (!includeVersions) { if (stats.isDirectory()) {
return koboldFiles.map((file) => { const launcherPath = this.getLauncherPath(itemPath);
const filePath = join(this.installDir, file); if (launcherPath && existsSync(launcherPath)) {
const stats = statSync(filePath); const launcherStats = statSync(launcherPath);
return { const launcherFilename = launcherPath.split(/[/\\]/).pop() || '';
version: 'unknown', launchers.push({
path: filePath, path: launcherPath,
filename: file, filename: launcherFilename,
size: stats.size, size: launcherStats.size,
};
}); });
} }
}
}
const versionPromises = koboldFiles.map(async (file) => { const versionPromises = launchers.map(async (launcher) => {
const filePath = join(this.installDir, file);
try { try {
const stats = statSync(filePath); const detectedVersion = await this.getVersionFromBinary(
const detectedVersion = await this.getVersionFromBinary(filePath); launcher.path
);
const version = detectedVersion || 'unknown'; const version = detectedVersion || 'unknown';
return { return {
version, version,
path: filePath, path: launcher.path,
filename: file, filename: launcher.filename,
size: stats.size, size: launcher.size,
} as InstalledVersion; } as InstalledVersion;
} catch (error) { } catch (error) {
console.warn(`Could not detect version for ${file}:`, error); console.error(
`Could not detect version for ${launcher.filename}:`,
error
);
return null; return null;
} }
}); });
@ -199,7 +276,7 @@ export class KoboldCppManager {
(version): version is InstalledVersion => version !== null (version): version is InstalledVersion => version !== null
); );
} catch (error) { } catch (error) {
console.warn('Error scanning install directory:', error); console.error('Error scanning install directory:', error);
return []; return [];
} }
} }
@ -230,7 +307,7 @@ export class KoboldCppManager {
} }
} }
} catch (error) { } catch (error) {
console.warn('Error scanning for config files:', error); console.error('Error scanning for config files:', error);
} }
return configFiles.sort((a, b) => a.name.localeCompare(b.name)); return configFiles.sort((a, b) => a.name.localeCompare(b.name));
@ -252,7 +329,7 @@ export class KoboldCppManager {
return config; return config;
} catch (error) { } catch (error) {
console.warn('Error parsing config file:', error); console.error('Error parsing config file:', error);
return null; return null;
} }
} }
@ -279,45 +356,64 @@ export class KoboldCppManager {
return result.filePaths[0]; return result.filePaths[0];
} catch (error) { } catch (error) {
console.warn('Error selecting model file:', error); console.error('Error selecting model file:', error);
return null; return null;
} }
} }
async getCurrentVersion(): Promise<InstalledVersion | null> { async getCurrentVersion(): Promise<InstalledVersion | null> {
const currentBinaryPath = this.configManager.getCurrentKoboldBinary(); const currentBinaryPath = this.configManager.getCurrentKoboldBinary();
const versions = await this.getInstalledVersions();
if (currentBinaryPath && existsSync(currentBinaryPath)) { if (currentBinaryPath && existsSync(currentBinaryPath)) {
try { const currentVersion = versions.find((v) => v.path === currentBinaryPath);
const filename = currentBinaryPath.split(/[/\\]/).pop() || ''; if (currentVersion) {
const version = return currentVersion;
(await this.getVersionFromBinary(currentBinaryPath)) || 'unknown';
return {
version,
path: currentBinaryPath,
filename,
};
} catch (error) {
console.warn('Failed to get current version info:', error);
this.configManager.setCurrentKoboldBinary('');
} }
} }
const versions = await this.getInstalledVersions();
const firstVersion = versions[0]; const firstVersion = versions[0];
if (firstVersion) { if (firstVersion) {
this.configManager.setCurrentKoboldBinary(firstVersion.path); this.configManager.setCurrentKoboldBinary(firstVersion.path);
return firstVersion; return firstVersion;
} }
if (currentBinaryPath) {
this.configManager.setCurrentKoboldBinary('');
}
return null;
}
async getCurrentBinaryInfo(): Promise<{
path: string;
filename: string;
} | null> {
const currentVersion = await this.getCurrentVersion();
if (currentVersion) {
const pathParts = currentVersion.path.split(/[/\\]/);
const filename =
pathParts[pathParts.length - 2] || currentVersion.filename;
return {
path: currentVersion.path,
filename,
};
}
return null; return null;
} }
async setCurrentVersion(binaryPath: string): Promise<boolean> { async setCurrentVersion(binaryPath: string): Promise<boolean> {
if (existsSync(binaryPath)) { if (existsSync(binaryPath)) {
this.configManager.setCurrentKoboldBinary(binaryPath); this.configManager.setCurrentKoboldBinary(binaryPath);
const mainWindow = this.windowManager.getMainWindow();
if (mainWindow) {
mainWindow.webContents.send('versions-updated');
}
return true; return true;
} }
@ -335,6 +431,7 @@ export class KoboldCppManager {
const process = spawn(binaryPath, ['--version'], { const process = spawn(binaryPath, ['--version'], {
stdio: ['ignore', 'pipe', 'pipe'], stdio: ['ignore', 'pipe', 'pipe'],
timeout: 10000, timeout: 10000,
detached: false,
}); });
let output = ''; let output = '';
@ -519,7 +616,7 @@ export class KoboldCppManager {
}; };
} }
} catch (error) { } catch (error) {
console.warn('Failed to fetch Windows ROCm release:', error); console.error('Failed to fetch Windows ROCm release:', error);
} }
} }
@ -540,6 +637,13 @@ export class KoboldCppManager {
}; };
} }
const tempPackedFilePath = join(
this.installDir,
`${rocmInfo.name}.packed`
);
const baseFilename = rocmInfo.name.replace(/\.exe$/, '');
const unpackedDirPath = join(this.installDir, baseFilename);
const response = await fetch(rocmInfo.url); const response = await fetch(rocmInfo.url);
if (!response.ok) { if (!response.ok) {
return { return {
@ -559,8 +663,7 @@ export class KoboldCppManager {
}; };
} }
const filePath = join(this.installDir, rocmInfo.name); const writer = createWriteStream(tempPackedFilePath);
const writer = createWriteStream(filePath);
try { try {
while (true) { while (true) {
@ -588,16 +691,50 @@ export class KoboldCppManager {
if (process.platform !== 'win32') { if (process.platform !== 'win32') {
try { try {
chmodSync(filePath, 0o755); chmodSync(tempPackedFilePath, 0o755);
} catch (error) { } catch (error) {
console.warn('Failed to make ROCm binary executable:', error); console.error('Failed to make ROCm binary executable:', error);
} }
} }
try {
await this.unpackKoboldCpp(tempPackedFilePath, unpackedDirPath);
try {
unlinkSync(tempPackedFilePath);
} catch (error) {
console.warn('Failed to cleanup packed ROCm file:', error);
}
const launcherPath = this.getLauncherPath(unpackedDirPath);
if (launcherPath && existsSync(launcherPath)) {
const currentBinary = this.configManager.getCurrentKoboldBinary();
if (!currentBinary) {
this.configManager.setCurrentKoboldBinary(launcherPath);
}
const mainWindow = this.windowManager.getMainWindow();
if (mainWindow) {
mainWindow.webContents.send('versions-updated');
}
return { return {
success: true, success: true,
path: filePath, path: launcherPath,
}; };
} else {
return {
success: false,
error: 'Failed to find koboldcpp-launcher after unpacking ROCm',
};
}
} catch (error) {
console.error('Failed to unpack ROCm:', error);
return {
success: false,
error: `Failed to unpack ROCm: ${(error as Error).message}`,
};
}
} catch (error) { } catch (error) {
return { return {
success: false, success: false,
@ -663,8 +800,20 @@ export class KoboldCppManager {
const availableAssets = latestRelease.assets.map((asset: GitHubAsset) => { const availableAssets = latestRelease.assets.map((asset: GitHubAsset) => {
const installedVersion = installedVersions.find((v) => { const installedVersion = installedVersions.find((v) => {
const filename = v.filename || v.path.split(/[/\\]/).pop() || ''; const pathParts = v.path.split(/[/\\]/);
return filename === asset.name; const launcherIndex = pathParts.findIndex(
(part) =>
part === 'koboldcpp-launcher' ||
part === 'koboldcpp.exe' ||
part === 'koboldcpp'
);
if (launcherIndex > 0) {
const directoryName = pathParts[launcherIndex - 1];
return directoryName === asset.name;
}
return false;
}); });
return { return {
@ -709,6 +858,11 @@ export class KoboldCppManager {
const child = spawn(currentVersion.path, finalArgs, { const child = spawn(currentVersion.path, finalArgs, {
stdio: ['pipe', 'pipe', 'pipe'], stdio: ['pipe', 'pipe', 'pipe'],
detached: false, detached: false,
env: {
...process.env,
PYTHONDONTWRITEBYTECODE: '1',
PYTHONUNBUFFERED: '1',
},
}); });
this.koboldProcess = child; this.koboldProcess = child;
@ -774,14 +928,14 @@ export class KoboldCppManager {
try { try {
this.koboldProcess.kill('SIGKILL'); this.koboldProcess.kill('SIGKILL');
} catch (error) { } catch (error) {
console.warn('Error force-killing KoboldCpp process:', error); console.error('Error force-killing KoboldCpp process:', error);
} }
} }
}, 5000); }, 5000);
this.koboldProcess = null; this.koboldProcess = null;
} catch (error) { } catch (error) {
console.warn('Error stopping KoboldCpp process:', error); console.error('Error stopping KoboldCpp process:', error);
this.koboldProcess = null; this.koboldProcess = null;
} }
} }
@ -789,7 +943,7 @@ export class KoboldCppManager {
async cleanup(): Promise<void> { async cleanup(): Promise<void> {
if (this.koboldProcess) { if (this.koboldProcess) {
return new Promise((resolve) => { await new Promise<void>((resolve) => {
if (!this.koboldProcess) { if (!this.koboldProcess) {
resolve(); resolve();
return; return;
@ -817,7 +971,7 @@ export class KoboldCppManager {
cleanup(); cleanup();
}, 3000); }, 3000);
} catch (error) { } catch (error) {
console.warn('Error during cleanup:', error); console.error('Error during cleanup:', error);
cleanup(); cleanup();
} }
}); });

View file

@ -203,14 +203,22 @@ export class HardwareService {
if (name) { if (name) {
let deviceType = ''; let deviceType = '';
const searchRangeLines = 20;
const searchStartIndex = Math.max(0, i - searchRangeLines);
const searchEndIndex = Math.min(
lines.length,
i + searchRangeLines
);
for ( for (
let j = Math.max(0, i - 10); let searchIndex = searchStartIndex;
j < Math.min(lines.length, i + 10); searchIndex < searchEndIndex;
j++ searchIndex++
) { ) {
if (lines[j].includes('Device Type:')) { if (lines[searchIndex].includes('Device Type:')) {
deviceType = deviceType =
lines[j].split('Device Type:')[1]?.trim() || ''; lines[searchIndex].split('Device Type:')[1]?.trim() ||
'';
break; break;
} }
} }

View file

@ -70,10 +70,8 @@ export class IPCHandlers {
} }
}); });
ipcMain.handle( ipcMain.handle('kobold:getInstalledVersions', () =>
'kobold:getInstalledVersions', this.koboldManager.getInstalledVersions()
(_, includeVersions?: boolean) =>
this.koboldManager.getInstalledVersions(includeVersions)
); );
ipcMain.handle('kobold:getConfigFiles', () => ipcMain.handle('kobold:getConfigFiles', () =>
@ -92,6 +90,10 @@ export class IPCHandlers {
this.koboldManager.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)
); );

View file

@ -8,9 +8,9 @@ import type {
const koboldAPI: KoboldAPI = { const koboldAPI: KoboldAPI = {
getInstalledVersion: () => ipcRenderer.invoke('kobold:getInstalledVersion'), getInstalledVersion: () => ipcRenderer.invoke('kobold:getInstalledVersion'),
getInstalledVersions: (includeVersions?: boolean) => getInstalledVersions: () => ipcRenderer.invoke('kobold:getInstalledVersions'),
ipcRenderer.invoke('kobold:getInstalledVersions', includeVersions),
getCurrentVersion: () => ipcRenderer.invoke('kobold:getCurrentVersion'), getCurrentVersion: () => ipcRenderer.invoke('kobold:getCurrentVersion'),
getCurrentBinaryInfo: () => ipcRenderer.invoke('kobold:getCurrentBinaryInfo'),
setCurrentVersion: (version: string) => setCurrentVersion: (version: string) =>
ipcRenderer.invoke('kobold:setCurrentVersion', version), ipcRenderer.invoke('kobold:setCurrentVersion', version),
getVersionFromBinary: (binaryPath: string) => getVersionFromBinary: (binaryPath: string) =>
@ -68,6 +68,14 @@ const koboldAPI: KoboldAPI = {
ipcRenderer.removeListener('install-dir-changed', handler); ipcRenderer.removeListener('install-dir-changed', handler);
}; };
}, },
onVersionsUpdated: (callback: () => void) => {
const handler = () => callback();
ipcRenderer.on('versions-updated', handler);
return () => {
ipcRenderer.removeListener('versions-updated', handler);
};
},
onKoboldOutput: (callback: (data: string) => void) => { onKoboldOutput: (callback: (data: string) => void) => {
const handler = (_: IpcRendererEvent, data: string) => callback(data); const handler = (_: IpcRendererEvent, data: string) => callback(data);
ipcRenderer.on('kobold-output', handler); ipcRenderer.on('kobold-output', handler);

View file

@ -55,10 +55,12 @@ interface ROCmDownload {
export interface KoboldAPI { export interface KoboldAPI {
getInstalledVersion: () => Promise<string | undefined>; getInstalledVersion: () => Promise<string | undefined>;
getInstalledVersions: ( getInstalledVersions: () => Promise<InstalledVersion[]>;
includeVersions?: boolean
) => Promise<InstalledVersion[]>;
getCurrentVersion: () => Promise<InstalledVersion | null>; getCurrentVersion: () => Promise<InstalledVersion | null>;
getCurrentBinaryInfo: () => Promise<{
path: string;
filename: string;
} | null>;
setCurrentVersion: (version: string) => Promise<boolean>; setCurrentVersion: (version: string) => Promise<boolean>;
getVersionFromBinary: (binaryPath: string) => Promise<string | null>; getVersionFromBinary: (binaryPath: string) => Promise<string | null>;
getLatestRelease: () => Promise<GitHubRelease>; getLatestRelease: () => Promise<GitHubRelease>;
@ -105,6 +107,7 @@ export interface KoboldAPI {
onDownloadProgress: (callback: (progress: number) => void) => void; onDownloadProgress: (callback: (progress: number) => void) => void;
onUpdateAvailable: (callback: (updateInfo: UpdateInfo) => void) => void; onUpdateAvailable: (callback: (updateInfo: UpdateInfo) => void) => void;
onInstallDirChanged: (callback: (newPath: string) => void) => () => void; onInstallDirChanged: (callback: (newPath: string) => void) => () => void;
onVersionsUpdated: (callback: () => void) => () => void;
onKoboldOutput: (callback: (data: string) => void) => () => void; onKoboldOutput: (callback: (data: string) => void) => () => void;
removeAllListeners: (channel: string) => void; removeAllListeners: (channel: string) => void;
} }