tool migrations: biome -> oxc, yarn -> pnpm

This commit is contained in:
Egor 2026-03-15 17:40:57 -07:00
parent 905edb2050
commit 1bead396d1
132 changed files with 8598 additions and 8457 deletions

5
.gitignore vendored
View file

@ -8,6 +8,5 @@ release/
Thumbs.db Thumbs.db
.VSCodeCounter/ .VSCodeCounter/
# Yarn # pnpm
.yarn/ pnpm-debug.log*
.pnp.*

7
.oxfmtrc.json Normal file
View file

@ -0,0 +1,7 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"singleQuote": true,
"sortImports": {
"groups": ["builtin", "external", "internal", ["parent", "sibling", "index"], "style"]
}
}

67
.oxlintrc.json Normal file
View file

@ -0,0 +1,67 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"options": {
"typeAware": true,
"typeCheck": true
},
"categories": {
"correctness": "error",
"suspicious": "warn",
"perf": "warn",
"style": "warn"
},
"plugins": ["typescript", "react", "react-perf", "import", "unicorn", "oxc"],
"rules": {
"import/no-cycle": ["error", { "maxDepth": 3 }],
"import/no-absolute-path": "off",
"import/no-unassigned-import": "off",
"import/no-named-export": "off",
"import/group-exports": "off",
"import/prefer-default-export": "off",
"import/exports-last": "off",
"import/no-nodejs-modules": "off",
"react/react-in-jsx-scope": "off",
"react/iframe-missing-sandbox": "off",
"react/rules-of-hooks": "error",
"react/jsx-no-target-blank": "error",
"react-perf/jsx-no-new-function-as-prop": "off",
"react-perf/jsx-no-new-object-as-prop": "off",
"react-perf/jsx-no-jsx-as-prop": "off",
"react-perf/jsx-no-new-array-as-prop": "off",
"react/jsx-max-depth": "off",
"react/jsx-props-no-spreading": "off",
"react/jsx-handler-names": "off",
"react/no-array-index-key": "off",
"typescript/no-floating-promises": "error",
"typescript/no-misused-promises": "error",
"typescript/no-unsafe-type-assertion": "off",
"typescript/ban-ts-comment": "warn",
"typescript/no-deprecated": "warn",
"typescript/prefer-nullish-coalescing": "warn",
"typescript/require-await": "warn",
"typescript/return-await": "warn",
"unicorn/no-null": "off",
"unicorn/prefer-global-this": "off",
"unicorn/filename-case": "off",
"unicorn/no-nested-ternary": "off",
"unicorn/catch-error-name": "off",
"unicorn/prefer-ternary": "off",
"no-ternary": "off",
"no-nested-ternary": "off",
"no-magic-numbers": "off",
"id-length": "off",
"func-style": "off",
"sort-imports": "off",
"sort-keys": "off",
"max-statements": "off",
"max-params": "off",
"prefer-destructuring": "off",
"no-continue": "off",
"init-declarations": "off",
"curly": "off",
"no-implicit-coercion": "off",
"no-duplicate-imports": "off",
"no-await-in-loop": "off"
},
"ignorePatterns": ["*.config.*", "scripts/**"]
}

View file

@ -1,3 +1,3 @@
{ {
"recommendations": ["biomejs.biome"] "recommendations": ["oxc.oxc-vscode"]
} }

23
.vscode/settings.json vendored
View file

@ -1,25 +1,4 @@
{ {
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit"
},
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.defaultFormatter": "biomejs.biome", "editor.defaultFormatter": "oxc.oxc-vscode"
"[javascript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[javascriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[jsonc]": {
"editor.defaultFormatter": "biomejs.biome"
}
} }

View file

@ -1 +0,0 @@
nodeLinker: node-modules

View file

@ -1,42 +0,0 @@
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"formatter": {
"indentStyle": "space",
"lineWidth": 100
},
"linter": {
"rules": {
"recommended": true,
"suspicious": {
"noArrayIndexKey": "off"
},
"a11y": {
"noSvgWithoutTitle": "off",
"useAltText": "off"
},
"security": {
"noDangerouslySetInnerHtml": "off"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"semicolons": "always",
"trailingCommas": "es5"
}
},
"overrides": [
{
"includes": ["*.config.*", "vite.config.*", "scripts/**"],
"linter": {
"enabled": false
}
}
]
}

View file

@ -1,6 +1,7 @@
import { resolve } from 'path';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import { defineConfig } from 'electron-vite'; import { defineConfig } from 'electron-vite';
import { resolve } from 'path';
import { visualizer } from 'rollup-plugin-visualizer'; import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({ export default defineConfig({

View file

@ -1,38 +1,33 @@
{ {
"name": "gerbil", "name": "gerbil",
"productName": "Gerbil", "version": "1.20.4",
"version": "1.20.3",
"description": "Run Large Language Models locally", "description": "Run Large Language Models locally",
"main": "out/main/index.js",
"homepage": "./",
"engines": {
"node": ">=24.0.0"
},
"packageManager": "yarn@4.13.0",
"scripts": {
"dev": "electron-vite dev",
"build": "electron-vite build",
"package": "electron-vite build && electron-builder --publish=never",
"analyze": "cross-env ANALYZE=true electron-vite build && yarn dlx open-cli dist/stats.html",
"check": "biome check . && tsc --noEmit",
"fix": "biome check --write .",
"release": "yarn dlx tsx scripts/release.ts"
},
"keywords": [ "keywords": [
"llm",
"ai", "ai",
"local-ai",
"language-model",
"koboldcpp",
"electron", "electron",
"privacy", "koboldcpp",
"offline" "language-model",
"llm",
"local-ai",
"offline",
"privacy"
], ],
"homepage": "./",
"license": "AGPL-3.0-or-later",
"author": { "author": {
"name": "lone-cloud", "name": "lone-cloud",
"email": "lonecloud604@proton.me" "email": "lonecloud604@proton.me"
}, },
"license": "AGPL-3.0-or-later", "main": "out/main/index.js",
"scripts": {
"dev": "electron-vite dev",
"build": "electron-vite build",
"package": "electron-vite build && electron-builder --publish=never",
"analyze": "cross-env ANALYZE=true electron-vite build && pnpm dlx open-cli dist/stats.html",
"check": "oxlint && oxfmt --check",
"fix": "oxlint --fix && oxfmt",
"release": "node --no-warnings scripts/release.ts"
},
"dependencies": { "dependencies": {
"@codemirror/search": "^6.6.0", "@codemirror/search": "^6.6.0",
"@codemirror/theme-one-dark": "^6.1.3", "@codemirror/theme-one-dark": "^6.1.3",
@ -60,7 +55,6 @@
"zustand": "^5.0.11" "zustand": "^5.0.11"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.4.7",
"@types/mime-types": "^3.0.1", "@types/mime-types": "^3.0.1",
"@types/node": "^25.5.0", "@types/node": "^25.5.0",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
@ -72,10 +66,17 @@
"electron-builder": "^26.8.1", "electron-builder": "^26.8.1",
"electron-vite": "^5.0.0", "electron-vite": "^5.0.0",
"jiti": "^2.6.1", "jiti": "^2.6.1",
"oxfmt": "^0.40.0",
"oxlint": "^1.55.0",
"oxlint-tsgolint": "^0.17.0",
"rollup-plugin-visualizer": "^7.0.1", "rollup-plugin-visualizer": "^7.0.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^8.0.0" "vite": "^8.0.0"
}, },
"engines": {
"node": ">=24.0.0"
},
"packageManager": "pnpm@10.32.1",
"build": { "build": {
"appId": "com.gerbil.app", "appId": "com.gerbil.app",
"productName": "Gerbil", "productName": "Gerbil",
@ -182,5 +183,6 @@
} }
] ]
} }
} },
"productName": "Gerbil"
} }

6453
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

4
pnpm-workspace.yaml Normal file
View file

@ -0,0 +1,4 @@
allowBuilds:
electron: true
electron-winstaller: true
esbuild: true

View file

