From 4f49f0bad31d5bb3302fa2e63edc33d1dd685a03 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 26 Aug 2025 13:01:19 -0700 Subject: [PATCH] display disabled backend options for available but currently hardware-unsupported backends, allow updatable binaries to be available to make current, persist skipping kcpp updates to app config --- package.json | 6 +- src/components/DownloadCard.tsx | 67 +++++++------ .../screens/Launch/ConfigFileManager.tsx | 33 +++---- .../Launch/GeneralTab/BackendSelectItem.tsx | 61 ++++++------ .../Launch/GeneralTab/BackendSelector.tsx | 98 ++++++++++++++----- .../screens/Launch/GeneralTab/index.tsx | 8 +- src/components/settings/VersionsTab.tsx | 30 ++---- src/hooks/useUpdateChecker.ts | 72 ++++++++++++-- src/utils/assets.ts | 2 +- yarn.lock | 20 ++-- 10 files changed, 244 insertions(+), 153 deletions(-) diff --git a/package.json b/package.json index 5567d7b..00725dc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "friendly-kobold", "productName": "Friendly Kobold", - "version": "0.8.0", + "version": "0.8.1", "description": "A desktop app for running Large Language Models locally", "main": "out/main/index.js", "homepage": "./", @@ -55,7 +55,7 @@ "@eslint/js": "^9.34.0", "@types/node": "^24.3.0", "@types/react": "^19.1.11", - "@types/react-dom": "^19.1.7", + "@types/react-dom": "^19.1.8", "@types/strip-ansi": "^5.2.1", "@typescript-eslint/eslint-plugin": "^8.41.0", "@typescript-eslint/parser": "^8.41.0", @@ -85,7 +85,7 @@ "@mantine/hooks": "^8.2.7", "execa": "^9.6.0", "got": "^14.4.7", - "lucide-react": "^0.541.0", + "lucide-react": "^0.542.0", "react": "^19.1.1", "react-dom": "^19.1.1", "strip-ansi": "^7.1.0", diff --git a/src/components/DownloadCard.tsx b/src/components/DownloadCard.tsx index d0458ed..14f2584 100644 --- a/src/components/DownloadCard.tsx +++ b/src/components/DownloadCard.tsx @@ -26,7 +26,7 @@ interface DownloadCardProps { disabled?: boolean; hasUpdate?: boolean; newerVersion?: string; - onDownload?: (e: MouseEvent) => void; + onDownload: (e: MouseEvent) => void; onMakeCurrent?: () => void; onUpdate?: (e: MouseEvent) => void; } @@ -51,32 +51,13 @@ export const DownloadCard = ({ getInitialValueInEffect: false, }); const isDark = computedColorScheme === 'dark'; - const renderActionButton = () => { - if (hasUpdate && onUpdate) { - return ( - - ); - } + const renderActionButtons = () => { + const buttons = []; - if (!isInstalled && onDownload) { - return ( + if (!isInstalled) { + buttons.push( ); } - return null; + if (hasUpdate && onUpdate) { + buttons.push( + + ); + } + + return buttons.length > 0 ? {buttons} : null; }; return ( @@ -157,7 +166,7 @@ export const DownloadCard = ({ - {renderActionButton()} + {renderActionButtons()} {isDownloading && downloadProgress !== undefined && ( diff --git a/src/components/screens/Launch/ConfigFileManager.tsx b/src/components/screens/Launch/ConfigFileManager.tsx index da7a197..88e1865 100644 --- a/src/components/screens/Launch/ConfigFileManager.tsx +++ b/src/components/screens/Launch/ConfigFileManager.tsx @@ -1,10 +1,5 @@ import { Stack, Text, Group, Button, Select, Badge } from '@mantine/core'; -import { - useState, - useCallback, - forwardRef, - type ComponentPropsWithoutRef, -} from 'react'; +import { useState, useCallback } from 'react'; import { Save, File, Plus, Check } from 'lucide-react'; import type { ConfigFile } from '@/types'; import styles from '@/styles/layout.module.css'; @@ -19,7 +14,7 @@ interface ConfigFileManagerProps { onLoadConfigFiles: () => Promise; } -interface SelectItemProps extends ComponentPropsWithoutRef<'div'> { +interface SelectItemProps { label: string; extension: string; } @@ -35,23 +30,17 @@ const getBadgeColor = (extension: string) => { } }; -const SelectItem = forwardRef( - ({ label, extension, ...others }, ref) => ( -
- - - {label} - - - {extension} - - -
- ) +const SelectItem = ({ label, extension }: SelectItemProps) => ( + + + {label} + + + {extension} + + ); -SelectItem.displayName = 'SelectItem'; - export const ConfigFileManager = ({ configFiles, selectedFile, diff --git a/src/components/screens/Launch/GeneralTab/BackendSelectItem.tsx b/src/components/screens/Launch/GeneralTab/BackendSelectItem.tsx index 9c2ac2e..811ab21 100644 --- a/src/components/screens/Launch/GeneralTab/BackendSelectItem.tsx +++ b/src/components/screens/Launch/GeneralTab/BackendSelectItem.tsx @@ -1,37 +1,38 @@ import { Text, Group, Badge } from '@mantine/core'; -import { forwardRef } from 'react'; -import type { ComponentPropsWithoutRef } from 'react'; -interface BackendSelectItemProps extends ComponentPropsWithoutRef<'div'> { +interface BackendSelectItemProps { label: string; devices?: string[]; + disabled?: boolean; } -export const BackendSelectItem = forwardRef< - HTMLDivElement, - BackendSelectItemProps ->(({ label, devices, ...others }, ref) => ( -
- - - {label} - - {devices && devices.length > 0 && ( - - {devices.slice(0, 2).map((device, index) => ( - - {device.length > 25 ? `${device.slice(0, 25)}...` : device} - - ))} - {devices.length > 2 && ( - - +{devices.length - 2} - - )} - +export const BackendSelectItem = ({ + label, + devices, + disabled = false, +}: BackendSelectItemProps) => ( + + + {label} + {disabled && ( + + (Compatible device(s) not found) + )} - -
-)); - -BackendSelectItem.displayName = 'BackendSelectItem'; + + {devices && devices.length > 0 && ( + + {devices.slice(0, 2).map((device, index) => ( + + {device.length > 25 ? `${device.slice(0, 25)}...` : device} + + ))} + {devices.length > 2 && ( + + +{devices.length - 2} + + )} + + )} + +); diff --git a/src/components/screens/Launch/GeneralTab/BackendSelector.tsx b/src/components/screens/Launch/GeneralTab/BackendSelector.tsx index 723381f..1d95917 100644 --- a/src/components/screens/Launch/GeneralTab/BackendSelector.tsx +++ b/src/components/screens/Launch/GeneralTab/BackendSelector.tsx @@ -5,11 +5,7 @@ import { BackendSelectItem } from '@/components/screens/Launch/GeneralTab/Backen import { GpuDeviceSelector } from '@/components/screens/Launch/GeneralTab/GpuDeviceSelector'; import { useLaunchConfig } from '@/hooks/useLaunchConfig'; -interface BackendSelectorProps { - onBackendsReady?: () => void; -} - -export const BackendSelector = ({ onBackendsReady }: BackendSelectorProps) => { +export const BackendSelector = () => { const { backend, gpuLayers, @@ -20,39 +16,95 @@ export const BackendSelector = ({ onBackendsReady }: BackendSelectorProps) => { } = useLaunchConfig(); const [availableBackends, setAvailableBackends] = useState< - Array<{ value: string; label: string; devices?: string[] }> + Array<{ + value: string; + label: string; + devices?: string[]; + disabled?: boolean; + }> >([]); const hasInitialized = useRef(false); useEffect(() => { const loadBackends = async () => { try { - const [cpuCapabilitiesResult, backends] = await Promise.all([ - window.electronAPI.kobold.detectCPU(), - window.electronAPI.kobold.getAvailableBackends(), - ]); + const [cpuCapabilitiesResult, binarySupport, gpuCapabilities] = + await Promise.all([ + window.electronAPI.kobold.detectCPU(), + window.electronAPI.kobold.detectBackendSupport(), + window.electronAPI.kobold.detectGPUCapabilities(), + ]); - const cpuBackend = backends.find((b) => b.value === 'cpu'); - if (cpuBackend) { - cpuBackend.devices = cpuCapabilitiesResult.devices; + if (!binarySupport) { + setAvailableBackends([]); + hasInitialized.current = true; + + return; } + const backends: Array<{ + value: string; + label: string; + devices?: string[]; + disabled?: boolean; + }> = []; + + if (binarySupport.cuda) { + backends.push({ + value: 'cuda', + label: 'CUDA', + devices: gpuCapabilities.cuda.devices, + disabled: !gpuCapabilities.cuda.supported, + }); + } + + if (binarySupport.rocm) { + backends.push({ + value: 'rocm', + label: 'ROCm', + devices: gpuCapabilities.rocm.devices, + disabled: !gpuCapabilities.rocm.supported, + }); + } + + if (binarySupport.vulkan) { + backends.push({ + value: 'vulkan', + label: 'Vulkan', + devices: gpuCapabilities.vulkan.devices, + disabled: !gpuCapabilities.vulkan.supported, + }); + } + + if (binarySupport.clblast) { + backends.push({ + value: 'clblast', + label: 'CLBlast', + devices: gpuCapabilities.clblast.devices, + disabled: !gpuCapabilities.clblast.supported, + }); + } + + backends.push({ + value: 'cpu', + label: 'CPU', + devices: cpuCapabilitiesResult.devices, + disabled: false, + }); + + backends.sort((a, b) => { + if (a.disabled === b.disabled) return 0; + return a.disabled ? 1 : -1; + }); + setAvailableBackends(backends); hasInitialized.current = true; - - if (onBackendsReady) { - onBackendsReady(); - } } catch (error) { window.electronAPI.logs.logError( 'Failed to detect available backends:', error as Error ); setAvailableBackends([]); - - if (onBackendsReady) { - onBackendsReady(); - } } }; @@ -66,7 +118,7 @@ export const BackendSelector = ({ onBackendsReady }: BackendSelectorProps) => { }); return cleanup; - }, [onBackendsReady]); + }, []); return (
@@ -89,6 +141,7 @@ export const BackendSelector = ({ onBackendsReady }: BackendSelectorProps) => { data={availableBackends.map((b) => ({ value: b.value, label: b.label, + disabled: b.disabled, }))} disabled={availableBackends.length === 0} renderOption={({ option }) => { @@ -99,6 +152,7 @@ export const BackendSelector = ({ onBackendsReady }: BackendSelectorProps) => { ); }} diff --git a/src/components/screens/Launch/GeneralTab/index.tsx b/src/components/screens/Launch/GeneralTab/index.tsx index e8333d5..8b9d6f7 100644 --- a/src/components/screens/Launch/GeneralTab/index.tsx +++ b/src/components/screens/Launch/GeneralTab/index.tsx @@ -4,11 +4,7 @@ import { BackendSelector } from '@/components/screens/Launch/GeneralTab/BackendS import { ModelFileField } from '@/components/ModelFileField'; import { useLaunchConfig } from '@/hooks/useLaunchConfig'; -interface GeneralTabProps { - onBackendsReady?: () => void; -} - -export const GeneralTab = ({ onBackendsReady }: GeneralTabProps) => { +export const GeneralTab = () => { const { modelPath, contextSize, @@ -19,7 +15,7 @@ export const GeneralTab = ({ onBackendsReady }: GeneralTabProps) => { return ( - + { disabled={downloading !== null} hasUpdate={version.hasUpdate} newerVersion={version.newerVersion} - onDownload={ - !version.isInstalled - ? (e) => { - e.stopPropagation(); - handleDownload(version); - } - : undefined - } - onUpdate={ - version.hasUpdate - ? (e) => { - e.stopPropagation(); - handleUpdate(version); - } - : undefined - } - onMakeCurrent={ - version.isInstalled && !version.isCurrent && !version.hasUpdate - ? () => makeCurrent(version) - : undefined - } + onDownload={(e) => { + e.stopPropagation(); + handleDownload(version); + }} + onUpdate={(e) => { + e.stopPropagation(); + handleUpdate(version); + }} + onMakeCurrent={() => makeCurrent(version)} />
); diff --git a/src/hooks/useUpdateChecker.ts b/src/hooks/useUpdateChecker.ts index cdadbf0..eb759de 100644 --- a/src/hooks/useUpdateChecker.ts +++ b/src/hooks/useUpdateChecker.ts @@ -1,4 +1,4 @@ -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect } from 'react'; import { getDisplayNameFromPath } from '@/utils/versionUtils'; import { compareVersions } from '@/utils'; import type { InstalledVersion, DownloadItem } from '@/types/electron'; @@ -12,8 +12,52 @@ export const useUpdateChecker = () => { const [updateInfo, setUpdateInfo] = useState(null); const [isChecking, setIsChecking] = useState(false); const [showUpdateModal, setShowUpdateModal] = useState(false); + const [dismissedUpdates, setDismissedUpdates] = useState>( + new Set() + ); + const [dismissedUpdatesLoaded, setDismissedUpdatesLoaded] = useState(false); + + useEffect(() => { + const loadDismissedUpdates = async () => { + try { + const dismissed = (await window.electronAPI.config.get( + 'dismissedUpdates' + )) as string[] | undefined; + if (dismissed) { + setDismissedUpdates(new Set(dismissed)); + } + setDismissedUpdatesLoaded(true); + } catch (error) { + window.electronAPI.logs.logError( + 'Failed to load dismissed updates:', + error as Error + ); + setDismissedUpdatesLoaded(true); + } + }; + + loadDismissedUpdates(); + }, []); + + const saveDismissedUpdates = useCallback(async (updates: Set) => { + try { + await window.electronAPI.config.set( + 'dismissedUpdates', + Array.from(updates) + ); + } catch (error) { + window.electronAPI.logs.logError( + 'Failed to save dismissed updates:', + error as Error + ); + } + }, []); const checkForUpdates = useCallback(async () => { + if (!dismissedUpdatesLoaded) { + return; + } + setIsChecking(true); try { @@ -59,11 +103,15 @@ export const useUpdateChecker = () => { compareVersions(matchingDownload.version, currentVersion.version) > 0; if (hasUpdate) { - setUpdateInfo({ - currentVersion, - availableUpdate: matchingDownload, - }); - setShowUpdateModal(true); + const updateKey = `${currentVersion.path}-${matchingDownload.version}`; + + if (!dismissedUpdates.has(updateKey)) { + setUpdateInfo({ + currentVersion, + availableUpdate: matchingDownload, + }); + setShowUpdateModal(true); + } } } } catch (error) { @@ -74,12 +122,18 @@ export const useUpdateChecker = () => { } finally { setIsChecking(false); } - }, []); + }, [dismissedUpdates, dismissedUpdatesLoaded]); - const dismissUpdate = useCallback(() => { + const dismissUpdate = useCallback(async () => { + if (updateInfo) { + const updateKey = `${updateInfo.currentVersion.path}-${updateInfo.availableUpdate.version}`; + const newDismissedUpdates = new Set([...dismissedUpdates, updateKey]); + setDismissedUpdates(newDismissedUpdates); + await saveDismissedUpdates(newDismissedUpdates); + } setShowUpdateModal(false); setUpdateInfo(null); - }, []); + }, [updateInfo, dismissedUpdates, saveDismissedUpdates]); return { updateInfo, diff --git a/src/utils/assets.ts b/src/utils/assets.ts index 85a547a..8d7e872 100644 --- a/src/utils/assets.ts +++ b/src/utils/assets.ts @@ -9,7 +9,7 @@ export const getAssetDescription = (assetName: string): string => { } if (name.endsWith(ASSET_SUFFIXES.OLDPC)) { - return 'Meant for old PCs with outdated CPUs that may not work with the standard build. Does not support modern AVX2 CPU architectures.'; + return 'Meant for old PCs with outdated CPUs that may not work with the standard build. Does not support modern AVX2 CPU architectures or modern CUDA.'; } if (name.endsWith(ASSET_SUFFIXES.NOCUDA)) { diff --git a/yarn.lock b/yarn.lock index 4a4a09e..4de2d3b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1324,12 +1324,12 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:^19.1.7": - version: 19.1.7 - resolution: "@types/react-dom@npm:19.1.7" +"@types/react-dom@npm:^19.1.8": + version: 19.1.8 + resolution: "@types/react-dom@npm:19.1.8" peerDependencies: "@types/react": ^19.0.0 - checksum: 10c0/8db5751c1567552fe4e1ece9f5823b682f2994ec8d30ed34ba0ef984e3c8ace1435f8be93d02f55c350147e78ac8c4dbcd8ed2c3b6a60f575bc5374f588c51c9 + checksum: 10c0/561f9679c99e93adba8ecdf7d5ad69cc9d5e35837fa996246a83713f0ce498fc5b871f9a2a3342c7d440fc02159abe10f29c4a4004527d5d38b2e84f21840793 languageName: node linkType: hard @@ -3612,7 +3612,7 @@ __metadata: "@mantine/hooks": "npm:^8.2.7" "@types/node": "npm:^24.3.0" "@types/react": "npm:^19.1.11" - "@types/react-dom": "npm:^19.1.7" + "@types/react-dom": "npm:^19.1.8" "@types/strip-ansi": "npm:^5.2.1" "@typescript-eslint/eslint-plugin": "npm:^8.41.0" "@typescript-eslint/parser": "npm:^8.41.0" @@ -3634,7 +3634,7 @@ __metadata: husky: "npm:^9.1.7" jiti: "npm:^2.5.1" lint-staged: "npm:^16.1.5" - lucide-react: "npm:^0.541.0" + lucide-react: "npm:^0.542.0" prettier: "npm:^3.6.2" react: "npm:^19.1.1" react-dom: "npm:^19.1.1" @@ -5005,12 +5005,12 @@ __metadata: languageName: node linkType: hard -"lucide-react@npm:^0.541.0": - version: 0.541.0 - resolution: "lucide-react@npm:0.541.0" +"lucide-react@npm:^0.542.0": + version: 0.542.0 + resolution: "lucide-react@npm:0.542.0" peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10c0/ffa23a9c9ead0832c7e0bb1eb7ed44e9f38bbf083d94b4d36a66eb23fa2099d4a52e1677c91eb7e8ea922621fa98e730f5d513bc56b3111b14b835bd8ab823ab + checksum: 10c0/3ccdb898a480f0194f93abef0b6f0347bc2647db24051e65a17c1dbd22ca0d10a7636d9af68c92665b42b6c9e761567a0d65944fe8568fb0e30a7ea57281ccec languageName: node linkType: hard