From f825cd0d2ffb2f3aa0dd843af4f558cf6c55de0c Mon Sep 17 00:00:00 2001 From: Egor Date: Mon, 1 Dec 2025 19:17:54 -0800 Subject: [PATCH] fix the backends tab to work in offline mode and allow it to import new backend binaries --- .github/copilot-instructions.md | 24 ++----- package.json | 2 +- src/components/ImportBackendLink.tsx | 63 ++++++++++++++++++ src/components/screens/Download.tsx | 51 ++------------- src/components/settings/BackendsTab.tsx | 87 ++++++++++++++----------- 5 files changed, 125 insertions(+), 102 deletions(-) create mode 100644 src/components/ImportBackendLink.tsx diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c6880bc..729ec2e 100755 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,21 +1,7 @@ # Copilot Instructions for Gerbil -### General Coding - -- Follow existing TypeScript/React patterns in the codebase -- Maintain consistency with the established project structure -- Use proper TypeScript types (avoid `any` when possible) -- Follow the ESLint configuration (includes SonarJS and security rules) -- Never create tests, docs or github workflows -- Stop asking me to run the "dev" script to test changes -- Try to move helper functions from component code to their own separate files to help minimize clutter -- Always use absolute imports (e.g. `import { MyComponent } from '@/components/MyComponent'`) -- Never add explicit return types to functions. We want to rely on implicit types as much as possible - -### Logging and Error Handling - -- **NEVER use console.\* calls** (console.log, console.error, console.warn, etc.) - they are blocked by ESLint -- **Backend errors**: Use `this.logManager.logError(message, error)` for main process errors -- **Frontend errors**: Use `window.electronAPI.logs.logError(message, error)` for renderer process errors -- All errors are logged asynchronously to avoid blocking the event loop -- Only use console.\* for critical system errors that must appear in terminal (very rare cases) +- **NEVER use console.\* calls** - they are blocked by ESLint. Use `logError()` from `@/utils/node/logging` (main process) or `window.electronAPI.logs.logError()` (renderer) +- Always use absolute imports: `import { X } from '@/components/X'` +- Never add explicit return types to functions - rely on TypeScript inference +- Never create tests, docs, or GitHub workflows +- Move helper functions out of component files into separate utility files diff --git a/package.json b/package.json index dd1609b..bf4e5cf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "gerbil", "productName": "Gerbil", - "version": "1.14.0", + "version": "1.14.1", "description": "Run Large Language Models locally", "main": "out/main/index.js", "homepage": "./", diff --git a/src/components/ImportBackendLink.tsx b/src/components/ImportBackendLink.tsx new file mode 100644 index 0000000..cc39f67 --- /dev/null +++ b/src/components/ImportBackendLink.tsx @@ -0,0 +1,63 @@ +import { useState } from 'react'; +import { Text, Anchor } from '@mantine/core'; + +interface ImportBackendLinkProps { + disabled?: boolean; + onSuccess: () => void | Promise; + onImportingChange?: (importing: boolean) => void; +} + +export const ImportBackendLink = ({ + disabled = false, + onSuccess, + onImportingChange, +}: ImportBackendLinkProps) => { + const [importing, setImporting] = useState(false); + + const updateImporting = (value: boolean) => { + setImporting(value); + onImportingChange?.(value); + }; + const [importError, setImportError] = useState(null); + + const isDisabled = disabled || importing; + + const handleImport = async () => { + setImportError(null); + updateImporting(true); + + try { + const result = await window.electronAPI.kobold.importLocalBackend(); + + if (result.success) { + await onSuccess(); + } else if (result.error) { + setImportError(result.error); + } + } finally { + updateImporting(false); + } + }; + + return ( + <> + {importError && ( + + {importError} + + )} + + Already have a backend downloaded?{' '} + + {importing ? 'Importing...' : 'Select a local file'} + + + + ); +}; diff --git a/src/components/screens/Download.tsx b/src/components/screens/Download.tsx index 46499da..f5b4b44 100644 --- a/src/components/screens/Download.tsx +++ b/src/components/screens/Download.tsx @@ -1,14 +1,7 @@ import { useState, useCallback, useRef, useEffect } from 'react'; -import { - Card, - Text, - Title, - Loader, - Stack, - Container, - Anchor, -} from '@mantine/core'; +import { Card, Text, Title, Loader, Stack, Container } from '@mantine/core'; import { DownloadCard } from '@/components/DownloadCard'; +import { ImportBackendLink } from '@/components/ImportBackendLink'; import { getPlatformDisplayName } from '@/utils/platform'; import { formatDownloadSize } from '@/utils/format'; import { getAssetDescription } from '@/utils/assets'; @@ -30,7 +23,6 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => { } = useKoboldBackendsStore(); const [downloadingAsset, setDownloadingAsset] = useState(null); - const [importError, setImportError] = useState(null); const [importing, setImporting] = useState(false); const downloadingItemRef = useRef(null); @@ -123,40 +115,11 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => { )} - {importError && ( - - {importError} - - )} - - - Already have a backend downloaded?{' '} - { - setImportError(null); - setImporting(true); - - try { - const result = - await window.electronAPI.kobold.importLocalBackend(); - - if (result.success) { - onDownloadComplete(); - } else if (result.error) { - setImportError(result.error); - } - } finally { - setImporting(false); - } - }} - > - {importing ? 'Importing...' : 'Select a local file'} - - + )} diff --git a/src/components/settings/BackendsTab.tsx b/src/components/settings/BackendsTab.tsx index ff3adc0..9fd6ae6 100644 --- a/src/components/settings/BackendsTab.tsx +++ b/src/components/settings/BackendsTab.tsx @@ -7,9 +7,11 @@ import { Loader, Center, Anchor, + Divider, } from '@mantine/core'; import { ExternalLink } from 'lucide-react'; import { DownloadCard } from '@/components/DownloadCard'; +import { ImportBackendLink } from '@/components/ImportBackendLink'; import { getAssetDescription } from '@/utils/assets'; import { getDisplayNameFromPath, @@ -30,7 +32,6 @@ export const BackendsTab = () => { downloading, handleDownload: handleDownloadFromStore, getLatestReleaseWithDownloadStatus, - initialize, } = useKoboldBackendsStore(); const [installedBackends, setInstalledBackends] = useState< @@ -43,11 +44,32 @@ export const BackendsTab = () => { const [latestRelease, setLatestRelease] = useState( null ); + const [importing, setImporting] = useState(false); const downloadingItemRef = useRef(null); - const loadInstalledBackends = useCallback(async () => { - setLoadingInstalled(true); + useEffect(() => { + const init = async () => { + setLoadingInstalled(true); + const [backends, current] = await Promise.all([ + window.electronAPI.kobold.getInstalledBackends(), + window.electronAPI.kobold.getCurrentBackend(), + ]); + + setInstalledBackends(backends); + setCurrentBackend(current); + setLoadingInstalled(false); + + const release = await getLatestReleaseWithDownloadStatus(); + if (release) { + setLatestRelease(release); + } + }; + + init(); + }, [getLatestReleaseWithDownloadStatus]); + + const loadInstalledBackends = useCallback(async () => { const [backends, current] = await Promise.all([ window.electronAPI.kobold.getInstalledBackends(), window.electronAPI.kobold.getCurrentBackend(), @@ -55,34 +77,8 @@ export const BackendsTab = () => { setInstalledBackends(backends); setCurrentBackend(current); - - setLoadingInstalled(false); }, []); - const loadLatestRelease = useCallback(async () => { - const release = await getLatestReleaseWithDownloadStatus(); - if (release) { - setLatestRelease(release); - } - }, [getLatestReleaseWithDownloadStatus]); - - useEffect(() => { - if (availableDownloads.length === 0 && !loadingRemote && !loadingPlatform) { - initialize(); - } - - // eslint-disable-next-line react-hooks/set-state-in-effect - loadInstalledBackends(); - loadLatestRelease(); - }, [ - loadInstalledBackends, - loadLatestRelease, - availableDownloads.length, - loadingRemote, - loadingPlatform, - initialize, - ]); - const allBackends = useMemo((): BackendInfo[] => { const backends: BackendInfo[] = []; const processedInstalled = new Set(); @@ -235,23 +231,19 @@ export const BackendsTab = () => { window.electronAPI.kobold.setCurrentBackend(backend.installedPath); }; - if (loadingInstalled || loadingPlatform || loadingRemote) { + if (loadingInstalled) { return (
- - {loadingInstalled && (loadingPlatform || loadingRemote) - ? 'Loading backends...' - : loadingInstalled - ? 'Scanning installed backends...' - : 'Checking for updates...'} - + Scanning installed backends...
); } + const isDisabled = downloading !== null || importing; + return ( <> @@ -276,6 +268,17 @@ export const BackendsTab = () => { )} + {(loadingPlatform || loadingRemote) && ( + + + + + Checking for available downloads... + + + + )} + {allBackends.map((backend, index) => { const isDownloading = downloading === backend.name; @@ -293,7 +296,7 @@ export const BackendsTab = () => { : '' } description={getAssetDescription(backend.name)} - disabled={downloading !== null} + disabled={isDisabled} onDownload={(e) => { e.stopPropagation(); handleDownload(backend); @@ -323,6 +326,14 @@ export const BackendsTab = () => { )} + + + + ); };