windows fixes, reformat everything

This commit is contained in:
lone-cloud 2025-08-19 16:01:41 -07:00
parent d04545f5ba
commit cd54275354
14 changed files with 268 additions and 171 deletions

3
.gitignore vendored
View file

@ -10,4 +10,5 @@ Thumbs.db
# Yarn # Yarn
.yarn/ .yarn/
.pnp.* .pnp.*
package-lock.json

View file

@ -39,9 +39,11 @@ A koboldcpp manager. <!-- markdownlint-disable MD033 -->
yarn dev yarn dev
``` ```
### Linux Wayland support ### Windows ROCm Support
Additional configurations have been written to help with ideal Wayland support, but as per current Electron guidelines, the user should set `ELECTRON_OZONE_PLATFORM_HINT` to `wayland` in their environment variable according to the [Electron Environment Variables documentation](https://www.electronjs.org/docs/latest/api/environment-variables#electron_ozone_platform_hint-linux). There is ROCm Windows support maintained by YellowRoseCx in a separate fork.
Unfortunately it does not properly support unpacking, which would greatly diminish its performance and provide a poor UX when used alongside this app.
For Friendly Kobold to work with this fork, this issue must be fixed first: https://github.com/YellowRoseCx/koboldcpp-rocm/issues/129
### Future features ### Future features

View file

@ -28,6 +28,7 @@
"hipblas", "hipblas",
"kcpps", "kcpps",
"kcppt", "kcppt",
"koboldai",
"KoboldAI", "KoboldAI",
"KOBOLDAI", "KOBOLDAI",
"koboldcpp", "koboldcpp",

View file

@ -46,7 +46,7 @@
"ai", "ai",
"llm" "llm"
], ],
"author": "Egor Philippov", "author": "lone-cloud",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"devDependencies": { "devDependencies": {
"@cspell/eslint-plugin": "^9.2.0", "@cspell/eslint-plugin": "^9.2.0",

View file

@ -1,4 +1,4 @@
import { useState, useCallback } from 'react'; import { useState, useCallback, useEffect, useRef } from 'react';
import { import {
Card, Card,
Text, Text,
@ -6,11 +6,10 @@ import {
Loader, Loader,
Stack, Stack,
Container, Container,
Badge, Anchor,
} from '@mantine/core'; } from '@mantine/core';
import { DownloadCard } from '@/components/DownloadCard'; import { DownloadCard } from '@/components/DownloadCard';
import { StyledTooltip } from '@/components/StyledTooltip'; import { getPlatformDisplayName, formatDownloadSize } from '@/utils';
import { getPlatformDisplayName, formatFileSizeInMB } from '@/utils';
import { import {
isAssetRecommended, isAssetRecommended,
sortAssetsByRecommendation, sortAssetsByRecommendation,
@ -38,6 +37,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
'asset' | 'rocm' | null 'asset' | 'rocm' | null
>(null); >(null);
const [downloadingAsset, setDownloadingAsset] = useState<string | null>(null); const [downloadingAsset, setDownloadingAsset] = useState<string | null>(null);
const downloadingItemRef = useRef<HTMLDivElement>(null);
const loading = loadingPlatform || loadingRemote; const loading = loadingPlatform || loadingRemote;
@ -80,31 +80,44 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
[sharedHandleDownload, onDownloadComplete] [sharedHandleDownload, onDownloadComplete]
); );
useEffect(() => {
if (downloading && downloadingItemRef.current) {
downloadingItemRef.current.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
}, [downloading]);
const renderROCmCard = () => { const renderROCmCard = () => {
if (!rocmDownload) return null; if (!rocmDownload) return null;
const isDownloading = Boolean(downloading) && downloadingType === 'rocm';
return ( return (
<DownloadCard <div ref={isDownloading ? downloadingItemRef : null}>
name={rocmDownload.name} <DownloadCard
size={`~${formatFileSizeInMB(rocmDownload.size)}`} name={rocmDownload.name}
description={getAssetDescription(rocmDownload.name)} size={formatDownloadSize(rocmDownload.size, rocmDownload.url, true)}
version={rocmDownload.version} description={getAssetDescription(rocmDownload.name)}
isRecommended={isAssetRecommended( version={rocmDownload.version}
rocmDownload.name, isRecommended={isAssetRecommended(
platformInfo.hasAMDGPU rocmDownload.name,
)} platformInfo.hasAMDGPU
isDownloading={Boolean(downloading) && downloadingType === 'rocm'} )}
downloadProgress={ isDownloading={isDownloading}
downloadingType === 'rocm' downloadProgress={
? downloadProgress[rocmDownload.name] || 0 downloadingType === 'rocm'
: 0 ? downloadProgress[rocmDownload.name] || 0
} : 0
disabled={Boolean(downloading) && downloadingType !== 'rocm'} }
onDownload={(e) => { disabled={Boolean(downloading) && downloadingType !== 'rocm'}
e.stopPropagation(); onDownload={(e) => {
handleDownload('rocm', rocmDownload); e.stopPropagation();
}} handleDownload('rocm', rocmDownload);
/> }}
/>
</div>
); );
}; };
@ -126,40 +139,42 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
<> <>
{availableDownloads.length > 0 ? ( {availableDownloads.length > 0 ? (
<Stack gap="sm"> <Stack gap="sm">
{platformInfo.hasAMDGPU && !platformInfo.hasROCm && ( {platformInfo.hasAMDGPU &&
<Card withBorder p="md" bg="orange.0"> !platformInfo.hasROCm &&
<Stack gap="xs"> platformInfo.platform === 'linux' && (
<div <Card withBorder p="md" bg="orange.0">
style={{ <Stack gap="xs">
display: 'flex', <div
alignItems: 'center', style={{
gap: '8px', display: 'flex',
}} alignItems: 'center',
> gap: '8px',
<Text fw={600} c="orange.9"> }}
AMD GPU Detected
</Text>
<StyledTooltip
label="ROCm is not installed. Install ROCm for optimal AMD GPU performance with KoboldCpp."
multiline
w={220}
> >
<Badge <Text fw={600} c="orange.9">
AMD GPU Detected
</Text>
</div>
<Text size="sm" c="orange.8">
For best performance with your AMD GPU,
consider installing ROCm support.{' '}
<Anchor
href="#"
onClick={(e) => {
e.preventDefault();
window.electronAPI.app.openExternal(
'https://rocm.docs.amd.com/projects/install-on-linux/en/latest/reference/system-requirements.html'
);
}}
size="sm" size="sm"
color="orange" c="orange.8"
variant="filled"
> >
ROCm Not Found Learn more
</Badge> </Anchor>
</StyledTooltip> </Text>
</div> </Stack>
<Text size="sm" c="orange.8"> </Card>
For best performance with your AMD GPU, consider )}
installing ROCm support.
</Text>
</Stack>
</Card>
)}
{rocmDownload && {rocmDownload &&
platformInfo.hasAMDGPU && platformInfo.hasAMDGPU &&
@ -168,39 +183,47 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
{sortAssetsByRecommendation( {sortAssetsByRecommendation(
regularDownloads, regularDownloads,
platformInfo.hasAMDGPU platformInfo.hasAMDGPU
).map((download) => ( ).map((download) => {
<DownloadCard const isDownloading =
key={download.name} Boolean(downloading) &&
name={download.name} downloadingType === 'asset' &&
size={formatFileSizeInMB(download.size)} downloadingAsset === download.name;
version={download.version}
description={getAssetDescription(download.name)} return (
isRecommended={isAssetRecommended( <div
download.name, key={download.name}
platformInfo.hasAMDGPU ref={isDownloading ? downloadingItemRef : null}
)} >
isDownloading={ <DownloadCard
Boolean(downloading) && name={download.name}
downloadingType === 'asset' && size={formatDownloadSize(
downloadingAsset === download.name download.size,
} download.url
downloadProgress={ )}
Boolean(downloading) && version={download.version}
downloadingType === 'asset' && description={getAssetDescription(download.name)}
downloadingAsset === download.name isRecommended={isAssetRecommended(
? downloadProgress[download.name] || 0 download.name,
: 0 platformInfo.hasAMDGPU
} )}
disabled={ isDownloading={isDownloading}
Boolean(downloading) && downloadProgress={
downloadingAsset !== download.name isDownloading
} ? downloadProgress[download.name] || 0
onDownload={(e) => { : 0
e.stopPropagation(); }
handleDownload('asset', download); disabled={
}} Boolean(downloading) &&
/> downloadingAsset !== download.name
))} }
onDownload={(e) => {
e.stopPropagation();
handleDownload('asset', download);
}}
/>
</div>
);
})}
{rocmDownload && {rocmDownload &&
!platformInfo.hasAMDGPU && !platformInfo.hasAMDGPU &&

View file

@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { import {
Stack, Stack,
Text, Text,
@ -17,35 +17,15 @@ import {
sortAssetsByRecommendation, sortAssetsByRecommendation,
isAssetRecommended, isAssetRecommended,
} from '@/utils/assets'; } from '@/utils/assets';
import { getDisplayNameFromPath, formatFileSizeInMB } from '@/utils'; import {
getDisplayNameFromPath,
formatDownloadSize,
stripAssetExtensions,
compareVersions,
} from '@/utils';
import { useKoboldVersions } from '@/hooks/useKoboldVersions'; import { useKoboldVersions } from '@/hooks/useKoboldVersions';
import type { InstalledVersion, ReleaseWithStatus } from '@/types/electron'; import type { InstalledVersion, ReleaseWithStatus } from '@/types/electron';
const compareVersions = (versionA: string, versionB: string): number => {
const cleanVersion = (version: string): string =>
version.replace(/^v/, '').replace(/[^0-9.]/g, '');
const parseVersion = (version: string): number[] =>
cleanVersion(version)
.split('.')
.map((num) => parseInt(num, 10) || 0);
const a = parseVersion(versionA);
const b = parseVersion(versionB);
const maxLength = Math.max(a.length, b.length);
for (let i = 0; i < maxLength; i++) {
const aVal = a[i] || 0;
const bVal = b[i] || 0;
if (aVal !== bVal) {
return aVal - bVal;
}
}
return 0;
};
interface VersionInfo { interface VersionInfo {
name: string; name: string;
version: string; version: string;
@ -81,6 +61,7 @@ export const VersionsTab = () => {
const [latestRelease, setLatestRelease] = useState<ReleaseWithStatus | null>( const [latestRelease, setLatestRelease] = useState<ReleaseWithStatus | null>(
null null
); );
const downloadingItemRef = useRef<HTMLDivElement>(null);
const loadInstalledVersions = useCallback(async () => { const loadInstalledVersions = useCallback(async () => {
setLoadingInstalled(true); setLoadingInstalled(true);
@ -146,14 +127,12 @@ export const VersionsTab = () => {
loadLatestRelease(); loadLatestRelease();
}, [loadInstalledVersions, loadLatestRelease]); }, [loadInstalledVersions, loadLatestRelease]);
const getAllVersions = (): VersionInfo[] => { const allVersions = useMemo((): VersionInfo[] => {
const versions: VersionInfo[] = []; const versions: VersionInfo[] = [];
const processedInstalled = new Set<string>(); const processedInstalled = new Set<string>();
availableDownloads.forEach((download) => { availableDownloads.forEach((download) => {
const downloadBaseName = download.name const downloadBaseName = stripAssetExtensions(download.name);
.replace(/\.(tar\.gz|zip|exe)$/i, '')
.replace(/\.packed$/, '');
const installedVersion = installedVersions.find((v) => { const installedVersion = installedVersions.find((v) => {
const displayName = getDisplayNameFromPath(v); const displayName = getDisplayNameFromPath(v);
@ -220,7 +199,21 @@ export const VersionsTab = () => {
}); });
return sortAssetsByRecommendation(versions, platformInfo.hasAMDGPU); return sortAssetsByRecommendation(versions, platformInfo.hasAMDGPU);
}; }, [
availableDownloads,
installedVersions,
currentVersion,
platformInfo.hasAMDGPU,
]);
useEffect(() => {
if (downloading && downloadingItemRef.current) {
downloadingItemRef.current.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
}, [downloading]);
const handleDownload = async (version: VersionInfo) => { const handleDownload = async (version: VersionInfo) => {
try { try {
@ -362,21 +355,24 @@ export const VersionsTab = () => {
</Button> </Button>
</Group> </Group>
{getAllVersions().map((version, index) => { {allVersions.map((version, index) => {
const isDownloading = downloading === version.name; const isDownloading = downloading === version.name;
return ( return (
<div <div
key={`${version.name}-${version.version}-${index}`} key={`${version.name}-${version.version}-${index}`}
style={{ paddingBottom: '8px' }} style={{ paddingBottom: '8px' }}
ref={isDownloading ? downloadingItemRef : null}
> >
<DownloadCard <DownloadCard
name={version.name} name={version.name}
size={ size={
version.size version.size
? version.isROCm ? formatDownloadSize(
? `~${formatFileSizeInMB(version.size)}` version.size,
: formatFileSizeInMB(version.size) version.downloadUrl,
version.isROCm
)
: '' : ''
} }
version={version.version} version={version.version}
@ -418,7 +414,7 @@ export const VersionsTab = () => {
); );
})} })}
{getAllVersions().length === 0 && ( {allVersions.length === 0 && (
<Card withBorder radius="md" padding="md"> <Card withBorder radius="md" padding="md">
<Text size="sm" c="dimmed" ta="center"> <Text size="sm" c="dimmed" ta="center">
No versions found No versions found

View file

@ -35,6 +35,11 @@ export const ASSET_SUFFIXES = {
export const KOBOLDAI_URLS = { export const KOBOLDAI_URLS = {
STANDARD_DOWNLOAD: 'https://koboldai.org/cpp', STANDARD_DOWNLOAD: 'https://koboldai.org/cpp',
ROCM_DOWNLOAD: 'https://koboldai.org/cpplinuxrocm', ROCM_DOWNLOAD: 'https://koboldai.org/cpplinuxrocm',
DOMAIN: 'koboldai.org',
} as const;
export const GITHUB_URLS = {
DOMAIN: 'github.com',
} as const; } as const;
export const ROCM = { export const ROCM = {

View file

@ -1,3 +1,4 @@
/* eslint-disable no-comments/disallowComments */
import { spawn, ChildProcess } from 'child_process'; import { spawn, ChildProcess } from 'child_process';
import { join } from 'path'; import { join } from 'path';
import { import {
@ -16,7 +17,8 @@ import { GitHubService } from '@/main/services/GitHubService';
import { ConfigManager } from '@/main/managers/ConfigManager'; import { ConfigManager } from '@/main/managers/ConfigManager';
import { LogManager } from '@/main/managers/LogManager'; import { LogManager } from '@/main/managers/LogManager';
import { WindowManager } from '@/main/managers/WindowManager'; import { WindowManager } from '@/main/managers/WindowManager';
import { ROCM, GITHUB_API } from '@/constants'; import { ROCM } from '@/constants';
import { stripAssetExtensions } from '@/utils/versionUtils';
import type { DownloadItem } from '@/types/electron'; import type { DownloadItem } from '@/types/electron';
interface GitHubAsset { interface GitHubAsset {
@ -85,7 +87,7 @@ export class KoboldCppManager {
onProgress?: (progress: number) => void onProgress?: (progress: number) => void
): Promise<string> { ): Promise<string> {
const tempPackedFilePath = join(this.installDir, `${asset.name}.packed`); const tempPackedFilePath = join(this.installDir, `${asset.name}.packed`);
const baseFilename = asset.name.replace(/\.exe$/, ''); const baseFilename = stripAssetExtensions(asset.name);
const unpackedDirPath = join(this.installDir, baseFilename); const unpackedDirPath = join(this.installDir, baseFilename);
if (asset.isUpdate && existsSync(unpackedDirPath)) { if (asset.isUpdate && existsSync(unpackedDirPath)) {
@ -666,32 +668,35 @@ export class KoboldCppManager {
type: 'rocm', type: 'rocm',
}; };
} else if (platform === 'win32') { } else if (platform === 'win32') {
try { return null;
const response = await fetch(GITHUB_API.ROCM_LATEST_RELEASE_URL); // The launcher doesn't exist in unpacked state yet.
if (!response.ok) { // Enable when it's ready.
return null; // try {
} // const response = await fetch(GITHUB_API.ROCM_LATEST_RELEASE_URL);
// if (!response.ok) {
// return null;
// }
const release = await response.json(); // const release = await response.json();
const rocmAsset = release.assets?.find((asset: GitHubAsset) => // const rocmAsset = release.assets?.find((asset: GitHubAsset) =>
asset.name.endsWith('rocm.exe') // asset.name.endsWith('rocm.exe')
); // );
if (rocmAsset) { // if (rocmAsset) {
return { // return {
name: rocmAsset.name, // name: rocmAsset.name,
url: rocmAsset.browser_download_url, // url: rocmAsset.browser_download_url,
size: rocmAsset.size, // size: rocmAsset.size,
version: release.tag_name?.replace(/^v/, '') || 'unknown', // version: release.tag_name?.replace(/^v/, '') || 'unknown',
type: 'rocm', // type: 'rocm',
}; // };
} // }
} catch (error) { // } catch (error) {
this.logManager.logError( // this.logManager.logError(
'Failed to fetch Windows ROCm release:', // 'Failed to fetch Windows ROCm release:',
error as Error // error as Error
); // );
} // }
} }
return null; return null;
@ -715,7 +720,7 @@ export class KoboldCppManager {
this.installDir, this.installDir,
`${rocmInfo.name}.packed` `${rocmInfo.name}.packed`
); );
const baseFilename = rocmInfo.name.replace(/\.exe$/, ''); const baseFilename = stripAssetExtensions(rocmInfo.name);
const unpackedDirPath = join(this.installDir, baseFilename); const unpackedDirPath = join(this.installDir, baseFilename);
const response = await fetch(rocmInfo.url); const response = await fetch(rocmInfo.url);

View file

@ -262,7 +262,7 @@ export class WindowManager {
}, },
}, },
{ {
label: 'Open Logs Directory', label: 'View Error Logs',
click: () => { click: () => {
const logsDir = join(app.getPath('userData'), 'logs'); const logsDir = join(app.getPath('userData'), 'logs');
shell.openPath(logsDir); shell.openPath(logsDir);

View file

@ -37,21 +37,18 @@ export class BinaryService {
const binaryDir = dirname(koboldBinaryPath); const binaryDir = dirname(koboldBinaryPath);
const internalDir = join(binaryDir, '_internal'); const internalDir = join(binaryDir, '_internal');
if (!existsSync(internalDir)) {
// eslint-disable-next-line no-console
console.warn(
'_internal directory not found, cannot detect backend support'
);
this.backendSupportCache.set(koboldBinaryPath, support);
return support;
}
const platform = process.platform; const platform = process.platform;
const isDynamicLib = (name: string) => { const isDynamicLib = (name: string) => {
if (platform === 'win32') { if (platform === 'win32') {
return existsSync(join(internalDir, `${name}.dll`)); return (
existsSync(join(internalDir, `${name}.dll`)) ||
existsSync(join(binaryDir, `${name}.dll`))
);
} else { } else {
return existsSync(join(internalDir, `${name}.so`)); return (
existsSync(join(internalDir, `${name}.so`)) ||
existsSync(join(binaryDir, `${name}.so`))
);
} }
}; };

View file

@ -1,7 +1,8 @@
import { ASSET_SUFFIXES } from '@/constants'; import { ASSET_SUFFIXES } from '@/constants';
import { stripAssetExtensions } from '@/utils/versionUtils';
export const getAssetDescription = (assetName: string): string => { export const getAssetDescription = (assetName: string): string => {
const name = assetName.toLowerCase(); const name = stripAssetExtensions(assetName).toLowerCase();
if (name.includes(ASSET_SUFFIXES.ROCM)) { if (name.includes(ASSET_SUFFIXES.ROCM)) {
return 'Optimized for AMD GPUs with ROCm support.'; return 'Optimized for AMD GPUs with ROCm support.';
@ -22,7 +23,7 @@ export const isAssetRecommended = (
assetName: string, assetName: string,
hasAMDGPU: boolean hasAMDGPU: boolean
): boolean => { ): boolean => {
const name = assetName.toLowerCase(); const name = stripAssetExtensions(assetName).toLowerCase();
if (hasAMDGPU && name.includes(ASSET_SUFFIXES.ROCM)) { if (hasAMDGPU && name.includes(ASSET_SUFFIXES.ROCM)) {
return true; return true;
@ -36,6 +37,16 @@ export const isAssetRecommended = (
); );
}; };
export const isAssetStandard = (assetName: string): boolean => {
const name = stripAssetExtensions(assetName).toLowerCase();
return (
!name.includes(ASSET_SUFFIXES.ROCM) &&
!name.endsWith(ASSET_SUFFIXES.OLDPC) &&
!name.endsWith(ASSET_SUFFIXES.NOCUDA)
);
};
export const sortAssetsByRecommendation = <T extends { name: string }>( export const sortAssetsByRecommendation = <T extends { name: string }>(
assets: T[], assets: T[],
hasAMDGPU: boolean hasAMDGPU: boolean
@ -43,9 +54,16 @@ export const sortAssetsByRecommendation = <T extends { name: string }>(
[...assets].sort((a, b) => { [...assets].sort((a, b) => {
const aRecommended = isAssetRecommended(a.name, hasAMDGPU); const aRecommended = isAssetRecommended(a.name, hasAMDGPU);
const bRecommended = isAssetRecommended(b.name, hasAMDGPU); const bRecommended = isAssetRecommended(b.name, hasAMDGPU);
const aStandard = isAssetStandard(a.name);
const bStandard = isAssetStandard(b.name);
if (aRecommended && !bRecommended) return -1; if (aRecommended && !bRecommended) return -1;
if (!aRecommended && bRecommended) return 1; if (!aRecommended && bRecommended) return 1;
if (aRecommended === bRecommended) {
if (aStandard && !bStandard) return -1;
if (!aStandard && bStandard) return 1;
}
return 0; return 0;
}); });

View file

@ -0,0 +1,43 @@
import { formatFileSizeInMB } from '@/utils/fileSize';
import { KOBOLDAI_URLS, GITHUB_URLS } from '@/constants';
export const formatDownloadSize = (
size: number,
url?: string,
isROCm?: boolean
): string => {
if (!size) return '';
const isApproximateSize =
url?.includes(KOBOLDAI_URLS.DOMAIN) ||
(isROCm && !url?.includes(GITHUB_URLS.DOMAIN));
return isApproximateSize
? `~${formatFileSizeInMB(size)}`
: formatFileSizeInMB(size);
};
export const compareVersions = (versionA: string, versionB: string): number => {
const cleanVersion = (version: string): string =>
version.replace(/^v/, '').replace(/[^0-9.]/g, '');
const parseVersion = (version: string): number[] =>
cleanVersion(version)
.split('.')
.map((num) => parseInt(num, 10) || 0);
const a = parseVersion(versionA);
const b = parseVersion(versionB);
const maxLength = Math.max(a.length, b.length);
for (let i = 0; i < maxLength; i++) {
const aVal = a[i] || 0;
const bVal = b[i] || 0;
if (aVal !== bVal) {
return aVal - bVal;
}
}
return 0;
};

View file

@ -1,4 +1,5 @@
export * from './assets'; export * from './assets';
export * from './downloadUtils';
export * from './fileSize'; export * from './fileSize';
export * from './imageModelPresets'; export * from './imageModelPresets';
export * from './nullish'; export * from './nullish';

View file

@ -14,3 +14,8 @@ export const getDisplayNameFromPath = (
return installedVersion.filename; return installedVersion.filename;
}; };
export const stripAssetExtensions = (assetName: string): string =>
assetName
.replace(/\.(tar\.gz|zip|exe|dmg|AppImage)$/i, '')
.replace(/\.packed$/, '');