From cd54275354ee95d051b26409c8f52dc732c5eae1 Mon Sep 17 00:00:00 2001 From: lone-cloud Date: Tue, 19 Aug 2025 16:01:41 -0700 Subject: [PATCH] windows fixes, reformat everything --- .gitignore | 3 +- README.md | 6 +- cspell.json | 1 + package.json | 2 +- src/components/screens/Download.tsx | 201 +++++++++++++----------- src/components/settings/VersionsTab.tsx | 70 ++++----- src/constants/index.ts | 5 + src/main/managers/KoboldCppManager.ts | 59 +++---- src/main/managers/WindowManager.ts | 2 +- src/main/services/BinaryService.ts | 19 +-- src/utils/assets.ts | 22 ++- src/utils/downloadUtils.ts | 43 +++++ src/utils/index.ts | 1 + src/utils/versionUtils.ts | 5 + 14 files changed, 268 insertions(+), 171 deletions(-) create mode 100644 src/utils/downloadUtils.ts diff --git a/.gitignore b/.gitignore index 6238533..f505643 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ Thumbs.db # Yarn .yarn/ -.pnp.* \ No newline at end of file +.pnp.* +package-lock.json diff --git a/README.md b/README.md index a8cc4ba..a00eff0 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,11 @@ A koboldcpp manager. 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 diff --git a/cspell.json b/cspell.json index b0cdfe8..82370b1 100644 --- a/cspell.json +++ b/cspell.json @@ -28,6 +28,7 @@ "hipblas", "kcpps", "kcppt", + "koboldai", "KoboldAI", "KOBOLDAI", "koboldcpp", diff --git a/package.json b/package.json index 79db6d8..4afd319 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/screens/Download.tsx b/src/components/screens/Download.tsx index 40dc234..c36b62b 100644 --- a/src/components/screens/Download.tsx +++ b/src/components/screens/Download.tsx @@ -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(null); + const downloadingItemRef = useRef(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 ( - { - e.stopPropagation(); - handleDownload('rocm', rocmDownload); - }} - /> +
+ { + e.stopPropagation(); + handleDownload('rocm', rocmDownload); + }} + /> +
); }; @@ -126,40 +139,42 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => { <> {availableDownloads.length > 0 ? ( - {platformInfo.hasAMDGPU && !platformInfo.hasROCm && ( - - -
- - AMD GPU Detected - - + +
- + AMD GPU Detected + +
+ + For best performance with your AMD GPU, + consider installing ROCm support.{' '} + { + 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 - -
-
- - For best performance with your AMD GPU, consider - installing ROCm support. - -
-
- )} + Learn more + + +
+ + )} {rocmDownload && platformInfo.hasAMDGPU && @@ -168,39 +183,47 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => { {sortAssetsByRecommendation( regularDownloads, platformInfo.hasAMDGPU - ).map((download) => ( - { - e.stopPropagation(); - handleDownload('asset', download); - }} - /> - ))} + ).map((download) => { + const isDownloading = + Boolean(downloading) && + downloadingType === 'asset' && + downloadingAsset === download.name; + + return ( +
+ { + e.stopPropagation(); + handleDownload('asset', download); + }} + /> +
+ ); + })} {rocmDownload && !platformInfo.hasAMDGPU && diff --git a/src/components/settings/VersionsTab.tsx b/src/components/settings/VersionsTab.tsx index 8fd71bb..387cfef 100644 --- a/src/components/settings/VersionsTab.tsx +++ b/src/components/settings/VersionsTab.tsx @@ -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( null ); + const downloadingItemRef = useRef(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(); 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 = () => { - {getAllVersions().map((version, index) => { + {allVersions.map((version, index) => { const isDownloading = downloading === version.name; return (
{ ); })} - {getAllVersions().length === 0 && ( + {allVersions.length === 0 && ( No versions found diff --git a/src/constants/index.ts b/src/constants/index.ts index 366a484..4927ca0 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -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 = { diff --git a/src/main/managers/KoboldCppManager.ts b/src/main/managers/KoboldCppManager.ts index adb21ee..8bbcc05 100644 --- a/src/main/managers/KoboldCppManager.ts +++ b/src/main/managers/KoboldCppManager.ts @@ -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 { 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); diff --git a/src/main/managers/WindowManager.ts b/src/main/managers/WindowManager.ts index 77eb72e..963f9d3 100644 --- a/src/main/managers/WindowManager.ts +++ b/src/main/managers/WindowManager.ts @@ -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); diff --git a/src/main/services/BinaryService.ts b/src/main/services/BinaryService.ts index 5d8e93e..f473f6e 100644 --- a/src/main/services/BinaryService.ts +++ b/src/main/services/BinaryService.ts @@ -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`)) + ); } }; diff --git a/src/utils/assets.ts b/src/utils/assets.ts index 083af03..784501b 100644 --- a/src/utils/assets.ts +++ b/src/utils/assets.ts @@ -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 = ( assets: T[], hasAMDGPU: boolean @@ -43,9 +54,16 @@ export const sortAssetsByRecommendation = ( [...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; }); diff --git a/src/utils/downloadUtils.ts b/src/utils/downloadUtils.ts new file mode 100644 index 0000000..58ab666 --- /dev/null +++ b/src/utils/downloadUtils.ts @@ -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; +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index e069c78..53d7081 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,5 @@ export * from './assets'; +export * from './downloadUtils'; export * from './fileSize'; export * from './imageModelPresets'; export * from './nullish'; diff --git a/src/utils/versionUtils.ts b/src/utils/versionUtils.ts index 26ef151..e4399db 100644 --- a/src/utils/versionUtils.ts +++ b/src/utils/versionUtils.ts @@ -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$/, '');