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/
.pnp.*
.pnp.*
package-lock.json

View file

@ -39,9 +39,11 @@ A koboldcpp manager. <!-- markdownlint-disable MD033 -->
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

View file

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

View file

@ -46,7 +46,7 @@
"ai",
"llm"
],
"author": "Egor Philippov",
"author": "lone-cloud",
"license": "AGPL-3.0-or-later",
"devDependencies": {
"@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 {
Card,
Text,
@ -6,11 +6,10 @@ import {
Loader,
Stack,
Container,
Badge,
Anchor,
} from '@mantine/core';
import { DownloadCard } from '@/components/DownloadCard';
import { StyledTooltip } from '@/components/StyledTooltip';
import { getPlatformDisplayName, formatFileSizeInMB } from '@/utils';
import { getPlatformDisplayName, formatDownloadSize } from '@/utils';
import {
isAssetRecommended,
sortAssetsByRecommendation,
@ -38,6 +37,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
'asset' | 'rocm' | null
>(null);
const [downloadingAsset, setDownloadingAsset] = useState<string | null>(null);
const downloadingItemRef = useRef<HTMLDivElement>(null);
const loading = loadingPlatform || loadingRemote;
@ -80,31 +80,44 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
[sharedHandleDownload, onDownloadComplete]
);
useEffect(() => {
if (downloading && downloadingItemRef.current) {
downloadingItemRef.current.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
}, [downloading]);
const renderROCmCard = () => {
if (!rocmDownload) return null;
const isDownloading = Boolean(downloading) && downloadingType === 'rocm';
return (
<DownloadCard
name={rocmDownload.name}
size={`~${formatFileSizeInMB(rocmDownload.size)}`}
description={getAssetDescription(rocmDownload.name)}
version={rocmDownload.version}
isRecommended={isAssetRecommended(
rocmDownload.name,
platformInfo.hasAMDGPU
)}
isDownloading={Boolean(downloading) && downloadingType === 'rocm'}
downloadProgress={
downloadingType === 'rocm'
? downloadProgress[rocmDownload.name] || 0
: 0
}
disabled={Boolean(downloading) && downloadingType !== 'rocm'}
onDownload={(e) => {
e.stopPropagation();
handleDownload('rocm', rocmDownload);
}}
/>
<div ref={isDownloading ? downloadingItemRef : null}>
<DownloadCard
name={rocmDownload.name}
size={formatDownloadSize(rocmDownload.size, rocmDownload.url, true)}
description={getAssetDescription(rocmDownload.name)}
version={rocmDownload.version}
isRecommended={isAssetRecommended(
rocmDownload.name,
platformInfo.hasAMDGPU
)}
isDownloading={isDownloading}
downloadProgress={
downloadingType === 'rocm'
? downloadProgress[rocmDownload.name] || 0
: 0
}
disabled={Boolean(downloading) && downloadingType !== 'rocm'}
onDownload={(e) => {
e.stopPropagation();
handleDownload('rocm', rocmDownload);
}}
/>
</div>
);
};
@ -126,40 +139,42 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
<>
{availableDownloads.length > 0 ? (
<Stack gap="sm">
{platformInfo.hasAMDGPU && !platformInfo.hasROCm && (
<Card withBorder p="md" bg="orange.0">
<Stack gap="xs">
<div
style={{
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}
{platformInfo.hasAMDGPU &&
!platformInfo.hasROCm &&
platformInfo.platform === 'linux' && (
<Card withBorder p="md" bg="orange.0">
<Stack gap="xs">
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
}}
>
<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"
color="orange"
variant="filled"
c="orange.8"
>
ROCm Not Found
</Badge>
</StyledTooltip>
</div>
<Text size="sm" c="orange.8">
For best performance with your AMD GPU, consider
installing ROCm support.
</Text>
</Stack>
</Card>
)}
Learn more
</Anchor>
</Text>
</Stack>
</Card>
)}
{rocmDownload &&
platformInfo.hasAMDGPU &&
@ -168,39 +183,47 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
{sortAssetsByRecommendation(
regularDownloads,
platformInfo.hasAMDGPU
).map((download) => (
<DownloadCard
key={download.name}
name={download.name}
size={formatFileSizeInMB(download.size)}
version={download.version}
description={getAssetDescription(download.name)}
isRecommended={isAssetRecommended(
download.name,
platformInfo.hasAMDGPU
)}
isDownloading={
Boolean(downloading) &&
downloadingType === 'asset' &&
downloadingAsset === download.name
}
downloadProgress={
Boolean(downloading) &&
downloadingType === 'asset' &&
downloadingAsset === download.name
? downloadProgress[download.name] || 0
: 0
}
disabled={
Boolean(downloading) &&
downloadingAsset !== download.name
}
onDownload={(e) => {
e.stopPropagation();
handleDownload('asset', download);
}}
/>
))}
).map((download) => {
const isDownloading =
Boolean(downloading) &&
downloadingType === 'asset' &&
downloadingAsset === download.name;
return (
<div
key={download.name}
ref={isDownloading ? downloadingItemRef : null}
>
<DownloadCard
name={download.name}
size={formatDownloadSize(
download.size,
download.url
)}
version={download.version}
description={getAssetDescription(download.name)}
isRecommended={isAssetRecommended(
download.name,
platformInfo.hasAMDGPU
)}
isDownloading={isDownloading}
downloadProgress={
isDownloading
? downloadProgress[download.name] || 0
: 0
}
disabled={
Boolean(downloading) &&
downloadingAsset !== download.name
}
onDownload={(e) => {
e.stopPropagation();
handleDownload('asset', download);
}}
/>
</div>
);
})}
{rocmDownload &&
!platformInfo.hasAMDGPU &&

View file

@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import {
Stack,
Text,
@ -17,35 +17,15 @@ import {
sortAssetsByRecommendation,
isAssetRecommended,
} from '@/utils/assets';
import { getDisplayNameFromPath, formatFileSizeInMB } from '@/utils';
import {
getDisplayNameFromPath,
formatDownloadSize,
stripAssetExtensions,
compareVersions,
} from '@/utils';
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
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 {
name: string;
version: string;
@ -81,6 +61,7 @@ export const VersionsTab = () => {
const [latestRelease, setLatestRelease] = useState<ReleaseWithStatus | null>(
null
);
const downloadingItemRef = useRef<HTMLDivElement>(null);
const loadInstalledVersions = useCallback(async () => {
setLoadingInstalled(true);
@ -146,14 +127,12 @@ export const VersionsTab = () => {
loadLatestRelease();
}, [loadInstalledVersions, loadLatestRelease]);
const getAllVersions = (): VersionInfo[] => {
const allVersions = useMemo((): VersionInfo[] => {
const versions: VersionInfo[] = [];
const processedInstalled = new Set<string>();
availableDownloads.forEach((download) => {
const downloadBaseName = download.name
.replace(/\.(tar\.gz|zip|exe)$/i, '')
.replace(/\.packed$/, '');
const downloadBaseName = stripAssetExtensions(download.name);
const installedVersion = installedVersions.find((v) => {
const displayName = getDisplayNameFromPath(v);
@ -220,7 +199,21 @@ export const VersionsTab = () => {
});
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) => {
try {
@ -362,21 +355,24 @@ export const VersionsTab = () => {
</Button>
</Group>
{getAllVersions().map((version, index) => {
{allVersions.map((version, index) => {
const isDownloading = downloading === version.name;
return (
<div
key={`${version.name}-${version.version}-${index}`}
style={{ paddingBottom: '8px' }}
ref={isDownloading ? downloadingItemRef : null}
>
<DownloadCard
name={version.name}
size={
version.size
? version.isROCm
? `~${formatFileSizeInMB(version.size)}`
: formatFileSizeInMB(version.size)
? formatDownloadSize(
version.size,
version.downloadUrl,
version.isROCm
)
: ''
}
version={version.version}
@ -418,7 +414,7 @@ export const VersionsTab = () => {
);
})}
{getAllVersions().length === 0 && (
{allVersions.length === 0 && (
<Card withBorder radius="md" padding="md">
<Text size="sm" c="dimmed" ta="center">
No versions found

View file

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

View file

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

View file

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

View file

@ -37,21 +37,18 @@ export class BinaryService {
const binaryDir = dirname(koboldBinaryPath);
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 isDynamicLib = (name: string) => {
if (platform === 'win32') {
return existsSync(join(internalDir, `${name}.dll`));
return (
existsSync(join(internalDir, `${name}.dll`)) ||
existsSync(join(binaryDir, `${name}.dll`))
);
} 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 { stripAssetExtensions } from '@/utils/versionUtils';
export const getAssetDescription = (assetName: string): string => {
const name = assetName.toLowerCase();
const name = stripAssetExtensions(assetName).toLowerCase();
if (name.includes(ASSET_SUFFIXES.ROCM)) {
return 'Optimized for AMD GPUs with ROCm support.';
@ -22,7 +23,7 @@ export const isAssetRecommended = (
assetName: string,
hasAMDGPU: boolean
): boolean => {
const name = assetName.toLowerCase();
const name = stripAssetExtensions(assetName).toLowerCase();
if (hasAMDGPU && name.includes(ASSET_SUFFIXES.ROCM)) {
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 }>(
assets: T[],
hasAMDGPU: boolean
@ -43,9 +54,16 @@ export const sortAssetsByRecommendation = <T extends { name: string }>(
[...assets].sort((a, b) => {
const aRecommended = isAssetRecommended(a.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) {
if (aStandard && !bStandard) return -1;
if (!aStandard && bStandard) return 1;
}
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 './downloadUtils';
export * from './fileSize';
export * from './imageModelPresets';
export * from './nullish';

View file

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