@ -1,8 +1,7 @@
#!/usr/bin/env tsx
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { join } from 'path'; import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
interface PackageJson { interface PackageJson {
version: string; version: string;
@ -10,7 +9,7 @@ interface PackageJson {
} }
const packageJson: PackageJson = JSON.parse( const packageJson: PackageJson = JSON.parse(
readFileSync(join(__dirname, '..', 'package.json'), 'utf8') readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'),
); );
const currentVersion = packageJson.version; const currentVersion = packageJson.version;
@ -22,7 +21,7 @@ try {
const gitStatus = execSync('git status --porcelain', { encoding: 'utf8' }); const gitStatus = execSync('git status --porcelain', { encoding: 'utf8' });
if (gitStatus.trim() !== '') { if (gitStatus.trim() !== '') {
console.error( console.error(
'Error: There are uncommitted changes. Please commit or stash them before creating a release.' 'Error: There are uncommitted changes. Please commit or stash them before creating a release.',
); );
process.exit(1); process.exit(1);
} }

View file

@ -1,4 +1,5 @@
import { Button, Group, Stack, Text } from '@mantine/core'; import { Button, Group, Stack, Text } from '@mantine/core';
import { Modal } from '@/components/Modal'; import { Modal } from '@/components/Modal';
import type { KoboldCrashInfo } from '@/types/ipc'; import type { KoboldCrashInfo } from '@/types/ipc';
@ -15,15 +16,15 @@ const getCrashDescription = (crashInfo: KoboldCrashInfo) => {
if (crashInfo.signal) { if (crashInfo.signal) {
const signalDescriptions: Record<string, string> = { const signalDescriptions: Record<string, string> = {
SIGKILL: 'The process was forcefully terminated',
SIGSEGV: 'Memory access violation (segmentation fault)',
SIGABRT: 'The process aborted unexpectedly', SIGABRT: 'The process aborted unexpectedly',
SIGBUS: 'Bus error (invalid memory access)', SIGBUS: 'Bus error (invalid memory access)',
SIGFPE: 'Floating-point exception', SIGFPE: 'Floating-point exception',
SIGILL: 'Illegal instruction',
SIGTERM: 'The process was terminated',
SIGSTOP: 'The process was stopped',
SIGHUP: 'The process lost its controlling terminal', SIGHUP: 'The process lost its controlling terminal',
SIGILL: 'Illegal instruction',
SIGKILL: 'The process was forcefully terminated',
SIGSEGV: 'Memory access violation (segmentation fault)',
SIGSTOP: 'The process was stopped',
SIGTERM: 'The process was terminated',
}; };
return signalDescriptions[crashInfo.signal] || `Terminated by signal ${crashInfo.signal}`; return signalDescriptions[crashInfo.signal] || `Terminated by signal ${crashInfo.signal}`;
@ -37,7 +38,9 @@ const getCrashDescription = (crashInfo: KoboldCrashInfo) => {
}; };
export const BackendCrashModal = ({ opened, onClose, crashInfo }: BackendCrashModalProps) => { export const BackendCrashModal = ({ opened, onClose, crashInfo }: BackendCrashModalProps) => {
if (!crashInfo) return null; if (!crashInfo) {
return null;
}
const description = getCrashDescription(crashInfo); const description = getCrashDescription(crashInfo);

View file

@ -1,5 +1,6 @@
import { Button, Checkbox, Group, Stack, Text } from '@mantine/core'; import { Button, Checkbox, Group, Stack, Text } from '@mantine/core';
import { useState } from 'react'; import { useState } from 'react';
import { Modal } from '@/components/Modal'; import { Modal } from '@/components/Modal';
interface EjectConfirmModalProps { interface EjectConfirmModalProps {

View file

@ -1,4 +1,4 @@
import { Alert, Button, Center, rem, Stack, Text } from '@mantine/core'; import { Alert, Button, Center, Stack, Text, rem } from '@mantine/core';
import { AlertTriangle, FolderOpen } from 'lucide-react'; import { AlertTriangle, FolderOpen } from 'lucide-react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';
@ -35,7 +35,7 @@ const ErrorFallback = ({
<Button <Button
variant="light" variant="light"
size="compact-sm" size="compact-sm"
leftSection={<FolderOpen style={{ width: rem(16), height: rem(16) }} />} leftSection={<FolderOpen style={{ height: rem(16), width: rem(16) }} />}
onClick={() => void window.electronAPI.app.showLogsFolder()} onClick={() => void window.electronAPI.app.showLogsFolder()}
> >
Show Logs Folder Show Logs Folder

View file

@ -8,20 +8,20 @@ interface PerformanceBadgeProps {
iconOnly?: boolean; iconOnly?: boolean;
} }
const handlePerformanceClick = async () => {
const result = await window.electronAPI.app.openPerformanceManager();
if (!result.success) {
window.electronAPI.logs.logError(`Failed to open performance manager: ${result.error}`);
}
};
export const PerformanceBadge = ({ export const PerformanceBadge = ({
label, label,
value, value,
tooltipLabel, tooltipLabel,
iconOnly = false, iconOnly = false,
}: PerformanceBadgeProps) => { }: PerformanceBadgeProps) => {
const handlePerformanceClick = async () => {
const result = await window.electronAPI.app.openPerformanceManager();
if (!result.success) {
window.electronAPI.logs.logError(`Failed to open performance manager: ${result.error}`);
}
};
if (iconOnly) { if (iconOnly) {
return ( return (
<Tooltip label={tooltipLabel} position="top"> <Tooltip label={tooltipLabel} position="top">
@ -38,14 +38,14 @@ export const PerformanceBadge = ({
size="xs" size="xs"
variant="light" variant="light"
style={{ style={{
minWidth: '5rem',
textAlign: 'center',
height: 'auto',
padding: '0.25rem 0.5rem',
margin: '0.125rem 0',
borderRadius: '0.75rem', borderRadius: '0.75rem',
fontSize: '0.7em', fontSize: '0.7em',
fontWeight: 500, fontWeight: 500,
height: 'auto',
margin: '0.125rem 0',
minWidth: '5rem',
padding: '0.25rem 0.5rem',
textAlign: 'center',
}} }}
onClick={() => void handlePerformanceClick()} onClick={() => void handlePerformanceClick()}
> >

View file

@ -1,11 +1,13 @@
import { ActionIcon, AppShell, CopyButton, Group, Tooltip } from '@mantine/core'; import { ActionIcon, AppShell, CopyButton, Group, Tooltip } from '@mantine/core';
import { Check, Globe, NotepadText } from 'lucide-react'; import { Check, Globe, NotepadText } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import type { CpuMetrics, GpuMetrics, MemoryMetrics } from '@/main/modules/monitoring'; import type { CpuMetrics, GpuMetrics, MemoryMetrics } from '@/main/modules/monitoring';
import { useLaunchConfigStore } from '@/stores/launchConfig'; import { useLaunchConfigStore } from '@/stores/launchConfig';
import { useNotepadStore } from '@/stores/notepad'; import { useNotepadStore } from '@/stores/notepad';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { getTunnelInterfaceUrl } from '@/utils/interface'; import { getTunnelInterfaceUrl } from '@/utils/interface';
import { PerformanceBadge } from './PerformanceBadge'; import { PerformanceBadge } from './PerformanceBadge';
export const StatusBar = () => { export const StatusBar = () => {
@ -23,7 +25,9 @@ export const StatusBar = () => {
const { isImageGenerationMode } = useLaunchConfigStore(); const { isImageGenerationMode } = useLaunchConfigStore();
const tunnelUrl = useMemo(() => { const tunnelUrl = useMemo(() => {
if (!tunnelBaseUrl) return null; if (!tunnelBaseUrl) {
return null;
}
if (frontendPreference === 'sillytavern' || frontendPreference === 'openwebui') { if (frontendPreference === 'sillytavern' || frontendPreference === 'openwebui') {
return tunnelBaseUrl; return tunnelBaseUrl;
} }
@ -42,17 +46,23 @@ export const StatusBar = () => {
let isMounted = true; let isMounted = true;
const handleCpuMetrics = (metrics: CpuMetrics) => { const handleCpuMetrics = (metrics: CpuMetrics) => {
if (!isMounted) return; if (!isMounted) {
return;
}
setCpuMetrics(metrics); setCpuMetrics(metrics);
}; };
const handleMemoryMetrics = (metrics: MemoryMetrics) => { const handleMemoryMetrics = (metrics: MemoryMetrics) => {
if (!isMounted) return; if (!isMounted) {
return;
}
setMemoryMetrics(metrics); setMemoryMetrics(metrics);
}; };
const handleGpuMetrics = (metrics: GpuMetrics) => { const handleGpuMetrics = (metrics: GpuMetrics) => {
if (!isMounted) return; if (!isMounted) {
return;
}
setGpuMetrics(metrics); setGpuMetrics(metrics);
}; };

View file

@ -1,6 +1,8 @@
import icon from '/icon.png';
import { ActionIcon, AppShell, Box, Group, Image, Tooltip } from '@mantine/core'; import { ActionIcon, AppShell, Box, Group, Image, Tooltip } from '@mantine/core';
import { Copy, Minus, Settings, Square, X } from 'lucide-react'; import { Copy, Minus, Settings, Square, X } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { UpdateButton } from '@/components/App/UpdateButton'; import { UpdateButton } from '@/components/App/UpdateButton';
import { Select } from '@/components/Select'; import { Select } from '@/components/Select';
import { SettingsModal } from '@/components/settings/SettingsModal'; import { SettingsModal } from '@/components/settings/SettingsModal';
@ -10,7 +12,6 @@ import { useLaunchConfigStore } from '@/stores/launchConfig';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import type { InterfaceTab, Screen, SelectOption } from '@/types'; import type { InterfaceTab, Screen, SelectOption } from '@/types';
import { getAvailableInterfaceOptions } from '@/utils/interface'; import { getAvailableInterfaceOptions } from '@/utils/interface';
import icon from '/icon.png';
interface TitleBarProps { interface TitleBarProps {
currentScreen: Screen; currentScreen: Screen;
@ -19,6 +20,18 @@ interface TitleBarProps {
onTabChange: (tab: InterfaceTab) => void; onTabChange: (tab: InterfaceTab) => void;
} }
const renderOption = ({ option }: { option: SelectOption }) => (
<Box
style={{
color: option.value === 'eject' ? 'var(--mantine-color-red-6)' : undefined,
fontWeight: option.value === 'eject' ? 600 : undefined,
textAlign: 'center',
}}
>
{option.label}
</Box>
);
export const TitleBar = ({ currentScreen, currentTab, onEject, onTabChange }: TitleBarProps) => { export const TitleBar = ({ currentScreen, currentTab, onEject, onTabChange }: TitleBarProps) => {
const { const {
resolvedColorScheme: colorScheme, resolvedColorScheme: colorScheme,
@ -40,18 +53,6 @@ export const TitleBar = ({ currentScreen, currentTab, onEject, onTabChange }: Ti
} }
}; };
const renderOption = ({ option }: { option: SelectOption }) => (
<Box
style={{
textAlign: 'center',
color: option.value === 'eject' ? 'var(--mantine-color-red-6)' : undefined,
fontWeight: option.value === 'eject' ? 600 : undefined,
}}
>
{option.label}
</Box>
);
useEffect(() => { useEffect(() => {
const initializeState = async () => { const initializeState = async () => {
const currentMaximizedState = await window.electronAPI.app.isMaximized(); const currentMaximizedState = await window.electronAPI.app.isMaximized();
@ -61,27 +62,27 @@ export const TitleBar = ({ currentScreen, currentTab, onEject, onTabChange }: Ti
void initializeState(); void initializeState();
const cleanup = window.electronAPI.app.onWindowStateToggle(() => const cleanup = window.electronAPI.app.onWindowStateToggle(() =>
setIsMaximized((prev) => !prev) setIsMaximized((prev) => !prev),
); );
return cleanup; return cleanup;
}, []); }, []);
return ( return (
<AppShell.Header style={{ display: 'flex', flexDirection: 'column', border: 'none' }}> <AppShell.Header style={{ border: 'none', display: 'flex', flexDirection: 'column' }}>
<Box <Box
style={{ style={{
height: TITLEBAR_HEIGHT, WebkitAppRegion: isSelectOpen ? 'no-drag' : 'drag',
padding: '0.125rem 0 0.125rem 0.5rem',
display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: backgroundColor:
colorScheme === 'dark' ? 'var(--mantine-color-dark-6)' : 'var(--mantine-color-gray-1)', colorScheme === 'dark' ? 'var(--mantine-color-dark-6)' : 'var(--mantine-color-gray-1)',
border: '1px solid var(--mantine-color-default-border)', border: '1px solid var(--mantine-color-default-border)',
WebkitAppRegion: isSelectOpen ? 'no-drag' : 'drag', display: 'flex',
userSelect: 'none', height: TITLEBAR_HEIGHT,
justifyContent: 'space-between',
padding: '0.125rem 0 0.125rem 0.5rem',
position: 'relative', position: 'relative',
userSelect: 'none',
}} }}
> >
<Group gap="0.5rem" align="center" style={{ WebkitAppRegion: 'no-drag' }}> <Group gap="0.5rem" align="center" style={{ WebkitAppRegion: 'no-drag' }}>
@ -97,10 +98,10 @@ export const TitleBar = ({ currentScreen, currentTab, onEject, onTabChange }: Ti
<Box <Box
style={{ style={{
position: 'absolute',
left: '50%',
transform: 'translateX(-50%)',
WebkitAppRegion: 'no-drag', WebkitAppRegion: 'no-drag',
left: '50%',
position: 'absolute',
transform: 'translateX(-50%)',
}} }}
> >
{currentScreen === 'interface' && ( {currentScreen === 'interface' && (
@ -113,19 +114,19 @@ export const TitleBar = ({ currentScreen, currentTab, onEject, onTabChange }: Ti
data={getAvailableInterfaceOptions({ data={getAvailableInterfaceOptions({
frontendPreference, frontendPreference,
imageGenerationFrontendPreference, imageGenerationFrontendPreference,
isTextMode,
isImageGenerationMode, isImageGenerationMode,
isTextMode,
})} })}
renderOption={renderOption} renderOption={renderOption}
variant="unstyled" variant="unstyled"
style={{ textAlign: 'center', minWidth: '7.5rem' }} style={{ minWidth: '7.5rem', textAlign: 'center' }}
styles={{ styles={{
input: { input: {
textAlign: 'center',
backgroundColor: 'transparent', backgroundColor: 'transparent',
border: 'none', border: 'none',
userSelect: 'none',
cursor: 'pointer', cursor: 'pointer',
textAlign: 'center',
userSelect: 'none',
}, },
}} }}
/> />
@ -152,34 +153,34 @@ export const TitleBar = ({ currentScreen, currentTab, onEject, onTabChange }: Ti
<Box <Box
style={{ style={{
width: '0.1rem',
height: '1.25rem',
backgroundColor: backgroundColor:
colorScheme === 'dark' colorScheme === 'dark'
? 'var(--mantine-color-dark-3)' ? 'var(--mantine-color-dark-3)'
: 'var(--mantine-color-gray-4)', : 'var(--mantine-color-gray-4)',
height: '1.25rem',
margin: '0 0.25rem', margin: '0 0.25rem',
width: '0.1rem',
}} }}
/> />
{[ {[
{ {
color: undefined,
icon: <Minus size="1rem" />, icon: <Minus size="1rem" />,
onClick: () => void window.electronAPI.app.minimizeWindow(),
color: undefined,
label: 'Minimize window', label: 'Minimize window',
onClick: () => void window.electronAPI.app.minimizeWindow(),
}, },
{ {
icon: isMaximized ? <Copy size="1rem" /> : <Square size="1rem" />,
onClick: () => void window.electronAPI.app.maximizeWindow(),
color: undefined, color: undefined,
icon: isMaximized ? <Copy size="1rem" /> : <Square size="1rem" />,
label: isMaximized ? 'Restore window' : 'Maximize window', label: isMaximized ? 'Restore window' : 'Maximize window',
onClick: () => void window.electronAPI.app.maximizeWindow(),
}, },
{ {
icon: <X size="1.25rem" />,
onClick: () => void window.electronAPI.app.closeWindow(),
color: 'red' as const, color: 'red' as const,
icon: <X size="1.25rem" />,
label: 'Close window', label: 'Close window',
onClick: () => void window.electronAPI.app.closeWindow(),
}, },
].map((button, index) => ( ].map((button, index) => (
<ActionIcon <ActionIcon

View file

@ -1,6 +1,7 @@
import { Anchor, Button, Card, Group, Loader, Progress, Stack, Text } from '@mantine/core'; import { Anchor, Button, Card, Group, Loader, Progress, Stack, Text } from '@mantine/core';
import { Download, ExternalLink, X } from 'lucide-react'; import { Download, ExternalLink, X } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import { Modal } from '@/components/Modal'; import { Modal } from '@/components/Modal';
import { GITHUB_API } from '@/constants'; import { GITHUB_API } from '@/constants';
import type { BinaryUpdateInfo } from '@/hooks/useUpdateChecker'; import type { BinaryUpdateInfo } from '@/hooks/useUpdateChecker';
@ -30,7 +31,7 @@ export const UpdateAvailableModal = ({
const availableUpdate = updateInfo?.availableUpdate; const availableUpdate = updateInfo?.availableUpdate;
const [isUpdating, setIsUpdating] = useState(false); const [isUpdating, setIsUpdating] = useState(false);
const isDownloading = !!availableUpdate && downloading === availableUpdate.name; const isDownloading = Boolean(availableUpdate) && downloading === availableUpdate?.name;
const currentProgress = availableUpdate ? downloadProgress[availableUpdate.name] || 0 : 0; const currentProgress = availableUpdate ? downloadProgress[availableUpdate.name] || 0 : 0;
const handleUpdate = async () => { const handleUpdate = async () => {

View file

@ -1,6 +1,8 @@
import { ActionIcon, Tooltip } from '@mantine/core'; import { ActionIcon, Tooltip } from '@mantine/core';
import { CircleFadingArrowUp, Download } from 'lucide-react'; import { CircleFadingArrowUp, Download } from 'lucide-react';
import { type MouseEvent, useState } from 'react'; import { useState } from 'react';
import type { MouseEvent } from 'react';
import { TITLEBAR_HEIGHT } from '@/constants'; import { TITLEBAR_HEIGHT } from '@/constants';
import { useAppUpdateChecker } from '@/hooks/useAppUpdateChecker'; import { useAppUpdateChecker } from '@/hooks/useAppUpdateChecker';
@ -17,7 +19,9 @@ export const UpdateButton = () => {
const [showDownload, setShowDownload] = useState(false); const [showDownload, setShowDownload] = useState(false);
if (!hasUpdate) return null; if (!hasUpdate) {
return null;
}
let color: 'green' | 'blue' | 'orange' = 'orange'; let color: 'green' | 'blue' | 'orange' = 'orange';
let label = 'Update available'; let label = 'Update available';

View file

@ -1,5 +1,6 @@
import { AppShell, Center, Loader, Stack, Text, useMantineColorScheme } from '@mantine/core'; import { AppShell, Center, Loader, Stack, Text, useMantineColorScheme } from '@mantine/core';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { BackendCrashModal } from '@/components/App/BackendCrashModal'; import { BackendCrashModal } from '@/components/App/BackendCrashModal';
import { EjectConfirmModal } from '@/components/App/EjectConfirmModal'; import { EjectConfirmModal } from '@/components/App/EjectConfirmModal';
import { ErrorBoundary } from '@/components/App/ErrorBoundary'; import { ErrorBoundary } from '@/components/App/ErrorBoundary';
@ -41,10 +42,10 @@ export const App = () => {
getDefaultInterfaceTab({ getDefaultInterfaceTab({
frontendPreference, frontendPreference,
imageGenerationFrontendPreference, imageGenerationFrontendPreference,
isTextMode,
isImageGenerationMode, isImageGenerationMode,
isTextMode,
}), }),
[frontendPreference, imageGenerationFrontendPreference, isTextMode, isImageGenerationMode] [frontendPreference, imageGenerationFrontendPreference, isTextMode, isImageGenerationMode],
); );
useEffect(() => { useEffect(() => {
@ -66,13 +67,13 @@ export const App = () => {
const updateTray = async () => { const updateTray = async () => {
const config = await window.electronAPI.kobold.getSelectedConfig(); const config = await window.electronAPI.kobold.getSelectedConfig();
const displayModel = model || sdmodel || null; const displayModel = model || sdmodel || null;
const modelName = displayModel ? displayModel.split('/').pop() || displayModel : null; const modelName = displayModel ? (displayModel.split('/').pop() ?? displayModel) : null;
void window.electronAPI.app.updateTrayState({ void window.electronAPI.app.updateTrayState({
screen: currentScreen, config: config ?? null,
model: modelName, model: modelName,
config: config || null,
monitoringEnabled: systemMonitoringEnabled, monitoringEnabled: systemMonitoringEnabled,
screen: currentScreen,
}); });
}; };
@ -141,11 +142,15 @@ export const App = () => {
}, [determineScreen]); }, [determineScreen]);
useEffect(() => { useEffect(() => {
if (loadingRemote || !hasInitialized) return; if (loadingRemote || !hasInitialized) {
return;
}
const runUpdateCheck = async () => { const runUpdateCheck = async () => {
const currentBackend = await window.electronAPI.kobold.getCurrentBackend(); const currentBackend = await window.electronAPI.kobold.getCurrentBackend();
if (!currentBackend) return; if (!currentBackend) {
return;
}
void checkForUpdates(); void checkForUpdates();
}; };
@ -158,7 +163,7 @@ export const App = () => {
() => { () => {
void runUpdateCheck(); void runUpdateCheck();
}, },
6 * 60 * 60 * 1000 6 * 60 * 60 * 1000,
); );
return () => { return () => {
@ -171,10 +176,10 @@ export const App = () => {
const currentBackend = await window.electronAPI.kobold.getCurrentBackend(); const currentBackend = await window.electronAPI.kobold.getCurrentBackend();
await handleDownload({ await handleDownload({
item: download,
isUpdate: true, isUpdate: true,
wasCurrentBinary: true, item: download,
oldBackendPath: currentBackend?.path, oldBackendPath: currentBackend?.path,
wasCurrentBinary: true,
}); });
closeModal(); closeModal();
@ -217,7 +222,7 @@ export const App = () => {
padding={isInterfaceScreen ? 0 : 'md'} padding={isInterfaceScreen ? 0 : 'md'}
> >
<TitleBar <TitleBar
currentScreen={currentScreen || 'launch'} currentScreen={currentScreen ?? 'launch'}
currentTab={activeInterfaceTab} currentTab={activeInterfaceTab}
onEject={() => void handleEject()} onEject={() => void handleEject()}
onTabChange={setActiveInterfaceTab} onTabChange={setActiveInterfaceTab}
@ -228,12 +233,12 @@ export const App = () => {
style={{ style={{
height: `calc(100vh - ${TITLEBAR_HEIGHT} - ${STATUSBAR_HEIGHT})`, height: `calc(100vh - ${TITLEBAR_HEIGHT} - ${STATUSBAR_HEIGHT})`,
minHeight: `calc(100vh - ${TITLEBAR_HEIGHT} - ${STATUSBAR_HEIGHT})`, minHeight: `calc(100vh - ${TITLEBAR_HEIGHT} - ${STATUSBAR_HEIGHT})`,
position: 'relative',
overflow: 'auto', overflow: 'auto',
paddingTop: 0,
paddingBottom: 0, paddingBottom: 0,
paddingLeft: isInterfaceScreen ? 0 : '.5rem', paddingLeft: isInterfaceScreen ? 0 : '.5rem',
paddingRight: isInterfaceScreen ? 0 : '.5rem', paddingRight: isInterfaceScreen ? 0 : '.5rem',
paddingTop: 0,
position: 'relative',
top: TITLEBAR_HEIGHT, top: TITLEBAR_HEIGHT,
}} }}
> >
@ -254,17 +259,17 @@ export const App = () => {
activeInterfaceTab={activeInterfaceTab} activeInterfaceTab={activeInterfaceTab}
isServerReady={isServerReady} isServerReady={isServerReady}
onWelcomeComplete={() => void handleWelcomeComplete()} onWelcomeComplete={() => void handleWelcomeComplete()}
onDownloadComplete={() => void handleDownloadComplete()} onDownloadComplete={() => handleDownloadComplete()}
onLaunch={handleLaunch} onLaunch={handleLaunch}
/> />
)} )}
</ErrorBoundary> </ErrorBoundary>
<UpdateAvailableModal <UpdateAvailableModal
opened={showUpdateModal && !!binaryUpdateInfo} opened={showUpdateModal && Boolean(binaryUpdateInfo)}
onClose={closeModal} onClose={closeModal}
onSkip={skipUpdate} onSkip={skipUpdate}
updateInfo={binaryUpdateInfo || undefined} updateInfo={binaryUpdateInfo ?? undefined}
onUpdate={handleBinaryUpdate} onUpdate={handleBinaryUpdate}
/> />
</AppShell.Main> </AppShell.Main>

View file

@ -1,4 +1,5 @@
import { Checkbox, Group } from '@mantine/core'; import { Checkbox, Group } from '@mantine/core';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
interface CheckboxWithTooltipProps { interface CheckboxWithTooltipProps {

View file

@ -1,6 +1,7 @@
import { Badge, Button, Card, Group, Loader, Progress, rem, Stack, Text } from '@mantine/core'; import { Badge, Button, Card, Group, Loader, Progress, Stack, Text, rem } from '@mantine/core';
import { Download, Trash2 } from 'lucide-react'; import { Download, Trash2 } from 'lucide-react';
import type { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
import { useKoboldBackendsStore } from '@/stores/koboldBackends'; import { useKoboldBackendsStore } from '@/stores/koboldBackends';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import type { BackendInfo } from '@/types'; import type { BackendInfo } from '@/types';
@ -35,7 +36,7 @@ export const DownloadCard = ({
const isLoading = downloading === backend.name; const isLoading = downloading === backend.name;
const currentProgress = isLoading ? Math.min(downloadProgress[backend.name], 100) || 0 : 0; const currentProgress = isLoading ? Math.min(downloadProgress[backend.name], 100) || 0 : 0;
const hasVersionMismatch = Boolean( const hasVersionMismatch = Boolean(
backend.version && backend.actualVersion && backend.version !== backend.actualVersion backend.version && backend.actualVersion && backend.version !== backend.actualVersion,
); );
const renderActionButtons = () => { const renderActionButtons = () => {
@ -54,7 +55,7 @@ export const DownloadCard = ({
isLoading ? ( isLoading ? (
<Loader size="1rem" /> <Loader size="1rem" />
) : ( ) : (
<Download style={{ width: rem(14), height: rem(14) }} /> <Download style={{ height: rem(14), width: rem(14) }} />
) )
} }
> >
@ -73,7 +74,7 @@ export const DownloadCard = ({
disabled={disabled} disabled={disabled}
> >
Make Current Make Current
</Button> </Button>,
); );
} }
@ -91,12 +92,12 @@ export const DownloadCard = ({
isLoading ? ( isLoading ? (
<Loader size="1rem" /> <Loader size="1rem" />
) : ( ) : (
<Download style={{ width: rem(14), height: rem(14) }} /> <Download style={{ height: rem(14), width: rem(14) }} />
) )
} }
> >
{isLoading ? 'Updating...' : `Update to ${backend.newerVersion}`} {isLoading ? 'Updating...' : `Update to ${backend.newerVersion}`}
</Button> </Button>,
); );
} }
@ -114,12 +115,12 @@ export const DownloadCard = ({
isLoading ? ( isLoading ? (
<Loader size="1rem" /> <Loader size="1rem" />
) : ( ) : (
<Download style={{ width: rem(14), height: rem(14) }} /> <Download style={{ height: rem(14), width: rem(14) }} />
) )
} }
> >
{isLoading ? 'Re-downloading...' : 'Re-download'} {isLoading ? 'Re-downloading...' : 'Re-download'}
</Button> </Button>,
); );
} }
@ -137,12 +138,12 @@ export const DownloadCard = ({
isLoading ? ( isLoading ? (
<Loader size="1rem" /> <Loader size="1rem" />
) : ( ) : (
<Trash2 style={{ width: rem(14), height: rem(14) }} /> <Trash2 style={{ height: rem(14), width: rem(14) }} />
) )
} }
> >
{isLoading ? 'Deleting...' : 'Delete'} {isLoading ? 'Deleting...' : 'Delete'}
</Button> </Button>,
); );
} }
@ -155,8 +156,8 @@ export const DownloadCard = ({
radius="sm" radius="sm"
padding="sm" padding="sm"
{...(backend.isCurrent && { {...(backend.isCurrent && {
bg: colorScheme === 'dark' ? 'dark.6' : 'gray.0',
bd: `2px solid var(--mantine-color-${colorScheme === 'dark' ? 'blue-4' : 'blue-6'})`, bd: `2px solid var(--mantine-color-${colorScheme === 'dark' ? 'blue-4' : 'blue-6'})`,
bg: colorScheme === 'dark' ? 'dark.6' : 'gray.0',
})} })}
> >
<Group justify="space-between" align="center"> <Group justify="space-between" align="center">

View file

@ -1,5 +1,6 @@
import { ActionIcon, Card, Group, rem, Stack, Text, Tooltip } from '@mantine/core'; import { ActionIcon, Card, Group, Stack, Text, Tooltip, rem } from '@mantine/core';
import { Copy } from 'lucide-react'; import { Copy } from 'lucide-react';
import { safeExecute } from '@/utils/logger'; import { safeExecute } from '@/utils/logger';
export interface InfoItem { export interface InfoItem {
@ -19,7 +20,7 @@ export const InfoCard = ({ title, items, loading = false }: InfoCardProps) => {
await safeExecute( await safeExecute(
() => navigator.clipboard.writeText(info), () => navigator.clipboard.writeText(info),
`Failed to copy ${title.toLowerCase()}` `Failed to copy ${title.toLowerCase()}`,
); );
}; };
@ -46,12 +47,12 @@ export const InfoCard = ({ title, items, loading = false }: InfoCardProps) => {
aria-label={`Copy ${title}`} aria-label={`Copy ${title}`}
style={{ style={{
position: 'absolute', position: 'absolute',
top: 8,
right: 8, right: 8,
top: 8,
zIndex: 1, zIndex: 1,
}} }}
> >
<Copy style={{ width: rem(14), height: rem(14) }} /> <Copy style={{ height: rem(14), width: rem(14) }} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
@ -69,8 +70,8 @@ export const InfoCard = ({ title, items, loading = false }: InfoCardProps) => {
size="sm" size="sm"
ff="monospace" ff="monospace"
style={{ style={{
wordBreak: 'break-all',
flex: 1, flex: 1,
wordBreak: 'break-all',
}} }}
> >
{item.value} {item.value}

View file

@ -1,4 +1,5 @@
import { Group, Text } from '@mantine/core'; import { Group, Text } from '@mantine/core';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
interface LabelWithTooltipProps { interface LabelWithTooltipProps {

View file

@ -4,12 +4,12 @@ import type { ReactNode } from 'react';
const TITLEBAR_HEIGHT = '2.5rem'; const TITLEBAR_HEIGHT = '2.5rem';
const MODAL_STYLES_WITH_TITLEBAR = { const MODAL_STYLES_WITH_TITLEBAR = {
overlay: {
top: TITLEBAR_HEIGHT,
},
content: { content: {
marginTop: TITLEBAR_HEIGHT, marginTop: TITLEBAR_HEIGHT,
}, },
overlay: {
top: TITLEBAR_HEIGHT,
},
} as const; } as const;
export interface ModalProps { export interface ModalProps {
@ -39,10 +39,10 @@ export const Modal = ({
const content = tallContent ? ( const content = tallContent ? (
<div <div
style={{ style={{
height: '75vh',
padding: 0,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
height: '75vh',
padding: 0,
position: 'relative', position: 'relative',
}} }}
> >
@ -54,10 +54,10 @@ export const Modal = ({
{showCloseButton && ( {showCloseButton && (
<Box <Box
style={{ style={{
padding: '1rem 0',
display: 'flex', display: 'flex',
justifyContent: 'flex-end',
flexShrink: 0, flexShrink: 0,
justifyContent: 'flex-end',
padding: '1rem 0',
}} }}
> >
<Button onClick={onClose} variant="filled"> <Button onClick={onClose} variant="filled">

View file

@ -1,4 +1,5 @@
import { Button, Group, Text } from '@mantine/core'; import { Button, Group, Text } from '@mantine/core';
import { Modal } from '@/components/Modal'; import { Modal } from '@/components/Modal';
interface CloseConfirmModalProps { interface CloseConfirmModalProps {

View file

@ -1,9 +1,12 @@
import { ActionIcon, Box, Paper } from '@mantine/core'; import { ActionIcon, Box, Paper } from '@mantine/core';
import { Minus } from 'lucide-react'; import { Minus } from 'lucide-react';
import { type MouseEvent, useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import type { MouseEvent } from 'react';
import { NOTEPAD_MIN_HEIGHT, NOTEPAD_MIN_WIDTH } from '@/constants/notepad'; import { NOTEPAD_MIN_HEIGHT, NOTEPAD_MIN_WIDTH } from '@/constants/notepad';
import { useNotepadStore } from '@/stores/notepad'; import { useNotepadStore } from '@/stores/notepad';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { CloseConfirmModal } from './CloseConfirmModal.tsx'; import { CloseConfirmModal } from './CloseConfirmModal.tsx';
import { NotepadEditor } from './Editor.tsx'; import { NotepadEditor } from './Editor.tsx';
import { NotepadTabs } from './Tabs.tsx'; import { NotepadTabs } from './Tabs.tsx';
@ -41,7 +44,9 @@ export const NotepadContainer = () => {
const handleTabCloseRequest = (title: string) => { const handleTabCloseRequest = (title: string) => {
const tab = tabs.find((t) => t.title === title); const tab = tabs.find((t) => t.title === title);
if (!tab) return; if (!tab) {
return;
}
if (tab.content.trim().length > 0) { if (tab.content.trim().length > 0) {
setConfirmCloseModal({ setConfirmCloseModal({
@ -71,7 +76,9 @@ export const NotepadContainer = () => {
const handleMouseMove = (e: globalThis.MouseEvent) => { const handleMouseMove = (e: globalThis.MouseEvent) => {
if (resizeDirection) { if (resizeDirection) {
const rect = containerRef.current?.getBoundingClientRect(); const rect = containerRef.current?.getBoundingClientRect();
if (!rect) return; if (!rect) {
return;
}
let newWidth = position.width; let newWidth = position.width;
let newHeight = position.height; let newHeight = position.height;
@ -85,8 +92,8 @@ export const NotepadContainer = () => {
setPosition({ setPosition({
...position, ...position,
width: newWidth,
height: newHeight, height: newHeight,
width: newWidth,
}); });
} }
}; };
@ -106,7 +113,9 @@ export const NotepadContainer = () => {
} }
}, [resizeDirection, position, setPosition]); }, [resizeDirection, position, setPosition]);
if (!isLoaded || !isVisible) return null; if (!isLoaded || !isVisible) {
return null;
}
return ( return (
<Paper <Paper
@ -114,52 +123,52 @@ export const NotepadContainer = () => {
shadow="lg" shadow="lg"
withBorder withBorder
style={{ style={{
position: 'fixed',
left: 0,
bottom: 24,
width: position.width,
height: position.height,
zIndex: 100,
backgroundColor: backgroundColor:
resolvedColorScheme === 'dark' resolvedColorScheme === 'dark'
? 'var(--mantine-color-dark-6)' ? 'var(--mantine-color-dark-6)'
: 'var(--mantine-color-white)', : 'var(--mantine-color-white)',
bottom: 24,
cursor: 'default', cursor: 'default',
height: position.height,
left: 0,
position: 'fixed',
userSelect: 'none', userSelect: 'none',
width: position.width,
zIndex: 100,
}} }}
> >
<Box <Box
style={{ style={{
cursor: 'ns-resize',
height: 4,
left: 0,
position: 'absolute', position: 'absolute',
top: -2, top: -2,
left: 0,
width: '100%', width: '100%',
height: 4,
cursor: 'ns-resize',
}} }}
onMouseDown={handleResizeStart('top')} onMouseDown={handleResizeStart('top')}
/> />
<Box <Box
style={{ style={{
position: 'absolute',
top: 0,
right: -2,
width: 4,
height: '100%',
cursor: 'ew-resize', cursor: 'ew-resize',
height: '100%',
position: 'absolute',
right: -2,
top: 0,
width: 4,
}} }}
onMouseDown={handleResizeStart('right')} onMouseDown={handleResizeStart('right')}
/> />
<Box <Box
style={{ style={{
position: 'absolute',
top: -2,
right: -2,
width: 12,
height: 12,
cursor: 'ne-resize', cursor: 'ne-resize',
height: 12,
position: 'absolute',
right: -2,
top: -2,
width: 12,
}} }}
onMouseDown={handleResizeStart('top-right')} onMouseDown={handleResizeStart('top-right')}
/> />
@ -167,12 +176,6 @@ export const NotepadContainer = () => {
{resizeDirection && ( {resizeDirection && (
<Box <Box
style={{ style={{
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
zIndex: 9999,
backgroundColor: 'transparent', backgroundColor: 'transparent',
cursor: cursor:
resizeDirection.includes('right') && resizeDirection.includes('top') resizeDirection.includes('right') && resizeDirection.includes('top')
@ -180,6 +183,12 @@ export const NotepadContainer = () => {
: resizeDirection.includes('right') : resizeDirection.includes('right')
? 'ew-resize' ? 'ew-resize'
: 'ns-resize', : 'ns-resize',
height: '100vh',
left: 0,
position: 'fixed',
top: 0,
width: '100vw',
zIndex: 9999,
}} }}
/> />
)} )}
@ -187,14 +196,14 @@ export const NotepadContainer = () => {
<Box h="100%" style={{ display: 'flex', flexDirection: 'column' }}> <Box h="100%" style={{ display: 'flex', flexDirection: 'column' }}>
<Box <Box
style={{ style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
borderBottom: `1px solid ${ borderBottom: `1px solid ${
resolvedColorScheme === 'dark' resolvedColorScheme === 'dark'
? 'var(--mantine-color-dark-4)' ? 'var(--mantine-color-dark-4)'
: 'var(--mantine-color-gray-3)' : 'var(--mantine-color-gray-3)'
}`, }`,
display: 'flex',
justifyContent: 'space-between',
minHeight: 28, minHeight: 28,
}} }}
> >
@ -203,10 +212,10 @@ export const NotepadContainer = () => {
<Box <Box
style={{ style={{
display: 'flex', display: 'flex',
flexShrink: 0,
gap: 4, gap: 4,
paddingLeft: 8, paddingLeft: 8,
paddingRight: 8, paddingRight: 8,
flexShrink: 0,
}} }}
> >
<ActionIcon variant="subtle" size="xs" onClick={() => setVisible(false)}> <ActionIcon variant="subtle" size="xs" onClick={() => setVisible(false)}>

View file

@ -3,8 +3,11 @@ import { search } from '@codemirror/search';
import { oneDark } from '@codemirror/theme-one-dark'; import { oneDark } from '@codemirror/theme-one-dark';
import { EditorView, highlightActiveLine, keymap } from '@codemirror/view'; import { EditorView, highlightActiveLine, keymap } from '@codemirror/view';
import { Box } from '@mantine/core'; import { Box } from '@mantine/core';
import CodeMirror, { type ReactCodeMirrorRef } from '@uiw/react-codemirror'; import CodeMirror from '@uiw/react-codemirror';
import { type MouseEvent, useCallback, useEffect, useRef, useState } from 'react'; import type { ReactCodeMirrorRef } from '@uiw/react-codemirror';
import { useCallback, useEffect, useRef, useState } from 'react';
import type { MouseEvent } from 'react';
import { useNotepadStore } from '@/stores/notepad'; import { useNotepadStore } from '@/stores/notepad';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import type { NotepadTab } from '@/types/electron'; import type { NotepadTab } from '@/types/electron';
@ -34,7 +37,7 @@ export const NotepadEditor = ({ tab }: NotepadEditorProps) => {
setSaveTimeout(timeout); setSaveTimeout(timeout);
}, },
[tab.title, saveTabContent, saveTimeout] [tab.title, saveTabContent, saveTimeout],
); );
const handleEditorContextMenu = (e: MouseEvent) => { const handleEditorContextMenu = (e: MouseEvent) => {
@ -57,7 +60,7 @@ export const NotepadEditor = ({ tab }: NotepadEditorProps) => {
clearTimeout(saveTimeout); clearTimeout(saveTimeout);
} }
}, },
[saveTimeout] [saveTimeout],
); );
const extensions = [ const extensions = [
@ -81,9 +84,9 @@ export const NotepadEditor = ({ tab }: NotepadEditorProps) => {
onClick={handleBoxClick} onClick={handleBoxClick}
onContextMenu={handleEditorContextMenu} onContextMenu={handleEditorContextMenu}
style={{ style={{
overflow: 'hidden',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
overflow: 'hidden',
}} }}
> >
<CodeMirror <CodeMirror
@ -93,20 +96,20 @@ export const NotepadEditor = ({ tab }: NotepadEditorProps) => {
theme={theme} theme={theme}
extensions={extensions} extensions={extensions}
basicSetup={{ basicSetup={{
lineNumbers: showLineNumbers,
foldGutter: false,
dropCursor: false,
allowMultipleSelections: false, allowMultipleSelections: false,
indentOnInput: true, autocompletion: true,
bracketMatching: true, bracketMatching: true,
closeBrackets: true, closeBrackets: true,
autocompletion: true, dropCursor: false,
foldGutter: false,
highlightSelectionMatches: false, highlightSelectionMatches: false,
indentOnInput: true,
lineNumbers: showLineNumbers,
searchKeymap: true, searchKeymap: true,
}} }}
style={{ style={{
height: '100%',
flex: '1 1 0', flex: '1 1 0',
height: '100%',
minHeight: '0', minHeight: '0',
}} }}
/> />

View file

@ -1,13 +1,8 @@
import { ActionIcon, Box, Text, TextInput } from '@mantine/core'; import { ActionIcon, Box, Text, TextInput } from '@mantine/core';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import { import { useEffect, useRef, useState } from 'react';
type DragEvent, import type { DragEvent, KeyboardEvent, MouseEvent } from 'react';
type KeyboardEvent,
type MouseEvent,
useEffect,
useRef,
useState,
} from 'react';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
interface TabProps { interface TabProps {
@ -109,7 +104,7 @@ export const Tab = ({
onDragOver={onDragOver} onDragOver={onDragOver}
onDrop={(e) => onDrop(e, index)} onDrop={(e) => onDrop(e, index)}
style={{ style={{
padding: '0.375rem 0.5rem', alignItems: 'center',
backgroundColor: isActive backgroundColor: isActive
? resolvedColorScheme === 'dark' ? resolvedColorScheme === 'dark'
? 'var(--mantine-color-dark-4)' ? 'var(--mantine-color-dark-4)'
@ -126,11 +121,11 @@ export const Tab = ({
}`, }`,
cursor: isEditing ? 'default' : 'pointer', cursor: isEditing ? 'default' : 'pointer',
display: 'flex', display: 'flex',
alignItems: 'center',
gap: '0.25rem', gap: '0.25rem',
minWidth: 0,
maxWidth: '7.5rem', maxWidth: '7.5rem',
minWidth: 0,
opacity: isDragOver ? 0.5 : 1, opacity: isDragOver ? 0.5 : 1,
padding: '0.375rem 0.5rem',
}} }}
> >
{isEditing ? ( {isEditing ? (
@ -149,10 +144,10 @@ export const Tab = ({
styles={{ styles={{
input: { input: {
fontSize: 'var(--mantine-font-size-xs)', fontSize: 'var(--mantine-font-size-xs)',
padding: 0,
minHeight: 'auto',
height: 'auto', height: 'auto',
lineHeight: 1, lineHeight: 1,
minHeight: 'auto',
padding: 0,
}, },
}} }}
/> />
@ -168,10 +163,10 @@ export const Tab = ({
setShowLineNumbers(!showLineNumbers); setShowLineNumbers(!showLineNumbers);
}} }}
style={{ style={{
flex: 1,
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
flex: 1,
}} }}
> >
{title} {title}

View file

@ -1,6 +1,8 @@
import { ActionIcon, Box } from '@mantine/core'; import { ActionIcon, Box } from '@mantine/core';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
import { type DragEvent, type MouseEvent, useState } from 'react'; import type { DragEvent, MouseEvent } from 'react';
import { useState } from 'react';
import { Tab } from '@/components/Notepad/Tab'; import { Tab } from '@/components/Notepad/Tab';
import { useNotepadStore } from '@/stores/notepad'; import { useNotepadStore } from '@/stores/notepad';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
@ -10,6 +12,11 @@ interface NotepadTabsProps {
onCloseTab: (title: string) => void; onCloseTab: (title: string) => void;
} }
const handleDragOver = (e: DragEvent) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
};
export const NotepadTabs = ({ onCreateNewTab, onCloseTab }: NotepadTabsProps) => { export const NotepadTabs = ({ onCreateNewTab, onCloseTab }: NotepadTabsProps) => {
const { const {
tabs, tabs,
@ -51,11 +58,6 @@ export const NotepadTabs = ({ onCreateNewTab, onCloseTab }: NotepadTabsProps) =>
e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.effectAllowed = 'move';
}; };
const handleDragOver = (e: DragEvent) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
};
const handleDragEnter = (index: number) => { const handleDragEnter = (index: number) => {
setDragOverIndex(index); setDragOverIndex(index);
}; };
@ -85,8 +87,8 @@ export const NotepadTabs = ({ onCreateNewTab, onCloseTab }: NotepadTabsProps) =>
: 'var(--mantine-color-gray-3)' : 'var(--mantine-color-gray-3)'
}`, }`,
display: 'flex', display: 'flex',
overflow: 'hidden',
minHeight: '2rem', minHeight: '2rem',
overflow: 'hidden',
}} }}
> >
{tabs.map((tab, index) => ( {tabs.map((tab, index) => (
@ -117,8 +119,8 @@ export const NotepadTabs = ({ onCreateNewTab, onCloseTab }: NotepadTabsProps) =>
size="xs" size="xs"
onClick={() => void onCreateNewTab()} onClick={() => void onCreateNewTab()}
style={{ style={{
margin: '0.25rem',
alignSelf: 'center', alignSelf: 'center',
margin: '0.25rem',
}} }}
> >
<Plus size="0.75rem" /> <Plus size="0.75rem" />

View file

@ -1,5 +1,6 @@
import type { ComboboxItem } from '@mantine/core'; import type { ComboboxItem } from '@mantine/core';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import { LabelWithTooltip } from '@/components/LabelWithTooltip'; import { LabelWithTooltip } from '@/components/LabelWithTooltip';
import { Select } from '@/components/Select'; import { Select } from '@/components/Select';
import type { SelectOption } from '@/types'; import type { SelectOption } from '@/types';

View file

@ -1,11 +1,12 @@
import { Switch as MantineSwitch, type SwitchProps } from '@mantine/core'; import { Switch as MantineSwitch } from '@mantine/core';
import type { SwitchProps } from '@mantine/core';
export const Switch = (props: SwitchProps) => ( export const Switch = (props: SwitchProps) => (
<MantineSwitch <MantineSwitch
{...props} {...props}
styles={{ styles={{
track: { transition: 'none' },
thumb: { transition: 'none' }, thumb: { transition: 'none' },
track: { transition: 'none' },
}} }}
/> />
); );

View file

@ -1,5 +1,6 @@
import { Card, Container, Loader, Stack, Text, Title } from '@mantine/core'; import { Card, Container, Loader, Stack, Text, Title } from '@mantine/core';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { DownloadCard } from '@/components/DownloadCard'; import { DownloadCard } from '@/components/DownloadCard';
import { ImportBackendLink } from '@/components/ImportBackendLink'; import { ImportBackendLink } from '@/components/ImportBackendLink';
import { useKoboldBackendsStore } from '@/stores/koboldBackends'; import { useKoboldBackendsStore } from '@/stores/koboldBackends';
@ -33,8 +34,8 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
setDownloadingAsset(download.name); setDownloadingAsset(download.name);
await handleDownloadFromStore({ await handleDownloadFromStore({
item: download,
isUpdate: false, isUpdate: false,
item: download,
wasCurrentBinary: false, wasCurrentBinary: false,
}); });
@ -44,7 +45,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
setDownloadingAsset(null); setDownloadingAsset(null);
}, 200); }, 200);
}, },
[handleDownloadFromStore, onDownloadComplete] [handleDownloadFromStore, onDownloadComplete],
); );
useEffect(() => { useEffect(() => {
@ -79,13 +80,13 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
<div key={download.name} ref={isDownloading ? downloadingItemRef : null}> <div key={download.name} ref={isDownloading ? downloadingItemRef : null}>
<DownloadCard <DownloadCard
backend={{ backend={{
name: download.name,
version: download.version || '',
size: download.size,
isInstalled: false,
isCurrent: false,
downloadUrl: download.url, downloadUrl: download.url,
hasUpdate: false, hasUpdate: false,
isCurrent: false,
isInstalled: false,
name: download.name,
size: download.size,
version: download.version ?? '',
}} }}
size={formatDownloadSize(download.size, download.url)} size={formatDownloadSize(download.size, download.url)}
description={getAssetDescription(download.name)} description={getAssetDescription(download.name)}

View file

@ -1,5 +1,6 @@
import { Box, Stack, Text } from '@mantine/core'; import { Box, Stack, Text } from '@mantine/core';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants'; import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants';
import { useLaunchConfigStore } from '@/stores/launchConfig'; import { useLaunchConfigStore } from '@/stores/launchConfig';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
@ -24,18 +25,18 @@ export const ServerTab = ({ isServerReady, activeTab }: ServerTabProps) => {
imageGenerationFrontendPreference, imageGenerationFrontendPreference,
isImageGenerationMode: effectiveImageMode, isImageGenerationMode: effectiveImageMode,
}), }),
[frontendPreference, imageGenerationFrontendPreference, effectiveImageMode] [frontendPreference, imageGenerationFrontendPreference, effectiveImageMode],
); );
if (!isServerReady) { if (!isServerReady) {
return ( return (
<Box <Box
style={{ style={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center', alignItems: 'center',
display: 'flex',
height: '100%',
justifyContent: 'center', justifyContent: 'center',
width: '100%',
}} }}
> >
<Stack align="center" gap="md" mt="xl"> <Stack align="center" gap="md" mt="xl">
@ -54,21 +55,22 @@ export const ServerTab = ({ isServerReady, activeTab }: ServerTabProps) => {
return ( return (
<Box <Box
style={{ style={{
width: '100%',
height: `calc(100vh - ${TITLEBAR_HEIGHT} - ${STATUSBAR_HEIGHT})`, height: `calc(100vh - ${TITLEBAR_HEIGHT} - ${STATUSBAR_HEIGHT})`,
overflow: 'hidden', overflow: 'hidden',
width: '100%',
}} }}
> >
<iframe <iframe
src={iframeUrl} src={iframeUrl}
title={title} title={title}
style={{ style={{
width: '100%',
height: '100%',
border: 'none', border: 'none',
borderRadius: 'inherit', borderRadius: 'inherit',
height: '100%',
width: '100%',
}} }}
allow="clipboard-read; clipboard-write; fullscreen; microphone; geolocation; camera; autoplay" allow="clipboard-read; clipboard-write; fullscreen; microphone; geolocation; camera; autoplay"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
/> />
</Box> </Box>
); );

View file

@ -1,6 +1,7 @@
import { ActionIcon, Box, ScrollArea } from '@mantine/core'; import { ActionIcon, Box, ScrollArea } from '@mantine/core';
import { ChevronDown } from 'lucide-react'; import { ChevronDown } from 'lucide-react';
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'; import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants'; import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { handleTerminalOutput, processTerminalContent } from '@/utils/terminal'; import { handleTerminalOutput, processTerminalContent } from '@/utils/terminal';
@ -28,7 +29,9 @@ export const TerminalTab = forwardRef<TerminalTabRef>((_props, ref) => {
}, []); }, []);
const handleScroll = ({ y }: { y: number }) => { const handleScroll = ({ y }: { y: number }) => {
if (!viewportRef.current) return; if (!viewportRef.current) {
return;
}
const { scrollHeight, clientHeight } = viewportRef.current; const { scrollHeight, clientHeight } = viewportRef.current;
const isAtBottomNow = y + clientHeight >= scrollHeight - 10; const isAtBottomNow = y + clientHeight >= scrollHeight - 10;
@ -83,14 +86,14 @@ export const TerminalTab = forwardRef<TerminalTabRef>((_props, ref) => {
return ( return (
<Box <Box
style={{ style={{
height: `calc(100vh - ${TITLEBAR_HEIGHT} - ${STATUSBAR_HEIGHT})`,
display: 'flex',
flexDirection: 'column',
backgroundColor: backgroundColor:
colorScheme === 'dark' colorScheme === 'dark'
? 'var(--mantine-color-dark-filled)' ? 'var(--mantine-color-dark-filled)'
: 'var(--mantine-color-gray-0)', : 'var(--mantine-color-gray-0)',
borderRadius: 'inherit', borderRadius: 'inherit',
display: 'flex',
flexDirection: 'column',
height: `calc(100vh - ${TITLEBAR_HEIGHT} - ${STATUSBAR_HEIGHT})`,
position: 'relative', position: 'relative',
}} }}
> >
@ -110,19 +113,19 @@ export const TerminalTab = forwardRef<TerminalTabRef>((_props, ref) => {
<Box p="md"> <Box p="md">
<div <div
style={{ style={{
margin: 0,
fontFamily:
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
fontSize: '0.875em',
lineHeight: 1.4,
color: color:
colorScheme === 'dark' colorScheme === 'dark'
? 'var(--mantine-color-gray-0)' ? 'var(--mantine-color-gray-0)'
: 'var(--mantine-color-dark-filled)', : 'var(--mantine-color-dark-filled)',
whiteSpace: 'pre-wrap', fontFamily:
wordBreak: 'break-word', 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
fontSize: '0.875em',
lineHeight: 1.4,
margin: 0,
opacity: isVisible ? 1 : 0, opacity: isVisible ? 1 : 0,
transition: 'opacity 0.2s ease-in-out', transition: 'opacity 0.2s ease-in-out',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}} }}
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: processTerminalContent(terminalContent), __html: processTerminalContent(terminalContent),
@ -139,11 +142,11 @@ export const TerminalTab = forwardRef<TerminalTabRef>((_props, ref) => {
radius="xl" radius="xl"
onClick={scrollToBottom} onClick={scrollToBottom}
style={{ style={{
position: 'absolute',
bottom: '1.25rem', bottom: '1.25rem',
boxShadow: '0 0.125rem 0.5rem rgba(0, 0, 0, 0.3)',
position: 'absolute',
right: '1.25rem', right: '1.25rem',
zIndex: 10, zIndex: 10,
boxShadow: '0 0.125rem 0.5rem rgba(0, 0, 0, 0.3)',
}} }}
aria-label="Scroll to bottom" aria-label="Scroll to bottom"
> >

View file

@ -1,6 +1,8 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { ServerTab } from '@/components/screens/Interface/ServerTab'; import { ServerTab } from '@/components/screens/Interface/ServerTab';
import { TerminalTab, type TerminalTabRef } from '@/components/screens/Interface/TerminalTab'; import { TerminalTab } from '@/components/screens/Interface/TerminalTab';
import type { TerminalTabRef } from '@/components/screens/Interface/TerminalTab';
import type { InterfaceTab } from '@/types'; import type { InterfaceTab } from '@/types';
interface InterfaceScreenProps { interface InterfaceScreenProps {
@ -20,24 +22,24 @@ export const InterfaceScreen = ({ activeTab, isServerReady }: InterfaceScreenPro
return ( return (
<div <div
style={{ style={{
height: '100%',
width: '100%',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
height: '100%',
width: '100%',
}} }}
> >
<div <div
style={{ style={{
flex: 1,
display: activeTab === 'chat-text' || activeTab === 'chat-image' ? 'block' : 'none', display: activeTab === 'chat-text' || activeTab === 'chat-image' ? 'block' : 'none',
flex: 1,
}} }}
> >
<ServerTab isServerReady={isServerReady} activeTab={activeTab || undefined} /> <ServerTab isServerReady={isServerReady} activeTab={activeTab ?? undefined} />
</div> </div>
<div <div
style={{ style={{
flex: 1,
display: activeTab === 'terminal' ? 'block' : 'none', display: activeTab === 'terminal' ? 'block' : 'none',
flex: 1,
}} }}
> >
<TerminalTab ref={terminalTabRef} /> <TerminalTab ref={terminalTabRef} />

View file

@ -1,6 +1,7 @@
import { ActionIcon, Button, Group, SimpleGrid, Stack, Text, TextInput } from '@mantine/core'; import { ActionIcon, Button, Group, SimpleGrid, Stack, Text, TextInput } from '@mantine/core';
import { Plus, Trash2 } from 'lucide-react'; import { Plus, Trash2 } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip'; import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
import { CommandLineArgumentsModal } from '@/components/screens/Launch/CommandLineArgumentsModal'; import { CommandLineArgumentsModal } from '@/components/screens/Launch/CommandLineArgumentsModal';
@ -38,11 +39,11 @@ export const AdvancedTab = () => {
if (support) { if (support) {
setBackendSupport({ setBackendSupport({
noavx2: support.noavx2,
failsafe: support.failsafe, failsafe: support.failsafe,
noavx2: support.noavx2,
}); });
} else { } else {
setBackendSupport({ noavx2: false, failsafe: false }); setBackendSupport({ failsafe: false, noavx2: false });
} }
setIsLoading(false); setIsLoading(false);

View file

@ -1,6 +1,7 @@
import { Accordion, Badge, Button, Code, Group, Stack, Text, TextInput } from '@mantine/core'; import { Accordion, Badge, Button, Code, Group, Stack, Text, TextInput } from '@mantine/core';
import { Code as CodeIcon } from 'lucide-react'; import { Code as CodeIcon } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import { Modal } from '@/components/Modal'; import { Modal } from '@/components/Modal';
interface CommandLineArgumentsModalProps { interface CommandLineArgumentsModalProps {
@ -95,684 +96,684 @@ const IGNORED_ARGS = new Set([
const COMMAND_LINE_ARGUMENTS = [ const COMMAND_LINE_ARGUMENTS = [
{ {
flag: '--threads',
aliases: ['-t'], aliases: ['-t'],
category: 'Basic',
description: description:
'Use a custom number of threads if specified. Otherwise, uses an amount based on CPU cores', 'Use a custom number of threads if specified. Otherwise, uses an amount based on CPU cores',
flag: '--threads',
metavar: '[threads]', metavar: '[threads]',
type: 'int', type: 'int',
category: 'Basic',
}, },
{ {
flag: '--analyze',
description: 'Reads the metadata, weight types and tensor names in any GGUF file.',
metavar: '[filename]',
default: '',
category: 'Advanced', category: 'Advanced',
default: '',
description: 'Reads the metadata, weight types and tensor names in any GGUF file.',
flag: '--analyze',
metavar: '[filename]',
}, },
{ {
flag: '--maingpu',
aliases: ['--main-gpu', '-mg'], aliases: ['--main-gpu', '-mg'],
category: 'Advanced',
default: -1,
description: description:
'Only used in a multi-gpu setup. Sets the index of the main GPU that will be used.', 'Only used in a multi-gpu setup. Sets the index of the main GPU that will be used.',
flag: '--maingpu',
metavar: '[Device ID]', metavar: '[Device ID]',
type: 'int', type: 'int',
default: -1,
category: 'Advanced',
}, },
{ {
flag: '--batchsize',
aliases: ['--blasbatchsize', '--batch-size', '-b'], aliases: ['--blasbatchsize', '--batch-size', '-b'],
description: category: 'Advanced',
'Sets the batch size used in batched processing (default 512). Setting it to -1 disables batched mode, but keeps other benefits like GPU offload.',
type: 'int',
choices: ['-1', '16', '32', '64', '128', '256', '512', '1024', '2048', '4096'], choices: ['-1', '16', '32', '64', '128', '256', '512', '1024', '2048', '4096'],
default: 512, default: 512,
category: 'Advanced', description:
'Sets the batch size used in batched processing (default 512). Setting it to -1 disables batched mode, but keeps other benefits like GPU offload.',
flag: '--batchsize',
type: 'int',
}, },
{ {
flag: '--blasthreads',
aliases: ['--batchthreads', '--threadsbatch', '--threads-batch'], aliases: ['--batchthreads', '--threadsbatch', '--threads-batch'],
category: 'Advanced',
default: 0,
description: description:
'Use a different number of threads during batching if specified. Otherwise, has the same value as --threads', 'Use a different number of threads during batching if specified. Otherwise, has the same value as --threads',
flag: '--blasthreads',
metavar: '[threads]', metavar: '[threads]',
type: 'int', type: 'int',
default: 0,
category: 'Advanced',
}, },
{ {
flag: '--lora', category: 'Advanced',
description: 'GGUF models only, applies a lora file on top of model.', description: 'GGUF models only, applies a lora file on top of model.',
flag: '--lora',
metavar: '[lora_filename]', metavar: '[lora_filename]',
type: 'string[]', type: 'string[]',
category: 'Advanced',
}, },
{ {
flag: '--loramult', category: 'Advanced',
default: 1,
description: 'Multiplier for the Text LORA model to be applied.', description: 'Multiplier for the Text LORA model to be applied.',
flag: '--loramult',
metavar: '[amount]', metavar: '[amount]',
type: 'float', type: 'float',
default: 1.0,
category: 'Advanced',
}, },
{ {
flag: '--nofastforward', category: 'Advanced',
description: description:
'If set, do not attempt to fast forward GGUF context (always reprocess). Will also enable noshift', 'If set, do not attempt to fast forward GGUF context (always reprocess). Will also enable noshift',
flag: '--nofastforward',
type: 'boolean', type: 'boolean',
category: 'Advanced',
}, },
{ {
flag: '--useswa', category: 'Advanced',
description: description:
'If set, allows Sliding Window Attention (SWA) KV Cache, which saves memory but cannot be used with context shifting.', 'If set, allows Sliding Window Attention (SWA) KV Cache, which saves memory but cannot be used with context shifting.',
flag: '--useswa',
type: 'boolean', type: 'boolean',
category: 'Advanced',
}, },
{ {
flag: '--ropeconfig', category: 'Advanced',
default: '0.0 10000.0',
description: description:
'If set, uses customized RoPE scaling from configured frequency scale and frequency base (e.g. --ropeconfig 0.25 10000). Otherwise, uses NTK-Aware scaling set automatically based on context size. For linear rope, simply set the freq-scale and ignore the freq-base', 'If set, uses customized RoPE scaling from configured frequency scale and frequency base (e.g. --ropeconfig 0.25 10000). Otherwise, uses NTK-Aware scaling set automatically based on context size. For linear rope, simply set the freq-scale and ignore the freq-base',
flag: '--ropeconfig',
metavar: '[rope-freq-scale] [rope-freq-base]', metavar: '[rope-freq-scale] [rope-freq-base]',
default: '0.0 10000.0',
type: 'float[]', type: 'float[]',
category: 'Advanced',
}, },
{ {
flag: '--overridenativecontext', category: 'Advanced',
default: 0,
description: description:
'Overrides the native trained context of the loaded model with a custom value to be used for Rope scaling.', 'Overrides the native trained context of the loaded model with a custom value to be used for Rope scaling.',
flag: '--overridenativecontext',
metavar: '[trained context]', metavar: '[trained context]',
type: 'int', type: 'int',
default: 0,
category: 'Advanced',
}, },
{ {
flag: '--usemlock',
aliases: ['--mlock'], aliases: ['--mlock'],
category: 'Advanced',
description: description:
'Enables mlock, preventing the RAM used to load the model from being paged out. Not usually recommended.', 'Enables mlock, preventing the RAM used to load the model from being paged out. Not usually recommended.',
flag: '--usemlock',
type: 'boolean', type: 'boolean',
category: 'Advanced',
}, },
{ {
flag: '--onready', category: 'Advanced',
default: '',
description: 'An optional shell command to execute after the model has been loaded.', description: 'An optional shell command to execute after the model has been loaded.',
flag: '--onready',
metavar: '[shell command]', metavar: '[shell command]',
type: 'string', type: 'string',
default: '',
category: 'Advanced',
}, },
{ {
flag: '--benchmark', category: 'Advanced',
default: 'stdout',
description: description:
'Do not start server, instead run benchmarks. If filename is provided, appends results to provided file.', 'Do not start server, instead run benchmarks. If filename is provided, appends results to provided file.',
flag: '--benchmark',
metavar: '[filename]', metavar: '[filename]',
type: 'string', type: 'string',
default: 'stdout',
category: 'Advanced',
}, },
{ {
flag: '--prompt',
aliases: ['-p'], aliases: ['-p'],
category: 'Advanced',
default: '',
description: description:
'Passing a prompt string triggers a direct inference, loading the model, outputs the response to stdout and exits. Can be used alone or with benchmark.', 'Passing a prompt string triggers a direct inference, loading the model, outputs the response to stdout and exits. Can be used alone or with benchmark.',
flag: '--prompt',
metavar: '[prompt]', metavar: '[prompt]',
type: 'string', type: 'string',
default: '',
category: 'Advanced',
}, },
{ {
flag: '--cli', category: 'Advanced',
description: description:
'Does not launch KoboldCpp HTTP server. Instead, enables KoboldCpp from the command line, accepting interactive console input and displaying responses to the terminal.', 'Does not launch KoboldCpp HTTP server. Instead, enables KoboldCpp from the command line, accepting interactive console input and displaying responses to the terminal.',
flag: '--cli',
type: 'boolean', type: 'boolean',
category: 'Advanced',
}, },
{ {
flag: '--genlimit',
aliases: ['--promptlimit'], aliases: ['--promptlimit'],
category: 'Advanced',
default: 0,
description: description:
'Sets the maximum number of generated tokens, it will restrict all generations to this or lower. Also usable with --prompt or --benchmark.', 'Sets the maximum number of generated tokens, it will restrict all generations to this or lower. Also usable with --prompt or --benchmark.',
flag: '--genlimit',
metavar: '[token limit]', metavar: '[token limit]',
type: 'int', type: 'int',
default: 0,
category: 'Advanced',
}, },
{ {
flag: '--highpriority', category: 'Advanced',
description: description:
'Experimental flag. If set, increases the process CPU priority, potentially speeding up generation. Use caution.', 'Experimental flag. If set, increases the process CPU priority, potentially speeding up generation. Use caution.',
flag: '--highpriority',
type: 'boolean', type: 'boolean',
category: 'Advanced',
}, },
{ {
flag: '--foreground', category: 'Advanced',
description: description:
'Windows only. Sends the terminal to the foreground every time a new prompt is generated. This helps avoid some idle slowdown issues.', 'Windows only. Sends the terminal to the foreground every time a new prompt is generated. This helps avoid some idle slowdown issues.',
flag: '--foreground',
type: 'boolean', type: 'boolean',
category: 'Advanced',
}, },
{ {
flag: '--preloadstory', category: 'Advanced',
default: '',
description: description:
'Configures a prepared story json save file to be hosted on the server, which frontends (such as KoboldAI Lite) can access over the API.', 'Configures a prepared story json save file to be hosted on the server, which frontends (such as KoboldAI Lite) can access over the API.',
flag: '--preloadstory',
metavar: '[savefile]', metavar: '[savefile]',
default: '',
category: 'Advanced',
}, },
{ {
flag: '--savedatafile', category: 'Advanced',
default: '',
description: description:
'If enabled, creates or opens a persistent database file on the server, that allows users to save and load their data remotely. A new file is created if it does not exist.', 'If enabled, creates or opens a persistent database file on the server, that allows users to save and load their data remotely. A new file is created if it does not exist.',
flag: '--savedatafile',
metavar: '[savefile]', metavar: '[savefile]',
default: '',
category: 'Advanced',
}, },
{ {
flag: '--quiet', category: 'Advanced',
description: description:
'Enable quiet mode, which hides generation inputs and outputs in the terminal. Quiet mode is automatically enabled when running a horde worker.', 'Enable quiet mode, which hides generation inputs and outputs in the terminal. Quiet mode is automatically enabled when running a horde worker.',
flag: '--quiet',
type: 'boolean', type: 'boolean',
category: 'Advanced',
}, },
{ {
flag: '--ssl', category: 'Advanced',
description: description:
'Allows all content to be served over SSL instead. A valid UNENCRYPTED SSL cert and key .pem files must be provided', 'Allows all content to be served over SSL instead. A valid UNENCRYPTED SSL cert and key .pem files must be provided',
flag: '--ssl',
metavar: '[cert_pem] [key_pem]', metavar: '[cert_pem] [key_pem]',
type: 'string[]', type: 'string[]',
category: 'Advanced',
}, },
{ {
flag: '--password', category: 'Advanced',
default: 'None',
description: description:
'Enter a password required to use this instance. This key will be required for all text endpoints. Image endpoints are not secured.', 'Enter a password required to use this instance. This key will be required for all text endpoints. Image endpoints are not secured.',
flag: '--password',
metavar: '[API key]', metavar: '[API key]',
default: 'None',
category: 'Advanced',
}, },
{ {
flag: '--mmproj', category: 'Multimodal',
description: 'Select a multimodal projector file for vision models like LLaVA.',
metavar: '[filename]',
default: '', default: '',
category: 'Multimodal', description: 'Select a multimodal projector file for vision models like LLaVA.',
flag: '--mmproj',
metavar: '[filename]',
}, },
{ {
flag: '--mmprojcpu',
aliases: ['--no-mmproj-offload'], aliases: ['--no-mmproj-offload'],
description: 'Force CLIP for Vision mmproj always on CPU.',
type: 'boolean',
category: 'Multimodal', category: 'Multimodal',
description: 'Force CLIP for Vision mmproj always on CPU.',
flag: '--mmprojcpu',
type: 'boolean',
}, },
{ {
flag: '--visionmaxres', category: 'Multimodal',
default: 1024,
description: description:
'Clamp MMProj vision maximum allowed resolution. Allowed values are between 512 to 2048 px (default 1024).', 'Clamp MMProj vision maximum allowed resolution. Allowed values are between 512 to 2048 px (default 1024).',
flag: '--visionmaxres',
metavar: '[max px]', metavar: '[max px]',
type: 'int', type: 'int',
default: 1024,
category: 'Multimodal',
}, },
{ {
flag: '--draftmodel',
aliases: ['--model-draft', '-md'], aliases: ['--model-draft', '-md'],
category: 'Speculative Decoding',
default: '',
description: description:
'Load a small draft model for speculative decoding. It will be fully offloaded. Vocab must match the main model.', 'Load a small draft model for speculative decoding. It will be fully offloaded. Vocab must match the main model.',
flag: '--draftmodel',
metavar: '[filename]', metavar: '[filename]',
default: '',
category: 'Speculative Decoding',
}, },
{ {
flag: '--draftamount',
aliases: ['--draft-max', '--draft-n'], aliases: ['--draft-max', '--draft-n'],
category: 'Speculative Decoding',
default: 8,
description: 'How many tokens to draft per chunk before verifying results', description: 'How many tokens to draft per chunk before verifying results',
flag: '--draftamount',
metavar: '[tokens]', metavar: '[tokens]',
type: 'int', type: 'int',
default: 8,
category: 'Speculative Decoding',
}, },
{ {
flag: '--draftgpulayers',
aliases: ['--gpu-layers-draft', '--n-gpu-layers-draft', '-ngld'], aliases: ['--gpu-layers-draft', '--n-gpu-layers-draft', '-ngld'],
category: 'Speculative Decoding',
default: 999,
description: 'How many layers to offload to GPU for the draft model (default=full offload)', description: 'How many layers to offload to GPU for the draft model (default=full offload)',
flag: '--draftgpulayers',
metavar: '[layers]', metavar: '[layers]',
type: 'int', type: 'int',
default: 999,
category: 'Speculative Decoding',
}, },
{ {
flag: '--draftgpusplit', category: 'Speculative Decoding',
description: description:
'GPU layer distribution ratio for draft model (default=same as main). Only works if multi-GPUs selected for MAIN model and tensor_split is set!', 'GPU layer distribution ratio for draft model (default=same as main). Only works if multi-GPUs selected for MAIN model and tensor_split is set!',
flag: '--draftgpusplit',
metavar: '[Ratios]', metavar: '[Ratios]',
type: 'float[]', type: 'float[]',
category: 'Speculative Decoding',
}, },
{ {
flag: '--defaultgenamt', category: 'Performance',
default: 896,
description: description:
'How many tokens to generate by default, if not specified. Must be smaller than context size. Usually, your frontend GUI will override this.', 'How many tokens to generate by default, if not specified. Must be smaller than context size. Usually, your frontend GUI will override this.',
flag: '--defaultgenamt',
type: 'int', type: 'int',
default: 896,
category: 'Performance',
}, },
{ {
flag: '--nobostoken', category: 'Performance',
description: description:
'Prevents BOS token from being added at the start of any prompt. Usually NOT recommended for most models.', 'Prevents BOS token from being added at the start of any prompt. Usually NOT recommended for most models.',
flag: '--nobostoken',
type: 'boolean', type: 'boolean',
category: 'Performance',
}, },
{ {
flag: '--enableguidance', category: 'Performance',
description: description:
'Enables the use of Classifier-Free-Guidance, which allows the use of negative prompts. Has performance and memory impact.', 'Enables the use of Classifier-Free-Guidance, which allows the use of negative prompts. Has performance and memory impact.',
flag: '--enableguidance',
type: 'boolean', type: 'boolean',
category: 'Performance',
}, },
{ {
flag: '--ratelimit', category: 'Advanced',
default: 0,
description: description:
'If enabled, rate limit generative request by IP address. Each IP can only send a new request once per X seconds.', 'If enabled, rate limit generative request by IP address. Each IP can only send a new request once per X seconds.',
flag: '--ratelimit',
metavar: '[seconds]', metavar: '[seconds]',
type: 'int', type: 'int',
default: 0,
category: 'Advanced',
}, },
{ {
flag: '--ignoremissing', category: 'Advanced',
description: 'Ignores all missing non-essential files, just skipping them instead.', description: 'Ignores all missing non-essential files, just skipping them instead.',
flag: '--ignoremissing',
type: 'boolean', type: 'boolean',
category: 'Advanced',
}, },
{ {
flag: '--chatcompletionsadapter', category: 'Advanced',
default: 'AutoGuess',
description: description:
'Select an optional ChatCompletions Adapter JSON file to force custom instruct tags.', 'Select an optional ChatCompletions Adapter JSON file to force custom instruct tags.',
flag: '--chatcompletionsadapter',
metavar: '[filename]', metavar: '[filename]',
default: 'AutoGuess',
category: 'Advanced',
}, },
{ {
flag: '--jinja', category: 'Advanced',
description: description:
'Enables using jinja chat template formatting for chat completions endpoint. Other endpoints are unaffected. Tool calls are done without jinja.', 'Enables using jinja chat template formatting for chat completions endpoint. Other endpoints are unaffected. Tool calls are done without jinja.',
flag: '--jinja',
type: 'boolean', type: 'boolean',
category: 'Advanced',
}, },
{ {
flag: '--jinja_tools',
aliases: ['--jinja-tools', '--jinjatools'], aliases: ['--jinja-tools', '--jinjatools'],
category: 'Advanced',
description: description:
'Enables using jinja chat template formatting for chat completions endpoint. Other endpoints are unaffected. Tool calls are done with jinja.', 'Enables using jinja chat template formatting for chat completions endpoint. Other endpoints are unaffected. Tool calls are done with jinja.',
flag: '--jinja_tools',
type: 'boolean', type: 'boolean',
category: 'Advanced',
}, },
{ {
flag: '--smartcontext', category: 'Advanced',
description: description:
'Reserving a portion of context to try processing less frequently. Outdated. Not recommended.', 'Reserving a portion of context to try processing less frequently. Outdated. Not recommended.',
flag: '--smartcontext',
type: 'boolean', type: 'boolean',
category: 'Advanced',
}, },
{ {
flag: '--unpack', category: 'Advanced',
default: '',
description: 'Extracts the file contents of the KoboldCpp binary into a target directory.', description: 'Extracts the file contents of the KoboldCpp binary into a target directory.',
flag: '--unpack',
metavar: 'destination', metavar: 'destination',
type: 'string', type: 'string',
default: '',
category: 'Advanced',
}, },
{ {
flag: '--exportconfig', category: 'Advanced',
default: '',
description: 'Exports the current selected arguments as a .kcpps settings file', description: 'Exports the current selected arguments as a .kcpps settings file',
flag: '--exportconfig',
metavar: '[filename]', metavar: '[filename]',
type: 'string', type: 'string',
default: '',
category: 'Advanced',
}, },
{ {
flag: '--exporttemplate', category: 'Advanced',
default: '',
description: 'Exports the current selected arguments as a .kcppt template file', description: 'Exports the current selected arguments as a .kcppt template file',
flag: '--exporttemplate',
metavar: '[filename]', metavar: '[filename]',
type: 'string', type: 'string',
default: '',
category: 'Advanced',
}, },
{ {
flag: '--nomodel', category: 'Advanced',
description: 'Allows you to launch the GUI alone, without selecting any model.', description: 'Allows you to launch the GUI alone, without selecting any model.',
flag: '--nomodel',
type: 'boolean', type: 'boolean',
category: 'Advanced',
}, },
{ {
flag: '--singleinstance', category: 'Advanced',
description: description:
'Allows this KoboldCpp instance to be shut down by any new instance requesting the same port, preventing duplicate servers from clashing on a port.', 'Allows this KoboldCpp instance to be shut down by any new instance requesting the same port, preventing duplicate servers from clashing on a port.',
flag: '--singleinstance',
type: 'boolean', type: 'boolean',
category: 'Advanced',
}, },
{ {
flag: '--gendefaults', category: 'Advanced',
description: 'Sets extra default parameters for some fields in API requests, as a JSON string.',
metavar: '{"parameter":"value",...}',
default: '', default: '',
category: 'Advanced', description: 'Sets extra default parameters for some fields in API requests, as a JSON string.',
flag: '--gendefaults',
metavar: '{"parameter":"value",...}',
}, },
{ {
flag: '--gendefaultsoverwrite', category: 'Advanced',
description: description:
'Allow the gendefaults parameters to overwrite the original value in API payloads.', 'Allow the gendefaults parameters to overwrite the original value in API payloads.',
flag: '--gendefaultsoverwrite',
type: 'boolean', type: 'boolean',
category: 'Advanced',
}, },
{ {
flag: '--maxrequestsize', category: 'Advanced',
default: 32,
description: description:
'Specify a max request payload size. Any requests to the server larger than this size will be dropped. Do not change if unsure.', 'Specify a max request payload size. Any requests to the server larger than this size will be dropped. Do not change if unsure.',
flag: '--maxrequestsize',
metavar: '[size in MB]', metavar: '[size in MB]',
type: 'int', type: 'int',
default: 32,
category: 'Advanced',
}, },
{ {
flag: '--overridekv',
aliases: ['--override-kv'], aliases: ['--override-kv'],
category: 'Advanced',
default: '',
description: description:
'Override metadata value by key. Separate multiple values with commas. Format is name=type:value. Types: int, float, bool, str', 'Override metadata value by key. Separate multiple values with commas. Format is name=type:value. Types: int, float, bool, str',
flag: '--overridekv',
metavar: '[name=type:value]', metavar: '[name=type:value]',
default: '',
category: 'Advanced',
}, },
{ {
flag: '--overridetensors',
aliases: ['--override-tensor', '-ot'], aliases: ['--override-tensor', '-ot'],
category: 'Advanced',
default: '',
description: description:
'Override selected backend for specific tensors matching tensor_name_regex_pattern=buffer_type, same as in llama.cpp.', 'Override selected backend for specific tensors matching tensor_name_regex_pattern=buffer_type, same as in llama.cpp.',
flag: '--overridetensors',
metavar: '[tensor name pattern=buffer type]', metavar: '[tensor name pattern=buffer type]',
default: '',
category: 'Advanced',
}, },
{ {
flag: '--admin', category: 'Administration',
description: description:
'Enables admin mode, allowing you to unload and reload different configurations or models.', 'Enables admin mode, allowing you to unload and reload different configurations or models.',
flag: '--admin',
type: 'boolean', type: 'boolean',
category: 'Administration',
}, },
{ {
flag: '--adminpassword', category: 'Administration',
default: 'None',
description: description:
'Require a password to access admin functions. You are strongly advised to use one for publically accessible instances!', 'Require a password to access admin functions. You are strongly advised to use one for publically accessible instances!',
flag: '--adminpassword',
metavar: '[password]', metavar: '[password]',
default: 'None',
category: 'Administration',
}, },
{ {
flag: '--admindir', category: 'Administration',
default: '',
description: description:
'Specify a directory to look for .kcpps configs in, which can be used to swap models.', 'Specify a directory to look for .kcpps configs in, which can be used to swap models.',
flag: '--admindir',
metavar: '[directory]', metavar: '[directory]',
default: '',
category: 'Administration',
}, },
{ {
flag: '--hordemodelname', category: 'Horde Worker',
default: '',
description: 'Sets your AI Horde display model name.', description: 'Sets your AI Horde display model name.',
flag: '--hordemodelname',
metavar: '[name]', metavar: '[name]',
default: '',
category: 'Horde Worker',
}, },
{ {
flag: '--hordeworkername', category: 'Horde Worker',
default: '',
description: 'Sets your AI Horde worker name.', description: 'Sets your AI Horde worker name.',
flag: '--hordeworkername',
metavar: '[name]', metavar: '[name]',
default: '',
category: 'Horde Worker',
}, },
{ {
flag: '--hordekey', category: 'Horde Worker',
default: '',
description: 'Sets your AI Horde API key.', description: 'Sets your AI Horde API key.',
flag: '--hordekey',
metavar: '[apikey]', metavar: '[apikey]',
default: '',
category: 'Horde Worker',
}, },
{ {
flag: '--hordemaxctx', category: 'Horde Worker',
default: 0,
description: description:
'Sets the maximum context length your worker will accept from an AI Horde job. If 0, matches main context limit.', 'Sets the maximum context length your worker will accept from an AI Horde job. If 0, matches main context limit.',
flag: '--hordemaxctx',
metavar: '[amount]', metavar: '[amount]',
type: 'int', type: 'int',
default: 0,
category: 'Horde Worker',
}, },
{ {
flag: '--hordegenlen', category: 'Horde Worker',
default: 0,
description: description:
'Sets the maximum number of tokens your worker will generate from an AI horde job.', 'Sets the maximum number of tokens your worker will generate from an AI horde job.',
flag: '--hordegenlen',
metavar: '[amount]', metavar: '[amount]',
type: 'int', type: 'int',
default: 0,
category: 'Horde Worker',
}, },
{ {
flag: '--sdthreads', category: 'Image Generation',
default: 0,
description: description:
'Use a different number of threads for image generation if specified. Otherwise, has the same value as --threads.', 'Use a different number of threads for image generation if specified. Otherwise, has the same value as --threads.',
flag: '--sdthreads',
metavar: '[threads]', metavar: '[threads]',
type: 'int', type: 'int',
default: 0,
category: 'Image Generation',
}, },
{ {
flag: '--sdquant', category: 'Image Generation',
description: 'If specified, loads the model quantized to save memory. 0=off, 1=q8, 2=q4',
metavar: '[quantization level 0/1/2]',
type: 'int',
choices: ['0', '1', '2'], choices: ['0', '1', '2'],
default: 0, default: 0,
category: 'Image Generation', description: 'If specified, loads the model quantized to save memory. 0=off, 1=q8, 2=q4',
flag: '--sdquant',
metavar: '[quantization level 0/1/2]',
type: 'int',
}, },
{ {
flag: '--sdclamped', category: 'Image Generation',
default: 0,
description: description:
'If specified, limit generation steps and image size for shared use. Accepts an extra optional parameter that indicates maximum resolution (eg. 768 clamps to 768x768, min 512px, disabled if 0).', 'If specified, limit generation steps and image size for shared use. Accepts an extra optional parameter that indicates maximum resolution (eg. 768 clamps to 768x768, min 512px, disabled if 0).',
flag: '--sdclamped',
metavar: '[maxres]', metavar: '[maxres]',
type: 'int', type: 'int',
default: 0,
category: 'Image Generation',
}, },
{ {
flag: '--sdclampedsoft', category: 'Image Generation',
default: 0,
description: description:
'If specified, limit max image size to curb memory usage. Similar to --sdclamped, but less strict, allows trade-offs between width and height (e.g. 640 would allow 640x640, 512x768 and 768x512 images). Total resolution cannot exceed 1MP.', 'If specified, limit max image size to curb memory usage. Similar to --sdclamped, but less strict, allows trade-offs between width and height (e.g. 640 would allow 640x640, 512x768 and 768x512 images). Total resolution cannot exceed 1MP.',
flag: '--sdclampedsoft',
metavar: '[maxres]', metavar: '[maxres]',
type: 'int', type: 'int',
default: 0,
category: 'Image Generation',
}, },
{ {
flag: '--sdvaeauto', category: 'Image Generation',
description: 'Uses a built-in VAE via TAE SD, which is very fast, and fixed bad VAEs.', description: 'Uses a built-in VAE via TAE SD, which is very fast, and fixed bad VAEs.',
flag: '--sdvaeauto',
type: 'boolean', type: 'boolean',
category: 'Image Generation',
}, },
{ {
flag: '--sdupscaler', category: 'Image Generation',
default: '',
description: description:
'You can use ESRGAN as an upscaling model to resize images. Leave blank if unused.', 'You can use ESRGAN as an upscaling model to resize images. Leave blank if unused.',
flag: '--sdupscaler',
metavar: '[filename]', metavar: '[filename]',
default: '',
category: 'Image Generation',
}, },
{ {
flag: '--sdloramult', category: 'Image Generation',
default: 1,
description: 'Multiplier for the image LORA model to be applied.', description: 'Multiplier for the image LORA model to be applied.',
flag: '--sdloramult',
metavar: '[amount]', metavar: '[amount]',
type: 'float', type: 'float',
default: 1.0,
category: 'Image Generation',
}, },
{ {
flag: '--sdtiledvae', category: 'Image Generation',
default: 768,
description: description:
'Adjust the automatic VAE tiling trigger for images above this size. 0 disables vae tiling.', 'Adjust the automatic VAE tiling trigger for images above this size. 0 disables vae tiling.',
flag: '--sdtiledvae',
metavar: '[maxres]', metavar: '[maxres]',
type: 'int', type: 'int',
default: 768,
category: 'Image Generation',
}, },
{ {
flag: '--sdoffloadcpu', category: 'Image Generation',
description: 'Offload image weights in RAM to save VRAM, swap into VRAM when needed.', description: 'Offload image weights in RAM to save VRAM, swap into VRAM when needed.',
flag: '--sdoffloadcpu',
type: 'boolean', type: 'boolean',
category: 'Image Generation',
}, },
{ {
flag: '--whispermodel', category: 'Audio',
default: '',
description: 'Specify a Whisper .bin model to enable Speech-To-Text transcription.', description: 'Specify a Whisper .bin model to enable Speech-To-Text transcription.',
flag: '--whispermodel',
metavar: '[filename]', metavar: '[filename]',
default: '',
category: 'Audio',
}, },
{ {
flag: '--ttsmodel', category: 'Audio',
default: '',
description: 'Specify the TTS Text-To-Speech GGUF model.', description: 'Specify the TTS Text-To-Speech GGUF model.',
flag: '--ttsmodel',
metavar: '[filename]', metavar: '[filename]',
default: '',
category: 'Audio',
}, },
{ {
flag: '--ttsgpu', category: 'Audio',
description: 'Use the GPU for TTS.', description: 'Use the GPU for TTS.',
flag: '--ttsgpu',
type: 'boolean', type: 'boolean',
category: 'Audio',
}, },
{ {
flag: '--ttswavtokenizer', category: 'Audio',
default: '',
description: 'Specify the WavTokenizer GGUF model.', description: 'Specify the WavTokenizer GGUF model.',
flag: '--ttswavtokenizer',
metavar: '[filename]', metavar: '[filename]',
default: '',
category: 'Audio',
}, },
{ {
flag: '--ttsdir', category: 'Audio',
default: '',
description: 'Select directory containing voices for voice cloning.', description: 'Select directory containing voices for voice cloning.',
flag: '--ttsdir',
metavar: '[directory]', metavar: '[directory]',
default: '',
category: 'Audio',
}, },
{ {
flag: '--ttsmaxlen', category: 'Audio',
description: 'Limit number of audio tokens generated with TTS.',
type: 'int',
default: 4096, default: 4096,
category: 'Audio', description: 'Limit number of audio tokens generated with TTS.',
flag: '--ttsmaxlen',
type: 'int',
}, },
{ {
flag: '--ttsthreads', category: 'Audio',
default: 0,
description: description:
'Use a different number of threads for TTS if specified. Otherwise, has the same value as --threads.', 'Use a different number of threads for TTS if specified. Otherwise, has the same value as --threads.',
flag: '--ttsthreads',
metavar: '[threads]', metavar: '[threads]',
type: 'int', type: 'int',
default: 0,
category: 'Audio',
}, },
{ {
flag: '--embeddingsmodel', category: 'Embeddings',
description: 'Specify an embeddings model to be loaded for generating embedding vectors.',
metavar: '[filename]',
default: '', default: '',
category: 'Embeddings', description: 'Specify an embeddings model to be loaded for generating embedding vectors.',
flag: '--embeddingsmodel',
metavar: '[filename]',
}, },
{ {
flag: '--embeddingsgpu', category: 'Embeddings',
description: 'Attempts to offload layers of the embeddings model to GPU. Usually not needed.', description: 'Attempts to offload layers of the embeddings model to GPU. Usually not needed.',
flag: '--embeddingsgpu',
type: 'boolean', type: 'boolean',
category: 'Embeddings',
}, },
{ {
flag: '--embeddingsmaxctx', category: 'Embeddings',
default: 0,
description: description:
'Overrides the default maximum supported context of an embeddings model (defaults to trained context).', 'Overrides the default maximum supported context of an embeddings model (defaults to trained context).',
flag: '--embeddingsmaxctx',
metavar: '[amount]', metavar: '[amount]',
type: 'int', type: 'int',
default: 0,
category: 'Embeddings',
}, },
{ {
flag: '--musicllm', category: 'Music Generation',
default: '',
description: 'Select music LLM model (e.g acestep-5Hz-lm-0.6B)', description: 'Select music LLM model (e.g acestep-5Hz-lm-0.6B)',
flag: '--musicllm',
metavar: '[filename]', metavar: '[filename]',
default: '',
category: 'Music Generation',
}, },
{ {
flag: '--musicembeddings', category: 'Music Generation',
default: '',
description: 'Select music embedding model (e.g Qwen3-Embedding-0.6B)', description: 'Select music embedding model (e.g Qwen3-Embedding-0.6B)',
flag: '--musicembeddings',
metavar: '[filename]', metavar: '[filename]',
default: '',
category: 'Music Generation',
}, },
{ {
flag: '--musicdiffusion', category: 'Music Generation',
default: '',
description: 'Select music diffusion (DiT) model (e.g acestep-v15-turbo)', description: 'Select music diffusion (DiT) model (e.g acestep-v15-turbo)',
flag: '--musicdiffusion',
metavar: '[filename]', metavar: '[filename]',
default: '',
category: 'Music Generation',
}, },
{ {
flag: '--musicvae', category: 'Music Generation',
default: '',
description: 'Select music VAE model', description: 'Select music VAE model',
flag: '--musicvae',
metavar: '[filename]', metavar: '[filename]',
default: '',
category: 'Music Generation',
}, },
{ {
flag: '--musiclowvram', category: 'Music Generation',
description: 'Unload music models when not in use', description: 'Unload music models when not in use',
flag: '--musiclowvram',
type: 'boolean', type: 'boolean',
category: 'Music Generation',
}, },
{ {
flag: '--mcpfile', category: 'Advanced',
default: '',
description: description:
'Specify path to mcp.json which contains the Cladue Desktop compatible MCP server config.', 'Specify path to mcp.json which contains the Cladue Desktop compatible MCP server config.',
flag: '--mcpfile',
metavar: '[mcp json file]', metavar: '[mcp json file]',
default: '',
category: 'Advanced',
}, },
{ {
flag: '--device',
aliases: ['-dev'], aliases: ['-dev'],
category: 'Advanced',
default: '',
description: description:
'Set llama.cpp compatible device selection override. Comma separated. Overrides normal device choices.', 'Set llama.cpp compatible device selection override. Comma separated. Overrides normal device choices.',
flag: '--device',
metavar: '<dev1,dev2,..>', metavar: '<dev1,dev2,..>',
default: '',
category: 'Advanced',
}, },
{ {
flag: '--downloaddir', category: 'Advanced',
default: '',
description: description:
'Specify a directory that models will be downloaded to or searched from, if unset uses the working directory.', 'Specify a directory that models will be downloaded to or searched from, if unset uses the working directory.',
flag: '--downloaddir',
metavar: '[directory]', metavar: '[directory]',
default: '',
category: 'Advanced',
}, },
{ {
flag: '--autofitpadding', category: 'Advanced',
description: description:
"How much spare allowance in MB should autofit reserve? If it's too little, the load might fail.", "How much spare allowance in MB should autofit reserve? If it's too little, the load might fail.",
flag: '--autofitpadding',
metavar: '[padding in MB]', metavar: '[padding in MB]',
type: 'int', type: 'int',
category: 'Advanced',
}, },
] as const; ] as const;
const AVAILABLE_ARGUMENTS = COMMAND_LINE_ARGUMENTS.filter( const AVAILABLE_ARGUMENTS = COMMAND_LINE_ARGUMENTS.filter(
(arg) => !UI_COVERED_ARGS.has(arg.flag) && !IGNORED_ARGS.has(arg.flag) (arg) => !UI_COVERED_ARGS.has(arg.flag) && !IGNORED_ARGS.has(arg.flag),
); );
export const CommandLineArgumentsModal = ({ export const CommandLineArgumentsModal = ({
@ -782,7 +783,7 @@ export const CommandLineArgumentsModal = ({
}: CommandLineArgumentsModalProps) => { }: CommandLineArgumentsModalProps) => {
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const argumentsByCategory = AVAILABLE_ARGUMENTS.reduce( const argumentsByCategory = AVAILABLE_ARGUMENTS.reduce<Record<string, ArgumentInfo[]>>(
(acc, arg) => { (acc, arg) => {
if (!acc[arg.category]) { if (!acc[arg.category]) {
acc[arg.category] = []; acc[arg.category] = [];
@ -790,16 +791,17 @@ export const CommandLineArgumentsModal = ({
acc[arg.category].push(arg); acc[arg.category].push(arg);
return acc; return acc;
}, },
{} as Record<string, ArgumentInfo[]> {},
); );
const filteredCategories = Object.entries(argumentsByCategory).reduce( const filteredCategories = Object.entries(argumentsByCategory).reduce<
(acc, [category, args]) => { Record<string, ArgumentInfo[]>
>((acc, [category, args]) => {
const filteredArgs = args.filter( const filteredArgs = args.filter(
(arg) => (arg) =>
arg.flag.toLowerCase().includes(searchQuery.toLowerCase()) || arg.flag.toLowerCase().includes(searchQuery.toLowerCase()) ||
arg.description.toLowerCase().includes(searchQuery.toLowerCase()) || arg.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
arg.aliases?.some((alias) => alias.toLowerCase().includes(searchQuery.toLowerCase())) arg.aliases?.some((alias) => alias.toLowerCase().includes(searchQuery.toLowerCase())),
); );
if (filteredArgs.length > 0) { if (filteredArgs.length > 0) {
@ -807,9 +809,7 @@ export const CommandLineArgumentsModal = ({
} }
return acc; return acc;
}, }, {});
{} as Record<string, ArgumentInfo[]>
);
const handleAddArgument = (arg: ArgumentInfo) => { const handleAddArgument = (arg: ArgumentInfo) => {
if (onAddArgument) { if (onAddArgument) {
@ -896,7 +896,7 @@ export const CommandLineArgumentsModal = ({
{arg.description} {arg.description}
</Text> </Text>
{(arg.metavar || arg.default !== undefined || arg.choices) && ( {((arg.metavar ?? arg.default !== undefined) || arg.choices) && (
<Group gap="lg"> <Group gap="lg">
{arg.metavar && ( {arg.metavar && (
<Text size="xs" c="dimmed"> <Text size="xs" c="dimmed">

View file

@ -1,9 +1,11 @@
import { Button, Group, Stack, Text, Tooltip } from '@mantine/core'; import { Button, Group, Stack, Text, Tooltip } from '@mantine/core';
import { Check, File, Plus, Save, Trash2 } from 'lucide-react'; import { Check, File, Plus, Save, Trash2 } from 'lucide-react';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { Select } from '@/components/Select'; import { Select } from '@/components/Select';
import type { ConfigFile } from '@/types'; import type { ConfigFile } from '@/types';
import { stripFileExtension } from '@/utils/format'; import { stripFileExtension } from '@/utils/format';
import { CreateConfigModal } from './CreateConfigModal'; import { CreateConfigModal } from './CreateConfigModal';
import { DeleteConfigModal } from './DeleteConfigModal'; import { DeleteConfigModal } from './DeleteConfigModal';
@ -31,7 +33,7 @@ export const ConfigFileManager = ({
const [configToDelete, setConfigToDelete] = useState<string | null>(null); const [configToDelete, setConfigToDelete] = useState<string | null>(null);
const existingConfigNames = configFiles.map((file) => const existingConfigNames = configFiles.map((file) =>
stripFileExtension(file.name).toLowerCase() stripFileExtension(file.name).toLowerCase(),
); );
const handleOpenConfigModal = () => { const handleOpenConfigModal = () => {
@ -77,13 +79,13 @@ export const ConfigFileManager = ({
}; };
const selectData = configFiles.map((file) => { const selectData = configFiles.map((file) => {
const extension = file.name.split('.').pop() || ''; const extension = file.name.split('.').pop() ?? '';
const nameWithoutExtension = stripFileExtension(file.name); const nameWithoutExtension = stripFileExtension(file.name);
return { return {
value: file.name,
label: nameWithoutExtension,
extension: `.${extension}`, extension: `.${extension}`,
label: nameWithoutExtension,
value: file.name,
}; };
}); });
@ -138,7 +140,7 @@ export const ConfigFileManager = ({
onClick={handleDeleteClick} onClick={handleDeleteClick}
disabled={!selectedFile} disabled={!selectedFile}
color="red" color="red"
style={{ width: '2.5rem', padding: '0 0.5rem' }} style={{ padding: '0 0.5rem', width: '2.5rem' }}
> >
<Trash2 size={16} /> <Trash2 size={16} />
</Button> </Button>

View file

@ -1,5 +1,6 @@
import { Button, Group, Stack, TextInput } from '@mantine/core'; import { Button, Group, Stack, TextInput } from '@mantine/core';
import { useState } from 'react'; import { useState } from 'react';
import { Modal } from '@/components/Modal'; import { Modal } from '@/components/Modal';
interface CreateConfigModalProps { interface CreateConfigModalProps {
@ -49,7 +50,7 @@ export const CreateConfigModal = ({
Cancel Cancel
</Button> </Button>
<Button <Button
disabled={!trimmedConfigName || !!configNameExists} disabled={!trimmedConfigName || Boolean(configNameExists)}
onClick={() => void handleSubmit()} onClick={() => void handleSubmit()}
> >
Create Create

View file

@ -1,4 +1,5 @@
import { Button, Group, Stack, Text } from '@mantine/core'; import { Button, Group, Stack, Text } from '@mantine/core';
import { Modal } from '@/components/Modal'; import { Modal } from '@/components/Modal';
import { stripFileExtension } from '@/utils/format'; import { stripFileExtension } from '@/utils/format';

View file

@ -1,20 +1,20 @@
import { Badge, Box, Group, Text } from '@mantine/core'; import { Badge, Box, Group, Text } from '@mantine/core';
import type { AccelerationOption } from '@/types'; import type { AccelerationOption } from '@/types';
import type { GPUDevice } from '@/types/hardware'; import type { GPUDevice } from '@/types/hardware';
type AccelerationSelectItemProps = Omit<AccelerationOption, 'value'>; type AccelerationSelectItemProps = Omit<AccelerationOption, 'value'>;
const renderDeviceName = (device: string | GPUDevice) => {
const deviceName = typeof device === 'string' ? device : device.name;
return deviceName.length > 25 ? `${deviceName.slice(0, 25)}...` : deviceName;
};
export const AccelerationSelectItem = ({ export const AccelerationSelectItem = ({
label, label,
devices, devices,
disabled = false, disabled = false,
}: AccelerationSelectItemProps) => { }: AccelerationSelectItemProps) => (
const renderDeviceName = (device: string | GPUDevice) => {
const deviceName = typeof device === 'string' ? device : device.name;
return deviceName.length > 25 ? `${deviceName.slice(0, 25)}...` : deviceName;
};
return (
<Group justify="space-between" wrap="nowrap"> <Group justify="space-between" wrap="nowrap">
<Box w={!disabled ? '3.5rem' : 'auto'}> <Box w={!disabled ? '3.5rem' : 'auto'}>
<Text size="sm" truncate> <Text size="sm" truncate>
@ -30,7 +30,7 @@ export const AccelerationSelectItem = ({
devices.length > 0 && devices.length > 0 &&
(() => { (() => {
const discreteDevices = devices.filter( const discreteDevices = devices.filter(
(device) => typeof device === 'string' || !device.isIntegrated (device) => typeof device === 'string' || !device.isIntegrated,
); );
return ( return (
discreteDevices.length > 0 && ( discreteDevices.length > 0 && (
@ -50,5 +50,4 @@ export const AccelerationSelectItem = ({
); );
})()} })()}
</Group> </Group>
); );
};

View file

@ -1,9 +1,10 @@
import { Checkbox, Group, Text, TextInput } from '@mantine/core'; import { Checkbox, Group, Text, TextInput } from '@mantine/core';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
import { Select } from '@/components/Select';
import { AccelerationSelectItem } from '@/components/screens/Launch/GeneralTab/AccelerationSelectItem'; import { AccelerationSelectItem } from '@/components/screens/Launch/GeneralTab/AccelerationSelectItem';
import { GpuDeviceSelector } from '@/components/screens/Launch/GeneralTab/GpuDeviceSelector'; import { GpuDeviceSelector } from '@/components/screens/Launch/GeneralTab/GpuDeviceSelector';
import { Select } from '@/components/Select';
import { useLaunchConfigStore } from '@/stores/launchConfig'; import { useLaunchConfigStore } from '@/stores/launchConfig';
import type { Acceleration, AccelerationOption } from '@/types'; import type { Acceleration, AccelerationOption } from '@/types';
@ -57,7 +58,7 @@ export const AccelerationSelector = () => {
useEffect(() => { useEffect(() => {
if (availableAccelerations.length > 0 && acceleration) { if (availableAccelerations.length > 0 && acceleration) {
const isAccelerationAvailable = availableAccelerations.some( const isAccelerationAvailable = availableAccelerations.some(
(a) => a.value === acceleration && !a.disabled (a) => a.value === acceleration && !a.disabled,
); );
if (!isAccelerationAvailable) { if (!isAccelerationAvailable) {
@ -104,7 +105,7 @@ export const AccelerationSelector = () => {
contextSize, contextSize,
availableVramGB, availableVramGB,
flashattention, flashattention,
acceleration acceleration,
); );
setGpuLayers(result.recommendedLayers); setGpuLayers(result.recommendedLayers);
@ -153,9 +154,9 @@ export const AccelerationSelector = () => {
} }
}} }}
data={availableAccelerations.map((a) => ({ data={availableAccelerations.map((a) => ({
value: a.value,
label: a.label,
disabled: a.disabled, disabled: a.disabled,
label: a.label,
value: a.value,
}))} }))}
disabled={isLoadingAccelerations || availableAccelerations.length === 0} disabled={isLoadingAccelerations || availableAccelerations.length === 0}
renderOption={({ option }) => { renderOption={({ option }) => {
@ -163,7 +164,7 @@ export const AccelerationSelector = () => {
return ( return (
<AccelerationSelectItem <AccelerationSelectItem
label={accelerationData?.label || option.label.split(' (')[0]} label={accelerationData?.label ?? option.label.split(' (')[0]}
devices={accelerationData?.devices} devices={accelerationData?.devices}
disabled={accelerationData?.disabled} disabled={accelerationData?.disabled}
/> />

View file

@ -1,11 +1,12 @@
import { Group, Text, TextInput } from '@mantine/core'; import { Group, Text, TextInput } from '@mantine/core';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
import { Select } from '@/components/Select'; import { Select } from '@/components/Select';
import { useLaunchConfigStore } from '@/stores/launchConfig'; import { useLaunchConfigStore } from '@/stores/launchConfig';
import type { AccelerationOption } from '@/types'; import type { AccelerationOption } from '@/types';
const GPU_ACCELERATIONS = ['cuda', 'rocm', 'vulkan']; const GPU_ACCELERATIONS = new Set(['cuda', 'rocm', 'vulkan']);
const TENSOR_SPLIT_ACCELERATIONS = ['cuda', 'rocm', 'vulkan']; const TENSOR_SPLIT_ACCELERATIONS = new Set(['cuda', 'rocm', 'vulkan']);
interface GpuDeviceSelectorProps { interface GpuDeviceSelectorProps {
availableAccelerations: AccelerationOption[]; availableAccelerations: AccelerationOption[];
@ -16,13 +17,15 @@ export const GpuDeviceSelector = ({ availableAccelerations }: GpuDeviceSelectorP
useLaunchConfigStore(); useLaunchConfigStore();
const selectedAcceleration = availableAccelerations.find((a) => a.value === acceleration); const selectedAcceleration = availableAccelerations.find((a) => a.value === acceleration);
const isGpuAcceleration = GPU_ACCELERATIONS.includes(acceleration); const isGpuAcceleration = GPU_ACCELERATIONS.has(acceleration);
const getDiscreteDeviceCount = () => { const getDiscreteDeviceCount = () => {
if (!selectedAcceleration?.devices) return 0; if (!selectedAcceleration?.devices) {
return 0;
}
if (acceleration === 'vulkan' || acceleration === 'rocm') { if (acceleration === 'vulkan' || acceleration === 'rocm') {
return selectedAcceleration.devices.filter( return selectedAcceleration.devices.filter(
(device) => typeof device === 'string' || !device.isIntegrated (device) => typeof device === 'string' || !device.isIntegrated,
).length; ).length;
} }
return selectedAcceleration.devices.length; return selectedAcceleration.devices.length;
@ -30,7 +33,7 @@ export const GpuDeviceSelector = ({ availableAccelerations }: GpuDeviceSelectorP
const hasMultipleDevices = getDiscreteDeviceCount() > 1; const hasMultipleDevices = getDiscreteDeviceCount() > 1;
const showTensorSplit = const showTensorSplit =
TENSOR_SPLIT_ACCELERATIONS.includes(acceleration) && TENSOR_SPLIT_ACCELERATIONS.has(acceleration) &&
hasMultipleDevices && hasMultipleDevices &&
gpuDeviceSelection === 'all'; gpuDeviceSelection === 'all';
@ -39,7 +42,9 @@ export const GpuDeviceSelector = ({ availableAccelerations }: GpuDeviceSelectorP
} }
const deviceOptions = (() => { const deviceOptions = (() => {
if (!selectedAcceleration?.devices) return []; if (!selectedAcceleration?.devices) {
return [];
}
if (acceleration === 'vulkan' || acceleration === 'rocm') { if (acceleration === 'vulkan' || acceleration === 'rocm') {
const discreteDeviceOptions = selectedAcceleration.devices const discreteDeviceOptions = selectedAcceleration.devices
@ -49,17 +54,17 @@ export const GpuDeviceSelector = ({ availableAccelerations }: GpuDeviceSelectorP
} }
const deviceName = typeof device === 'string' ? device : device.name; const deviceName = typeof device === 'string' ? device : device.name;
return { return {
value: index.toString(),
label: `GPU ${index}: ${deviceName}`, label: `GPU ${index}: ${deviceName}`,
value: index.toString(),
}; };
}) })
.filter((option): option is NonNullable<typeof option> => option !== null); .filter((option): option is NonNullable<typeof option> => option !== null);
return [{ value: 'all', label: 'All GPUs' }, ...discreteDeviceOptions]; return [{ label: 'All GPUs', value: 'all' }, ...discreteDeviceOptions];
} }
return [ return [
{ value: 'all', label: 'All GPUs' }, { label: 'All GPUs', value: 'all' },
...selectedAcceleration.devices.map((device, index) => { ...selectedAcceleration.devices.map((device, index) => {
const deviceName = const deviceName =
typeof device === 'string' typeof device === 'string'
@ -68,8 +73,8 @@ export const GpuDeviceSelector = ({ availableAccelerations }: GpuDeviceSelectorP
? device.name ? device.name
: String(device); : String(device);
return { return {
value: index.toString(),
label: `GPU ${index}: ${deviceName}`, label: `GPU ${index}: ${deviceName}`,
value: index.toString(),
}; };
}), }),
]; ];

View file

@ -1,5 +1,6 @@
import { Group, Slider, Stack, Text, TextInput, Tooltip, Transition } from '@mantine/core'; import { Group, Slider, Stack, Text, TextInput, Tooltip, Transition } from '@mantine/core';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
import { AccelerationSelector } from '@/components/screens/Launch/GeneralTab/AccelerationSelector'; import { AccelerationSelector } from '@/components/screens/Launch/GeneralTab/AccelerationSelector';
import { ModelFileField } from '@/components/screens/Launch/ModelFileField'; import { ModelFileField } from '@/components/screens/Launch/ModelFileField';
@ -44,8 +45,8 @@ export const GeneralTab = ({ configLoaded = true }: GeneralTabProps) => {
onChange={setModel} onChange={setModel}
onSelectFile={() => void selectFile('model', 'Select Text Model')} onSelectFile={() => void selectFile('model', 'Select Text Model')}
searchParams={{ searchParams={{
pipelineTag: 'text-generation',
filter: 'gguf', filter: 'gguf',
pipelineTag: 'text-generation',
sort: 'trendingScore', sort: 'trendingScore',
}} }}
showAnalyze showAnalyze
@ -65,7 +66,7 @@ export const GeneralTab = ({ configLoaded = true }: GeneralTabProps) => {
onChange={(event) => setContextSizeWithStep(Number(event.target.value) || 256)} onChange={(event) => setContextSizeWithStep(Number(event.target.value) || 256)}
type="number" type="number"
min={256} min={256}
max={262144} max={262_144}
step={256} step={256}
size="sm" size="sm"
w={100} w={100}
@ -75,7 +76,7 @@ export const GeneralTab = ({ configLoaded = true }: GeneralTabProps) => {
<Slider <Slider
value={contextSize} value={contextSize}
min={256} min={256}
max={262144} max={262_144}
step={256} step={256}
onChange={setContextSizeWithStep} onChange={setContextSizeWithStep}
label={null} label={null}
@ -87,7 +88,7 @@ export const GeneralTab = ({ configLoaded = true }: GeneralTabProps) => {
withinPortal withinPortal
opened={isSliding ? true : undefined} opened={isSliding ? true : undefined}
> >
<span style={{ display: 'block', width: '100%', height: '100%' }} /> <span style={{ display: 'block', height: '100%', width: '100%' }} />
</Tooltip> </Tooltip>
} }
/> />

View file

@ -1,4 +1,5 @@
import { Group, Loader, Table, Text } from '@mantine/core'; import { Group, Loader, Table, Text } from '@mantine/core';
import type { HuggingFaceFileInfo } from '@/types'; import type { HuggingFaceFileInfo } from '@/types';
import { formatBytes } from '@/utils/format'; import { formatBytes } from '@/utils/format';

View file

@ -4,6 +4,7 @@ import ReactMarkdown from 'react-markdown';
import rehypeRaw from 'rehype-raw'; import rehypeRaw from 'rehype-raw';
import rehypeSanitize from 'rehype-sanitize'; import rehypeSanitize from 'rehype-sanitize';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import { HUGGINGFACE_BASE_URL } from '@/constants'; import { HUGGINGFACE_BASE_URL } from '@/constants';
interface ModelCardProps { interface ModelCardProps {
@ -16,7 +17,9 @@ export const ModelCard = ({ modelId }: ModelCardProps) => {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const loadReadme = async () => { const loadReadme = async () => {
if (readme !== null) return; if (readme !== null) {
return;
}
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
@ -57,15 +60,23 @@ export const ModelCard = ({ modelId }: ModelCardProps) => {
p="md" p="md"
withBorder withBorder
style={{ style={{
overflow: 'auto',
maxWidth: '100%', maxWidth: '100%',
overflow: 'auto',
}} }}
> >
<ReactMarkdown <ReactMarkdown
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw, rehypeSanitize]} rehypePlugins={[rehypeRaw, rehypeSanitize]}
components={{ components={{
img: ({ node, ...props }) => ( code: ({ node: _, ...props }) => (
<code
{...props}
style={{
wordBreak: 'break-word',
}}
/>
),
img: ({ node: _, ...props }) => (
<img <img
{...props} {...props}
style={{ style={{
@ -75,7 +86,7 @@ export const ModelCard = ({ modelId }: ModelCardProps) => {
}} }}
/> />
), ),
pre: ({ node, ...props }) => ( pre: ({ node: _, ...props }) => (
<pre <pre
{...props} {...props}
style={{ style={{
@ -84,14 +95,6 @@ export const ModelCard = ({ modelId }: ModelCardProps) => {
}} }}
/> />
), ),
code: ({ node, ...props }) => (
<code
{...props}
style={{
wordBreak: 'break-word',
}}
/>
),
}} }}
> >
{readme} {readme}

View file

@ -1,5 +1,6 @@
import { Badge, Group, Loader, Stack, Table, Text, Tooltip } from '@mantine/core'; import { Badge, Group, Loader, Stack, Table, Text, Tooltip } from '@mantine/core';
import { Download, Heart, Lock } from 'lucide-react'; import { Download, Heart, Lock } from 'lucide-react';
import { HUGGINGFACE_BASE_URL } from '@/constants'; import { HUGGINGFACE_BASE_URL } from '@/constants';
import type { HuggingFaceModelInfo, HuggingFaceSortOption } from '@/types'; import type { HuggingFaceModelInfo, HuggingFaceSortOption } from '@/types';
import { formatDate, formatDownloads } from '@/utils/format'; import { formatDate, formatDownloads } from '@/utils/format';
@ -42,12 +43,12 @@ export const ModelsTable = ({
<Table.Th>Model</Table.Th> <Table.Th>Model</Table.Th>
<Table.Th style={{ width: 80 }}>Params</Table.Th> <Table.Th style={{ width: 80 }}>Params</Table.Th>
<Table.Th <Table.Th
style={{ width: 120, cursor: 'pointer' }} style={{ cursor: 'pointer', width: 120 }}
onClick={() => onSortChange('downloads')} onClick={() => onSortChange('downloads')}
> >
Downloads{sortBy === 'downloads' && ' ↓'} Downloads{sortBy === 'downloads' && ' ↓'}
</Table.Th> </Table.Th>
<Table.Th style={{ width: 90, cursor: 'pointer' }} onClick={() => onSortChange('likes')}> <Table.Th style={{ cursor: 'pointer', width: 90 }} onClick={() => onSortChange('likes')}>
Likes{sortBy === 'likes' && ' ↓'} Likes{sortBy === 'likes' && ' ↓'}
</Table.Th> </Table.Th>
</Table.Tr> </Table.Tr>

View file

@ -14,6 +14,7 @@ import {
import { useDebouncedValue } from '@mantine/hooks'; import { useDebouncedValue } from '@mantine/hooks';
import { ArrowLeft, ExternalLink, Search } from 'lucide-react'; import { ArrowLeft, ExternalLink, Search } from 'lucide-react';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { Modal } from '@/components/Modal'; import { Modal } from '@/components/Modal';
import { HUGGINGFACE_BASE_URL } from '@/constants'; import { HUGGINGFACE_BASE_URL } from '@/constants';
import { useHuggingFaceSearch } from '@/hooks/useHuggingFaceSearch'; import { useHuggingFaceSearch } from '@/hooks/useHuggingFaceSearch';
@ -23,6 +24,7 @@ import type {
HuggingFaceSearchParams, HuggingFaceSearchParams,
HuggingFaceSortOption, HuggingFaceSortOption,
} from '@/types'; } from '@/types';
import { FilesTable } from './FilesTable'; import { FilesTable } from './FilesTable';
import { ModelCard } from './ModelCard'; import { ModelCard } from './ModelCard';
import { ModelsTable } from './ModelsTable'; import { ModelsTable } from './ModelsTable';
@ -40,7 +42,7 @@ export const HuggingFaceSearchModal = ({
onSelect, onSelect,
searchParams: initialSearchParams, searchParams: initialSearchParams,
}: HuggingFaceSearchModalProps) => { }: HuggingFaceSearchModalProps) => {
const [searchQuery, setSearchQuery] = useState(initialSearchParams.search || ''); const [searchQuery, setSearchQuery] = useState(initialSearchParams.search ?? '');
const [debouncedQuery] = useDebouncedValue(searchQuery, 600); const [debouncedQuery] = useDebouncedValue(searchQuery, 600);
const scrollAreaRef = useRef<HTMLDivElement>(null); const scrollAreaRef = useRef<HTMLDivElement>(null);
const prevOpenedRef = useRef(false); const prevOpenedRef = useRef(false);
@ -84,7 +86,7 @@ export const HuggingFaceSearchModal = ({
}, [debouncedQuery, opened, searchModels]); }, [debouncedQuery, opened, searchModels]);
const handleClose = () => { const handleClose = () => {
setSearchQuery(initialSearchParams.search || ''); setSearchQuery(initialSearchParams.search ?? '');
onClose(); onClose();
}; };
@ -93,7 +95,9 @@ export const HuggingFaceSearchModal = ({
}; };
const handleFileSelect = (file: HuggingFaceFileInfo) => { const handleFileSelect = (file: HuggingFaceFileInfo) => {
if (!selectedModel) return; if (!selectedModel) {
return;
}
const url = getFileDownloadUrl(selectedModel.id, file.path); const url = getFileDownloadUrl(selectedModel.id, file.path);
onSelect(url); onSelect(url);
handleClose(); handleClose();
@ -112,8 +116,12 @@ export const HuggingFaceSearchModal = ({
const getFilterBadges = () => { const getFilterBadges = () => {
const badges: string[] = []; const badges: string[] = [];
if (searchParams?.filter) badges.push(searchParams.filter.toUpperCase()); if (searchParams?.filter) {
if (searchParams?.pipelineTag) badges.push(searchParams.pipelineTag.replace('-', ' ')); badges.push(searchParams.filter.toUpperCase());
}
if (searchParams?.pipelineTag) {
badges.push(searchParams.pipelineTag.replace('-', ' '));
}
return badges; return badges;
}; };
@ -187,7 +195,9 @@ export const HuggingFaceSearchModal = ({
style={{ flex: 1 }} style={{ flex: 1 }}
onScrollPositionChange={({ y }) => { onScrollPositionChange={({ y }) => {
const target = scrollAreaRef.current; const target = scrollAreaRef.current;
if (!target) return; if (!target) {
return;
}
const isNearBottom = target.scrollHeight - y <= target.clientHeight + 100; const isNearBottom = target.scrollHeight - y <= target.clientHeight + 100;
if (isNearBottom && hasMore && !loading && !selectedModel) { if (isNearBottom && hasMore && !loading && !selectedModel) {
void loadMoreModels(); void loadMoreModels();

View file

@ -1,8 +1,9 @@
import { Group, Stack } from '@mantine/core'; import { Group, Stack } from '@mantine/core';
import { useState } from 'react'; import { useState } from 'react';
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip'; import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
import { SelectWithTooltip } from '@/components/SelectWithTooltip';
import { ModelFileField } from '@/components/screens/Launch/ModelFileField'; import { ModelFileField } from '@/components/screens/Launch/ModelFileField';
import { SelectWithTooltip } from '@/components/SelectWithTooltip';
import { IMAGE_MODEL_PRESETS } from '@/constants/imageModelPresets'; import { IMAGE_MODEL_PRESETS } from '@/constants/imageModelPresets';
import { useLaunchConfigStore } from '@/stores/launchConfig'; import { useLaunchConfigStore } from '@/stores/launchConfig';
@ -41,8 +42,8 @@ export const ImageGenerationTab = () => {
tooltip="Quick presets for popular image generation models with pre-configured encoders." tooltip="Quick presets for popular image generation models with pre-configured encoders."
placeholder="Choose a preset..." placeholder="Choose a preset..."
data={IMAGE_MODEL_PRESETS.map((preset) => ({ data={IMAGE_MODEL_PRESETS.map((preset) => ({
value: preset.name,
label: preset.name, label: preset.name,
value: preset.name,
}))} }))}
value={selectedPreset ?? ''} value={selectedPreset ?? ''}
onChange={(value) => { onChange={(value) => {
@ -70,8 +71,8 @@ export const ImageGenerationTab = () => {
onChange={setSdmodel} onChange={setSdmodel}
onSelectFile={() => void selectFile('sdmodel', 'Select Image Model')} onSelectFile={() => void selectFile('sdmodel', 'Select Image Model')}
searchParams={{ searchParams={{
pipelineTag: 'text-to-image',
filter: 'gguf', filter: 'gguf',
pipelineTag: 'text-to-image',
sort: 'trendingScore', sort: 'trendingScore',
}} }}
showAnalyze showAnalyze
@ -86,8 +87,8 @@ export const ImageGenerationTab = () => {
onChange={setSdt5xxl} onChange={setSdt5xxl}
onSelectFile={() => void selectFile('sdt5xxl', 'Select T5XXL Model')} onSelectFile={() => void selectFile('sdt5xxl', 'Select T5XXL Model')}
searchParams={{ searchParams={{
search: 't5xxl',
filter: 'safetensors', filter: 'safetensors',
search: 't5xxl',
sort: 'trendingScore', sort: 'trendingScore',
}} }}
paramType="sdt5xxl" paramType="sdt5xxl"
@ -101,8 +102,8 @@ export const ImageGenerationTab = () => {
onChange={setSdclipl} onChange={setSdclipl}
onSelectFile={() => void selectFile('sdclipl', 'Select CLIP-L Model')} onSelectFile={() => void selectFile('sdclipl', 'Select CLIP-L Model')}
searchParams={{ searchParams={{
search: 'clip',
filter: 'safetensors', filter: 'safetensors',
search: 'clip',
sort: 'trendingScore', sort: 'trendingScore',
}} }}
paramType="sdclipl" paramType="sdclipl"
@ -116,8 +117,8 @@ export const ImageGenerationTab = () => {
onChange={setSdclipg} onChange={setSdclipg}
onSelectFile={() => void selectFile('sdclipg', 'Select CLIP-G Model')} onSelectFile={() => void selectFile('sdclipg', 'Select CLIP-G Model')}
searchParams={{ searchParams={{
search: 'clip',
filter: 'gguf', filter: 'gguf',
search: 'clip',
sort: 'trendingScore', sort: 'trendingScore',
}} }}
paramType="sdclipg" paramType="sdclipg"
@ -131,8 +132,8 @@ export const ImageGenerationTab = () => {
onChange={setSdphotomaker} onChange={setSdphotomaker}
onSelectFile={() => void selectFile('sdphotomaker', 'Select PhotoMaker Model')} onSelectFile={() => void selectFile('sdphotomaker', 'Select PhotoMaker Model')}
searchParams={{ searchParams={{
search: 'photomaker',
filter: 'safetensors', filter: 'safetensors',
search: 'photomaker',
sort: 'trendingScore', sort: 'trendingScore',
}} }}
paramType="sdphotomaker" paramType="sdphotomaker"
@ -146,8 +147,8 @@ export const ImageGenerationTab = () => {
onChange={setSdvae} onChange={setSdvae}
onSelectFile={() => void selectFile('sdvae', 'Select VAE Model')} onSelectFile={() => void selectFile('sdvae', 'Select VAE Model')}
searchParams={{ searchParams={{
search: 'vae',
filter: 'safetensors', filter: 'safetensors',
search: 'vae',
sort: 'trendingScore', sort: 'trendingScore',
}} }}
paramType="sdvae" paramType="sdvae"
@ -161,8 +162,8 @@ export const ImageGenerationTab = () => {
onChange={setSdlora} onChange={setSdlora}
onSelectFile={() => void selectFile('sdlora', 'Select LoRA Model')} onSelectFile={() => void selectFile('sdlora', 'Select LoRA Model')}
searchParams={{ searchParams={{
search: 'lora',
filter: 'safetensors', filter: 'safetensors',
search: 'lora',
sort: 'trendingScore', sort: 'trendingScore',
}} }}
paramType="sdlora" paramType="sdlora"
@ -178,9 +179,9 @@ export const ImageGenerationTab = () => {
} }
}} }}
data={[ data={[
{ value: 'off', label: 'Off' }, { label: 'Off', value: 'off' },
{ value: 'vaeonly', label: 'VAE Only' }, { label: 'VAE Only', value: 'vaeonly' },
{ value: 'full', label: 'Full' }, { label: 'Full', value: 'full' },
]} ]}
/> />

View file

@ -1,5 +1,6 @@
import { Alert, Group, rem, Stack, Text } from '@mantine/core'; import { Alert, Group, Stack, Text, rem } from '@mantine/core';
import { Info } from 'lucide-react'; import { Info } from 'lucide-react';
import { Modal } from '@/components/Modal'; import { Modal } from '@/components/Modal';
import type { ModelAnalysis } from '@/types'; import type { ModelAnalysis } from '@/types';
@ -17,7 +18,9 @@ interface InfoRowProps {
} }
const InfoRow = ({ label, value }: InfoRowProps) => { const InfoRow = ({ label, value }: InfoRowProps) => {
if (!value) return null; if (!value) {
return null;
}
return ( return (
<Group gap="md" wrap="nowrap"> <Group gap="md" wrap="nowrap">
@ -79,7 +82,7 @@ export const ModelAnalysisModal = ({
{analysis.architecture.layers && ( {analysis.architecture.layers && (
<InfoRow <InfoRow
label="Full VRAM" label="Full VRAM"
value={`${analysis.architecture.layers} x ${analysis.estimates.vramPerLayer || 'N/A'} per layer = ${analysis.estimates.fullGpuVram}`} value={`${analysis.architecture.layers} x ${analysis.estimates.vramPerLayer ?? 'N/A'} per layer = ${analysis.estimates.fullGpuVram}`}
/> />
)} )}
{!analysis.architecture.layers && ( {!analysis.architecture.layers && (

View file

@ -9,6 +9,7 @@ import {
} from '@mantine/core'; } from '@mantine/core';
import { File, Info, Search } from 'lucide-react'; import { File, Info, Search } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { LabelWithTooltip } from '@/components/LabelWithTooltip'; import { LabelWithTooltip } from '@/components/LabelWithTooltip';
import { HuggingFaceSearchModal } from '@/components/screens/Launch/HuggingFaceSearchModal'; import { HuggingFaceSearchModal } from '@/components/screens/Launch/HuggingFaceSearchModal';
import { ModelAnalysisModal } from '@/components/screens/Launch/ModelAnalysisModal'; import { ModelAnalysisModal } from '@/components/screens/Launch/ModelAnalysisModal';
@ -66,7 +67,9 @@ export const ModelFileField = ({
)); ));
const getHelperText = () => { const getHelperText = () => {
if (validationState === 'neutral') return undefined; if (validationState === 'neutral') {
return undefined;
}
if (validationState === 'invalid') { if (validationState === 'invalid') {
return 'Enter a valid URL or file path'; return 'Enter a valid URL or file path';
@ -76,7 +79,9 @@ export const ModelFileField = ({
}; };
const handleAnalyzeModel = async () => { const handleAnalyzeModel = async () => {
if (validationState === 'neutral' || validationState === 'invalid') return; if (validationState === 'neutral' || validationState === 'invalid') {
return;
}
setAnalysisModalOpened(true); setAnalysisModalOpened(true);
setAnalysisLoading(true); setAnalysisLoading(true);

View file

@ -1,4 +1,5 @@
import { Group, Stack, Text, TextInput } from '@mantine/core'; import { Group, Stack, Text, TextInput } from '@mantine/core';
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip'; import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
import { useLaunchConfigStore } from '@/stores/launchConfig'; import { useLaunchConfigStore } from '@/stores/launchConfig';
@ -49,7 +50,7 @@ export const NetworkTab = () => {
placeholder="5001" placeholder="5001"
value={port?.toString() ?? ''} value={port?.toString() ?? ''}
onChange={(event) => { onChange={(event) => {
const value = event.currentTarget.value; const { value } = event.currentTarget;
if (value === '') { if (value === '') {
setPort(undefined); setPort(undefined);
@ -57,13 +58,13 @@ export const NetworkTab = () => {
} }
const numValue = Number(value); const numValue = Number(value);
if (!Number.isNaN(numValue) && numValue >= 1 && numValue <= 65535) { if (!Number.isNaN(numValue) && numValue >= 1 && numValue <= 65_535) {
setPort(numValue); setPort(numValue);
} }
}} }}
type="number" type="number"
min={1} min={1}
max={65535} max={65_535}
w={120} w={120}
/> />
</div> </div>

View file

@ -1,4 +1,5 @@
import { Group, NumberInput, SimpleGrid, Stack, Text } from '@mantine/core'; import { Group, NumberInput, SimpleGrid, Stack, Text } from '@mantine/core';
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip'; import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
import { useLaunchConfigStore } from '@/stores/launchConfig'; import { useLaunchConfigStore } from '@/stores/launchConfig';

View file

@ -1,5 +1,6 @@
import { Button, Card, Container, Group, Stack, Tabs } from '@mantine/core'; import { Button, Card, Container, Group, Stack, Tabs } from '@mantine/core';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { AdvancedTab } from '@/components/screens/Launch/AdvancedTab'; import { AdvancedTab } from '@/components/screens/Launch/AdvancedTab';
import { ConfigFileManager } from '@/components/screens/Launch/ConfigFileManager'; import { ConfigFileManager } from '@/components/screens/Launch/ConfigFileManager';
import { GeneralTab } from '@/components/screens/Launch/GeneralTab/index'; import { GeneralTab } from '@/components/screens/Launch/GeneralTab/index';
@ -74,15 +75,15 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
const { isLaunching, handleLaunch } = useLaunchLogic({ const { isLaunching, handleLaunch } = useLaunchLogic({
model, model,
sdmodel,
onLaunch, onLaunch,
sdmodel,
}); });
const { warnings: combinedWarnings } = useWarnings({ const { warnings: combinedWarnings } = useWarnings({
model,
sdmodel,
acceleration, acceleration,
configLoaded, configLoaded,
model,
sdmodel,
}); });
const setHappyDefaults = useCallback(async () => { const setHappyDefaults = useCallback(async () => {
@ -100,14 +101,14 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
defaultsSetRef.current = true; defaultsSetRef.current = true;
} }
}, },
[setModel] [setModel],
); );
useEffect(() => { useEffect(() => {
if (configLoaded && !defaultsSetRef.current) { if (configLoaded && !defaultsSetRef.current) {
void setHappyDefaults(); void setHappyDefaults();
if (!model.trim() && !sdmodel.trim()) { if (!model.trim() && !sdmodel.trim()) {
void setInitialDefaults(model, sdmodel); setInitialDefaults(model, sdmodel);
} }
} }
}, [configLoaded, setHappyDefaults, model, sdmodel, setInitialDefaults]); }, [configLoaded, setHappyDefaults, model, sdmodel, setInitialDefaults]);
@ -147,50 +148,50 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
}; };
const buildConfigData = () => ({ const buildConfigData = () => ({
autoGpuLayers,
gpulayers: gpuLayers,
contextsize: contextSize,
model,
additionalArguments, additionalArguments,
preLaunchCommands: preLaunchCommands.filter((cmd) => cmd.trim() !== ''), autoGpuLayers,
port, contextsize: contextSize,
host,
multiuser: multiuser ? 1 : 0,
multiplayer,
remotetunnel,
nocertify,
websearch,
noshift,
flashattention,
noavx2,
failsafe,
usemmap,
debugmode, debugmode,
failsafe,
flashattention,
gpuDeviceSelection,
gpulayers: gpuLayers,
host,
model,
moecpu, moecpu,
moeexperts, moeexperts,
smartcache, multiplayer,
multiuser: multiuser ? 1 : 0,
noavx2,
nocertify,
noshift,
pipelineparallel, pipelineparallel,
usecuda: acceleration === 'cuda' || acceleration === 'rocm', port,
usevulkan: acceleration === 'vulkan', preLaunchCommands: preLaunchCommands.filter((cmd) => cmd.trim() !== ''),
gpuDeviceSelection, remotetunnel,
tensorSplit,
sdmodel,
sdt5xxl,
sdclipl,
sdclipg, sdclipg,
sdphotomaker,
sdvae,
sdlora,
sdconvdirect,
sdvaecpu,
sdclipgpu, sdclipgpu,
sdclipl,
sdconvdirect,
sdlora,
sdmodel,
sdphotomaker,
sdt5xxl,
sdvae,
sdvaecpu,
smartcache,
tensorSplit,
usecuda: acceleration === 'cuda' || acceleration === 'rocm',
usemmap,
usevulkan: acceleration === 'vulkan',
websearch,
}); });
const handleCreateNewConfig = async (configName: string) => { const handleCreateNewConfig = async (configName: string) => {
const fullConfigName = `${configName}.json`; const fullConfigName = `${configName}.json`;
const saveSuccess = await window.electronAPI.kobold.saveConfigFile( const saveSuccess = await window.electronAPI.kobold.saveConfigFile(
fullConfigName, fullConfigName,
buildConfigData() buildConfigData(),
); );
if (saveSuccess) { if (saveSuccess) {
@ -210,7 +211,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
const saveSuccess = await window.electronAPI.kobold.saveConfigFile( const saveSuccess = await window.electronAPI.kobold.saveConfigFile(
selectedFile, selectedFile,
buildConfigData() buildConfigData(),
); );
if (!saveSuccess) { if (!saveSuccess) {
@ -260,43 +261,43 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
const handleLaunchClick = useCallback(() => { const handleLaunchClick = useCallback(() => {
void handleLaunch({ void handleLaunch({
autoGpuLayers,
gpuLayers,
contextSize,
port: port ?? 5001,
host,
multiuser,
multiplayer,
remotetunnel,
nocertify,
websearch,
noshift,
flashattention,
noavx2,
failsafe,
acceleration, acceleration,
lowvram,
gpuDeviceSelection,
gpuPlatform,
tensorSplit,
quantmatmul,
usemmap,
debugmode,
additionalArguments, additionalArguments,
preLaunchCommands, autoGpuLayers,
sdt5xxl, contextSize,
sdclipl, debugmode,
sdclipg, failsafe,
sdphotomaker, flashattention,
sdvae, gpuDeviceSelection,
sdlora, gpuLayers,
sdconvdirect, gpuPlatform,
sdvaecpu, host,
sdclipgpu, lowvram,
moecpu, moecpu,
moeexperts, moeexperts,
smartcache, multiplayer,
multiuser,
noavx2,
nocertify,
noshift,
pipelineparallel, pipelineparallel,
port: port ?? 5001,
preLaunchCommands,
quantmatmul,
remotetunnel,
sdclipg,
sdclipgpu,
sdclipl,
sdconvdirect,
sdlora,
sdphotomaker,
sdt5xxl,
sdvae,
sdvaecpu,
smartcache,
tensorSplit,
usemmap,
websearch,
}); });
}, [ }, [
handleLaunch, handleLaunch,
@ -358,17 +359,17 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
value={activeTab} value={activeTab}
onChange={setActiveTab} onChange={setActiveTab}
styles={{ styles={{
root: {
maxHeight: '51vh',
display: 'flex',
flexDirection: 'column',
},
panel: { panel: {
flex: 1, flex: 1,
overflow: 'auto', overflow: 'auto',
paddingTop: '1rem', paddingTop: '1rem',
paddingRight: '0.5rem', paddingRight: '0.5rem',
}, },
root: {
maxHeight: '51vh',
display: 'flex',
flexDirection: 'column',
},
}} }}
> >
<Tabs.List> <Tabs.List>
@ -410,12 +411,12 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
variant="filled" variant="filled"
color="blue" color="blue"
style={{ style={{
fontWeight: 600,
fontSize: '1em', fontSize: '1em',
padding: '0.75rem 1.75rem', fontWeight: 600,
minWidth: '7.5rem',
textTransform: 'uppercase',
letterSpacing: '0.03125rem', letterSpacing: '0.03125rem',
minWidth: '7.5rem',
padding: '0.75rem 1.75rem',
textTransform: 'uppercase',
}} }}
> >
Launch Launch

View file

@ -1,3 +1,4 @@
import iconUrl from '/icon.png';
import { import {
Button, Button,
Card, Card,
@ -11,8 +12,8 @@ import {
Title, Title,
} from '@mantine/core'; } from '@mantine/core';
import { Check } from 'lucide-react'; import { Check } from 'lucide-react';
import { PRODUCT_NAME } from '@/constants'; import { PRODUCT_NAME } from '@/constants';
import iconUrl from '/icon.png';
interface WelcomeScreenProps { interface WelcomeScreenProps {
onGetStarted: () => void; onGetStarted: () => void;

View file

@ -1,12 +1,12 @@
import icon from '/icon.png';
import { Badge, Button, Card, Center, Group, Image, rem, Stack, Text } from '@mantine/core'; import { Badge, Button, Card, Center, Group, Image, rem, Stack, Text } from '@mantine/core';
import { Github } from 'lucide-react'; import { Github } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { GITHUB_API, PRODUCT_NAME } from '@/constants'; import { GITHUB_API, PRODUCT_NAME } from '@/constants';
import { useLogoClickSounds } from '@/hooks/useLogoClickSounds'; import { useLogoClickSounds } from '@/hooks/useLogoClickSounds';
import type { SystemVersionInfo } from '@/types/electron'; import type { SystemVersionInfo } from '@/types/electron';
import icon from '/icon.png';
export const AboutTab = () => { export const AboutTab = () => {
const [versionInfo, setVersionInfo] = useState<SystemVersionInfo | null>(null); const [versionInfo, setVersionInfo] = useState<SystemVersionInfo | null>(null);
const { handleLogoClick, getLogoStyles } = useLogoClickSounds(); const { handleLogoClick, getLogoStyles } = useLogoClickSounds();
@ -32,6 +32,7 @@ export const AboutTab = () => {
const actionButtons = [ const actionButtons = [
{ {
// eslint-disable-next-line typescript/no-deprecated
icon: Github, icon: Github,
label: 'GitHub', label: 'GitHub',
onClick: () => window.electronAPI.app.openExternal(GITHUB_API.GERBIL_GITHUB_URL), onClick: () => window.electronAPI.app.openExternal(GITHUB_API.GERBIL_GITHUB_URL),
@ -49,8 +50,8 @@ export const AboutTab = () => {
h={64} h={64}
onClick={() => void handleLogoClick()} onClick={() => void handleLogoClick()}
style={{ style={{
minWidth: 64,
minHeight: 64, minHeight: 64,
minWidth: 64,
...getLogoStyles(), ...getLogoStyles(),
}} }}
/> />
@ -72,7 +73,7 @@ export const AboutTab = () => {
key={button.label} key={button.label}
variant="light" variant="light"
size="compact-sm" size="compact-sm"
leftSection={<button.icon style={{ width: rem(16), height: rem(16) }} />} leftSection={<button.icon style={{ height: rem(16), width: rem(16) }} />}
onClick={() => void button.onClick()} onClick={() => void button.onClick()}
style={button.label === 'GitHub' ? { textDecoration: 'none' } : undefined} style={button.label === 'GitHub' ? { textDecoration: 'none' } : undefined}
> >

View file

@ -1,15 +1,8 @@
import { import { Group, rem, SegmentedControl, Slider, Stack, Text, TextInput } from '@mantine/core';
Group, import type { MantineColorScheme } from '@mantine/core';
type MantineColorScheme,
rem,
SegmentedControl,
Slider,
Stack,
Text,
TextInput,
} from '@mantine/core';
import { Monitor, Moon, Sun } from 'lucide-react'; import { Monitor, Moon, Sun } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { FrontendInterfaceSelector } from '@/components/settings/FrontendInterfaceSelector'; import { FrontendInterfaceSelector } from '@/components/settings/FrontendInterfaceSelector';
import { ZOOM } from '@/constants'; import { ZOOM } from '@/constants';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
@ -82,19 +75,19 @@ export const AppearanceTab = ({ isOnInterfaceScreen = false }: AppearanceTabProp
value={rawColorScheme} value={rawColorScheme}
onChange={handleColorSchemeChange} onChange={handleColorSchemeChange}
styles={(theme) => ({ styles={(theme) => ({
root: {
border: `0.5px solid ${isDark ? theme.colors.dark[4] : theme.colors.gray[4]}`,
},
indicator: { indicator: {
backgroundColor: isDark ? theme.colors.dark[5] : theme.colors.gray[2], backgroundColor: isDark ? theme.colors.dark[5] : theme.colors.gray[2],
border: `1px solid ${isDark ? theme.colors.dark[4] : theme.colors.gray[4]}`, border: `1px solid ${isDark ? theme.colors.dark[4] : theme.colors.gray[4]}`,
}, },
root: {
border: `0.5px solid ${isDark ? theme.colors.dark[4] : theme.colors.gray[4]}`,
},
})} })}
data={[ data={[
{ {
label: ( label: (
<Group gap="xs" justify="center"> <Group gap="xs" justify="center">
<Sun style={{ width: rem(16), height: rem(16) }} /> <Sun style={{ height: rem(16), width: rem(16) }} />
<span>Light</span> <span>Light</span>
</Group> </Group>
), ),
@ -103,7 +96,7 @@ export const AppearanceTab = ({ isOnInterfaceScreen = false }: AppearanceTabProp
{ {
label: ( label: (
<Group gap="xs" justify="center"> <Group gap="xs" justify="center">
<Moon style={{ width: rem(16), height: rem(16) }} /> <Moon style={{ height: rem(16), width: rem(16) }} />
<span>Dark</span> <span>Dark</span>
</Group> </Group>
), ),
@ -112,7 +105,7 @@ export const AppearanceTab = ({ isOnInterfaceScreen = false }: AppearanceTabProp
{ {
label: ( label: (
<Group gap="xs" justify="center"> <Group gap="xs" justify="center">
<Monitor style={{ width: rem(16), height: rem(16) }} /> <Monitor style={{ height: rem(16), width: rem(16) }} />
<span>System</span> <span>System</span>
</Group> </Group>
), ),
@ -133,7 +126,7 @@ export const AppearanceTab = ({ isOnInterfaceScreen = false }: AppearanceTabProp
value={zoomPercentage} value={zoomPercentage}
onChange={(event) => handleZoomPercentageChange(event.currentTarget.value)} onChange={(event) => handleZoomPercentageChange(event.currentTarget.value)}
onBlur={(event) => { onBlur={(event) => {
const value = event.currentTarget.value; const { value } = event.currentTarget;
const numValue = Number(value); const numValue = Number(value);
if (Number.isNaN(numValue) || !isValidZoomPercentage(numValue)) { if (Number.isNaN(numValue) || !isValidZoomPercentage(numValue)) {
setZoomPercentage(zoomLevelToPercentage(zoomLevel).toString()); setZoomPercentage(zoomLevelToPercentage(zoomLevel).toString());

View file

@ -1,6 +1,7 @@
import { Anchor, Card, Center, Group, Loader, Stack, Text } from '@mantine/core'; import { Anchor, Card, Center, Group, Loader, Stack, Text } from '@mantine/core';
import { ExternalLink } from 'lucide-react'; import { ExternalLink } from 'lucide-react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { DownloadCard } from '@/components/DownloadCard'; import { DownloadCard } from '@/components/DownloadCard';
import { ImportBackendLink } from '@/components/ImportBackendLink'; import { ImportBackendLink } from '@/components/ImportBackendLink';
import { useKoboldBackendsStore } from '@/stores/koboldBackends'; import { useKoboldBackendsStore } from '@/stores/koboldBackends';
@ -75,35 +76,35 @@ export const BackendsTab = () => {
}); });
const isCurrent = Boolean( const isCurrent = Boolean(
installedBackend && currentBackend && currentBackend.path === installedBackend.path installedBackend && currentBackend && currentBackend.path === installedBackend.path,
); );
if (installedBackend) { if (installedBackend) {
processedInstalled.add(installedBackend.path); processedInstalled.add(installedBackend.path);
const hasUpdate = const hasUpdate =
compareVersions(download.version || 'unknown', installedBackend.version) > 0; compareVersions(download.version ?? 'unknown', installedBackend.version) > 0;
backends.push({ backends.push({
name: download.name,
version: installedBackend.version,
size: undefined,
isInstalled: true,
isCurrent,
downloadUrl: download.url,
installedPath: installedBackend.path,
hasUpdate,
newerVersion: hasUpdate ? download.version : undefined,
actualVersion: installedBackend.actualVersion, actualVersion: installedBackend.actualVersion,
downloadUrl: download.url,
hasUpdate,
installedPath: installedBackend.path,
isCurrent,
isInstalled: true,
name: download.name,
newerVersion: hasUpdate ? download.version : undefined,
size: undefined,
version: installedBackend.version,
}); });
} else { } else {
backends.push({ backends.push({
name: download.name,
version: download.version || 'unknown',
size: download.size,
isInstalled: false,
isCurrent: false,
downloadUrl: download.url, downloadUrl: download.url,
isCurrent: false,
isInstalled: false,
name: download.name,
size: download.size,
version: download.version ?? 'unknown',
}); });
} }
}); });
@ -114,20 +115,24 @@ export const BackendsTab = () => {
const isCurrent = Boolean(currentBackend && currentBackend.path === installed.path); const isCurrent = Boolean(currentBackend && currentBackend.path === installed.path);
backends.push({ backends.push({
name: displayName,
version: installed.version,
size: undefined,
isInstalled: true,
isCurrent,
installedPath: installed.path,
actualVersion: installed.actualVersion, actualVersion: installed.actualVersion,
installedPath: installed.path,
isCurrent,
isInstalled: true,
name: displayName,
size: undefined,
version: installed.version,
}); });
} }
}); });
return backends.sort((a, b) => { return backends.toSorted((a, b) => {
if (a.isInstalled && !b.isInstalled) return -1; if (a.isInstalled && !b.isInstalled) {
if (!a.isInstalled && b.isInstalled) return 1; return -1;
}
if (!a.isInstalled && b.isInstalled) {
return 1;
}
return 0; return 0;
}); });
@ -144,11 +149,13 @@ export const BackendsTab = () => {
const handleDownload = async (backend: BackendInfo) => { const handleDownload = async (backend: BackendInfo) => {
const download = availableDownloads.find((d) => d.name === backend.name); const download = availableDownloads.find((d) => d.name === backend.name);
if (!download) return; if (!download) {
return;
}
await handleDownloadFromStore({ await handleDownloadFromStore({
item: download,
isUpdate: false, isUpdate: false,
item: download,
wasCurrentBinary: false, wasCurrentBinary: false,
}); });
@ -157,13 +164,15 @@ export const BackendsTab = () => {
const handleUpdate = async (backend: BackendInfo) => { const handleUpdate = async (backend: BackendInfo) => {
const download = availableDownloads.find((d) => d.name === backend.name); const download = availableDownloads.find((d) => d.name === backend.name);
if (!download) return; if (!download) {
return;
}
await handleDownloadFromStore({ await handleDownloadFromStore({
item: download,
isUpdate: true, isUpdate: true,
wasCurrentBinary: backend.isCurrent, item: download,
oldBackendPath: backend.installedPath, oldBackendPath: backend.installedPath,
wasCurrentBinary: backend.isCurrent,
}); });
await loadInstalledBackends(); await loadInstalledBackends();
@ -171,20 +180,24 @@ export const BackendsTab = () => {
const handleRedownload = async (backend: BackendInfo) => { const handleRedownload = async (backend: BackendInfo) => {
const download = availableDownloads.find((d) => d.name === backend.name); const download = availableDownloads.find((d) => d.name === backend.name);
if (!download) return; if (!download) {
return;
}
await handleDownloadFromStore({ await handleDownloadFromStore({
item: download,
isUpdate: true, isUpdate: true,
wasCurrentBinary: backend.isCurrent, item: download,
oldBackendPath: backend.installedPath, oldBackendPath: backend.installedPath,
wasCurrentBinary: backend.isCurrent,
}); });
await loadInstalledBackends(); await loadInstalledBackends();
}; };
const handleDelete = async (backend: BackendInfo) => { const handleDelete = async (backend: BackendInfo) => {
if (!backend.installedPath || backend.isCurrent) return; if (!backend.installedPath || backend.isCurrent) {
return;
}
const result = await window.electronAPI.kobold.deleteRelease(backend.installedPath); const result = await window.electronAPI.kobold.deleteRelease(backend.installedPath);
if (result.success) { if (result.success) {
@ -193,7 +206,9 @@ export const BackendsTab = () => {
}; };
const makeCurrent = (backend: BackendInfo) => { const makeCurrent = (backend: BackendInfo) => {
if (!backend.installedPath) return; if (!backend.installedPath) {
return;
}
const targetBackend = installedBackends.find((b) => b.path === backend.installedPath); const targetBackend = installedBackends.find((b) => b.path === backend.installedPath);
if (targetBackend) { if (targetBackend) {

View file

@ -1,6 +1,7 @@
import { Anchor, Box, Button, Group, rem, Stack, Text } from '@mantine/core'; import { Anchor, Box, Button, Group, Stack, Text, rem } from '@mantine/core';
import { Image, Monitor } from 'lucide-react'; import { Image, Monitor } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { Modal } from '@/components/Modal'; import { Modal } from '@/components/Modal';
import { Select } from '@/components/Select'; import { Select } from '@/components/Select';
import { FRONTENDS } from '@/constants'; import { FRONTENDS } from '@/constants';
@ -41,16 +42,16 @@ export const FrontendInterfaceSelector = ({
const frontendConfigs: FrontendConfig[] = useMemo( const frontendConfigs: FrontendConfig[] = useMemo(
() => [ () => [
{ {
value: 'llamacpp',
label: FRONTENDS.LLAMA_CPP, label: FRONTENDS.LLAMA_CPP,
value: 'llamacpp',
}, },
{ {
value: 'koboldcpp',
label: FRONTENDS.KOBOLDAI_LITE, label: FRONTENDS.KOBOLDAI_LITE,
value: 'koboldcpp',
}, },
{ {
value: 'sillytavern',
label: FRONTENDS.SILLYTAVERN, label: FRONTENDS.SILLYTAVERN,
requirementCheck: () => window.electronAPI.dependencies.isNpxAvailable(),
requirements: [ requirements: [
{ {
id: 'nodejs', id: 'nodejs',
@ -58,11 +59,11 @@ export const FrontendInterfaceSelector = ({
url: 'https://nodejs.org/', url: 'https://nodejs.org/',
}, },
], ],
requirementCheck: () => window.electronAPI.dependencies.isNpxAvailable(), value: 'sillytavern',
}, },
{ {
value: 'openwebui',
label: FRONTENDS.OPENWEBUI, label: FRONTENDS.OPENWEBUI,
requirementCheck: () => window.electronAPI.dependencies.isUvAvailable(),
requirements: [ requirements: [
{ {
id: 'uv', id: 'uv',
@ -70,10 +71,10 @@ export const FrontendInterfaceSelector = ({
url: 'https://docs.astral.sh/uv/getting-started/installation/', url: 'https://docs.astral.sh/uv/getting-started/installation/',
}, },
], ],
requirementCheck: () => window.electronAPI.dependencies.isUvAvailable(), value: 'openwebui',
}, },
], ],
[] [],
); );
const checkAllFrontendRequirements = useCallback(async () => { const checkAllFrontendRequirements = useCallback(async () => {
@ -91,7 +92,7 @@ export const FrontendInterfaceSelector = ({
setFrontendRequirements(requirementResults); setFrontendRequirements(requirementResults);
const currentFrontendConfig = frontendConfigs.find( const currentFrontendConfig = frontendConfigs.find(
(config) => config.value === frontendPreference (config) => config.value === frontendPreference,
); );
if (currentFrontendConfig && !requirementResults.get(frontendPreference)) { if (currentFrontendConfig && !requirementResults.get(frontendPreference)) {
setFrontendPreference('llamacpp'); setFrontendPreference('llamacpp');
@ -103,7 +104,9 @@ export const FrontendInterfaceSelector = ({
const getUnmetRequirements = () => { const getUnmetRequirements = () => {
const selectedConfig = getSelectedFrontendConfig(); const selectedConfig = getSelectedFrontendConfig();
if (!selectedConfig || !selectedConfig.requirements) return []; if (!selectedConfig || !selectedConfig.requirements) {
return [];
}
const isAvailable = frontendRequirements.get(selectedConfig.value) ?? true; const isAvailable = frontendRequirements.get(selectedConfig.value) ?? true;
return isAvailable ? [] : selectedConfig.requirements; return isAvailable ? [] : selectedConfig.requirements;
@ -111,7 +114,9 @@ export const FrontendInterfaceSelector = ({
const getUnmetRequirementsForFrontend = (frontendValue: string) => { const getUnmetRequirementsForFrontend = (frontendValue: string) => {
const config = frontendConfigs.find((c) => c.value === frontendValue); const config = frontendConfigs.find((c) => c.value === frontendValue);
if (!config || !config.requirements) return []; if (!config || !config.requirements) {
return [];
}
const isAvailable = frontendRequirements.get(frontendValue) ?? true; const isAvailable = frontendRequirements.get(frontendValue) ?? true;
return isAvailable ? [] : config.requirements; return isAvailable ? [] : config.requirements;
@ -135,7 +140,7 @@ export const FrontendInterfaceSelector = ({
const renderDisabledFrontendWarnings = () => { const renderDisabledFrontendWarnings = () => {
const disabledFrontends = frontendConfigs.filter( const disabledFrontends = frontendConfigs.filter(
(config) => !isFrontendAvailable(config.value) (config) => !isFrontendAvailable(config.value),
); );
if (disabledFrontends.length === 0) { if (disabledFrontends.length === 0) {
@ -155,12 +160,12 @@ export const FrontendInterfaceSelector = ({
return ( return (
<Box mt="sm"> <Box mt="sm">
{Array.from(requirementGroups.entries()).map(([reqKey, frontendLabels]) => { {[...requirementGroups.entries()].map(([reqKey, frontendLabels]) => {
const firstDisabledFrontend = disabledFrontends.find( const firstDisabledFrontend = disabledFrontends.find(
(config) => (config) =>
getUnmetRequirementsForFrontend(config.value) getUnmetRequirementsForFrontend(config.value)
.map((req) => req.id) .map((req) => req.id)
.join(',') === reqKey .join(',') === reqKey,
); );
const unmetReqs = firstDisabledFrontend const unmetReqs = firstDisabledFrontend
? getUnmetRequirementsForFrontend(firstDisabledFrontend.value) ? getUnmetRequirementsForFrontend(firstDisabledFrontend.value)
@ -253,11 +258,11 @@ export const FrontendInterfaceSelector = ({
onChange={handleFrontendPreferenceChange} onChange={handleFrontendPreferenceChange}
disabled={isOnInterfaceScreen} disabled={isOnInterfaceScreen}
data={frontendConfigs.map((config) => ({ data={frontendConfigs.map((config) => ({
value: config.value,
label: config.label,
disabled: !isFrontendAvailable(config.value), disabled: !isFrontendAvailable(config.value),
label: config.label,
value: config.value,
}))} }))}
leftSection={<Monitor style={{ width: rem(16), height: rem(16) }} />} leftSection={<Monitor style={{ height: rem(16), width: rem(16) }} />}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
@ -322,10 +327,10 @@ export const FrontendInterfaceSelector = ({
onChange={handleImageGenerationFrontendChange} onChange={handleImageGenerationFrontendChange}
disabled={isOnInterfaceScreen} disabled={isOnInterfaceScreen}
data={[ data={[
{ value: 'match', label: 'Match Frontend' }, { label: 'Match Frontend', value: 'match' },
{ value: 'builtin', label: 'Built-in' }, { label: 'Built-in', value: 'builtin' },
]} ]}
leftSection={<Image style={{ width: rem(16), height: rem(16) }} />} leftSection={<Image style={{ height: rem(16), width: rem(16) }} />}
/> />
</div> </div>
</> </>

View file

@ -1,5 +1,6 @@
import { Stack, Text } from '@mantine/core'; import { Stack, Text } from '@mantine/core';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Switch } from '@/components/Switch'; import { Switch } from '@/components/Switch';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';

View file

@ -1,4 +1,4 @@
import { Group, rem, Tabs, Text } from '@mantine/core'; import { Group, Tabs, Text, rem } from '@mantine/core';
import { import {
GitBranch, GitBranch,
Info, Info,
@ -9,6 +9,7 @@ import {
Wrench, Wrench,
} from 'lucide-react'; } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Modal } from '@/components/Modal'; import { Modal } from '@/components/Modal';
import { AboutTab } from '@/components/settings/AboutTab'; import { AboutTab } from '@/components/settings/AboutTab';
import { AppearanceTab } from '@/components/settings/AppearanceTab'; import { AppearanceTab } from '@/components/settings/AppearanceTab';
@ -72,16 +73,16 @@ export const SettingsModal = ({
orientation="vertical" orientation="vertical"
variant="pills" variant="pills"
styles={{ styles={{
root: {
flex: 1,
minHeight: 0,
},
panel: { panel: {
height: '100%', height: '100%',
overflow: 'auto', overflow: 'auto',
paddingLeft: '1.5rem', paddingLeft: '1.5rem',
paddingRight: '1.5rem', paddingRight: '1.5rem',
}, },
root: {
flex: 1,
minHeight: 0,
},
tabLabel: { tabLabel: {
textAlign: 'left', textAlign: 'left',
justifyContent: 'flex-start', justifyContent: 'flex-start',
@ -91,39 +92,39 @@ export const SettingsModal = ({
<Tabs.List> <Tabs.List>
<Tabs.Tab <Tabs.Tab
value="general" value="general"
leftSection={<SlidersHorizontal style={{ width: rem(16), height: rem(16) }} />} leftSection={<SlidersHorizontal style={{ height: rem(16), width: rem(16) }} />}
> >
General General
</Tabs.Tab> </Tabs.Tab>
{showBackendsTab && ( {showBackendsTab && (
<Tabs.Tab <Tabs.Tab
value="backends" value="backends"
leftSection={<GitBranch style={{ width: rem(16), height: rem(16) }} />} leftSection={<GitBranch style={{ height: rem(16), width: rem(16) }} />}
> >
Backends Backends
</Tabs.Tab> </Tabs.Tab>
)} )}
<Tabs.Tab <Tabs.Tab
value="appearance" value="appearance"
leftSection={<Palette style={{ width: rem(16), height: rem(16) }} />} leftSection={<Palette style={{ height: rem(16), width: rem(16) }} />}
> >
Appearance Appearance
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab <Tabs.Tab
value="system" value="system"
leftSection={<Monitor style={{ width: rem(16), height: rem(16) }} />} leftSection={<Monitor style={{ height: rem(16), width: rem(16) }} />}
> >
System System
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab <Tabs.Tab
value="troubleshooting" value="troubleshooting"
leftSection={<Wrench style={{ width: rem(16), height: rem(16) }} />} leftSection={<Wrench style={{ height: rem(16), width: rem(16) }} />}
> >
Troubleshooting Troubleshooting
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab <Tabs.Tab
value="about" value="about"
leftSection={<Info style={{ width: rem(16), height: rem(16) }} />} leftSection={<Info style={{ height: rem(16), width: rem(16) }} />}
> >
About About
</Tabs.Tab> </Tabs.Tab>

View file

@ -1,5 +1,6 @@
import { Center, Stack, Text } from '@mantine/core'; import { Center, Stack, Text } from '@mantine/core';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { InfoCard } from '@/components/InfoCard'; import { InfoCard } from '@/components/InfoCard';
import type { SystemVersionInfo } from '@/types/electron'; import type { SystemVersionInfo } from '@/types/electron';
import type { HardwareInfo } from '@/types/hardware'; import type { HardwareInfo } from '@/types/hardware';
@ -22,7 +23,7 @@ export const SystemTab = () => {
]); ]);
setVersionInfo(info); setVersionInfo(info);
setKoboldVersion(currentBackend?.version || null); setKoboldVersion(currentBackend?.version ?? null);
const [cpu, gpu, gpuCapabilities, gpuMemory, systemMemory] = await Promise.all([ const [cpu, gpu, gpuCapabilities, gpuMemory, systemMemory] = await Promise.all([
window.electronAPI.kobold.detectCPU(), window.electronAPI.kobold.detectCPU(),

View file

@ -1,4 +1,4 @@
import { Button, Group, rem, Stack, Text, TextInput } from '@mantine/core'; import { Button, Group, Stack, Text, TextInput, rem } from '@mantine/core';
import { ExternalLink, Folder, FolderOpen, Monitor } from 'lucide-react'; import { ExternalLink, Folder, FolderOpen, Monitor } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@ -44,12 +44,12 @@ export const TroubleshootingTab = () => {
readOnly readOnly
placeholder="Default installation directory" placeholder="Default installation directory"
style={{ flex: 1 }} style={{ flex: 1 }}
leftSection={<Folder style={{ width: rem(16), height: rem(16) }} />} leftSection={<Folder style={{ height: rem(16), width: rem(16) }} />}
/> />
<Button <Button
variant="outline" variant="outline"
onClick={() => void handleSelectInstallDir()} onClick={() => void handleSelectInstallDir()}
leftSection={<FolderOpen style={{ width: rem(16), height: rem(16) }} />} leftSection={<FolderOpen style={{ height: rem(16), width: rem(16) }} />}
> >
Browse Browse
</Button> </Button>
@ -57,7 +57,7 @@ export const TroubleshootingTab = () => {
variant="outline" variant="outline"
onClick={() => void handleOpenInstallDir()} onClick={() => void handleOpenInstallDir()}
disabled={!installDir} disabled={!installDir}
leftSection={<ExternalLink style={{ width: rem(16), height: rem(16) }} />} leftSection={<ExternalLink style={{ height: rem(16), width: rem(16) }} />}
> >
Open Open
</Button> </Button>
@ -75,7 +75,7 @@ export const TroubleshootingTab = () => {
<Button <Button
variant="outline" variant="outline"
size="compact-sm" size="compact-sm"
leftSection={<FolderOpen style={{ width: rem(16), height: rem(16) }} />} leftSection={<FolderOpen style={{ height: rem(16), width: rem(16) }} />}
onClick={() => void window.electronAPI.app.showLogsFolder()} onClick={() => void window.electronAPI.app.showLogsFolder()}
> >
Show Logs Show Logs
@ -83,7 +83,7 @@ export const TroubleshootingTab = () => {
<Button <Button
variant="outline" variant="outline"
size="compact-sm" size="compact-sm"
leftSection={<Monitor style={{ width: rem(16), height: rem(16) }} />} leftSection={<Monitor style={{ height: rem(16), width: rem(16) }} />}
onClick={() => void window.electronAPI.app.viewConfigFile()} onClick={() => void window.electronAPI.app.viewConfigFile()}
> >
View Config View Config

View file

@ -13,38 +13,38 @@ export interface ImageModelPreset {
export const IMAGE_MODEL_PRESETS: readonly ImageModelPreset[] = [ export const IMAGE_MODEL_PRESETS: readonly ImageModelPreset[] = [
{ {
name: 'Z-Image', name: 'Z-Image',
sdmodel: `${HUGGINGFACE_BASE_URL}/leejet/Z-Image-Turbo-GGUF/resolve/main/z_image_turbo-Q4_0.gguf`,
sdt5xxl: '',
sdclipl: `${HUGGINGFACE_BASE_URL}/unsloth/Qwen3-4B-Instruct-2507-GGUF/resolve/main/Qwen3-4B-Instruct-2507-Q4_K_S.gguf`,
sdclipg: '', sdclipg: '',
sdclipl: `${HUGGINGFACE_BASE_URL}/unsloth/Qwen3-4B-Instruct-2507-GGUF/resolve/main/Qwen3-4B-Instruct-2507-Q4_K_S.gguf`,
sdmodel: `${HUGGINGFACE_BASE_URL}/leejet/Z-Image-Turbo-GGUF/resolve/main/z_image_turbo-Q4_0.gguf`,
sdphotomaker: '', sdphotomaker: '',
sdt5xxl: '',
sdvae: `${HUGGINGFACE_BASE_URL}/koboldcpp/GGUFDumps/resolve/main/flux1vae.safetensors`, sdvae: `${HUGGINGFACE_BASE_URL}/koboldcpp/GGUFDumps/resolve/main/flux1vae.safetensors`,
}, },
{ {
name: 'FLUX.1', name: 'FLUX.1',
sdmodel: `${HUGGINGFACE_BASE_URL}/bullerwins/FLUX.1-Kontext-dev-GGUF/resolve/main/flux1-kontext-dev-Q4_K_S.gguf`,
sdt5xxl: `${HUGGINGFACE_BASE_URL}/camenduru/FLUX.1-dev/resolve/main/t5xxl_fp8_e4m3fn.safetensors`,
sdclipl: `${HUGGINGFACE_BASE_URL}/camenduru/FLUX.1-dev/resolve/main/clip_l.safetensors`,
sdclipg: '', sdclipg: '',
sdclipl: `${HUGGINGFACE_BASE_URL}/camenduru/FLUX.1-dev/resolve/main/clip_l.safetensors`,
sdmodel: `${HUGGINGFACE_BASE_URL}/bullerwins/FLUX.1-Kontext-dev-GGUF/resolve/main/flux1-kontext-dev-Q4_K_S.gguf`,
sdphotomaker: '', sdphotomaker: '',
sdt5xxl: `${HUGGINGFACE_BASE_URL}/camenduru/FLUX.1-dev/resolve/main/t5xxl_fp8_e4m3fn.safetensors`,
sdvae: `${HUGGINGFACE_BASE_URL}/camenduru/FLUX.1-dev/resolve/main/ae.safetensors`, sdvae: `${HUGGINGFACE_BASE_URL}/camenduru/FLUX.1-dev/resolve/main/ae.safetensors`,
}, },
{ {
name: 'Chroma', name: 'Chroma',
sdmodel: `${HUGGINGFACE_BASE_URL}/silveroxides/Chroma-GGUF/resolve/main/chroma-unlocked-v45/chroma-unlocked-v45-Q4_0.gguf`,
sdt5xxl: `${HUGGINGFACE_BASE_URL}/camenduru/FLUX.1-dev/resolve/main/t5xxl_fp8_e4m3fn.safetensors`,
sdclipl: `${HUGGINGFACE_BASE_URL}/camenduru/FLUX.1-dev/resolve/main/clip_l.safetensors`,
sdclipg: '', sdclipg: '',
sdclipl: `${HUGGINGFACE_BASE_URL}/camenduru/FLUX.1-dev/resolve/main/clip_l.safetensors`,
sdmodel: `${HUGGINGFACE_BASE_URL}/silveroxides/Chroma-GGUF/resolve/main/chroma-unlocked-v45/chroma-unlocked-v45-Q4_0.gguf`,
sdphotomaker: '', sdphotomaker: '',
sdt5xxl: `${HUGGINGFACE_BASE_URL}/camenduru/FLUX.1-dev/resolve/main/t5xxl_fp8_e4m3fn.safetensors`,
sdvae: `${HUGGINGFACE_BASE_URL}/lodestones/Chroma/resolve/main/ae.safetensors`, sdvae: `${HUGGINGFACE_BASE_URL}/lodestones/Chroma/resolve/main/ae.safetensors`,
}, },
{ {
name: 'Qwen Image Edit 2509', name: 'Qwen Image Edit 2509',
sdmodel: `${HUGGINGFACE_BASE_URL}/QuantStack/Qwen-Image-Edit-2509-GGUF/resolve/main/Qwen-Image-Edit-2509-Q4_K_S.gguf`,
sdt5xxl: '',
sdclipl: `${HUGGINGFACE_BASE_URL}/mradermacher/Qwen2.5-VL-7B-Instruct-GGUF/resolve/main/Qwen2.5-VL-7B-Instruct.Q4_K_S.gguf`,
sdclipg: `${HUGGINGFACE_BASE_URL}/mradermacher/Qwen2.5-VL-7B-Instruct-GGUF/resolve/main/Qwen2.5-VL-7B-Instruct.mmproj-Q8_0.gguf`, sdclipg: `${HUGGINGFACE_BASE_URL}/mradermacher/Qwen2.5-VL-7B-Instruct-GGUF/resolve/main/Qwen2.5-VL-7B-Instruct.mmproj-Q8_0.gguf`,
sdclipl: `${HUGGINGFACE_BASE_URL}/mradermacher/Qwen2.5-VL-7B-Instruct-GGUF/resolve/main/Qwen2.5-VL-7B-Instruct.Q4_K_S.gguf`,
sdmodel: `${HUGGINGFACE_BASE_URL}/QuantStack/Qwen-Image-Edit-2509-GGUF/resolve/main/Qwen-Image-Edit-2509-Q4_K_S.gguf`,
sdphotomaker: '', sdphotomaker: '',
sdt5xxl: '',
sdvae: `${HUGGINGFACE_BASE_URL}/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/vae/qwen_image_vae.safetensors`, sdvae: `${HUGGINGFACE_BASE_URL}/Comfy-Org/Qwen-Image_ComfyUI/resolve/main/split_files/vae/qwen_image_vae.safetensors`,
}, },
] as const; ] as const;

View file

@ -10,8 +10,8 @@ export const HUGGINGFACE_BASE_URL = 'https://huggingface.co';
export const SERVER_READY_SIGNALS = { export const SERVER_READY_SIGNALS = {
KOBOLDCPP: 'Please connect to custom endpoint at', KOBOLDCPP: 'Please connect to custom endpoint at',
SILLYTAVERN: 'SillyTavern is listening on',
OPENWEBUI: 'Started server process', OPENWEBUI: 'Started server process',
SILLYTAVERN: 'SillyTavern is listening on',
} as const; } as const;
export const DEFAULT_CONTEXT_SIZE = 4096; export const DEFAULT_CONTEXT_SIZE = 4096;
@ -24,12 +24,12 @@ export const SILLYTAVERN = {
HOST: 'localhost', HOST: 'localhost',
PORT: 3000, PORT: 3000,
PROXY_PORT: 3001, PROXY_PORT: 3001,
get URL() {
return `http://${this.HOST}:${this.PORT}` as const;
},
get PROXY_URL() { get PROXY_URL() {
return `http://${this.HOST}:${this.PROXY_PORT}` as const; return `http://${this.HOST}:${this.PROXY_PORT}` as const;
}, },
get URL() {
return `http://${this.HOST}:${this.PORT}` as const;
},
} as const; } as const;
export const OPENWEBUI = { export const OPENWEBUI = {
@ -41,29 +41,29 @@ export const OPENWEBUI = {
} as const; } as const;
export const GITHUB_API = { export const GITHUB_API = {
BASE_URL: 'https://api.github.com',
GITHUB_BASE_URL: 'https://github.com',
KOBOLDCPP_REPO: 'LostRuins/koboldcpp',
KOBOLDCPP_ROCM_REPO: 'YellowRoseCx/koboldcpp-rocm',
GERBIL_REPO: 'lone-cloud/gerbil',
CLOUDFLARED_REPO: 'cloudflare/cloudflared',
get LATEST_RELEASE_URL() {
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_REPO}/releases/latest` as const;
},
get ALL_RELEASES_URL() { get ALL_RELEASES_URL() {
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_REPO}/releases` as const; return `${this.BASE_URL}/repos/${this.KOBOLDCPP_REPO}/releases` as const;
}, },
get ROCM_LATEST_RELEASE_URL() { BASE_URL: 'https://api.github.com',
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_ROCM_REPO}/releases/latest` as const; get CLOUDFLARED_LATEST_RELEASE_URL() {
return `${this.BASE_URL}/repos/${this.CLOUDFLARED_REPO}/releases/latest` as const;
}, },
CLOUDFLARED_REPO: 'cloudflare/cloudflared',
get GERBIL_GITHUB_URL() { get GERBIL_GITHUB_URL() {
return `${this.GITHUB_BASE_URL}/${this.GERBIL_REPO}` as const; return `${this.GITHUB_BASE_URL}/${this.GERBIL_REPO}` as const;
}, },
get GERBIL_LATEST_RELEASE_URL() { get GERBIL_LATEST_RELEASE_URL() {
return `${this.BASE_URL}/repos/${this.GERBIL_REPO}/releases/latest` as const; return `${this.BASE_URL}/repos/${this.GERBIL_REPO}/releases/latest` as const;
}, },
get CLOUDFLARED_LATEST_RELEASE_URL() { GERBIL_REPO: 'lone-cloud/gerbil',
return `${this.BASE_URL}/repos/${this.CLOUDFLARED_REPO}/releases/latest` as const; GITHUB_BASE_URL: 'https://github.com',
KOBOLDCPP_REPO: 'LostRuins/koboldcpp',
KOBOLDCPP_ROCM_REPO: 'YellowRoseCx/koboldcpp-rocm',
get LATEST_RELEASE_URL() {
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_REPO}/releases/latest` as const;
},
get ROCM_LATEST_RELEASE_URL() {
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_ROCM_REPO}/releases/latest` as const;
}, },
getCloudflaredDownloadUrl(version: string, filename: string) { getCloudflaredDownloadUrl(version: string, filename: string) {
return `${this.GITHUB_BASE_URL}/${this.CLOUDFLARED_REPO}/releases/download/${version}/${filename}` as const; return `${this.GITHUB_BASE_URL}/${this.CLOUDFLARED_REPO}/releases/download/${version}/${filename}` as const;
@ -71,9 +71,9 @@ export const GITHUB_API = {
} as const; } as const;
export const ASSET_SUFFIXES = { export const ASSET_SUFFIXES = {
ROCM: 'rocm',
NOCUDA: 'nocuda', NOCUDA: 'nocuda',
OLDPC: 'oldpc', OLDPC: 'oldpc',
ROCM: 'rocm',
} as const; } as const;
export const ROCM = { export const ROCM = {
@ -87,16 +87,16 @@ export const ROCM = {
export const FRONTENDS = { export const FRONTENDS = {
KOBOLDAI_LITE: 'KoboldAI Lite', KOBOLDAI_LITE: 'KoboldAI Lite',
LLAMA_CPP: 'llama.cpp', LLAMA_CPP: 'llama.cpp',
STABLE_UI: 'Stable UI',
SILLYTAVERN: 'SillyTavern',
OPENWEBUI: 'Open WebUI', OPENWEBUI: 'Open WebUI',
SILLYTAVERN: 'SillyTavern',
STABLE_UI: 'Stable UI',
} as const; } as const;
export const ZOOM = { export const ZOOM = {
MIN_LEVEL: -3,
MAX_LEVEL: 3,
MIN_PERCENTAGE: 25,
MAX_PERCENTAGE: 300,
DEFAULT_LEVEL: 0, DEFAULT_LEVEL: 0,
DEFAULT_PERCENTAGE: 100, DEFAULT_PERCENTAGE: 100,
MAX_LEVEL: 3,
MAX_PERCENTAGE: 300,
MIN_LEVEL: -3,
MIN_PERCENTAGE: 25,
} as const; } as const;

View file

@ -1,6 +1,6 @@
export const DEFAULT_NOTEPAD_POSITION = { export const DEFAULT_NOTEPAD_POSITION = {
width: 400,
height: 400, height: 400,
width: 400,
}; };
export const NOTEPAD_MIN_WIDTH = 300; export const NOTEPAD_MIN_WIDTH = 300;

View file

@ -1,4 +1,5 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { GITHUB_API } from '@/constants'; import { GITHUB_API } from '@/constants';
import { compareVersions } from '@/utils/version'; import { compareVersions } from '@/utils/version';
@ -30,7 +31,9 @@ export const useAppUpdateChecker = () => {
}, []); }, []);
const downloadUpdate = useCallback(async () => { const downloadUpdate = useCallback(async () => {
if (!canAutoUpdate) return; if (!canAutoUpdate) {
return;
}
setIsDownloading(true); setIsDownloading(true);
@ -59,7 +62,7 @@ export const useAppUpdateChecker = () => {
const currentVersion = await window.electronAPI.app.getVersion(); const currentVersion = await window.electronAPI.app.getVersion();
const response = await fetch( const response = await fetch(
`${GITHUB_API.BASE_URL}/repos/${GITHUB_API.GERBIL_REPO}/releases/latest` `${GITHUB_API.BASE_URL}/repos/${GITHUB_API.GERBIL_REPO}/releases/latest`,
); );
if (!response.ok) { if (!response.ok) {
@ -67,7 +70,7 @@ export const useAppUpdateChecker = () => {
} }
const release = await response.json(); const release = await response.json();
const latestVersion = release.tag_name?.replace(/^v/, '') || ''; const latestVersion = release.tag_name?.replace(/^v/, '') ?? '';
if (!latestVersion) { if (!latestVersion) {
throw new Error('Invalid release data'); throw new Error('Invalid release data');
@ -75,18 +78,18 @@ export const useAppUpdateChecker = () => {
const hasUpdate = compareVersions(latestVersion, currentVersion) > 0; const hasUpdate = compareVersions(latestVersion, currentVersion) > 0;
const updateInfo: AppUpdateInfo = { const info: AppUpdateInfo = {
currentVersion, currentVersion,
hasUpdate,
latestVersion, latestVersion,
releaseUrl: release.html_url, releaseUrl: release.html_url,
hasUpdate,
}; };
setUpdateInfo(updateInfo); setUpdateInfo(info);
} catch (error) { } catch (error) {
window.electronAPI.logs.logError( window.electronAPI.logs.logError(
'Failed to check for app updates', 'Failed to check for app updates',
error instanceof Error ? error : undefined error instanceof Error ? error : undefined,
); );
} }
}, []); }, []);
@ -96,12 +99,12 @@ export const useAppUpdateChecker = () => {
}, [checkForAppUpdates]); }, [checkForAppUpdates]);
return { return {
releaseUrl: updateInfo?.releaseUrl,
hasUpdate: updateInfo?.hasUpdate || false,
canAutoUpdate, canAutoUpdate,
isUpdateDownloaded,
isDownloading,
downloadUpdate, downloadUpdate,
hasUpdate: updateInfo?.hasUpdate ?? false,
installUpdate, installUpdate,
isDownloading,
isUpdateDownloaded,
releaseUrl: updateInfo?.releaseUrl,
}; };
}; };

View file

@ -1,4 +1,5 @@
import { useCallback, useRef, useState } from 'react'; import { useCallback, useRef, useState } from 'react';
import { HUGGINGFACE_BASE_URL } from '@/constants'; import { HUGGINGFACE_BASE_URL } from '@/constants';
import type { import type {
HuggingFaceFileInfo, HuggingFaceFileInfo,
@ -49,7 +50,7 @@ const extractParamSize = (name: string) => {
const extractBaseModelId = (tags: string[]) => { const extractBaseModelId = (tags: string[]) => {
const baseModelTag = tags.find( const baseModelTag = tags.find(
(tag) => tag.startsWith('base_model:') && !tag.includes('quantized') (tag) => tag.startsWith('base_model:') && !tag.includes('quantized'),
); );
return baseModelTag?.replace('base_model:', ''); return baseModelTag?.replace('base_model:', '');
}; };
@ -69,7 +70,9 @@ const formatParamCount = (paramCount: number) => {
const fetchBaseModelParams = async (baseModelId: string) => { const fetchBaseModelParams = async (baseModelId: string) => {
try { try {
const response = await fetch(`${HF_API_BASE}/models/${baseModelId}`); const response = await fetch(`${HF_API_BASE}/models/${baseModelId}`);
if (!response.ok) return undefined; if (!response.ok) {
return undefined;
}
const data: HFApiBaseModel = await response.json(); const data: HFApiBaseModel = await response.json();
return data.safetensors?.total; return data.safetensors?.total;
} catch { } catch {
@ -109,9 +112,15 @@ export const useHuggingFaceSearch = (initialParams: HuggingFaceSearchParams) =>
const searchQuery = query !== undefined ? query.trim() || undefined : searchParams.search; const searchQuery = query !== undefined ? query.trim() || undefined : searchParams.search;
const params = new URLSearchParams(); const params = new URLSearchParams();
if (searchQuery) params.set('search', searchQuery); if (searchQuery) {
if (searchParams.pipelineTag) params.set('pipeline_tag', searchParams.pipelineTag); params.set('search', searchQuery);
if (searchParams.filter) params.set('filter', searchParams.filter); }
if (searchParams.pipelineTag) {
params.set('pipeline_tag', searchParams.pipelineTag);
}
if (searchParams.filter) {
params.set('filter', searchParams.filter);
}
params.set('sort', sort); params.set('sort', sort);
params.set('limit', String(MODELS_PER_PAGE)); params.set('limit', String(MODELS_PER_PAGE));
params.set('full', 'false'); params.set('full', 'false');
@ -142,16 +151,16 @@ export const useHuggingFaceSearch = (initialParams: HuggingFaceSearchParams) =>
} }
return { return {
id: model.id,
name,
author, author,
downloads: model.downloads ?? 0, downloads: model.downloads ?? 0,
likes: model.likes ?? 0,
updatedAt: new Date(model.lastModified),
gated: model.gated ?? false, gated: model.gated ?? false,
id: model.id,
likes: model.likes ?? 0,
name,
paramSize, paramSize,
updatedAt: new Date(model.lastModified),
}; };
}) }),
); );
setModels(results); setModels(results);
@ -165,18 +174,20 @@ export const useHuggingFaceSearch = (initialParams: HuggingFaceSearchParams) =>
setLoading(false); setLoading(false);
} }
}, },
[searchParams, sortBy] [searchParams, sortBy],
); );
const changeSortOrder = useCallback( const changeSortOrder = useCallback(
(sort: HuggingFaceSortOption, query?: string) => { (sort: HuggingFaceSortOption, query?: string) => {
void searchModels(query, true, sort); void searchModels(query, true, sort);
}, },
[searchModels] [searchModels],
); );
const loadMoreModels = useCallback(async () => { const loadMoreModels = useCallback(async () => {
if (loading || !hasMore) return; if (loading || !hasMore) {
return;
}
setLoading(true); setLoading(true);
@ -187,9 +198,15 @@ export const useHuggingFaceSearch = (initialParams: HuggingFaceSearchParams) =>
: searchParams.search; : searchParams.search;
const params = new URLSearchParams(); const params = new URLSearchParams();
if (searchQuery) params.set('search', searchQuery); if (searchQuery) {
if (searchParams.pipelineTag) params.set('pipeline_tag', searchParams.pipelineTag); params.set('search', searchQuery);
if (searchParams.filter) params.set('filter', searchParams.filter); }
if (searchParams.pipelineTag) {
params.set('pipeline_tag', searchParams.pipelineTag);
}
if (searchParams.filter) {
params.set('filter', searchParams.filter);
}
params.set('sort', sortBy); params.set('sort', sortBy);
params.set('limit', String(MODELS_PER_PAGE)); params.set('limit', String(MODELS_PER_PAGE));
params.set('skip', String(pageRef.current * MODELS_PER_PAGE)); params.set('skip', String(pageRef.current * MODELS_PER_PAGE));
@ -220,16 +237,16 @@ export const useHuggingFaceSearch = (initialParams: HuggingFaceSearchParams) =>
} }
return { return {
id: model.id,
name,
author, author,
downloads: model.downloads ?? 0, downloads: model.downloads ?? 0,
likes: model.likes ?? 0,
updatedAt: new Date(model.lastModified),
gated: model.gated ?? false, gated: model.gated ?? false,
id: model.id,
likes: model.likes ?? 0,
name,
paramSize, paramSize,
updatedAt: new Date(model.lastModified),
}; };
}) }),
); );
setModels((prev) => [...prev, ...results]); setModels((prev) => [...prev, ...results]);
@ -283,13 +300,13 @@ export const useHuggingFaceSearch = (initialParams: HuggingFaceSearchParams) =>
setLoadingFiles(false); setLoadingFiles(false);
} }
}, },
[searchParams?.filter] [searchParams?.filter],
); );
const getFileDownloadUrl = useCallback( const getFileDownloadUrl = useCallback(
(modelId: string, filePath: string) => (modelId: string, filePath: string) =>
`${HUGGINGFACE_BASE_URL}/${modelId}/resolve/main/${filePath}`, `${HUGGINGFACE_BASE_URL}/${modelId}/resolve/main/${filePath}`,
[] [],
); );
const reset = useCallback(() => { const reset = useCallback(() => {
@ -302,31 +319,34 @@ export const useHuggingFaceSearch = (initialParams: HuggingFaceSearchParams) =>
}, []); }, []);
return { return {
models, changeSortOrder,
error,
files, files,
getFileDownloadUrl,
hasMore,
loadModelFiles,
loadMoreModels,
loading, loading,
loadingFiles, loadingFiles,
error, models,
hasMore, reset,
searchModels,
searchParams,
selectedModel, selectedModel,
sortBy, sortBy,
searchParams,
searchModels,
loadMoreModels,
loadModelFiles,
changeSortOrder,
getFileDownloadUrl,
reset,
}; };
}; };
const getExtensionsForLibrary = (filter?: string) => { const getExtensionsForLibrary = (filter?: string) => {
switch (filter) { switch (filter) {
case 'gguf': case 'gguf': {
return ['.gguf']; return ['.gguf'];
case 'safetensors': }
case 'safetensors': {
return ['.safetensors']; return ['.safetensors'];
default: }
default: {
return ['.gguf', '.safetensors', '.bin', '.pt', '.pth']; return ['.gguf', '.safetensors', '.bin', '.pt', '.pth'];
} }
}
}; };

View file

@ -1,4 +1,5 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import type { SdConvDirectMode } from '@/types'; import type { SdConvDirectMode } from '@/types';
interface UseLaunchLogicProps { interface UseLaunchLogicProps {
@ -134,7 +135,9 @@ const buildConfigArgs = (isImageMode: boolean, launchArgs: LaunchArgs) => {
flagMappings.forEach(([condition, flag, value]) => { flagMappings.forEach(([condition, flag, value]) => {
if (condition) { if (condition) {
args.push(flag); args.push(flag);
if (value) args.push(value); if (value) {
args.push(value);
}
} }
}); });
@ -259,17 +262,17 @@ export const useLaunchLogic = ({ model, sdmodel, onLaunch }: UseLaunchLogicProps
if (result.success) { if (result.success) {
onLaunch(); onLaunch();
} else { } else {
const errorMessage = result.error || 'Unknown launch error'; const errorMessage = result.error ?? 'Unknown launch error';
window.electronAPI.logs.logError('Launch failed:', new Error(errorMessage)); window.electronAPI.logs.logError('Launch failed:', new Error(errorMessage));
} }
setIsLaunching(false); setIsLaunching(false);
}, },
[model, sdmodel, isLaunching, onLaunch] [model, sdmodel, isLaunching, onLaunch],
); );
return { return {
isLaunching,
handleLaunch, handleLaunch,
isLaunching,
}; };
}; };

View file

@ -1,4 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { initializeAudio, playSound, soundAssets } from '@/utils/sounds'; import { initializeAudio, playSound, soundAssets } from '@/utils/sounds';
export const useLogoClickSounds = () => { export const useLogoClickSounds = () => {
@ -33,20 +34,20 @@ export const useLogoClickSounds = () => {
}; };
const getLogoStyles = () => ({ const getLogoStyles = () => ({
cursor: 'pointer',
userSelect: 'none' as const,
transition: 'transform 0.15s ease-in-out',
transform: isElephantMode ? 'scale(1.3) rotate(5deg)' : 'scale(1) rotate(0deg)',
animation: isElephantMode animation: isElephantMode
? 'elephantShake 1.5s ease-in-out' ? 'elephantShake 1.5s ease-in-out'
: isMouseSqueaking : isMouseSqueaking
? 'mouseSqueak 0.3s ease-in-out' ? 'mouseSqueak 0.3s ease-in-out'
: 'none', : 'none',
cursor: 'pointer',
transform: isElephantMode ? 'scale(1.3) rotate(5deg)' : 'scale(1) rotate(0deg)',
transition: 'transform 0.15s ease-in-out',
userSelect: 'none' as const,
}); });
return { return {
handleLogoClick,
getLogoStyles, getLogoStyles,
handleLogoClick,
isElephantMode, isElephantMode,
isMouseSqueaking, isMouseSqueaking,
}; };

View file

@ -1,4 +1,5 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useKoboldBackendsStore } from '@/stores/koboldBackends'; import { useKoboldBackendsStore } from '@/stores/koboldBackends';
import type { DismissedUpdate } from '@/types'; import type { DismissedUpdate } from '@/types';
import type { DownloadItem, InstalledBackend } from '@/types/electron'; import type { DownloadItem, InstalledBackend } from '@/types/electron';
@ -66,13 +67,13 @@ export const useUpdateChecker = () => {
const isUpdateDismissed = dismissedUpdates.some( const isUpdateDismissed = dismissedUpdates.some(
(dismissedUpdate) => (dismissedUpdate) =>
dismissedUpdate.currentBackendPath === currentBackend.path && dismissedUpdate.currentBackendPath === currentBackend.path &&
dismissedUpdate.targetVersion === matchingDownload.version dismissedUpdate.targetVersion === matchingDownload.version,
); );
if (!isUpdateDismissed) { if (!isUpdateDismissed) {
setUpdateInfo({ setUpdateInfo({
currentBackend,
availableUpdate: matchingDownload, availableUpdate: matchingDownload,
currentBackend,
}); });
setShowUpdateModal(true); setShowUpdateModal(true);
} }
@ -103,11 +104,11 @@ export const useUpdateChecker = () => {
}, []); }, []);
return { return {
updateInfo,
showUpdateModal,
isChecking,
checkForUpdates, checkForUpdates,
skipUpdate,
closeModal, closeModal,
isChecking,
showUpdateModal,
skipUpdate,
updateInfo,
}; };
}; };

View file

@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import type { AccelerationOption, AccelerationSupport } from '@/types'; import type { AccelerationOption, AccelerationSupport } from '@/types';
import type { CPUCapabilities, GPUDevice } from '@/types/hardware'; import type { CPUCapabilities, GPUDevice } from '@/types/hardware';
@ -24,16 +25,16 @@ const checkModelWarnings = (model: string, sdmodel: string, configLoaded: boolea
if (showModelPriorityWarning) { if (showModelPriorityWarning) {
warnings.push({ warnings.push({
type: 'info',
message: message:
'Both text and image generation models are selected. This may load both models into VRAM simultaneously, which requires significant memory (typically 16GB+ VRAM recommended). Ensure your system has sufficient VRAM to avoid crashes or poor performance.', 'Both text and image generation models are selected. This may load both models into VRAM simultaneously, which requires significant memory (typically 16GB+ VRAM recommended). Ensure your system has sufficient VRAM to avoid crashes or poor performance.',
type: 'info',
}); });
} }
if (showNoModelWarning) { if (showNoModelWarning) {
warnings.push({ warnings.push({
type: 'info',
message: 'Select a model in the General or Image Generation tab to enable launch.', message: 'Select a model in the General or Image Generation tab to enable launch.',
type: 'info',
}); });
} }
@ -54,15 +55,15 @@ interface GpuInfo {
const checkGpuWarnings = async ( const checkGpuWarnings = async (
accelerationSupport: AccelerationSupport, accelerationSupport: AccelerationSupport,
gpuCapabilities: GpuCapabilities, gpuCapabilities: GpuCapabilities,
gpuInfo: GpuInfo gpuInfo: GpuInfo,
) => { ) => {
const warnings: Warning[] = []; const warnings: Warning[] = [];
if (accelerationSupport.cuda && gpuCapabilities.cuda.devices.length === 0 && gpuInfo.hasNVIDIA) { if (accelerationSupport.cuda && gpuCapabilities.cuda.devices.length === 0 && gpuInfo.hasNVIDIA) {
warnings.push({ warnings.push({
type: 'warning',
message: message:
'Your binary supports CUDA and you have an NVIDIA GPU, but CUDA runtime is not detected on your system.', 'Your binary supports CUDA and you have an NVIDIA GPU, but CUDA runtime is not detected on your system.',
type: 'warning',
}); });
} }
@ -78,8 +79,8 @@ const checkGpuWarnings = async (
} }
warnings.push({ warnings.push({
type: 'info',
message, message,
type: 'info',
}); });
} }
@ -96,10 +97,10 @@ const checkVramWarnings = async (acceleration: string): Promise<Warning[]> => {
if (gpuMemoryInfo) { if (gpuMemoryInfo) {
const lowVramThreshold = 8; const lowVramThreshold = 8;
const validGpus = gpuMemoryInfo.filter( const validGpus = gpuMemoryInfo.filter(
(gpu) => gpu.totalMemoryGB !== null && gpu.totalMemoryGB !== '' (gpu) => gpu.totalMemoryGB !== null && gpu.totalMemoryGB !== '',
); );
const lowVramGpus = validGpus.filter( const lowVramGpus = validGpus.filter(
(gpu) => parseFloat(gpu.totalMemoryGB ?? '0') < lowVramThreshold (gpu) => parseFloat(gpu.totalMemoryGB ?? '0') < lowVramThreshold,
); );
if (validGpus.length > 0 && lowVramGpus.length === validGpus.length) { if (validGpus.length > 0 && lowVramGpus.length === validGpus.length) {
@ -108,8 +109,8 @@ const checkVramWarnings = async (acceleration: string): Promise<Warning[]> => {
.join(', '); .join(', ');
warnings.push({ warnings.push({
type: 'warning',
message: `Low VRAM detected (${memoryDetails}). Consider using smaller models, reducing GPU layers, or enabling the "Low VRAM" option on the Advanced tab.`, message: `Low VRAM detected (${memoryDetails}). Consider using smaller models, reducing GPU layers, or enabling the "Low VRAM" option on the Advanced tab.`,
type: 'warning',
}); });
} }
} }
@ -125,11 +126,11 @@ const checkCpuWarnings = (acceleration: string, availableAccelerations: Accelera
return warnings; return warnings;
} }
if (availableAccelerations.length > 0 && availableAccelerations.some((a) => a.value === 'cpu')) { if (availableAccelerations.some((a) => a.value === 'cpu')) {
warnings.push({ warnings.push({
type: 'info',
message: message:
"LLMs run significantly faster on GPU-accelerated systems. Consider using NVIDIA's CUDA, AMD's ROCm or Vulkan backends for optimal performance.", "LLMs run significantly faster on GPU-accelerated systems. Consider using NVIDIA's CUDA, AMD's ROCm or Vulkan backends for optimal performance.",
type: 'info',
}); });
} }
@ -181,7 +182,7 @@ export const useWarnings = ({
const modelWarnings = useMemo( const modelWarnings = useMemo(
() => checkModelWarnings(model, sdmodel, configLoaded), () => checkModelWarnings(model, sdmodel, configLoaded),
[model, sdmodel, configLoaded] [model, sdmodel, configLoaded],
); );
const updateBackendWarnings = useCallback(async () => { const updateBackendWarnings = useCallback(async () => {
@ -197,8 +198,8 @@ export const useWarnings = ({
const result = await checkBackendWarnings({ const result = await checkBackendWarnings({
acceleration, acceleration,
cpuCapabilities: cpuCapabilitiesResult,
availableAccelerations, availableAccelerations,
cpuCapabilities: cpuCapabilitiesResult,
}); });
setBackendWarnings(result); setBackendWarnings(result);
@ -210,7 +211,7 @@ export const useWarnings = ({
const allWarnings = useMemo( const allWarnings = useMemo(
() => [...modelWarnings, ...backendWarnings], () => [...modelWarnings, ...backendWarnings],
[modelWarnings, backendWarnings] [modelWarnings, backendWarnings],
); );
return { return {

View file

@ -1,12 +1,16 @@
import { MantineProvider } from '@mantine/core'; import { MantineProvider } from '@mantine/core';
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { App } from '@/components/App'; import { App } from '@/components/App';
import { cssVariablesResolver, theme } from '@/theme'; import { cssVariablesResolver, theme } from '@/theme';
import '@/styles/index.css'; import '@/styles/index.css';
const rootElement = document.getElementById('root'); const rootElement = document.getElementById('root');
if (!rootElement) throw new Error('Root element not found'); if (!rootElement) {
throw new Error('Root element not found');
}
createRoot(rootElement).render( createRoot(rootElement).render(
<StrictMode> <StrictMode>
@ -17,5 +21,5 @@ createRoot(rootElement).render(
> >
<App /> <App />
</MantineProvider> </MantineProvider>
</StrictMode> </StrictMode>,
); );

View file

@ -1,5 +1,6 @@
import { spawn } from 'node:child_process'; import { spawn } from 'node:child_process';
import { exit, platform, stderr, stdin, stdout } from 'node:process'; import { exit, platform, stderr, stdin, stdout } from 'node:process';
import { pathExists, readJsonFile } from '@/utils/node/fs'; import { pathExists, readJsonFile } from '@/utils/node/fs';
import { getConfigDir } from '@/utils/node/path'; import { getConfigDir } from '@/utils/node/path';
import { terminateProcess } from '@/utils/node/process'; import { terminateProcess } from '@/utils/node/process';
@ -12,7 +13,7 @@ async function getCurrentKoboldBinary() {
} }
const config = await readJsonFile<{ currentKoboldBinary?: string }>(configPath); const config = await readJsonFile<{ currentKoboldBinary?: string }>(configPath);
return config?.currentKoboldBinary || null; return config?.currentKoboldBinary ?? null;
} catch { } catch {
return null; return null;
} }
@ -36,8 +37,8 @@ export async function handleCliMode(args: string[]) {
const isWindows = platform === 'win32'; const isWindows = platform === 'win32';
const child = spawn(currentBinary, args, { const child = spawn(currentBinary, args, {
stdio: isWindows ? 'pipe' : 'inherit',
detached: false, detached: false,
stdio: isWindows ? 'pipe' : 'inherit',
}); });
if (isWindows) { if (isWindows) {

View file

@ -1,5 +1,7 @@
import { platform } from 'node:process'; import { platform } from 'node:process';
import { app } from 'electron'; import { app } from 'electron';
import { PRODUCT_NAME } from '@/constants'; import { PRODUCT_NAME } from '@/constants';
import { setupIPCHandlers } from '@/main/ipc'; import { setupIPCHandlers } from '@/main/ipc';
import { import {
@ -77,7 +79,7 @@ export async function initializeApp(options?: { startMinimized?: boolean }) {
const timeoutPromise = new Promise<void>((resolve) => { const timeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => { setTimeout(() => {
resolve(); resolve();
}, 10000); }, 10_000);
}); });
await Promise.race([Promise.all(cleanupPromises), timeoutPromise]); await Promise.race([Promise.all(cleanupPromises), timeoutPromise]);

View file

@ -1,6 +1,8 @@
import { join } from 'node:path'; import { join } from 'node:path';
import { platform } from 'node:process'; import { platform } from 'node:process';
import { app, ipcMain } from 'electron'; import { app, ipcMain } from 'electron';
import { import {
canAutoUpdate, canAutoUpdate,
checkForUpdates, checkForUpdates,
@ -78,7 +80,7 @@ export function setupIPCHandlers() {
const mainWindow = getMainWindow(); const mainWindow = getMainWindow();
ipcMain.handle('kobold:downloadRelease', async (_, asset, options) => ipcMain.handle('kobold:downloadRelease', async (_, asset, options) =>
downloadRelease(asset, options) downloadRelease(asset, options),
); );
ipcMain.handle('kobold:getInstalledBackends', () => getInstalledBackends()); ipcMain.handle('kobold:getInstalledBackends', () => getInstalledBackends());
@ -88,7 +90,7 @@ export function setupIPCHandlers() {
ipcMain.handle('kobold:getConfigFiles', () => getConfigFiles()); ipcMain.handle('kobold:getConfigFiles', () => getConfigFiles());
ipcMain.handle('kobold:saveConfigFile', async (_, configName, configData) => ipcMain.handle('kobold:saveConfigFile', async (_, configName, configData) =>
saveConfigFile(configName, configData) saveConfigFile(configName, configData),
); );
ipcMain.handle('kobold:deleteConfigFile', async (_, configName) => deleteConfigFile(configName)); ipcMain.handle('kobold:deleteConfigFile', async (_, configName) => deleteConfigFile(configName));
@ -96,7 +98,7 @@ export function setupIPCHandlers() {
ipcMain.handle('kobold:getSelectedConfig', () => getSelectedConfig()); ipcMain.handle('kobold:getSelectedConfig', () => getSelectedConfig());
ipcMain.handle('kobold:setSelectedConfig', (_, configName) => ipcMain.handle('kobold:setSelectedConfig', (_, configName) =>
setConfig('selectedConfig', configName) setConfig('selectedConfig', configName),
); );
ipcMain.handle('kobold:setCurrentBackend', (_, version) => setCurrentBackend(version)); ipcMain.handle('kobold:setCurrentBackend', (_, version) => setCurrentBackend(version));
@ -120,13 +122,13 @@ export function setupIPCHandlers() {
ipcMain.handle('kobold:detectAccelerationSupport', () => detectAccelerationSupport()); ipcMain.handle('kobold:detectAccelerationSupport', () => detectAccelerationSupport());
ipcMain.handle('kobold:getAvailableAccelerations', (_, includeDisabled = false) => ipcMain.handle('kobold:getAvailableAccelerations', (_, includeDisabled = false) =>
getAvailableAccelerations(includeDisabled) getAvailableAccelerations(includeDisabled),
); );
ipcMain.handle('kobold:getPlatform', () => platform); ipcMain.handle('kobold:getPlatform', () => platform);
ipcMain.handle('kobold:launchKoboldCpp', (_, args, preLaunchCommands) => ipcMain.handle('kobold:launchKoboldCpp', (_, args, preLaunchCommands) =>
launchKoboldCppWithCustomFrontends(args, preLaunchCommands) launchKoboldCppWithCustomFrontends(args, preLaunchCommands),
); );
ipcMain.handle('kobold:deleteRelease', (_, binaryPath) => deleteRelease(binaryPath)); ipcMain.handle('kobold:deleteRelease', (_, binaryPath) => deleteRelease(binaryPath));
@ -144,7 +146,7 @@ export function setupIPCHandlers() {
ipcMain.handle('kobold:importLocalBackend', () => importLocalBackend()); ipcMain.handle('kobold:importLocalBackend', () => importLocalBackend());
ipcMain.handle('kobold:getLocalModels', (_, paramType: string) => ipcMain.handle('kobold:getLocalModels', (_, paramType: string) =>
getLocalModelsForType(paramType as Parameters<typeof getLocalModelsForType>[0]) getLocalModelsForType(paramType as Parameters<typeof getLocalModelsForType>[0]),
); );
ipcMain.handle('kobold:analyzeModel', async (_, filePath: string) => analyzeGGUFModel(filePath)); ipcMain.handle('kobold:analyzeModel', async (_, filePath: string) => analyzeGGUFModel(filePath));
@ -157,15 +159,15 @@ export function setupIPCHandlers() {
contextSize: number, contextSize: number,
availableVramGB: number, availableVramGB: number,
flashAttention: boolean, flashAttention: boolean,
acceleration: Acceleration acceleration: Acceleration,
) => ) =>
calculateOptimalGpuLayers({ calculateOptimalGpuLayers({
modelPath,
contextSize,
availableVramGB,
flashAttention,
acceleration, acceleration,
}) availableVramGB,
contextSize,
flashAttention,
modelPath,
}),
); );
ipcMain.handle('config:get', (_, key) => getConfig(key)); ipcMain.handle('config:get', (_, key) => getConfig(key));
@ -179,7 +181,7 @@ export function setupIPCHandlers() {
ipcMain.handle('app:openPath', async (_, path) => openPathHandler(path)); ipcMain.handle('app:openPath', async (_, path) => openPathHandler(path));
ipcMain.handle('app:showLogsFolder', () => ipcMain.handle('app:showLogsFolder', () =>
openPathHandler(join(app.getPath('userData'), 'logs')) openPathHandler(join(app.getPath('userData'), 'logs')),
); );
ipcMain.handle('app:viewConfigFile', () => openPathHandler(getConfigDir())); ipcMain.handle('app:viewConfigFile', () => openPathHandler(getConfigDir()));
@ -208,7 +210,7 @@ export function setupIPCHandlers() {
ipcMain.handle('app:getColorScheme', () => getColorScheme()); ipcMain.handle('app:getColorScheme', () => getColorScheme());
ipcMain.handle('app:setColorScheme', async (_, colorScheme) => ipcMain.handle('app:setColorScheme', async (_, colorScheme) =>
setConfig('colorScheme', colorScheme) setConfig('colorScheme', colorScheme),
); );
ipcMain.handle('app:getEnableSystemTray', () => getEnableSystemTray()); ipcMain.handle('app:getEnableSystemTray', () => getEnableSystemTray());
@ -237,10 +239,10 @@ export function setupIPCHandlers() {
model?: string | null; model?: string | null;
config?: string | null; config?: string | null;
monitoringEnabled?: boolean; monitoringEnabled?: boolean;
} },
) => { ) => {
updateTrayState(state); updateTrayState(state);
} },
); );
ipcMain.handle('app:openExternal', async (_, url) => openUrl(url)); ipcMain.handle('app:openExternal', async (_, url) => openUrl(url));
@ -264,11 +266,11 @@ export function setupIPCHandlers() {
const { rm } = await import('node:fs/promises'); const { rm } = await import('node:fs/promises');
const openWebUIDataDir = join(getInstallDir(), 'openwebui-data'); const openWebUIDataDir = join(getInstallDir(), 'openwebui-data');
try { try {
await rm(openWebUIDataDir, { recursive: true, force: true }); await rm(openWebUIDataDir, { force: true, recursive: true });
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
logError('Failed to clear Open WebUI data:', error as Error); logError('Failed to clear Open WebUI data:', error as Error);
return { success: false, error: (error as Error).message }; return { error: (error as Error).message, success: false };
} }
}); });

View file

@ -1,8 +1,11 @@
import { platform } from 'node:process'; import { platform } from 'node:process';
import { app } from 'electron'; import { app } from 'electron';
import { autoUpdater } from 'electron-updater'; import { autoUpdater } from 'electron-updater';
import { isDevelopment } from '@/utils/node/environment'; import { isDevelopment } from '@/utils/node/environment';
import { logError, safeExecute } from '@/utils/node/logging'; import { logError, safeExecute } from '@/utils/node/logging';
import { getAURVersion, isWindowsPortableInstallation } from './dependencies'; import { getAURVersion, isWindowsPortableInstallation } from './dependencies';
export interface UpdateInfo { export interface UpdateInfo {
@ -67,7 +70,9 @@ export function quitAndInstall() {
export const isUpdateDownloaded = () => updateDownloaded; export const isUpdateDownloaded = () => updateDownloaded;
export const canAutoUpdate = async () => { export const canAutoUpdate = async () => {
if (!app.isPackaged) return false; if (!app.isPackaged) {
return false;
}
if (platform === 'linux' && (await getAURVersion()) !== null) { if (platform === 'linux' && (await getAURVersion()) !== null) {
return false; return false;

View file

@ -1,8 +1,10 @@
import { homedir } from 'node:os'; import { homedir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
import { platform } from 'node:process'; import { platform } from 'node:process';
import type { MantineColorScheme } from '@mantine/core'; import type { MantineColorScheme } from '@mantine/core';
import { nativeTheme } from 'electron'; import { nativeTheme } from 'electron';
import { PRODUCT_NAME } from '@/constants'; import { PRODUCT_NAME } from '@/constants';
import type { import type {
DismissedUpdate, DismissedUpdate,
@ -43,11 +45,11 @@ let config: AppConfig = {};
let configPath: string; let configPath: string;
async function loadConfig() { async function loadConfig() {
const config = await safeExecute( const loaded = await safeExecute(
() => readJsonFile<AppConfig>(configPath), () => readJsonFile<AppConfig>(configPath),
'Error loading config' 'Error loading config',
); );
return config || {}; return loaded ?? {};
} }
async function saveConfig() { async function saveConfig() {
@ -71,16 +73,19 @@ function getDefaultInstallDir() {
const home = homedir(); const home = homedir();
switch (platform) { switch (platform) {
case 'win32': case 'win32': {
return join(home, PRODUCT_NAME); return join(home, PRODUCT_NAME);
case 'darwin': }
case 'darwin': {
return join(home, 'Applications', PRODUCT_NAME); return join(home, 'Applications', PRODUCT_NAME);
default: }
default: {
return join(home, '.local', 'share', PRODUCT_NAME); return join(home, '.local', 'share', PRODUCT_NAME);
} }
}
} }
export const getInstallDir = () => config.installDir || getDefaultInstallDir(); export const getInstallDir = () => config.installDir ?? getDefaultInstallDir();
export async function setInstallDir(dir: string) { export async function setInstallDir(dir: string) {
config.installDir = dir; config.installDir = dir;
@ -99,7 +104,7 @@ export async function setCurrentKoboldBinary(binaryPath: string) {
export const getSelectedConfig = () => config.selectedConfig; export const getSelectedConfig = () => config.selectedConfig;
export const getColorScheme = () => config.colorScheme || 'auto'; export const getColorScheme = () => config.colorScheme ?? 'auto';
export function getBackgroundColor() { export function getBackgroundColor() {
const colorScheme = getColorScheme(); const colorScheme = getColorScheme();

View file

@ -2,8 +2,10 @@ import { access, readdir, readlink } from 'node:fs/promises';
import { homedir, release } from 'node:os'; import { homedir, release } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
import { arch, platform, env as processEnv, versions } from 'node:process'; import { arch, platform, env as processEnv, versions } from 'node:process';
import { app } from 'electron'; import { app } from 'electron';
import { execa } from 'execa'; import { execa } from 'execa';
import { PRODUCT_NAME } from '@/constants'; import { PRODUCT_NAME } from '@/constants';
const PATH_SEPARATOR = platform === 'win32' ? ';' : ':'; const PATH_SEPARATOR = platform === 'win32' ? ';' : ':';
@ -31,15 +33,15 @@ async function executeCommand(
command: string, command: string,
args: string[], args: string[],
env: Record<string, string | undefined>, env: Record<string, string | undefined>,
timeout = 5000 timeout = 5000,
) { ) {
try { try {
const { stdout } = await execa(command, args, { const { stdout } = await execa(command, args, {
env, env,
timeout,
reject: false, reject: false,
timeout,
}); });
return { success: true, output: stdout.trim() }; return { output: stdout.trim(), success: true };
} catch { } catch {
return { success: false }; return { success: false };
} }
@ -68,7 +70,7 @@ export async function getSystemNodeVersion() {
export async function isUvAvailable() { export async function isUvAvailable() {
const env = await getUvEnvironment(); const env = await getUvEnvironment();
const result = await executeCommand('uv', ['--version'], env, 10000); const result = await executeCommand('uv', ['--version'], env, 10_000);
return result.success; return result.success;
} }
@ -89,7 +91,7 @@ export async function getUvEnvironment() {
} }
if (platform === 'win32') { if (platform === 'win32') {
env.PYTHONIOENCODING = 'utf-8'; env.PYTHONIOENCODING = 'utf8';
env.PYTHONLEGACYWINDOWSSTDIO = '1'; env.PYTHONLEGACYWINDOWSSTDIO = '1';
env.PYTHONUTF8 = '1'; env.PYTHONUTF8 = '1';
env.CHCP = '65001'; env.CHCP = '65001';
@ -169,7 +171,7 @@ async function tryVersionManagerPath(basePath: string, env: Record<string, strin
try { try {
const entries = await readdir(basePath); const entries = await readdir(basePath);
const nodeVersions = entries.filter((v) => v.startsWith('v')).sort(); const nodeVersions = entries.filter((v) => v.startsWith('v')).toSorted();
const latestVersion = nodeVersions.pop(); const latestVersion = nodeVersions.pop();
if (latestVersion) { if (latestVersion) {
@ -198,8 +200,8 @@ export async function getAURVersion() {
try { try {
const { stdout } = await execa('pacman', ['-Q', packageName], { const { stdout } = await execa('pacman', ['-Q', packageName], {
timeout: 1000,
reject: false, reject: false,
timeout: 1000,
}); });
const trimmed = stdout.trim(); const trimmed = stdout.trim();
if (trimmed.length > 0) { if (trimmed.length > 0) {
@ -226,16 +228,16 @@ export async function getVersionInfo() {
return { return {
appVersion: app.getVersion(), appVersion: app.getVersion(),
electronVersion: versions.electron, arch,
nodeVersion: versions.node, aurPackageVersion,
chromeVersion: versions.chrome, chromeVersion: versions.chrome,
v8Version: versions.v8, electronVersion: versions.electron,
nodeJsSystemVersion,
nodeVersion: versions.node,
osVersion: release(), osVersion: release(),
platform, platform,
arch,
nodeJsSystemVersion,
uvVersion, uvVersion,
aurPackageVersion, v8Version: versions.v8,
}; };
} }
@ -265,7 +267,7 @@ export function isWindowsPortableInstallation() {
execPath.toLowerCase().includes('\\temp\\') || execPath.toLowerCase().includes('\\tmp\\'); execPath.toLowerCase().includes('\\temp\\') || execPath.toLowerCase().includes('\\tmp\\');
const isInProgramFiles = execPath.toLowerCase().includes('program files'); const isInProgramFiles = execPath.toLowerCase().includes('program files');
const isInAppDataPrograms = execPath.toLowerCase().includes('appdata\\local\\programs'); const isInAppDataPrograms = execPath.toLowerCase().includes(String.raw`appdata\local\programs`);
windowsPortableInstallationCache = isInTemp && !isInProgramFiles && !isInAppDataPrograms; windowsPortableInstallationCache = isInTemp && !isInProgramFiles && !isInAppDataPrograms;

View file

@ -1,6 +1,8 @@
import { platform } from 'node:process'; import { platform } from 'node:process';
import { execa } from 'execa'; import { execa } from 'execa';
import { cpu as siCpu, mem as siMem, memLayout as siMemLayout } from 'systeminformation'; import { cpu as siCpu, mem as siMem, memLayout as siMemLayout } from 'systeminformation';
import type { import type {
BasicGPUInfo, BasicGPUInfo,
CPUCapabilities, CPUCapabilities,
@ -14,8 +16,8 @@ import { safeExecute } from '@/utils/node/logging';
import { detectGPUViaVulkan, getVulkanInfo } from '@/utils/node/vulkan'; import { detectGPUViaVulkan, getVulkanInfo } from '@/utils/node/vulkan';
const COMMON_EXEC_OPTIONS = { const COMMON_EXEC_OPTIONS = {
timeout: 3000,
reject: false, reject: false,
timeout: 3000,
}; };
let cpuCapabilitiesCache: CPUCapabilities | null = null; let cpuCapabilitiesCache: CPUCapabilities | null = null;
@ -36,8 +38,8 @@ export async function detectCPU() {
const name = formatDeviceName(cpu.brand); const name = formatDeviceName(cpu.brand);
devices.push({ devices.push({
name,
detailedName: `${name} (${cpu.cores} cores) @ ${cpu.speed} GHz`, detailedName: `${name} (${cpu.cores} cores) @ ${cpu.speed} GHz`,
name,
}); });
} }
@ -49,7 +51,7 @@ export async function detectCPU() {
return capabilities; return capabilities;
}, 'CPU detection failed'); }, 'CPU detection failed');
cpuCapabilitiesCache = result || { cpuCapabilitiesCache = result ?? {
devices: [], devices: [],
}; };
@ -64,12 +66,12 @@ export async function detectGPU() {
const result = await safeExecute(() => detectGPUViaVulkan(), 'GPU detection failed'); const result = await safeExecute(() => detectGPUViaVulkan(), 'GPU detection failed');
const fallbackGPUInfo = { const fallbackGPUInfo = {
gpuInfo: [],
hasAMD: false, hasAMD: false,
hasNVIDIA: false, hasNVIDIA: false,
gpuInfo: [],
}; };
basicGPUInfoCache = result || fallbackGPUInfo; basicGPUInfoCache = result ?? fallbackGPUInfo;
return basicGPUInfoCache; return basicGPUInfoCache;
} }
@ -92,17 +94,17 @@ async function detectVulkan() {
const devices: GPUDevice[] = []; const devices: GPUDevice[] = [];
for (const gpu of vulkanInfo.allGPUs) { for (const gpu of vulkanInfo.allGPUs) {
const isIntegrated = gpu.isIntegrated; const { isIntegrated } = gpu;
devices.push({ devices.push({
name: isIntegrated ? gpu.name : formatDeviceName(gpu.name),
isIntegrated, isIntegrated,
name: isIntegrated ? gpu.name : formatDeviceName(gpu.name),
}); });
} }
return { return {
devices, devices,
version: vulkanInfo.apiVersion || 'Unknown', version: vulkanInfo.apiVersion ?? 'Unknown',
}; };
} catch { } catch {
return { devices: [], version: 'Unknown' }; return { devices: [], version: 'Unknown' };
@ -114,7 +116,7 @@ async function detectCUDA() {
const { stdout } = await execa( const { stdout } = await execa(
'nvidia-smi', 'nvidia-smi',
['--query-gpu=name,driver_version', '--format=csv,noheader'], ['--query-gpu=name,driver_version', '--format=csv,noheader'],
COMMON_EXEC_OPTIONS COMMON_EXEC_OPTIONS,
); );
if (stdout.trim()) { if (stdout.trim()) {
@ -127,7 +129,7 @@ async function detectCUDA() {
]; ];
const hasError = errorPatterns.some((pattern) => const hasError = errorPatterns.some((pattern) =>
stdout.toLowerCase().includes(pattern.toLowerCase()) stdout.toLowerCase().includes(pattern.toLowerCase()),
); );
if (hasError) { if (hasError) {
@ -194,7 +196,7 @@ export async function detectROCm() {
'-Command', '-Command',
`Get-CimInstance -ClassName Win32_VideoController | Where-Object { $_.Name -like '*AMD*' -or $_.Name -like '*Radeon*' } | Select-Object -First 1 -ExpandProperty DriverVersion`, `Get-CimInstance -ClassName Win32_VideoController | Where-Object { $_.Name -like '*AMD*' -or $_.Name -like '*Radeon*' } | Select-Object -First 1 -ExpandProperty DriverVersion`,
], ],
COMMON_EXEC_OPTIONS COMMON_EXEC_OPTIONS,
); );
if (driverOutput.trim()) { if (driverOutput.trim()) {
@ -214,8 +216,8 @@ export async function detectROCm() {
return { return {
devices, devices,
version,
driverVersion, driverVersion,
version,
}; };
} }
@ -248,7 +250,7 @@ function parseRocmOutput(output: string, vulkanInfo: { allGPUs: GPUDevice[] }) {
} }
if (currentDevice?.name) { if (currentDevice?.name) {
devices.push(createDevice(currentDevice.name, currentDevice.isIntegrated || false)); devices.push(createDevice(currentDevice.name, currentDevice.isIntegrated ?? false));
} }
return devices; return devices;
@ -257,11 +259,11 @@ function parseRocmOutput(output: string, vulkanInfo: { allGPUs: GPUDevice[] }) {
function handleHipInfoLine( function handleHipInfoLine(
trimmedLine: string, trimmedLine: string,
currentDevice: Partial<GPUDevice> | null, currentDevice: Partial<GPUDevice> | null,
devices: GPUDevice[] devices: GPUDevice[],
) { ) {
if (trimmedLine.startsWith('device#')) { if (trimmedLine.startsWith('device#')) {
if (currentDevice?.name) { if (currentDevice?.name) {
devices.push(createDevice(currentDevice.name, currentDevice.isIntegrated || false)); devices.push(createDevice(currentDevice.name, currentDevice.isIntegrated ?? false));
} }
return true; return true;
} }
@ -283,21 +285,25 @@ function parseRocmInfoDevice(
line: string, line: string,
lines: string[], lines: string[],
index: number, index: number,
vulkanInfo: { allGPUs: GPUDevice[] } vulkanInfo: { allGPUs: GPUDevice[] },
) { ) {
const name = line.split('Marketing Name:')[1]?.trim(); const name = line.split('Marketing Name:')[1]?.trim();
if (!name) return null; if (!name) {
return null;
}
const deviceType = findDeviceType(lines, index); const deviceType = findDeviceType(lines, index);
if (deviceType === 'CPU') return null; if (deviceType === 'CPU') {
return null;
}
const isIntegrated = determineIfIntegrated(name, vulkanInfo); const isIntegrated = determineIfIntegrated(name, vulkanInfo);
return createDevice(name, isIntegrated); return createDevice(name, isIntegrated);
} }
const createDevice = (name: string, isIntegrated: boolean) => ({ const createDevice = (name: string, isIntegrated: boolean) => ({
name: isIntegrated ? name : formatDeviceName(name),
isIntegrated, isIntegrated,
name: isIntegrated ? name : formatDeviceName(name),
}); });
function findDeviceType(lines: string[], startIndex: number) { function findDeviceType(lines: string[], startIndex: number) {
@ -316,7 +322,7 @@ function findDeviceType(lines: string[], startIndex: number) {
function determineIfIntegrated(name: string, vulkanInfo: { allGPUs: GPUDevice[] }) { function determineIfIntegrated(name: string, vulkanInfo: { allGPUs: GPUDevice[] }) {
try { try {
const matchingGPU = vulkanInfo.allGPUs.find( const matchingGPU = vulkanInfo.allGPUs.find(
(gpu) => gpu.name.includes(name) || name.includes(gpu.name) (gpu) => gpu.name.includes(name) || name.includes(gpu.name),
); );
return matchingGPU ? matchingGPU.isIntegrated : false; return matchingGPU ? matchingGPU.isIntegrated : false;
} catch { } catch {
@ -341,14 +347,14 @@ export async function detectGPUMemory() {
} }
memoryInfo.push({ memoryInfo.push({
totalMemoryGB: vram?.toFixed(2) || null, totalMemoryGB: vram?.toFixed(2) ?? null,
}); });
} }
return memoryInfo; return memoryInfo;
}, 'GPU memory detection failed'); }, 'GPU memory detection failed');
gpuMemoryInfoCache = result || []; gpuMemoryInfoCache = result ?? [];
return gpuMemoryInfoCache; return gpuMemoryInfoCache;
} }
@ -365,7 +371,7 @@ export const detectSystemMemory = async () => {
const populatedSlot = memLayout.find( const populatedSlot = memLayout.find(
(slot) => (slot) =>
slot.size > 0 && slot.size > 0 &&
((slot.clockSpeed && slot.clockSpeed > 0) || (slot.type && slot.type !== '')) ((slot.clockSpeed && slot.clockSpeed > 0) ?? (slot.type && slot.type !== '')),
); );
if (populatedSlot) { if (populatedSlot) {
@ -378,13 +384,14 @@ export const detectSystemMemory = async () => {
} }
return { return {
totalGB,
speed, speed,
totalGB,
type, type,
}; };
} catch { } catch {
const mem = await siMem();
return { return {
totalGB: ((await siMem()).total / 1024 ** 3).toFixed(2), totalGB: (mem.total / 1024 ** 3).toFixed(2),
}; };
} }
}; };

View file

@ -1,8 +1,10 @@
import { dirname, join } from 'node:path'; import { dirname, join } from 'node:path';
import { platform } from 'node:process'; import { platform } from 'node:process';
import type { AccelerationOption, AccelerationSupport } from '@/types'; import type { AccelerationOption, AccelerationSupport } from '@/types';
import { pathExists } from '@/utils/node/fs'; import { pathExists } from '@/utils/node/fs';
import { safeExecute, tryExecute } from '@/utils/node/logging'; import { safeExecute, tryExecute } from '@/utils/node/logging';
import { detectCPU, detectGPUCapabilities } from '../hardware'; import { detectCPU, detectGPUCapabilities } from '../hardware';
import { getCurrentBinaryInfo } from './backend'; import { getCurrentBinaryInfo } from './backend';
@ -18,11 +20,11 @@ async function detectAccelerationSupportFromPath(koboldBinaryPath: string) {
} }
const support: AccelerationSupport = { const support: AccelerationSupport = {
cuda: false,
failsafe: false,
noavx2: false,
rocm: false, rocm: false,
vulkan: false, vulkan: false,
noavx2: false,
failsafe: false,
cuda: false,
}; };
await tryExecute(async () => { await tryExecute(async () => {
@ -75,11 +77,11 @@ export const detectAccelerationSupport = async () =>
} }
return detectAccelerationSupportFromPath(currentBinaryInfo.path); return detectAccelerationSupportFromPath(currentBinaryInfo.path);
}, 'Error detecting current binary acceleration support')) || null; }, 'Error detecting current binary acceleration support')) ?? null;
export async function getAvailableAccelerations(includeDisabled = false) { export async function getAvailableAccelerations(includeDisabled = false) {
if (platform === 'darwin') { if (platform === 'darwin') {
return [{ value: 'cpu', label: CPU_LABEL }]; return [{ label: CPU_LABEL, value: 'cpu' }];
} }
const result = await safeExecute(async () => { const result = await safeExecute(async () => {
@ -90,7 +92,7 @@ export async function getAvailableAccelerations(includeDisabled = false) {
]); ]);
if (!currentBinaryInfo?.path) { if (!currentBinaryInfo?.path) {
return [{ value: 'cpu', label: CPU_LABEL }]; return [{ label: CPU_LABEL, value: 'cpu' }];
} }
const cacheKey = `${currentBinaryInfo.path}:${includeDisabled}`; const cacheKey = `${currentBinaryInfo.path}:${includeDisabled}`;
@ -112,10 +114,10 @@ export async function getAvailableAccelerations(includeDisabled = false) {
const isSupported = hardwareCapabilities.cuda.devices.length > 0; const isSupported = hardwareCapabilities.cuda.devices.length > 0;
if (isSupported || includeDisabled) { if (isSupported || includeDisabled) {
accelerations.push({ accelerations.push({
value: 'cuda',
label: 'CUDA',
devices: hardwareCapabilities.cuda.devices, devices: hardwareCapabilities.cuda.devices,
disabled: includeDisabled ? !isSupported : undefined, disabled: includeDisabled ? !isSupported : undefined,
label: 'CUDA',
value: 'cuda',
}); });
} }
} }
@ -124,10 +126,10 @@ export async function getAvailableAccelerations(includeDisabled = false) {
const isSupported = hardwareCapabilities.rocm.devices.length > 0; const isSupported = hardwareCapabilities.rocm.devices.length > 0;
if (isSupported || includeDisabled) { if (isSupported || includeDisabled) {
accelerations.push({ accelerations.push({
value: 'rocm',
label: 'ROCm',
devices: hardwareCapabilities.rocm.devices, devices: hardwareCapabilities.rocm.devices,
disabled: includeDisabled ? !isSupported : undefined, disabled: includeDisabled ? !isSupported : undefined,
label: 'ROCm',
value: 'rocm',
}); });
} }
} }
@ -136,24 +138,26 @@ export async function getAvailableAccelerations(includeDisabled = false) {
const isSupported = hardwareCapabilities.vulkan.devices.length > 0; const isSupported = hardwareCapabilities.vulkan.devices.length > 0;
if (isSupported || includeDisabled) { if (isSupported || includeDisabled) {
accelerations.push({ accelerations.push({
value: 'vulkan',
label: 'Vulkan',
devices: hardwareCapabilities.vulkan.devices, devices: hardwareCapabilities.vulkan.devices,
disabled: includeDisabled ? !isSupported : undefined, disabled: includeDisabled ? !isSupported : undefined,
label: 'Vulkan',
value: 'vulkan',
}); });
} }
} }
accelerations.push({ accelerations.push({
value: 'cpu', devices: cpuCapabilities?.devices.map((device) => device.name) ?? [],
label: CPU_LABEL,
devices: cpuCapabilities?.devices.map((device) => device.name) || [],
disabled: false, disabled: false,
label: CPU_LABEL,
value: 'cpu',
}); });
if (includeDisabled) { if (includeDisabled) {
accelerations.sort((a, b) => { accelerations.sort((a, b) => {
if (a.disabled === b.disabled) return 0; if (a.disabled === b.disabled) {
return 0;
}
return a.disabled ? 1 : -1; return a.disabled ? 1 : -1;
}); });
} }
@ -162,5 +166,5 @@ export async function getAvailableAccelerations(includeDisabled = false) {
return accelerations; return accelerations;
}, 'Failed to get available accelerations'); }, 'Failed to get available accelerations');
return result || [{ value: 'cpu', label: CPU_LABEL }]; return result ?? [{ label: CPU_LABEL, value: 'cpu' }];
} }

View file

@ -1,5 +1,7 @@
import { stat } from 'node:fs/promises'; import { stat } from 'node:fs/promises';
import { gguf } from '@huggingface/gguf'; import { gguf } from '@huggingface/gguf';
import { formatBytes } from '@/utils/format'; import { formatBytes } from '@/utils/format';
import { logError } from '@/utils/node/logging'; import { logError } from '@/utils/node/logging';
@ -17,23 +19,39 @@ function estimateMemoryRequirements(fileSize: number) {
} }
function estimateVramPerLayer(fileSize: number, layers?: number) { function estimateVramPerLayer(fileSize: number, layers?: number) {
if (!layers || layers === 0) return undefined; if (!layers || layers === 0) {
return undefined;
}
const perLayer = fileSize / layers; const perLayer = fileSize / layers;
return formatBytes(perLayer); return formatBytes(perLayer);
} }
function formatParameterCount(params?: number) { function formatParameterCount(params?: number) {
if (!params) return undefined; if (!params) {
if (params >= 1e9) return `${(params / 1e9).toFixed(1)}B`; return undefined;
if (params >= 1e6) return `${(params / 1e6).toFixed(1)}M`; }
if (params >= 1e3) return `${(params / 1e3).toFixed(1)}K`; if (params >= 1e9) {
return `${(params / 1e9).toFixed(1)}B`;
}
if (params >= 1e6) {
return `${(params / 1e6).toFixed(1)}M`;
}
if (params >= 1e3) {
return `${(params / 1e3).toFixed(1)}K`;
}
return params.toString(); return params.toString();
} }
function formatContextLength(length?: number) { function formatContextLength(length?: number) {
if (!length) return undefined; if (!length) {
if (length >= 1e6) return `${(length / 1e6).toFixed(1)}M`; return undefined;
if (length >= 1e3) return `${(length / 1e3).toFixed(0)}K`; }
if (length >= 1e6) {
return `${(length / 1e6).toFixed(1)}M`;
}
if (length >= 1e3) {
return `${(length / 1e3).toFixed(0)}K`;
}
return length.toString(); return length.toString();
} }
@ -82,29 +100,30 @@ export async function analyzeGGUFModel(filePath: string) {
const vramPerLayer = estimateVramPerLayer(fileSize, blockCount); const vramPerLayer = estimateVramPerLayer(fileSize, blockCount);
return { return {
general: {
architecture,
name,
fileSize: formatBytes(fileSize),
parameterCount: formatParameterCount(paramCount),
},
context: {
maxContextLength: formatContextLength(contextLength),
},
architecture: { architecture: {
layers: blockCount, layers: blockCount,
expertCount, expertCount,
}, },
context: {
maxContextLength: formatContextLength(contextLength),
},
estimates: { estimates: {
fullGpuVram: memoryEstimates.fullGpuVram, fullGpuVram: memoryEstimates.fullGpuVram,
systemRam: memoryEstimates.systemRam, systemRam: memoryEstimates.systemRam,
vramPerLayer, vramPerLayer,
}, },
general: {
architecture,
name,
fileSize: formatBytes(fileSize),
parameterCount: formatParameterCount(paramCount),
},
}; };
} catch (error) { } catch (error) {
logError('Error analyzing GGUF model:', error as Error); logError('Error analyzing GGUF model:', error as Error);
throw new Error( throw new Error(
`Failed to analyze model: ${error instanceof Error ? error.message : 'Unknown error'}` `Failed to analyze model: ${error instanceof Error ? error.message : 'Unknown error'}`,
{ cause: error },
); );
} }
} }

View file

@ -1,10 +1,13 @@
import { readdir, rm, stat } from 'node:fs/promises'; import { readdir, rm, stat } from 'node:fs/promises';
import { join } from 'node:path'; import { join } from 'node:path';
import { execa } from 'execa'; import { execa } from 'execa';
import type { InstalledBackend } from '@/types/electron'; import type { InstalledBackend } from '@/types/electron';
import { pathExists } from '@/utils/node/fs'; import { pathExists } from '@/utils/node/fs';
import { logError } from '@/utils/node/logging'; import { logError } from '@/utils/node/logging';
import { getLauncherPath } from '@/utils/node/path'; import { getLauncherPath } from '@/utils/node/path';
import { getCurrentKoboldBinary, getInstallDir, setCurrentKoboldBinary } from '../config'; import { getCurrentKoboldBinary, getInstallDir, setCurrentKoboldBinary } from '../config';
import { sendToRenderer } from '../window'; import { sendToRenderer } from '../window';
@ -36,10 +39,10 @@ export async function getInstalledBackends() {
const launcherPath = await getLauncherPath(itemPath); const launcherPath = await getLauncherPath(itemPath);
if (launcherPath && (await pathExists(launcherPath))) { if (launcherPath && (await pathExists(launcherPath))) {
const launcherStats = await stat(launcherPath); const launcherStats = await stat(launcherPath);
const launcherFilename = launcherPath.split(/[/\\]/).pop() || ''; const launcherFilename = launcherPath.split(/[/\\]/).pop() ?? '';
launchers.push({ launchers.push({
path: launcherPath,
filename: launcherFilename, filename: launcherFilename,
path: launcherPath,
size: launcherStats.size, size: launcherStats.size,
}); });
} }
@ -55,11 +58,11 @@ export async function getInstalledBackends() {
} }
return { return {
version: versionInfo.version,
path: launcher.path,
filename: launcher.filename,
size: launcher.size,
actualVersion: versionInfo.actualVersion, actualVersion: versionInfo.actualVersion,
filename: launcher.filename,
path: launcher.path,
size: launcher.size,
version: versionInfo.version,
} as InstalledBackend; } as InstalledBackend;
} catch (error) { } catch (error) {
logError(`Could not detect version for ${launcher.filename}:`, error as Error); logError(`Could not detect version for ${launcher.filename}:`, error as Error);
@ -107,8 +110,8 @@ export async function getCurrentBinaryInfo() {
const filename = pathParts[pathParts.length - 2] || currentBackend.filename; const filename = pathParts[pathParts.length - 2] || currentBackend.filename;
return { return {
path: currentBackend.path,
filename, filename,
path: currentBackend.path,
}; };
} }
@ -130,21 +133,21 @@ export async function setCurrentBackend(binaryPath: string) {
export async function deleteRelease(binaryPath: string) { export async function deleteRelease(binaryPath: string) {
try { try {
if (!(await pathExists(binaryPath))) { if (!(await pathExists(binaryPath))) {
return { success: false, error: 'Release not found' }; return { error: 'Release not found', success: false };
} }
const currentBinaryPath = getCurrentKoboldBinary(); const currentBinaryPath = getCurrentKoboldBinary();
if (currentBinaryPath === binaryPath) { if (currentBinaryPath === binaryPath) {
return { return {
success: false,
error: 'Cannot delete the currently active release', error: 'Cannot delete the currently active release',
success: false,
}; };
} }
const releaseDir = binaryPath.split(/[/\\]/).slice(0, -1).join('/'); const releaseDir = binaryPath.split(/[/\\]/).slice(0, -1).join('/');
if (await pathExists(releaseDir)) { if (await pathExists(releaseDir)) {
await rm(releaseDir, { recursive: true, force: true }); await rm(releaseDir, { force: true, recursive: true });
clearBackendVersionCache(binaryPath); clearBackendVersionCache(binaryPath);
sendToRenderer('versions-updated'); sendToRenderer('versions-updated');
@ -152,9 +155,9 @@ export async function deleteRelease(binaryPath: string) {
return { success: true }; return { success: true };
} }
return { success: false, error: 'Release directory not found' }; return { error: 'Release directory not found', success: false };
} catch (error) { } catch (error) {
return { success: false, error: (error as Error).message }; return { error: (error as Error).message, success: false };
} }
} }
@ -174,7 +177,7 @@ export async function getVersionFromBinary(launcherPath: string) {
const folderName = launcherPath.split(/[/\\]/).slice(-2, -1)[0]; const folderName = launcherPath.split(/[/\\]/).slice(-2, -1)[0];
if (folderName) { if (folderName) {
const versionMatch = folderName.match( const versionMatch = folderName.match(
/-(\d+\.\d+(?:\.\d+)?(?:\.[a-zA-Z0-9]+)*(?:-[a-zA-Z0-9]+)*)$/ /-(\d+\.\d+(?:\.\d+)?(?:\.[a-zA-Z0-9]+)*(?:-[a-zA-Z0-9]+)*)$/,
); );
if (versionMatch) { if (versionMatch) {
folderVersion = versionMatch[1]; folderVersion = versionMatch[1];
@ -183,8 +186,8 @@ export async function getVersionFromBinary(launcherPath: string) {
try { try {
const result = await execa(launcherPath, ['--version'], { const result = await execa(launcherPath, ['--version'], {
timeout: 30000,
stdio: ['ignore', 'pipe', 'pipe'], stdio: ['ignore', 'pipe', 'pipe'],
timeout: 30_000,
}); });
const allOutput = (result.stdout + result.stderr).trim(); const allOutput = (result.stdout + result.stderr).trim();
@ -193,7 +196,7 @@ export async function getVersionFromBinary(launcherPath: string) {
if (lines.length > 0) { if (lines.length > 0) {
const lastLine = lines[lines.length - 1].trim(); const lastLine = lines[lines.length - 1].trim();
const versionMatch = lastLine.match( const versionMatch = lastLine.match(
/^(\d+\.\d+(?:\.\d+)?(?:\.[a-zA-Z0-9]+)*(?:-[a-zA-Z0-9]+)*)$/ /^(\d+\.\d+(?:\.\d+)?(?:\.[a-zA-Z0-9]+)*(?:-[a-zA-Z0-9]+)*)$/,
); );
if (versionMatch) { if (versionMatch) {
actualVersion = versionMatch[1]; actualVersion = versionMatch[1];
@ -202,11 +205,11 @@ export async function getVersionFromBinary(launcherPath: string) {
} catch {} } catch {}
const result = { const result = {
version: folderVersion || actualVersion || 'unknown',
actualVersion: actualVersion:
folderVersion && actualVersion && folderVersion !== actualVersion folderVersion && actualVersion && folderVersion !== actualVersion
? actualVersion ? actualVersion
: undefined, : undefined,
version: folderVersion ?? actualVersion ?? 'unknown',
}; };
backendVersionCache.set(launcherPath, result); backendVersionCache.set(launcherPath, result);

View file

@ -1,10 +1,13 @@
import { readdir, stat, unlink } from 'node:fs/promises'; import { readdir, stat, unlink } from 'node:fs/promises';
import { join } from 'node:path'; import { join } from 'node:path';
import { dialog } from 'electron'; import { dialog } from 'electron';
import { PRODUCT_NAME } from '@/constants'; import { PRODUCT_NAME } from '@/constants';
import type { KoboldConfig } from '@/types/electron'; import type { KoboldConfig } from '@/types/electron';
import { pathExists, readJsonFile, writeJsonFile } from '@/utils/node/fs'; import { pathExists, readJsonFile, writeJsonFile } from '@/utils/node/fs';
import { logError, safeExecute, tryExecute } from '@/utils/node/logging'; import { logError, safeExecute, tryExecute } from '@/utils/node/logging';
import { getInstallDir, setInstallDir } from '../config'; import { getInstallDir, setInstallDir } from '../config';
import { getMainWindow, sendToRenderer } from '../window'; import { getMainWindow, sendToRenderer } from '../window';
@ -36,7 +39,7 @@ export async function getConfigFiles() {
logError('Error scanning for config files:', error as Error); logError('Error scanning for config files:', error as Error);
} }
return configFiles.sort((a, b) => a.name.localeCompare(b.name)); return configFiles.toSorted((a, b) => a.name.localeCompare(b.name));
} }
export async function parseConfigFile(filePath: string) { export async function parseConfigFile(filePath: string) {
@ -74,7 +77,6 @@ export async function selectModelFile(title = 'Select Model File') {
} }
const result = await dialog.showOpenDialog(mainWindow, { const result = await dialog.showOpenDialog(mainWindow, {
title,
filters: [ filters: [
{ {
name: 'Model Files', name: 'Model Files',
@ -83,6 +85,7 @@ export async function selectModelFile(title = 'Select Model File') {
{ name: 'All Files', extensions: ['*'] }, { name: 'All Files', extensions: ['*'] },
], ],
properties: ['openFile'], properties: ['openFile'],
title,
}); });
if (result.canceled || result.filePaths.length === 0) { if (result.canceled || result.filePaths.length === 0) {
@ -95,10 +98,10 @@ export async function selectModelFile(title = 'Select Model File') {
export async function selectInstallDirectory() { export async function selectInstallDirectory() {
const result = await dialog.showOpenDialog({ const result = await dialog.showOpenDialog({
buttonLabel: 'Select Directory',
defaultPath: getInstallDir(),
properties: ['openDirectory', 'createDirectory'], properties: ['openDirectory', 'createDirectory'],
title: `Select the ${PRODUCT_NAME} Installation Directory`, title: `Select the ${PRODUCT_NAME} Installation Directory`,
defaultPath: getInstallDir(),
buttonLabel: 'Select Directory',
}); });
if (!result.canceled && result.filePaths.length > 0) { if (!result.canceled && result.filePaths.length > 0) {

View file

@ -2,20 +2,23 @@ import { createWriteStream } from 'node:fs';
import { chmod, copyFile, mkdir, rename, rm, unlink } from 'node:fs/promises'; import { chmod, copyFile, mkdir, rename, rm, unlink } from 'node:fs/promises';
import { basename, join } from 'node:path'; import { basename, join } from 'node:path';
import { platform } from 'node:process'; import { platform } from 'node:process';
import { dialog } from 'electron'; import { dialog } from 'electron';
import { execa } from 'execa'; import { execa } from 'execa';
import type { DownloadReleaseOptions, GitHubAsset } from '@/types/electron'; import type { DownloadReleaseOptions, GitHubAsset } from '@/types/electron';
import { pathExists } from '@/utils/node/fs'; import { pathExists } from '@/utils/node/fs';
import { logError } from '@/utils/node/logging'; import { logError } from '@/utils/node/logging';
import { getLauncherPath } from '@/utils/node/path'; import { getLauncherPath } from '@/utils/node/path';
import { stripAssetExtensions } from '@/utils/version'; import { stripAssetExtensions } from '@/utils/version';
import { getCurrentKoboldBinary, getInstallDir, setCurrentKoboldBinary } from '../config'; import { getCurrentKoboldBinary, getInstallDir, setCurrentKoboldBinary } from '../config';
import { getMainWindow, sendToRenderer } from '../window'; import { getMainWindow, sendToRenderer } from '../window';
import { clearBackendVersionCache, getVersionFromBinary } from './backend'; import { clearBackendVersionCache, getVersionFromBinary } from './backend';
async function removeDirectoryWithRetry(dirPath: string, maxRetries = 3, currentRetry = 0) { async function removeDirectoryWithRetry(dirPath: string, maxRetries = 3, currentRetry = 0) {
try { try {
await rm(dirPath, { recursive: true, force: true }); await rm(dirPath, { force: true, recursive: true });
} catch (error) { } catch (error) {
if (currentRetry < maxRetries) { if (currentRetry < maxRetries) {
const delay = 2 ** currentRetry * 1000; const delay = 2 ** currentRetry * 1000;
@ -39,7 +42,7 @@ async function downloadFile(asset: GitHubAsset, tempPackedFilePath: string) {
throw new Error(`Failed to download: ${response.statusText}`); throw new Error(`Failed to download: ${response.statusText}`);
} }
const totalBytes = parseInt(response.headers.get('content-length') || '0', 10); const totalBytes = parseInt(response.headers.get('content-length') ?? '0', 10);
const reader = response.body.getReader(); const reader = response.body.getReader();
const pump = async () => { const pump = async () => {
@ -81,7 +84,7 @@ async function downloadFile(asset: GitHubAsset, tempPackedFilePath: string) {
} }
} }
resolve(); resolve();
})() })(),
); );
writer.on('error', reject); writer.on('error', reject);
}); });
@ -122,8 +125,8 @@ async function unpackKoboldCpp(packedPath: string, unpackDir: string) {
try { try {
await mkdir(unpackDir, { recursive: true }); await mkdir(unpackDir, { recursive: true });
await execa(packedPath, ['--unpack', unpackDir], { await execa(packedPath, ['--unpack', unpackDir], {
timeout: 60000,
stdio: ['ignore', 'pipe', 'pipe'], stdio: ['ignore', 'pipe', 'pipe'],
timeout: 60_000,
}); });
} catch (error) { } catch (error) {
const execaError = error as { const execaError = error as {
@ -131,8 +134,8 @@ async function unpackKoboldCpp(packedPath: string, unpackDir: string) {
stdout?: string; stdout?: string;
message: string; message: string;
}; };
const errorMessage = execaError.stderr || execaError.stdout || execaError.message; const errorMessage = execaError.stderr ?? execaError.stdout ?? execaError.message;
throw new Error(`Unpack failed: ${errorMessage}`); throw new Error(`Unpack failed: ${errorMessage}`, { cause: error });
} }
} }
@ -199,21 +202,20 @@ export async function downloadRelease(asset: GitHubAsset, options: DownloadRelea
await downloadFile(asset, tempPackedFilePath); await downloadFile(asset, tempPackedFilePath);
await installBackend({ await installBackend({
isUpdate: options.isUpdate,
oldBackendPath: options.oldBackendPath,
packedFilePath: tempPackedFilePath, packedFilePath: tempPackedFilePath,
unpackedDirPath, unpackedDirPath,
isUpdate: options.isUpdate,
wasCurrentBinary: options.wasCurrentBinary, wasCurrentBinary: options.wasCurrentBinary,
oldBackendPath: options.oldBackendPath,
}); });
} catch (error) { } catch (error) {
logError('Failed to download or unpack binary:', error as Error); logError('Failed to download or unpack binary:', error as Error);
throw new Error('Failed to download or unpack binary'); throw new Error('Failed to download or unpack binary', { cause: error });
} }
} }
export async function importLocalBackend() { export async function importLocalBackend() {
const result = await dialog.showOpenDialog(getMainWindow(), { const result = await dialog.showOpenDialog(getMainWindow(), {
title: 'Select Backend Executable',
filters: filters:
platform === 'win32' platform === 'win32'
? [ ? [
@ -222,6 +224,7 @@ export async function importLocalBackend() {
] ]
: [{ name: 'All Files', extensions: ['*'] }], : [{ name: 'All Files', extensions: ['*'] }],
properties: ['openFile'], properties: ['openFile'],
title: 'Select Backend Executable',
}); });
if (result.canceled || result.filePaths.length === 0) { if (result.canceled || result.filePaths.length === 0) {
@ -239,12 +242,12 @@ export async function importLocalBackend() {
if (!backendVersion || backendVersion.version === 'unknown') { if (!backendVersion || backendVersion.version === 'unknown') {
return { return {
success: false,
error: 'Invalid backend executable. Could not determine version information.', error: 'Invalid backend executable. Could not determine version information.',
success: false,
}; };
} }
const version = backendVersion.actualVersion || backendVersion.version; const version = backendVersion.actualVersion ?? backendVersion.version;
const filename = basename(selectedPath); const filename = basename(selectedPath);
const baseFilename = stripAssetExtensions(filename); const baseFilename = stripAssetExtensions(filename);
const folderName = `${baseFilename}-${version}`; const folderName = `${baseFilename}-${version}`;
@ -255,13 +258,13 @@ export async function importLocalBackend() {
await installBackend({ await installBackend({
packedFilePath, packedFilePath,
unpackedDirPath: installDir,
skipUnpackError: true, skipUnpackError: true,
unpackedDirPath: installDir,
}); });
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
logError('Failed to import local backend:', error as Error); logError('Failed to import local backend:', error as Error);
return { success: false, error: (error as Error).message }; return { error: (error as Error).message, success: false };
} }
} }

View file

@ -1,5 +1,7 @@
import { type ChildProcess, spawn } from 'node:child_process'; import type { ChildProcess } from 'node:child_process';
import { spawn } from 'node:child_process';
import { platform } from 'node:process'; import { platform } from 'node:process';
import { SERVER_READY_SIGNALS } from '@/constants'; import { SERVER_READY_SIGNALS } from '@/constants';
import { get as getConfig, getCurrentKoboldBinary, getInstallDir } from '@/main/modules/config'; import { get as getConfig, getCurrentKoboldBinary, getInstallDir } from '@/main/modules/config';
import { startFrontend as startOpenWebUIFrontend } from '@/main/modules/openwebui'; import { startFrontend as startOpenWebUIFrontend } from '@/main/modules/openwebui';
@ -15,6 +17,7 @@ import { pathExists } from '@/utils/node/fs';
import { parseKoboldConfig } from '@/utils/node/kobold'; import { parseKoboldConfig } from '@/utils/node/kobold';
import { logError, safeExecute } from '@/utils/node/logging'; import { logError, safeExecute } from '@/utils/node/logging';
import { terminateProcess } from '@/utils/node/process'; import { terminateProcess } from '@/utils/node/process';
import { getCurrentBackend } from '../backend'; import { getCurrentBackend } from '../backend';
import { abortActiveDownloads, resolveModelPath } from '../model-download'; import { abortActiveDownloads, resolveModelPath } from '../model-download';
import { startProxy, stopProxy } from '../proxy'; import { startProxy, stopProxy } from '../proxy';
@ -32,15 +35,17 @@ function spawnPreLaunchCommands(commands: string[]) {
const shellFlag = platform === 'win32' ? '/c' : '-c'; const shellFlag = platform === 'win32' ? '/c' : '-c';
for (const command of commands) { for (const command of commands) {
if (!command.trim()) continue; if (!command.trim()) {
continue;
}
sendKoboldOutput(`Running: ${command}\n`); sendKoboldOutput(`Running: ${command}\n`);
try { try {
const child = spawn(shell, [shellFlag, command], { const child = spawn(shell, [shellFlag, command], {
cwd: installDir, cwd: installDir,
stdio: ['ignore', 'pipe', 'pipe'],
detached: false, detached: false,
stdio: ['ignore', 'pipe', 'pipe'],
}); });
preLaunchProcesses.add(child); preLaunchProcesses.add(child);
@ -68,14 +73,14 @@ function spawnPreLaunchCommands(commands: string[]) {
}); });
} catch (error) { } catch (error) {
sendKoboldOutput( sendKoboldOutput(
`Failed to start "${command}": ${error instanceof Error ? error.message : String(error)}\n` `Failed to start "${command}": ${error instanceof Error ? error.message : String(error)}\n`,
); );
} }
} }
} }
async function stopPreLaunchProcesses() { async function stopPreLaunchProcesses() {
const terminations = Array.from(preLaunchProcesses).map((process) => terminateProcess(process)); const terminations = [...preLaunchProcesses].map((process) => terminateProcess(process));
await Promise.all(terminations); await Promise.all(terminations);
preLaunchProcesses.clear(); preLaunchProcesses.clear();
@ -83,7 +88,7 @@ async function stopPreLaunchProcesses() {
async function resolveModelPaths(args: string[]) { async function resolveModelPaths(args: string[]) {
const resolvedArgs: string[] = []; const resolvedArgs: string[] = [];
const modelParams = [ const modelParams = new Set([
'--model', '--model',
'--sdmodel', '--sdmodel',
'--sdt5xxl', '--sdt5xxl',
@ -98,12 +103,12 @@ async function resolveModelPaths(args: string[]) {
'--ttsmodel', '--ttsmodel',
'--ttswavtokenizer', '--ttswavtokenizer',
'--embeddingsmodel', '--embeddingsmodel',
]; ]);
for (let i = 0; i < args.length; i++) { for (let i = 0; i < args.length; i++) {
const arg = args[i]; const arg = args[i];
if (modelParams.includes(arg) && i + 1 < args.length) { if (modelParams.has(arg) && i + 1 < args.length) {
resolvedArgs.push(arg); resolvedArgs.push(arg);
const urlOrPath = args[i + 1]; const urlOrPath = args[i + 1];
try { try {
@ -132,7 +137,7 @@ export async function launchKoboldCpp(
args: string[] = [], args: string[] = [],
frontendPreference: FrontendPreference = 'koboldcpp', frontendPreference: FrontendPreference = 'koboldcpp',
imageGenerationFrontendPreference?: ImageGenerationFrontendPreference, imageGenerationFrontendPreference?: ImageGenerationFrontendPreference,
preLaunchCommands: string[] = [] preLaunchCommands: string[] = [],
) { ) {
try { try {
if (koboldProcess) { if (koboldProcess) {
@ -154,12 +159,12 @@ export async function launchKoboldCpp(
: 'No backend configured'; : 'No backend configured';
logError( logError(
`Launch failed: ${error}. Raw config path: "${rawPath}", Current backend: ${JSON.stringify(currentBackend)}` `Launch failed: ${error}. Raw config path: "${rawPath}", Current backend: ${JSON.stringify(currentBackend)}`,
); );
return { return {
success: false,
error, error,
success: false,
}; };
} }
@ -196,8 +201,8 @@ export async function launchKoboldCpp(
const child = spawn(currentBackend.path, finalArgs, { const child = spawn(currentBackend.path, finalArgs, {
cwd: binaryDir, cwd: binaryDir,
stdio: ['pipe', 'pipe', 'pipe'],
detached: false, detached: false,
stdio: ['pipe', 'pipe', 'pipe'],
}); });
koboldProcess = child; koboldProcess = child;
@ -234,7 +239,7 @@ export async function launchKoboldCpp(
sendToRenderer('server-ready'); sendToRenderer('server-ready');
} }
readyResolve?.({ success: true, pid: child.pid }); readyResolve?.({ pid: child.pid, success: true });
}; };
const handleOutput = (data: Buffer) => { const handleOutput = (data: Buffer) => {
@ -281,7 +286,7 @@ export async function launchKoboldCpp(
if (!isReady) { if (!isReady) {
readyReject?.( readyReject?.(
new Error(`Process exited before ready signal (code: ${code}, signal: ${signal})`) new Error(`Process exited before ready signal (code: ${code}, signal: ${signal})`),
); );
} }
}); });
@ -294,9 +299,9 @@ export async function launchKoboldCpp(
if (isReady) { if (isReady) {
const crashInfo: KoboldCrashInfo = { const crashInfo: KoboldCrashInfo = {
errorMessage: error.message,
exitCode: null, exitCode: null,
signal: null, signal: null,
errorMessage: error.message,
}; };
sendToRenderer('kobold-crashed', crashInfo); sendToRenderer('kobold-crashed', crashInfo);
} }
@ -306,18 +311,18 @@ export async function launchKoboldCpp(
} }
}); });
return readyPromise; return await readyPromise;
} catch (error) { } catch (error) {
const errorMessage = (error as Error).message; const errorMessage = (error as Error).message;
logError(`Failed to launch: ${errorMessage}`, error as Error); logError(`Failed to launch: ${errorMessage}`, error as Error);
return { success: false, error: errorMessage }; return { error: errorMessage, success: false };
} }
} }
export async function stopKoboldCpp() { export async function stopKoboldCpp() {
abortActiveDownloads(); abortActiveDownloads();
void stopProxy(); void stopProxy();
void stopTunnel(); stopTunnel();
void stopPreLaunchProcesses(); void stopPreLaunchProcesses();
isIntentionalStop = true; isIntentionalStop = true;
return terminateProcess(koboldProcess); return terminateProcess(koboldProcess);
@ -325,7 +330,7 @@ export async function stopKoboldCpp() {
export const launchKoboldCppWithCustomFrontends = async ( export const launchKoboldCppWithCustomFrontends = async (
args: string[] = [], args: string[] = [],
preLaunchCommands: string[] = [] preLaunchCommands: string[] = [],
) => ) =>
safeExecute(async () => { safeExecute(async () => {
const frontendPreference = getConfig('frontendPreference'); const frontendPreference = getConfig('frontendPreference');
@ -337,7 +342,7 @@ export const launchKoboldCppWithCustomFrontends = async (
args, args,
frontendPreference, frontendPreference,
imageGenerationFrontendPreference, imageGenerationFrontendPreference,
preLaunchCommands preLaunchCommands,
); );
if ( if (
@ -351,14 +356,14 @@ export const launchKoboldCppWithCustomFrontends = async (
startSillyTavernFrontend(args).catch((error) => { startSillyTavernFrontend(args).catch((error) => {
logError('Failed to start SillyTavern frontend:', error); logError('Failed to start SillyTavern frontend:', error);
sendKoboldOutput( sendKoboldOutput(
`Failed to start SillyTavern: ${error instanceof Error ? error.message : String(error)}` `Failed to start SillyTavern: ${error instanceof Error ? error.message : String(error)}`,
); );
}); });
} else if (frontendPreference === 'openwebui') { } else if (frontendPreference === 'openwebui') {
startOpenWebUIFrontend(args).catch((error) => { startOpenWebUIFrontend(args).catch((error) => {
logError('Failed to start OpenWebUI frontend:', error); logError('Failed to start OpenWebUI frontend:', error);
sendKoboldOutput( sendKoboldOutput(
`Failed to start OpenWebUI: ${error instanceof Error ? error.message : String(error)}` `Failed to start OpenWebUI: ${error instanceof Error ? error.message : String(error)}`,
); );
}); });
} }

View file

@ -1,5 +1,6 @@
import { copyFile, readFile, writeFile } from 'node:fs/promises'; import { copyFile, readFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path'; import { join } from 'node:path';
import { pathExists } from '@/utils/node/fs'; import { pathExists } from '@/utils/node/fs';
import { tryExecute } from '@/utils/node/logging'; import { tryExecute } from '@/utils/node/logging';
import { getAssetPath } from '@/utils/node/path'; import { getAssetPath } from '@/utils/node/path';
@ -81,7 +82,7 @@ export const patchKliteEmbd = (unpackedDir: string) =>
if (content.includes('gerbil-css-override')) { if (content.includes('gerbil-css-override')) {
patchedContent = patchedContent.replace( patchedContent = patchedContent.replace(
/<style id="gerbil-css-override">[\s\S]*?<\/style>\s*/g, /<style id="gerbil-css-override">[\s\S]*?<\/style>\s*/g,
'' '',
); );
} }

View file

@ -4,6 +4,7 @@ import type { IncomingMessage } from 'node:http';
import { get as httpGet } from 'node:http'; import { get as httpGet } from 'node:http';
import { get as httpsGet } from 'node:https'; import { get as httpsGet } from 'node:https';
import { basename, dirname, join } from 'node:path'; import { basename, dirname, join } from 'node:path';
import { getInstallDir } from '@/main/modules/config'; import { getInstallDir } from '@/main/modules/config';
import { sendKoboldOutput, sendToRenderer } from '@/main/modules/window'; import { sendKoboldOutput, sendToRenderer } from '@/main/modules/window';
import type { CachedModel, ModelParamType } from '@/types'; import type { CachedModel, ModelParamType } from '@/types';
@ -34,15 +35,15 @@ function parseHuggingFaceUrl(url: string) {
const pathWithoutQuery = pathWithQuery.split('?')[0]; const pathWithoutQuery = pathWithQuery.split('?')[0];
return { return {
author: hfMatch[1], author: hfMatch[1],
model: hfMatch[2],
filename: basename(pathWithoutQuery), filename: basename(pathWithoutQuery),
model: hfMatch[2],
}; };
} }
return { return {
author: 'external', author: 'external',
model: 'models',
filename: basename(url.split('?')[0]), filename: basename(url.split('?')[0]),
model: 'models',
}; };
} }
@ -63,7 +64,7 @@ function normalizeUrl(url: string) {
async function downloadFile( async function downloadFile(
url: string, url: string,
outputPath: string, outputPath: string,
onProgress: (progress: DownloadProgress) => void onProgress: (progress: DownloadProgress) => void,
) { ) {
const normalizedUrl = normalizeUrl(url); const normalizedUrl = normalizeUrl(url);
const outputDir = dirname(outputPath); const outputDir = dirname(outputPath);
@ -78,11 +79,11 @@ async function downloadFile(
let isAborted = false; let isAborted = false;
const cleanup = async () => { const cleanup = async () => {
await new Promise<void>((resolve) => { await new Promise<void>((done) => {
if (fileStream) { if (fileStream) {
fileStream.close(() => resolve()); fileStream.close(() => done());
} else { } else {
resolve(); done();
} }
}); });
await unlink(tempPath).catch(() => void 0); await unlink(tempPath).catch(() => void 0);
@ -101,7 +102,9 @@ async function downloadFile(
activeDownloads.add(abortController); activeDownloads.add(abortController);
const handleRedirect = (requestUrl: string, redirectCount = 0): void => { const handleRedirect = (requestUrl: string, redirectCount = 0): void => {
if (isAborted) return; if (isAborted) {
return;
}
if (redirectCount > 10) { if (redirectCount > 10) {
activeDownloads.delete(abortController); activeDownloads.delete(abortController);
@ -110,7 +113,9 @@ async function downloadFile(
} }
httpModule(requestUrl, (response) => { httpModule(requestUrl, (response) => {
if (isAborted) return; if (isAborted) {
return;
}
currentRequest = response; currentRequest = response;
if ( if (
response.statusCode === 301 || response.statusCode === 301 ||
@ -133,7 +138,7 @@ async function downloadFile(
return; return;
} }
const totalBytes = parseInt(response.headers['content-length'] || '0', 10); const totalBytes = parseInt(response.headers['content-length'] ?? '0', 10);
let downloadedBytes = 0; let downloadedBytes = 0;
let lastReportTime = Date.now(); let lastReportTime = Date.now();
let lastReportedBytes = 0; let lastReportedBytes = 0;
@ -169,12 +174,12 @@ async function downloadFile(
sendToRenderer('kobold-output', `\r${progressMsg}`); sendToRenderer('kobold-output', `\r${progressMsg}`);
onProgress({ onProgress({
type: 'progress',
percent,
downloaded: `${downloadedMB}MB`, downloaded: `${downloadedMB}MB`,
total: totalBytes ? `${totalMB}MB` : undefined,
speed: `${speedMBPerSec}MB/s`,
eta: totalBytes ? etaStr : undefined, eta: totalBytes ? etaStr : undefined,
percent,
speed: `${speedMBPerSec}MB/s`,
total: totalBytes ? `${totalMB}MB` : undefined,
type: 'progress',
}); });
lastReportTime = now; lastReportTime = now;
@ -183,15 +188,17 @@ async function downloadFile(
}); });
response.on('end', () => { response.on('end', () => {
if (isAborted) return; if (isAborted) {
return;
}
if (totalBytes > 0 && downloadedBytes !== totalBytes) { if (totalBytes > 0 && downloadedBytes !== totalBytes) {
activeDownloads.delete(abortController); activeDownloads.delete(abortController);
void cleanup(); void cleanup();
reject( reject(
new Error( new Error(
`Incomplete download: received ${downloadedBytes} bytes, expected ${totalBytes} bytes` `Incomplete download: received ${downloadedBytes} bytes, expected ${totalBytes} bytes`,
) ),
); );
return; return;
} }
@ -206,11 +213,11 @@ async function downloadFile(
sendKoboldOutput('\n'); sendKoboldOutput('\n');
activeDownloads.delete(abortController); activeDownloads.delete(abortController);
resolve(true); resolve(true);
} catch (err) { } catch (error) {
activeDownloads.delete(abortController); activeDownloads.delete(abortController);
reject(err instanceof Error ? err : new Error(String(err))); reject(error instanceof Error ? error : new Error(String(error)));
} }
})() })(),
); );
}); });
@ -218,25 +225,31 @@ async function downloadFile(
'error', 'error',
(err) => (err) =>
void (async () => { void (async () => {
if (isAborted) return; if (isAborted) {
return;
}
activeDownloads.delete(abortController); activeDownloads.delete(abortController);
await cleanup(); await cleanup();
reject(err); reject(err);
})() })(),
); );
fileStream.on( fileStream.on(
'error', 'error',
(err) => (err) =>
void (async () => { void (async () => {
if (isAborted) return; if (isAborted) {
return;
}
activeDownloads.delete(abortController); activeDownloads.delete(abortController);
await cleanup(); await cleanup();
reject(err); reject(err);
})() })(),
); );
}).on('error', (err) => { }).on('error', (err) => {
if (isAborted) return; if (isAborted) {
return;
}
activeDownloads.delete(abortController); activeDownloads.delete(abortController);
reject(err); reject(err);
}); });
@ -263,7 +276,7 @@ function isValidModelUrl(url: string) {
export async function resolveModelPath( export async function resolveModelPath(
urlOrPath: string, urlOrPath: string,
paramType: ModelParamType, paramType: ModelParamType,
onProgress?: (progress: DownloadProgress) => void onProgress?: (progress: DownloadProgress) => void,
) { ) {
if (!isValidModelUrl(urlOrPath)) { if (!isValidModelUrl(urlOrPath)) {
return urlOrPath; return urlOrPath;
@ -274,32 +287,33 @@ export async function resolveModelPath(
if (await pathExists(localPath)) { if (await pathExists(localPath)) {
sendKoboldOutput(`Using cached model at: ${localPath}`); sendKoboldOutput(`Using cached model at: ${localPath}`);
onProgress?.({ onProgress?.({
type: 'complete',
localPath, localPath,
type: 'complete',
}); });
return localPath; return localPath;
} }
sendKoboldOutput(`Downloading model from ${urlOrPath} to ${localPath}...`); sendKoboldOutput(`Downloading model from ${urlOrPath} to ${localPath}...`);
const progressCallback = onProgress || ((p: DownloadProgress) => p); const progressCallback = onProgress ?? ((p: DownloadProgress) => p);
try { try {
await downloadFile(urlOrPath, localPath, progressCallback); await downloadFile(urlOrPath, localPath, progressCallback);
sendKoboldOutput(`Model downloaded successfully to: ${localPath}\n`); sendKoboldOutput(`Model downloaded successfully to: ${localPath}\n`);
progressCallback({ progressCallback({
type: 'complete',
localPath, localPath,
type: 'complete',
}); });
return localPath; return localPath;
} catch (error) { } catch (error) {
progressCallback({ progressCallback({
type: 'error',
error: `Download failed: ${error instanceof Error ? error.message : 'Unknown error'}`, error: `Download failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
type: 'error',
}); });
throw new Error( throw new Error(
`Failed to download model from ${urlOrPath}: ${error instanceof Error ? error.message : 'Unknown error'}` `Failed to download model from ${urlOrPath}: ${error instanceof Error ? error.message : 'Unknown error'}`,
{ cause: error },
); );
} }
} }
@ -337,9 +351,9 @@ export async function getLocalModelsForType(paramType: ModelParamType) {
if (fileStat.isFile()) { if (fileStat.isFile()) {
models.push({ models.push({
path: filePath,
author, author,
model: modelDir, model: modelDir,
path: filePath,
}); });
} }
} }
@ -355,7 +369,7 @@ export async function getLocalModelsForType(paramType: ModelParamType) {
} }
export function abortActiveDownloads() { export function abortActiveDownloads() {
const downloads = Array.from(activeDownloads); const downloads = [...activeDownloads];
for (const controller of downloads) { for (const controller of downloads) {
controller.abort(); controller.abort();
} }

View file

@ -1,7 +1,9 @@
import type { IncomingMessage, ServerResponse } from 'node:http'; import type { IncomingMessage, ServerResponse } from 'node:http';
import http from 'node:http'; import http from 'node:http';
import { PROXY } from '@/constants/proxy'; import { PROXY } from '@/constants/proxy';
import { logError } from '@/utils/node/logging'; import { logError } from '@/utils/node/logging';
import { sendKoboldOutput } from '../window'; import { sendKoboldOutput } from '../window';
let proxyServer: http.Server | null = null; let proxyServer: http.Server | null = null;
@ -18,11 +20,11 @@ const replaceKoboldWithGerbil = (data: string) => {
const proxyRequest = (clientReq: IncomingMessage, clientRes: ServerResponse) => { const proxyRequest = (clientReq: IncomingMessage, clientRes: ServerResponse) => {
const options = { const options = {
hostname: koboldCppHost,
port: koboldCppPort,
path: clientReq.url,
method: clientReq.method,
headers: clientReq.headers, headers: clientReq.headers,
hostname: koboldCppHost,
method: clientReq.method,
path: clientReq.url,
port: koboldCppPort,
}; };
const proxyReq = http.request(options, (proxyRes) => { const proxyReq = http.request(options, (proxyRes) => {
@ -38,14 +40,14 @@ const proxyRequest = (clientReq: IncomingMessage, clientRes: ServerResponse) =>
proxyRes.on('end', () => { proxyRes.on('end', () => {
const modifiedBody = replaceKoboldWithGerbil(body); const modifiedBody = replaceKoboldWithGerbil(body);
clientRes.writeHead(proxyRes.statusCode || 200, { clientRes.writeHead(proxyRes.statusCode ?? 200, {
...proxyRes.headers, ...proxyRes.headers,
'content-length': Buffer.byteLength(modifiedBody), 'content-length': Buffer.byteLength(modifiedBody),
}); });
clientRes.end(modifiedBody); clientRes.end(modifiedBody);
}); });
} else { } else {
clientRes.writeHead(proxyRes.statusCode || 200, proxyRes.headers); clientRes.writeHead(proxyRes.statusCode ?? 200, proxyRes.headers);
proxyRes.pipe(clientRes); proxyRes.pipe(clientRes);
} }
}); });

View file

@ -4,12 +4,16 @@ import path from 'node:path';
import { arch, platform } from 'node:process'; import { arch, platform } from 'node:process';
import { Readable } from 'node:stream'; import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises'; import { pipeline } from 'node:stream/promises';
import { execa, type ResultPromise } from 'execa';
import { execa } from 'execa';
import type { ResultPromise } from 'execa';
import { GITHUB_API, OPENWEBUI, SILLYTAVERN } from '@/constants'; import { GITHUB_API, OPENWEBUI, SILLYTAVERN } from '@/constants';
import { PROXY } from '@/constants/proxy'; import { PROXY } from '@/constants/proxy';
import { getInstallDir } from '@/main/modules/config'; import { getInstallDir } from '@/main/modules/config';
import type { FrontendPreference } from '@/types'; import type { FrontendPreference } from '@/types';
import { logError } from '@/utils/node/logging'; import { logError } from '@/utils/node/logging';
import { sendKoboldOutput, sendToRenderer } from '../window'; import { sendKoboldOutput, sendToRenderer } from '../window';
let activeTunnel: ResultPromise | null = null; let activeTunnel: ResultPromise | null = null;
@ -51,7 +55,6 @@ const downloadCloudflared = async (binPath: string) => {
throw new Error(`Failed to download cloudflared: ${response.statusText}`); throw new Error(`Failed to download cloudflared: ${response.statusText}`);
} }
// biome-ignore lint/suspicious/noExplicitAny: Node.js stream types are incompatible with web streams
await pipeline(Readable.fromWeb(response.body as any), createWriteStream(binPath)); await pipeline(Readable.fromWeb(response.body as any), createWriteStream(binPath));
if (platform !== 'win32') { if (platform !== 'win32') {
@ -63,16 +66,19 @@ const downloadCloudflared = async (binPath: string) => {
const getTunnelTarget = (frontendPreference: FrontendPreference) => { const getTunnelTarget = (frontendPreference: FrontendPreference) => {
switch (frontendPreference) { switch (frontendPreference) {
case 'sillytavern': case 'sillytavern': {
return `http://${SILLYTAVERN.HOST}:${SILLYTAVERN.PROXY_PORT}`; return `http://${SILLYTAVERN.HOST}:${SILLYTAVERN.PROXY_PORT}`;
case 'openwebui': }
case 'openwebui': {
return `http://${OPENWEBUI.HOST}:${OPENWEBUI.PORT}`; return `http://${OPENWEBUI.HOST}:${OPENWEBUI.PORT}`;
default: }
default: {
return `http://${PROXY.HOST}:${PROXY.PORT}`; return `http://${PROXY.HOST}:${PROXY.PORT}`;
} }
}
}; };
const waitForBackend = async (url: string, timeoutMs = 30000) => { const waitForBackend = async (url: string, timeoutMs = 30_000) => {
const startTime = Date.now(); const startTime = Date.now();
while (Date.now() - startTime < timeoutMs) { while (Date.now() - startTime < timeoutMs) {
@ -104,7 +110,7 @@ export const startTunnel = async (frontendPreference: FrontendPreference = 'kobo
if (!backendReady) { if (!backendReady) {
throw new Error( throw new Error(
'Backend not ready after 30 seconds. Start your backend first before enabling tunnel.' 'Backend not ready after 30 seconds. Start your backend first before enabling tunnel.',
); );
} }
@ -147,7 +153,7 @@ export const startTunnel = async (frontendPreference: FrontendPreference = 'kobo
: 'Tunnel connection timed out'; : 'Tunnel connection timed out';
tunnel.kill(); tunnel.kill();
reject(new Error(message)); reject(new Error(message));
}, 30000); }, 30_000);
const checkForUrl = () => { const checkForUrl = () => {
const match = output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/); const match = output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);

View file

@ -1,5 +1,6 @@
import { spawn } from 'node:child_process'; import { spawn } from 'node:child_process';
import { platform } from 'node:process'; import { platform } from 'node:process';
import type { BrowserWindow } from 'electron'; import type { BrowserWindow } from 'electron';
import { import {
cpuTemperature, cpuTemperature,
@ -8,8 +9,10 @@ import {
powerShellRelease, powerShellRelease,
powerShellStart, powerShellStart,
} from 'systeminformation'; } from 'systeminformation';
import { getGPUData } from '@/utils/node/gpu'; import { getGPUData } from '@/utils/node/gpu';
import { safeExecute, tryExecute } from '@/utils/node/logging'; import { safeExecute, tryExecute } from '@/utils/node/logging';
import { detectGPU } from './hardware'; import { detectGPU } from './hardware';
import { isTrayActive, updateMetrics } from './tray'; import { isTrayActive, updateMetrics } from './tray';
@ -65,7 +68,9 @@ let latestMemoryMetrics: MemoryMetrics | null = null;
let latestGpuMetrics: GpuMetrics | null = null; let latestGpuMetrics: GpuMetrics | null = null;
export function startMonitoring(window: BrowserWindow) { export function startMonitoring(window: BrowserWindow) {
if (isRunning) return; if (isRunning) {
return;
}
mainWindow = window; mainWindow = window;
isRunning = true; isRunning = true;
@ -148,9 +153,9 @@ async function collectAndSendMemoryMetrics() {
const usedBytes = memData.active || memData.used; const usedBytes = memData.active || memData.used;
const totalBytes = memData.total; const totalBytes = memData.total;
const metrics: MemoryMetrics = { const metrics: MemoryMetrics = {
used: usedBytes / (1024 * 1024 * 1024),
total: totalBytes / (1024 * 1024 * 1024), total: totalBytes / (1024 * 1024 * 1024),
usage: Math.round((usedBytes / totalBytes) * 100), usage: Math.round((usedBytes / totalBytes) * 100),
used: usedBytes / (1024 * 1024 * 1024),
}; };
latestMemoryMetrics = metrics; latestMemoryMetrics = metrics;
@ -166,16 +171,13 @@ async function collectAndSendGpuMetrics() {
await tryExecute(async () => { await tryExecute(async () => {
const [gpuData, gpuInfo] = await Promise.all([getGPUData(), detectGPU()]); const [gpuData, gpuInfo] = await Promise.all([getGPUData(), detectGPU()]);
const metrics: GpuMetrics = { const metrics: GpuMetrics = {
gpus: gpuData.map((gpuData, index) => ({ gpus: gpuData.map((gpu, index) => ({
memoryTotal: gpu.memoryTotal,
memoryUsage: gpu.memoryTotal > 0 ? Math.round((gpu.memoryUsed / gpu.memoryTotal) * 100) : 0,
memoryUsed: gpu.memoryUsed,
name: gpuInfo.gpuInfo[index] || `GPU ${index}`, name: gpuInfo.gpuInfo[index] || `GPU ${index}`,
usage: Math.round(gpuData.usage), temperature: gpu.temperature,
memoryUsed: gpuData.memoryUsed, usage: Math.round(gpu.usage),
memoryTotal: gpuData.memoryTotal,
memoryUsage:
gpuData.memoryTotal > 0
? Math.round((gpuData.memoryUsed / gpuData.memoryTotal) * 100)
: 0,
temperature: gpuData.temperature,
})), })),
}; };
@ -258,17 +260,17 @@ export const openPerformanceManager = async () =>
case 'darwin': { case 'darwin': {
const success = await tryLaunchCommand('open', ['-a', 'Activity Monitor']); const success = await tryLaunchCommand('open', ['-a', 'Activity Monitor']);
if (success) { if (success) {
return { success: true, app: 'Activity Monitor' }; return { app: 'Activity Monitor', success: true };
} }
return { success: false, error: 'Could not open Activity Monitor' }; return { error: 'Could not open Activity Monitor', success: false };
} }
case 'win32': { case 'win32': {
const success = await tryLaunchCommand('taskmgr'); const success = await tryLaunchCommand('taskmgr');
if (success) { if (success) {
return { success: true, app: 'Task Manager' }; return { app: 'Task Manager', success: true };
} }
return { success: false, error: 'Could not open Task Manager' }; return { error: 'Could not open Task Manager', success: false };
} }
case 'linux': { case 'linux': {
@ -276,7 +278,7 @@ export const openPerformanceManager = async () =>
if (app) { if (app) {
const success = await tryLaunchCommand(app); const success = await tryLaunchCommand(app);
if (success) { if (success) {
return { success: true, app }; return { app, success: true };
} }
cachedLinuxApp = null; cachedLinuxApp = null;
linuxAppSearchComplete = false; linuxAppSearchComplete = false;
@ -284,24 +286,24 @@ export const openPerformanceManager = async () =>
if (fallbackApp) { if (fallbackApp) {
const fallbackSuccess = await tryLaunchCommand(fallbackApp); const fallbackSuccess = await tryLaunchCommand(fallbackApp);
if (fallbackSuccess) { if (fallbackSuccess) {
return { success: true, app: fallbackApp }; return { app: fallbackApp, success: true };
} }
} }
} }
return { return {
success: false,
error: `Could not find any performance monitoring app. Tried: ${LINUX_PERFORMANCE_APPS.join(', ')}`, error: `Could not find any performance monitoring app. Tried: ${LINUX_PERFORMANCE_APPS.join(', ')}`,
success: false,
}; };
} }
default: { default: {
return { return {
success: false,
error: `Unsupported platform: ${platform}`, error: `Unsupported platform: ${platform}`,
};
}
}
}, 'Failed to open performance manager')) || {
success: false, success: false,
error: 'Failed to open performance manager', };
}
}
}, 'Failed to open performance manager')) ?? {
error: 'Failed to open performance manager',
success: false,
}; };

View file

@ -1,14 +1,16 @@
import { readdir, readFile, rename, unlink, writeFile } from 'node:fs/promises'; import { readdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
import { join } from 'node:path'; import { join } from 'node:path';
import { DEFAULT_NOTEPAD_POSITION, DEFAULT_TAB_CONTENT } from '@/constants/notepad'; import { DEFAULT_NOTEPAD_POSITION, DEFAULT_TAB_CONTENT } from '@/constants/notepad';
import type { SavedNotepadState } from '@/types/electron'; import type { SavedNotepadState } from '@/types/electron';
import { pathExists } from '@/utils/node/fs'; import { pathExists } from '@/utils/node/fs';
import { get, getInstallDir, set } from './config'; import { get, getInstallDir, set } from './config';
const DEFAULT_NOTEPAD_STATE: SavedNotepadState = { const DEFAULT_NOTEPAD_STATE: SavedNotepadState = {
activeTabId: null, activeTabId: null,
position: DEFAULT_NOTEPAD_POSITION,
isVisible: false, isVisible: false,
position: DEFAULT_NOTEPAD_POSITION,
}; };
const getNotepadDir = () => join(getInstallDir(), 'notepad'); const getNotepadDir = () => join(getInstallDir(), 'notepad');
@ -50,7 +52,7 @@ export const saveTabContent = async (title: string, content: string) => {
const notepadDir = getNotepadDir(); const notepadDir = getNotepadDir();
const filePath = join(notepadDir, `${title}.txt`); const filePath = join(notepadDir, `${title}.txt`);
await writeFile(filePath, content, 'utf-8'); await writeFile(filePath, content, 'utf8');
return true; return true;
} catch { } catch {
return false; return false;
@ -60,7 +62,7 @@ export const saveTabContent = async (title: string, content: string) => {
export const loadTabContent = async (title: string) => { export const loadTabContent = async (title: string) => {
try { try {
const notepadDir = getNotepadDir(); const notepadDir = getNotepadDir();
return readFile(join(notepadDir, `${title}.txt`), 'utf-8'); return await readFile(join(notepadDir, `${title}.txt`), 'utf8');
} catch { } catch {
return ''; return '';
} }
@ -72,14 +74,14 @@ export const saveNotepadState = async (state: SavedNotepadState) => {
}; };
export const loadNotepadState = async () => { export const loadNotepadState = async () => {
const stored = get('notepad') || DEFAULT_NOTEPAD_STATE; const stored = get('notepad') ?? DEFAULT_NOTEPAD_STATE;
const tabs = await getTabsFromStorage(); const tabs = await getTabsFromStorage();
const activeTabId = const activeTabId =
stored.activeTabId && tabs.some((tab) => tab.title === stored.activeTabId) stored.activeTabId && tabs.some((tab) => tab.title === stored.activeTabId)
? stored.activeTabId ? stored.activeTabId
: tabs[0]?.title || null; : tabs[0]?.title || null;
return { ...stored, tabs, activeTabId }; return { ...stored, activeTabId, tabs };
}; };
export const deleteTabFile = async (title: string) => { export const deleteTabFile = async (title: string) => {
@ -98,16 +100,19 @@ export const createNewTab = async (title?: string) => {
.map((tab) => tab.title.match(/^Note (\d+)$/)?.[1]) .map((tab) => tab.title.match(/^Note (\d+)$/)?.[1])
.filter(Boolean) .filter(Boolean)
.map(Number) .map(Number)
.sort((a, b) => a - b); .toSorted((a, b) => a - b);
let counter = 1; let counter = 1;
for (const num of noteNumbers) { for (const num of noteNumbers) {
if (num === counter) counter++; if (num === counter) {
else break; counter++;
} else {
break;
}
} }
title = `Note ${counter}`; title = `Note ${counter}`;
} }
await saveTabContent(title, DEFAULT_TAB_CONTENT); await saveTabContent(title, DEFAULT_TAB_CONTENT);
return { title, content: DEFAULT_TAB_CONTENT }; return { content: DEFAULT_TAB_CONTENT, title };
}; };

View file

@ -3,11 +3,13 @@ import { spawn } from 'node:child_process';
import { join } from 'node:path'; import { join } from 'node:path';
import { app } from 'electron'; import { app } from 'electron';
import { OPENWEBUI, SERVER_READY_SIGNALS } from '@/constants'; import { OPENWEBUI, SERVER_READY_SIGNALS } from '@/constants';
import { PROXY } from '@/constants/proxy'; import { PROXY } from '@/constants/proxy';
import { parseKoboldConfig } from '@/utils/node/kobold'; import { parseKoboldConfig } from '@/utils/node/kobold';
import { logError } from '@/utils/node/logging'; import { logError } from '@/utils/node/logging';
import { terminateProcess } from '@/utils/node/process'; import { terminateProcess } from '@/utils/node/process';
import { getInstallDir } from './config'; import { getInstallDir } from './config';
import { getUvEnvironment } from './dependencies'; import { getUvEnvironment } from './dependencies';
import { sendKoboldOutput, sendToRenderer } from './window'; import { sendKoboldOutput, sendToRenderer } from './window';
@ -30,9 +32,9 @@ async function createUvProcess(args: string[], env?: Record<string, string>) {
const mergedEnv = { ...uvEnv, ...env }; const mergedEnv = { ...uvEnv, ...env };
return spawn('uvx', args, { return spawn('uvx', args, {
stdio: ['pipe', 'pipe', 'pipe'],
detached: false, detached: false,
env: mergedEnv, env: mergedEnv,
stdio: ['pipe', 'pipe', 'pipe'],
}); });
} }
@ -41,7 +43,9 @@ async function waitForOpenWebUIToStart() {
let resolved = false; let resolved = false;
const checkForOutput = (data: Buffer) => { const checkForOutput = (data: Buffer) => {
if (resolved) return; if (resolved) {
return;
}
const output = data.toString(); const output = data.toString();
if (output.includes(SERVER_READY_SIGNALS.OPENWEBUI)) { if (output.includes(SERVER_READY_SIGNALS.OPENWEBUI)) {
resolved = true; resolved = true;
@ -76,7 +80,7 @@ export async function startFrontend(args: string[]) {
const appVersion = app.getVersion(); const appVersion = app.getVersion();
sendKoboldOutput( sendKoboldOutput(
`Preparing Open WebUI to connect at ${koboldHost}:${koboldPort}${isImageMode ? ' (with image generation)' : ''}...` `Preparing Open WebUI to connect at ${koboldHost}:${koboldPort}${isImageMode ? ' (with image generation)' : ''}...`,
); );
const koboldUrl = `http://${koboldHost}:${koboldPort}`; const koboldUrl = `http://${koboldHost}:${koboldPort}`;
@ -85,21 +89,21 @@ export async function startFrontend(args: string[]) {
const openWebUIArgs = [...OPENWEBUI_BASE_ARGS, '--port', config.port.toString()]; const openWebUIArgs = [...OPENWEBUI_BASE_ARGS, '--port', config.port.toString()];
sendKoboldOutput( sendKoboldOutput(
`Starting Open WebUI with uv${isImageMode ? ' (image generation enabled)' : ''}...` `Starting Open WebUI with uv${isImageMode ? ' (image generation enabled)' : ''}...`,
); );
const installDir = getInstallDir(); const installDir = getInstallDir();
const openWebUIDataDir = join(installDir, 'openwebui-data'); const openWebUIDataDir = join(installDir, 'openwebui-data');
const envConfig: Record<string, string> = { const envConfig: Record<string, string> = {
OPENAI_API_BASE_URL: `${proxyUrl}/v1`,
DATA_DIR: openWebUIDataDir, DATA_DIR: openWebUIDataDir,
ENABLE_OLLAMA_API: 'false',
ENABLE_VERSION_UPDATE_CHECK: 'false',
GLOBAL_LOG_LEVEL: 'warning',
OPENAI_API_BASE_URL: `${proxyUrl}/v1`,
USER_AGENT: `Gerbil/${appVersion}`,
WEBUI_AUTH: 'false', WEBUI_AUTH: 'false',
WEBUI_SECRET_KEY: 'gerbil', WEBUI_SECRET_KEY: 'gerbil',
ENABLE_OLLAMA_API: 'false',
USER_AGENT: `Gerbil/${appVersion}`,
GLOBAL_LOG_LEVEL: 'warning',
ENABLE_VERSION_UPDATE_CHECK: 'false',
}; };
if (isImageMode) { if (isImageMode) {

View file

@ -1,5 +1,7 @@
import { platform } from 'node:process'; import { platform } from 'node:process';
import { safeExecute } from '@/utils/node/logging'; import { safeExecute } from '@/utils/node/logging';
import { detectGPU } from './hardware'; import { detectGPU } from './hardware';
export interface PlatformGPUInfo { export interface PlatformGPUInfo {
@ -25,7 +27,7 @@ export async function getPlatformGPUInfo(): Promise<PlatformGPUInfo> {
}, 'Failed to detect platform GPU information'); }, 'Failed to detect platform GPU information');
return ( return (
result || { result ?? {
hasAMD: false, hasAMD: false,
hasNVIDIA: false, hasNVIDIA: false,
isWindows: platform === 'win32', isWindows: platform === 'win32',

View file

@ -1,14 +1,17 @@
import type { ChildProcess } from 'node:child_process'; import type { ChildProcess } from 'node:child_process';
import { spawn } from 'node:child_process'; import { spawn } from 'node:child_process';
import { createServer, request, type Server } from 'node:http'; import { createServer, request } from 'node:http';
import type { Server } from 'node:http';
import { join } from 'node:path'; import { join } from 'node:path';
import { platform } from 'node:process'; import { platform } from 'node:process';
import { SERVER_READY_SIGNALS, SILLYTAVERN } from '@/constants'; import { SERVER_READY_SIGNALS, SILLYTAVERN } from '@/constants';
import { PROXY } from '@/constants/proxy'; import { PROXY } from '@/constants/proxy';
import { pathExists, readJsonFile, writeJsonFile } from '@/utils/node/fs'; import { pathExists, readJsonFile, writeJsonFile } from '@/utils/node/fs';
import { parseKoboldConfig } from '@/utils/node/kobold'; import { parseKoboldConfig } from '@/utils/node/kobold';
import { logError, tryExecute } from '@/utils/node/logging'; import { logError, tryExecute } from '@/utils/node/logging';
import { terminateProcess } from '@/utils/node/process'; import { terminateProcess } from '@/utils/node/process';
import { getInstallDir } from './config'; import { getInstallDir } from './config';
import { getNodeEnvironment } from './dependencies'; import { getNodeEnvironment } from './dependencies';
import { sendKoboldOutput, sendToRenderer } from './window'; import { sendKoboldOutput, sendToRenderer } from './window';
@ -53,11 +56,11 @@ async function ensureSillyTavernInstalled() {
platform === 'win32' ? ['/s', '/q', nodeModulesPath] : ['-rf', nodeModulesPath]; platform === 'win32' ? ['/s', '/q', nodeModulesPath] : ['-rf', nodeModulesPath];
spawn(rmCmd, rmArgs, { spawn(rmCmd, rmArgs, {
stdio: 'inherit',
shell: true, shell: true,
stdio: 'inherit',
}) })
.on('exit', (code) => .on('exit', (code) =>
code === 0 ? resolve() : reject(new Error(`Failed with code ${code}`)) code === 0 ? resolve() : reject(new Error(`Failed with code ${code}`)),
) )
.on('error', reject); .on('error', reject);
}); });
@ -83,10 +86,10 @@ async function ensureSillyTavernInstalled() {
'--silent', '--silent',
], ],
{ {
stdio: ['pipe', 'pipe', 'pipe'],
env, env,
shell: platform === 'win32', shell: platform === 'win32',
} stdio: ['pipe', 'pipe', 'pipe'],
},
); );
let errorOutput = ''; let errorOutput = '';
@ -121,11 +124,11 @@ async function createNpxProcess(args: string[]) {
const installDir = getSillyTavernInstallDir(); const installDir = getSillyTavernInstallDir();
return spawn('node', [serverJsPath, ...args], { return spawn('node', [serverJsPath, ...args], {
stdio: ['pipe', 'pipe', 'pipe'], cwd: installDir,
detached: false, detached: false,
env, env,
cwd: installDir,
shell: platform === 'win32', shell: platform === 'win32',
stdio: ['pipe', 'pipe', 'pipe'],
}); });
} }
@ -138,7 +141,7 @@ async function ensureSillyTavernSettings() {
} }
sendKoboldOutput( sendKoboldOutput(
'SillyTavern settings not found, starting SillyTavern briefly to generate config...' 'SillyTavern settings not found, starting SillyTavern briefly to generate config...',
); );
const initProcess = await createNpxProcess(SILLYTAVERN_BASE_ARGS); const initProcess = await createNpxProcess(SILLYTAVERN_BASE_ARGS);
@ -189,7 +192,7 @@ async function ensureSillyTavernSettings() {
resolve(); resolve();
} }
})(), })(),
2000 2000,
); );
} }
}); });
@ -223,14 +226,14 @@ async function setupSillyTavernConfig(isImageMode: boolean) {
const proxyUrl = PROXY.URL; const proxyUrl = PROXY.URL;
if (!settings.power_user) settings.power_user = {}; settings.power_user ??= {};
const powerUser = settings.power_user as Record<string, unknown>; const powerUser = settings.power_user as Record<string, unknown>;
powerUser.auto_connect = true; powerUser.auto_connect = true;
if (!settings.textgenerationwebui_settings) settings.textgenerationwebui_settings = {}; settings.textgenerationwebui_settings ??= {};
const textgenSettings = settings.textgenerationwebui_settings as Record<string, unknown>; const textgenSettings = settings.textgenerationwebui_settings as Record<string, unknown>;
if (!textgenSettings.server_urls) textgenSettings.server_urls = {}; textgenSettings.server_urls ??= {};
const serverUrls = textgenSettings.server_urls as Record<string, unknown>; const serverUrls = textgenSettings.server_urls as Record<string, unknown>;
serverUrls.koboldcpp = proxyUrl; serverUrls.koboldcpp = proxyUrl;
@ -246,7 +249,7 @@ async function setupSillyTavernConfig(isImageMode: boolean) {
`2. Go to 'Extensions' tab and enable 'Image Generation'\n` + `2. Go to 'Extensions' tab and enable 'Image Generation'\n` +
`3. Set Source to 'Stable Diffusion WebUI (AUTOMATIC1111)'\n` + `3. Set Source to 'Stable Diffusion WebUI (AUTOMATIC1111)'\n` +
`4. Set API URL to: ${proxyUrl}/sdui\n` + `4. Set API URL to: ${proxyUrl}/sdui\n` +
`5. Click 'Connect' to test the connection` `5. Click 'Connect' to test the connection`,
); );
} }
@ -287,17 +290,17 @@ async function waitForSillyTavernToStart() {
const createProxyServer = (targetPort: number, proxyPort: number) => { const createProxyServer = (targetPort: number, proxyPort: number) => {
proxyServer = createServer((req, res) => { proxyServer = createServer((req, res) => {
const options = { const options = {
hostname: 'localhost',
port: targetPort,
path: req.url,
method: req.method,
headers: req.headers, headers: req.headers,
hostname: 'localhost',
method: req.method,
path: req.url,
port: targetPort,
}; };
const proxyReq = request(options, (proxyRes) => { const proxyReq = request(options, (proxyRes) => {
const headers = { ...proxyRes.headers }; const headers = { ...proxyRes.headers };
delete headers['x-frame-options']; delete headers['x-frame-options'];
res.writeHead(proxyRes.statusCode || 200, headers); res.writeHead(proxyRes.statusCode ?? 200, headers);
proxyRes.pipe(res); proxyRes.pipe(res);
}); });
@ -382,7 +385,7 @@ export async function startFrontend(args: string[]) {
} catch (error) { } catch (error) {
logError( logError(
`Failed to start SillyTavern: ${error instanceof Error ? error.message : String(error)}`, `Failed to start SillyTavern: ${error instanceof Error ? error.message : String(error)}`,
error as Error error as Error,
); );
throw error; throw error;
} }
@ -402,7 +405,7 @@ export async function stopFrontend() {
proxyServer = null; proxyServer = null;
resolve(); resolve();
}); });
}) }),
); );
} }

View file

@ -1,7 +1,10 @@
import { readFile } from 'node:fs/promises'; import { readFile } from 'node:fs/promises';
import { createServer, type Server } from 'node:http'; import { createServer } from 'node:http';
import type { Server } from 'node:http';
import { join, normalize, resolve as resolvePath, sep } from 'node:path'; import { join, normalize, resolve as resolvePath, sep } from 'node:path';
import { lookup } from 'mime-types'; import { lookup } from 'mime-types';
import { pathExists } from '@/utils/node/fs'; import { pathExists } from '@/utils/node/fs';
let server: Server | null = null; let server: Server | null = null;
@ -40,7 +43,7 @@ export const startStaticServer = (distPath: string) =>
res.writeHead(404); res.writeHead(404);
res.end('Not found'); res.end('Not found');
} }
})() })(),
); );
server.listen(0, 'localhost', () => { server.listen(0, 'localhost', () => {

View file

@ -0,0 +1,7 @@
let trayActive = false;
export const setTrayActive = (active: boolean) => {
trayActive = active;
};
export const isTrayActive = () => trayActive;

View file

@ -1,11 +1,16 @@
import { join } from 'node:path'; import { join } from 'node:path';
import { platform, resourcesPath } from 'node:process'; import { platform, resourcesPath } from 'node:process';
import { app, Menu, type MenuItemConstructorOptions, nativeImage, Tray } from 'electron';
import type { MenuItemConstructorOptions } from 'electron';
import { app, Menu, nativeImage, Tray } from 'electron';
import { PRODUCT_NAME } from '@/constants'; import { PRODUCT_NAME } from '@/constants';
import type { Screen } from '@/types'; import type { Screen } from '@/types';
import { stripFileExtension } from '@/utils/format'; import { stripFileExtension } from '@/utils/format';
import { getEnableSystemTray } from './config'; import { getEnableSystemTray } from './config';
import type { CpuMetrics, GpuMetrics, MemoryMetrics } from './monitoring'; import type { CpuMetrics, GpuMetrics, MemoryMetrics } from './monitoring';
import { setTrayActive } from './tray-active';
import { getMainWindow } from './window'; import { getMainWindow } from './window';
let tray: Tray | null = null; let tray: Tray | null = null;
@ -15,8 +20,8 @@ let currentMetrics: {
gpu: GpuMetrics | null; gpu: GpuMetrics | null;
} = { } = {
cpu: null, cpu: null,
memory: null,
gpu: null, gpu: null,
memory: null,
}; };
interface TrayAppState { interface TrayAppState {
@ -27,9 +32,9 @@ interface TrayAppState {
} }
const appState: TrayAppState = { const appState: TrayAppState = {
currentConfig: null,
currentScreen: null, currentScreen: null,
isLaunched: false, isLaunched: false,
currentConfig: null,
monitoringEnabled: false, monitoringEnabled: false,
}; };
@ -58,9 +63,9 @@ export function updateTrayState(state: {
export function updateMetrics( export function updateMetrics(
cpu: CpuMetrics | null, cpu: CpuMetrics | null,
memory: MemoryMetrics | null, memory: MemoryMetrics | null,
gpu: GpuMetrics | null gpu: GpuMetrics | null,
) { ) {
currentMetrics = { cpu, memory, gpu }; currentMetrics = { cpu, gpu, memory };
updateTrayTooltip(); updateTrayTooltip();
} }
@ -120,15 +125,15 @@ function buildContextMenu() {
if (isVisible) { if (isVisible) {
menuTemplate.push({ menuTemplate.push({
label: 'Hide Gerbil',
click: () => { click: () => {
mainWindow.hide(); mainWindow.hide();
}, },
label: 'Hide Gerbil',
}); });
} else { } else {
menuTemplate.push({ menuTemplate.push({
label: 'Show Gerbil',
click: () => showAndFocusWindow(), click: () => showAndFocusWindow(),
label: 'Show Gerbil',
}); });
} }
@ -137,28 +142,27 @@ function buildContextMenu() {
if (appState.isLaunched && appState.currentScreen === 'interface') { if (appState.isLaunched && appState.currentScreen === 'interface') {
if (appState.currentConfig) { if (appState.currentConfig) {
menuTemplate.push({ menuTemplate.push({
label: `Running: ${stripFileExtension(appState.currentConfig || '')}`,
enabled: false, enabled: false,
label: `Running: ${stripFileExtension(appState.currentConfig || '')}`,
}); });
} }
menuTemplate.push({ menuTemplate.push({
label: 'Eject Model',
click: () => { click: () => {
const mainWindow = getMainWindow(); getMainWindow().webContents.send('tray:eject');
mainWindow.webContents.send('tray:eject');
}, },
label: 'Eject Model',
}); });
} }
menuTemplate.push( menuTemplate.push(
{ type: 'separator' }, { type: 'separator' },
{ {
label: 'Quit',
click: () => { click: () => {
app.quit(); app.quit();
}, },
} label: 'Quit',
},
); );
return Menu.buildFromTemplate(menuTemplate); return Menu.buildFromTemplate(menuTemplate);
@ -179,7 +183,8 @@ export function createTray() {
return; return;
} }
tray = new Tray(icon.resize({ width: 16, height: 16 })); tray = new Tray(icon.resize({ height: 16, width: 16 }));
setTrayActive(true);
updateTrayTooltip(); updateTrayTooltip();
tray.setContextMenu(buildContextMenu()); tray.setContextMenu(buildContextMenu());
@ -218,7 +223,8 @@ export function destroyTray() {
if (tray && platform !== 'linux') { if (tray && platform !== 'linux') {
tray.destroy(); tray.destroy();
tray = null; tray = null;
setTrayActive(false);
} }
} }
export const isTrayActive = () => tray !== null; export { isTrayActive } from './tray-active';

Some files were not shown because too many files have changed in this diff Show more