simplify things by switching from eslint+prettier to biome

This commit is contained in:
lone-cloud 2026-01-12 16:02:02 -08:00
parent 43f6ba10a3
commit d5b470233d
129 changed files with 1400 additions and 4925 deletions

View file

@ -1,6 +0,0 @@
# Prettier-specific ignores (beyond .gitignore)
# Keep files that should be tracked by git but not formatted by prettier
.vscode/settings.json
coverage/
package-lock.json

View file

@ -1,9 +0,0 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"endOfLine": "lf"
}

View file

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

5
.vscode/launch.json vendored
View file

@ -27,10 +27,7 @@
"compounds": [ "compounds": [
{ {
"name": "Debug Electron (Main + Renderer)", "name": "Debug Electron (Main + Renderer)",
"configurations": [ "configurations": ["Debug Electron Main Process", "Debug Electron Renderer Process"]
"Debug Electron Main Process",
"Debug Electron Renderer Process"
]
} }
] ]
} }

27
.vscode/settings.json vendored
View file

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

57
biome.json Normal file
View file

@ -0,0 +1,57 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": true
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"noExplicitAny": "warn",
"noArrayIndexKey": "off"
},
"style": {
"noUnusedTemplateLiteral": "error",
"useConst": "error"
},
"correctness": {
"noUnusedVariables": "error",
"useExhaustiveDependencies": "warn"
},
"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,5 +1,5 @@
import { defineConfig } from 'electron-vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import { defineConfig } from 'electron-vite';
import { resolve } from 'path'; import { resolve } from 'path';
import { visualizer } from 'rollup-plugin-visualizer'; import { visualizer } from 'rollup-plugin-visualizer';

View file

@ -1,239 +0,0 @@
import { defineConfig } from 'eslint/config';
import js from '@eslint/js';
import globals from 'globals';
import tseslint from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
import reactHooks from 'eslint-plugin-react-hooks';
import react from 'eslint-plugin-react';
import importPlugin from 'eslint-plugin-import';
import sonarjs from 'eslint-plugin-sonarjs';
// @ts-ignore - No types available
import noComments from 'eslint-plugin-no-comments';
// @ts-ignore - No types available
import promise from 'eslint-plugin-promise';
const config = defineConfig([
js.configs.recommended,
{
ignores: [
'dist',
'dist-electron',
'out',
'electron',
'scripts',
'release',
'build',
],
},
{
files: ['**/*.{js,mjs,cjs,ts,tsx}'],
ignores: [
'*.config.*',
'vite.config.*',
'eslint.config.*',
'postcss.config.*',
],
languageOptions: {
ecmaVersion: 2020,
parser: tsParser,
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
projectService: true,
allowDefaultProject: true,
ecmaFeatures: {
jsx: true,
},
},
globals: {
...globals.browser,
...globals.node,
},
},
plugins: {
'@typescript-eslint': tseslint as any,
'react-hooks': reactHooks as any,
react,
sonarjs,
'no-comments': noComments,
import: importPlugin,
promise,
},
settings: {
react: {
version: 'detect',
},
},
rules: {
...reactHooks.configs.recommended.rules,
...react.configs.recommended.rules,
...importPlugin.configs.recommended.rules,
...importPlugin.configs.typescript.rules,
...tseslint.configs.recommended.rules,
...tseslint.configs['recommended-type-checked'].rules,
...tseslint.configs.stylistic.rules,
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
'no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'react/function-component-definition': [
'error',
{
namedComponents: 'arrow-function',
unnamedComponents: 'arrow-function',
},
],
'react/react-in-jsx-scope': 'off',
'react/jsx-boolean-value': ['error', 'never'],
'react/jsx-curly-brace-presence': [
'error',
{ props: 'never', children: 'never' },
],
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['react'],
importNames: ['default'],
message:
'Import specific React hooks/utilities instead of default React import. Use automatic JSX transform.',
},
{
group: ['fs'],
importNames: ['readFileSync', 'writeFileSync', 'existsSync'],
message:
'Use async file operations instead: readFile, writeFile, access from fs/promises',
},
],
},
],
'no-restricted-syntax': [
'error',
{
selector: 'CallExpression[callee.name=/.*Sync$/]',
message:
'Synchronous file operations are forbidden. Use async alternatives.',
},
{
selector:
'CallExpression[callee.object.object.object.name="window"][callee.object.object.property.name="electronAPI"][callee.object.property.name="config"][callee.property.name="get"] Literal[value="currentKoboldBinary"]',
message:
'Direct access to currentKoboldBinary config is forbidden. Use window.electronAPI.kobold.getCurrentVersion() instead to get proper fallback logic.',
},
{
selector:
'CallExpression[callee.object.object.object.name="window"][callee.object.object.property.name="electronAPI"][callee.object.property.name="config"][callee.property.name="set"] Literal[value="currentKoboldBinary"]',
message:
'Direct setting of currentKoboldBinary config is forbidden. Use window.electronAPI.kobold.setCurrentVersion() instead.',
},
],
'import/no-default-export': 'error',
'import/prefer-default-export': 'off',
'import/no-unresolved': 'off',
'import/no-relative-parent-imports': 'error',
'@typescript-eslint/array-type': ['error', { default: 'array' }],
'@typescript-eslint/no-require-imports': 'error',
'arrow-body-style': ['error', 'as-needed'],
'prefer-arrow-callback': ['error', { allowNamedFunctions: false }],
'object-shorthand': ['error', 'always'],
'prefer-const': 'error',
'no-console': 'error',
'no-restricted-globals': [
'error',
{
name: 'process',
message:
'Import specific properties from "process" instead of using the global: import { platform, env } from "process"',
},
],
'@typescript-eslint/no-inferrable-types': 'warn',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/return-await': ['error', 'never'],
'@typescript-eslint/require-await': 'error',
'@typescript-eslint/prefer-promise-reject-errors': 'error',
'sonarjs/cognitive-complexity': ['warn', 25],
'no-empty': ['error', { allowEmptyCatch: true }],
'promise/prefer-await-to-then': 'error',
'promise/prefer-await-to-callbacks': 'off',
'promise/no-nesting': 'error',
'promise/no-promise-in-callback': 'off',
'promise/no-callback-in-promise': 'off',
'promise/avoid-new': 'off',
'promise/no-new-statics': 'error',
'promise/no-return-wrap': 'error',
'promise/param-names': 'error',
'promise/catch-or-return': 'off',
'promise/no-native': 'off',
'no-comments/disallowComments': 'error',
},
},
{
files: ['**/*.d.ts'],
rules: {
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
},
{
files: [
'*.config.*',
'vite.config.*',
'eslint.config.*',
'postcss.config.*',
],
languageOptions: {
parser: tsParser,
parserOptions: {
sourceType: 'module',
},
globals: {
...globals.node,
},
},
rules: {
'import/no-default-export': 'off',
'@typescript-eslint/return-await': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-require-imports': 'off',
},
},
{
files: ['**/*.d.ts'],
rules: {
'no-comments/disallowComments': 'off',
'import/no-default-export': 'off',
},
},
]);
export default config;

View file

@ -6,8 +6,7 @@
"main": "out/main/index.js", "main": "out/main/index.js",
"homepage": "./", "homepage": "./",
"engines": { "engines": {
"node": ">=22.0.0", "node": ">=22.0.0"
"yarn": "^4.0.0"
}, },
"packageManager": "yarn@4.12.0", "packageManager": "yarn@4.12.0",
"scripts": { "scripts": {
@ -15,12 +14,8 @@
"build": "electron-vite build", "build": "electron-vite build",
"package": "electron-vite build && electron-builder --publish=never", "package": "electron-vite build && electron-builder --publish=never",
"analyze": "cross-env ANALYZE=true electron-vite build && yarn dlx open-cli dist/stats.html", "analyze": "cross-env ANALYZE=true electron-vite build && yarn dlx open-cli dist/stats.html",
"format": "prettier --write . --ignore-path .gitignore | grep -v '(unchanged)' || true", "check": "biome check . && tsc --noEmit",
"format:check": "prettier --check . --ignore-path .gitignore", "fix": "biome check --write .",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"compile": "tsc --noEmit",
"check": "yarn format:check && yarn lint && yarn compile",
"release": "yarn dlx tsx scripts/release.ts" "release": "yarn dlx tsx scripts/release.ts"
}, },
"keywords": [ "keywords": [
@ -65,29 +60,18 @@
"zustand": "^5.0.10" "zustand": "^5.0.10"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.2", "@biomejs/biome": "^2.3.11",
"@types/mime-types": "^3.0.1", "@types/mime-types": "^3.0.1",
"@types/node": "^25.0.6", "@types/node": "^25.0.6",
"@types/react": "^19.2.8", "@types/react": "^19.2.8",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@types/yauzl": "^2.10.3", "@types/yauzl": "^2.10.3",
"@typescript-eslint/eslint-plugin": "^8.53.0",
"@typescript-eslint/parser": "^8.53.0",
"@vitejs/plugin-react": "^5.1.2", "@vitejs/plugin-react": "^5.1.2",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
"electron": "^38.7.2", "electron": "^38.7.2",
"electron-builder": "^26.4.0", "electron-builder": "^26.4.0",
"electron-vite": "^5.0.0", "electron-vite": "^5.0.0",
"eslint": "^9.39.2",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-no-comments": "^1.1.10",
"eslint-plugin-promise": "^7.2.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-sonarjs": "^3.0.5",
"globals": "^17.0.0",
"jiti": "^2.6.1", "jiti": "^2.6.1",
"prettier": "^3.7.4",
"rollup-plugin-visualizer": "^6.0.5", "rollup-plugin-visualizer": "^6.0.5",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.3.1" "vite": "^7.3.1"

View file

@ -37,9 +37,7 @@ try {
console.log(`✅ Release ${tagName} created successfully!`); console.log(`✅ Release ${tagName} created successfully!`);
console.log(`📦 GitHub Actions will now build and publish the release.`); console.log(`📦 GitHub Actions will now build and publish the release.`);
console.log( console.log(`🔗 Check the progress at: https://github.com/lone-cloud/gerbil/actions`);
`🔗 Check the progress at: https://github.com/lone-cloud/gerbil/actions`
);
} catch (error: unknown) { } catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error); const errorMessage = error instanceof Error ? error.message : String(error);
console.error('❌ Error creating release:', errorMessage); console.error('❌ Error creating release:', errorMessage);

View file

@ -1,4 +1,4 @@
import { Text, Group, Button, Stack } 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';
@ -26,10 +26,7 @@ const getCrashDescription = (crashInfo: KoboldCrashInfo) => {
SIGHUP: 'The process lost its controlling terminal', SIGHUP: 'The process lost its controlling terminal',
}; };
return ( return signalDescriptions[crashInfo.signal] || `Terminated by signal ${crashInfo.signal}`;
signalDescriptions[crashInfo.signal] ||
`Terminated by signal ${crashInfo.signal}`
);
} }
if (crashInfo.exitCode !== null) { if (crashInfo.exitCode !== null) {
@ -39,11 +36,7 @@ const getCrashDescription = (crashInfo: KoboldCrashInfo) => {
return 'The process terminated unexpectedly'; return 'The process terminated unexpectedly';
}; };
export const BackendCrashModal = ({ export const BackendCrashModal = ({ opened, onClose, crashInfo }: BackendCrashModalProps) => {
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,5 @@
import { Button, Checkbox, Group, Stack, Text } from '@mantine/core';
import { useState } from 'react'; import { useState } from 'react';
import { Text, Group, Button, Checkbox, Stack } from '@mantine/core';
import { Modal } from '@/components/Modal'; import { Modal } from '@/components/Modal';
interface EjectConfirmModalProps { interface EjectConfirmModalProps {
@ -8,11 +8,7 @@ interface EjectConfirmModalProps {
onConfirm: (skipConfirmation: boolean) => void; onConfirm: (skipConfirmation: boolean) => void;
} }
export const EjectConfirmModal = ({ export const EjectConfirmModal = ({ opened, onClose, onConfirm }: EjectConfirmModalProps) => {
opened,
onClose,
onConfirm,
}: EjectConfirmModalProps) => {
const [skipConfirmation, setSkipConfirmation] = useState(false); const [skipConfirmation, setSkipConfirmation] = useState(false);
const handleConfirm = () => { const handleConfirm = () => {
@ -26,15 +22,10 @@ export const EjectConfirmModal = ({
}; };
return ( return (
<Modal <Modal opened={opened} onClose={handleClose} title="Are you sure you want to eject?">
opened={opened}
onClose={handleClose}
title="Are you sure you want to eject?"
>
<Stack gap="md"> <Stack gap="md">
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
This will terminate the running process and return to the launch This will terminate the running process and return to the launch screen.
screen.
</Text> </Text>
<Checkbox <Checkbox

View file

@ -1,6 +1,6 @@
import { ReactNode } from 'react'; import { Alert, Button, Center, rem, Stack, Text } from '@mantine/core';
import { Center, Stack, Text, Button, Alert, rem } from '@mantine/core';
import { AlertTriangle, FolderOpen } from 'lucide-react'; import { AlertTriangle, FolderOpen } from 'lucide-react';
import type { ReactNode } from 'react';
import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';
interface ErrorBoundaryProps { interface ErrorBoundaryProps {
@ -24,8 +24,8 @@ const ErrorFallback = ({
<Alert color="red" title="Something went wrong" w="100%"> <Alert color="red" title="Something went wrong" w="100%">
<Text size="sm" mb="md"> <Text size="sm" mb="md">
The application encountered an unexpected error and crashed. You can The application encountered an unexpected error and crashed. You can view the error
view the error details in the logs folder. details in the logs folder.
</Text> </Text>
<Text size="xs" c="dimmed" style={{ fontFamily: 'monospace' }} mb="sm"> <Text size="xs" c="dimmed" style={{ fontFamily: 'monospace' }} mb="sm">
@ -35,9 +35,7 @@ const ErrorFallback = ({
<Button <Button
variant="light" variant="light"
size="compact-sm" size="compact-sm"
leftSection={ leftSection={<FolderOpen style={{ width: rem(16), height: rem(16) }} />}
<FolderOpen style={{ width: rem(16), height: rem(16) }} />
}
onClick={() => void window.electronAPI.app.showLogsFolder()} onClick={() => void window.electronAPI.app.showLogsFolder()}
> >
Show Logs Folder Show Logs Folder
@ -68,10 +66,7 @@ export const ErrorBoundary = ({ children }: ErrorBoundaryProps) => (
<ReactErrorBoundary <ReactErrorBoundary
FallbackComponent={ErrorFallback} FallbackComponent={ErrorFallback}
onError={(error) => { onError={(error) => {
window.electronAPI?.logs?.logError( window.electronAPI?.logs?.logError('App crashed with unhandled error:', error);
'App crashed with unhandled error:',
error
);
}} }}
> >
{children} {children}

View file

@ -1,4 +1,4 @@
import { Button, Tooltip, ActionIcon } from '@mantine/core'; import { ActionIcon, Button, Tooltip } from '@mantine/core';
import { Activity } from 'lucide-react'; import { Activity } from 'lucide-react';
interface PerformanceBadgeProps { interface PerformanceBadgeProps {
@ -18,20 +18,14 @@ export const PerformanceBadge = ({
const result = await window.electronAPI.app.openPerformanceManager(); const result = await window.electronAPI.app.openPerformanceManager();
if (!result.success) { if (!result.success) {
window.electronAPI.logs.logError( window.electronAPI.logs.logError(`Failed to open performance manager: ${result.error}`);
`Failed to open performance manager: ${result.error}`
);
} }
}; };
if (iconOnly) { if (iconOnly) {
return ( return (
<Tooltip label={tooltipLabel} position="top"> <Tooltip label={tooltipLabel} position="top">
<ActionIcon <ActionIcon size="sm" variant="subtle" onClick={() => void handlePerformanceClick()}>
size="sm"
variant="subtle"
onClick={() => void handlePerformanceClick()}
>
<Activity size="1.125rem" /> <Activity size="1.125rem" />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>

View file

@ -1,7 +1,7 @@
import { ScreenTransition } from '@/components/App/ScreenTransition'; import { ScreenTransition } from '@/components/App/ScreenTransition';
import { DownloadScreen } from '@/components/screens/Download'; import { DownloadScreen } from '@/components/screens/Download';
import { LaunchScreen } from '@/components/screens/Launch';
import { InterfaceScreen } from '@/components/screens/Interface'; import { InterfaceScreen } from '@/components/screens/Interface';
import { LaunchScreen } from '@/components/screens/Launch';
import { WelcomeScreen } from '@/components/screens/Welcome'; import { WelcomeScreen } from '@/components/screens/Welcome';
import type { InterfaceTab, Screen } from '@/types'; import type { InterfaceTab, Screen } from '@/types';
@ -28,35 +28,20 @@ export const AppRouter = ({
return ( return (
<> <>
<ScreenTransition <ScreenTransition isActive={currentScreen === 'welcome'} shouldAnimate={hasInitialized}>
isActive={currentScreen === 'welcome'}
shouldAnimate={hasInitialized}
>
<WelcomeScreen onGetStarted={onWelcomeComplete} /> <WelcomeScreen onGetStarted={onWelcomeComplete} />
</ScreenTransition> </ScreenTransition>
<ScreenTransition <ScreenTransition isActive={currentScreen === 'download'} shouldAnimate={hasInitialized}>
isActive={currentScreen === 'download'}
shouldAnimate={hasInitialized}
>
<DownloadScreen onDownloadComplete={onDownloadComplete} /> <DownloadScreen onDownloadComplete={onDownloadComplete} />
</ScreenTransition> </ScreenTransition>
<ScreenTransition <ScreenTransition isActive={currentScreen === 'launch'} shouldAnimate={hasInitialized}>
isActive={currentScreen === 'launch'}
shouldAnimate={hasInitialized}
>
<LaunchScreen onLaunch={onLaunch} /> <LaunchScreen onLaunch={onLaunch} />
</ScreenTransition> </ScreenTransition>
<ScreenTransition <ScreenTransition isActive={isInterfaceScreen} shouldAnimate={hasInitialized}>
isActive={isInterfaceScreen} <InterfaceScreen activeTab={activeInterfaceTab} isServerReady={isServerReady} />
shouldAnimate={hasInitialized}
>
<InterfaceScreen
activeTab={activeInterfaceTab}
isServerReady={isServerReady}
/>
</ScreenTransition> </ScreenTransition>
</> </>
); );

View file

@ -1,5 +1,5 @@
import { ReactNode } from 'react';
import { Transition } from '@mantine/core'; import { Transition } from '@mantine/core';
import type { ReactNode } from 'react';
interface ScreenTransitionProps { interface ScreenTransitionProps {
isActive: boolean; isActive: boolean;
@ -7,11 +7,7 @@ interface ScreenTransitionProps {
children: ReactNode; children: ReactNode;
} }
export const ScreenTransition = ({ export const ScreenTransition = ({ isActive, shouldAnimate, children }: ScreenTransitionProps) => (
isActive,
shouldAnimate,
children,
}: ScreenTransitionProps) => (
<Transition <Transition
mounted={isActive} mounted={isActive}
transition="fade" transition="fade"

View file

@ -1,32 +1,16 @@
import { useEffect, useState, useMemo } from 'react'; import { ActionIcon, AppShell, CopyButton, Group, Tooltip } from '@mantine/core';
import { import { Check, Globe, NotepadText } from 'lucide-react';
Group, import { useEffect, useMemo, useState } from 'react';
AppShell, import type { CpuMetrics, GpuMetrics, MemoryMetrics } from '@/main/modules/monitoring';
ActionIcon,
Tooltip,
CopyButton,
} from '@mantine/core';
import { NotepadText, Globe, Check } from 'lucide-react';
import { usePreferencesStore } from '@/stores/preferences';
import { useNotepadStore } from '@/stores/notepad';
import { useLaunchConfigStore } from '@/stores/launchConfig'; import { useLaunchConfigStore } from '@/stores/launchConfig';
import { useNotepadStore } from '@/stores/notepad';
import { usePreferencesStore } from '@/stores/preferences';
import { getTunnelInterfaceUrl } from '@/utils/interface'; import { getTunnelInterfaceUrl } from '@/utils/interface';
import type {
CpuMetrics,
MemoryMetrics,
GpuMetrics,
} from '@/main/modules/monitoring';
import { PerformanceBadge } from './PerformanceBadge'; import { PerformanceBadge } from './PerformanceBadge';
interface StatusBarProps { export const StatusBar = () => {
maxDataPoints?: number;
}
export const StatusBar = ({ maxDataPoints = 60 }: StatusBarProps) => {
const [cpuMetrics, setCpuMetrics] = useState<CpuMetrics | null>(null); const [cpuMetrics, setCpuMetrics] = useState<CpuMetrics | null>(null);
const [memoryMetrics, setMemoryMetrics] = useState<MemoryMetrics | null>( const [memoryMetrics, setMemoryMetrics] = useState<MemoryMetrics | null>(null);
null
);
const [gpuMetrics, setGpuMetrics] = useState<GpuMetrics | null>(null); const [gpuMetrics, setGpuMetrics] = useState<GpuMetrics | null>(null);
const [tunnelBaseUrl, setTunnelBaseUrl] = useState<string | null>(null); const [tunnelBaseUrl, setTunnelBaseUrl] = useState<string | null>(null);
const { const {
@ -40,10 +24,7 @@ export const StatusBar = ({ maxDataPoints = 60 }: StatusBarProps) => {
const tunnelUrl = useMemo(() => { const tunnelUrl = useMemo(() => {
if (!tunnelBaseUrl) return null; if (!tunnelBaseUrl) return null;
if ( if (frontendPreference === 'sillytavern' || frontendPreference === 'openwebui') {
frontendPreference === 'sillytavern' ||
frontendPreference === 'openwebui'
) {
return tunnelBaseUrl; return tunnelBaseUrl;
} }
return getTunnelInterfaceUrl(tunnelBaseUrl, { return getTunnelInterfaceUrl(tunnelBaseUrl, {
@ -51,12 +32,7 @@ export const StatusBar = ({ maxDataPoints = 60 }: StatusBarProps) => {
imageGenerationFrontendPreference, imageGenerationFrontendPreference,
isImageGenerationMode, isImageGenerationMode,
}); });
}, [ }, [tunnelBaseUrl, frontendPreference, imageGenerationFrontendPreference, isImageGenerationMode]);
tunnelBaseUrl,
frontendPreference,
imageGenerationFrontendPreference,
isImageGenerationMode,
]);
useEffect(() => { useEffect(() => {
if (!systemMonitoringEnabled) { if (!systemMonitoringEnabled) {
@ -80,12 +56,9 @@ export const StatusBar = ({ maxDataPoints = 60 }: StatusBarProps) => {
setGpuMetrics(metrics); setGpuMetrics(metrics);
}; };
const cleanupCpu = const cleanupCpu = window.electronAPI.monitoring.onCpuMetrics(handleCpuMetrics);
window.electronAPI.monitoring.onCpuMetrics(handleCpuMetrics); const cleanupMemory = window.electronAPI.monitoring.onMemoryMetrics(handleMemoryMetrics);
const cleanupMemory = const cleanupGpu = window.electronAPI.monitoring.onGpuMetrics(handleGpuMetrics);
window.electronAPI.monitoring.onMemoryMetrics(handleMemoryMetrics);
const cleanupGpu =
window.electronAPI.monitoring.onGpuMetrics(handleGpuMetrics);
const stopMonitoring = window.electronAPI.monitoring.start(); const stopMonitoring = window.electronAPI.monitoring.start();
return () => { return () => {
@ -95,11 +68,10 @@ export const StatusBar = ({ maxDataPoints = 60 }: StatusBarProps) => {
cleanupGpu?.(); cleanupGpu?.();
stopMonitoring?.(); stopMonitoring?.();
}; };
}, [maxDataPoints, systemMonitoringEnabled]); }, [systemMonitoringEnabled]);
useEffect(() => { useEffect(() => {
const cleanup = const cleanup = window.electronAPI.kobold.onTunnelUrlChanged(setTunnelBaseUrl);
window.electronAPI.kobold.onTunnelUrlChanged(setTunnelBaseUrl);
return cleanup; return cleanup;
}, []); }, []);
@ -133,21 +105,14 @@ export const StatusBar = ({ maxDataPoints = 60 }: StatusBarProps) => {
{tunnelUrl && ( {tunnelUrl && (
<CopyButton value={tunnelUrl}> <CopyButton value={tunnelUrl}>
{({ copied, copy }) => ( {({ copied, copy }) => (
<Tooltip <Tooltip label={copied ? 'Copied!' : 'Copy Tunnel URL'} position="top">
label={copied ? 'Copied!' : 'Copy Tunnel URL'}
position="top"
>
<ActionIcon <ActionIcon
variant="subtle" variant="subtle"
size="sm" size="sm"
color={copied ? 'teal' : undefined} color={copied ? 'teal' : undefined}
onClick={copy} onClick={copy}
> >
{copied ? ( {copied ? <Check size="1.25rem" /> : <Globe size="1.25rem" />}
<Check size="1.25rem" />
) : (
<Globe size="1.25rem" />
)}
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
)} )}

View file

@ -1,23 +1,16 @@
import { import { ActionIcon, AppShell, Box, Group, Image, Tooltip } from '@mantine/core';
Group, import { Copy, Minus, Settings, Square, X } from 'lucide-react';
ActionIcon, import { useEffect, useState } from 'react';
Box, import { UpdateButton } from '@/components/App/UpdateButton';
Image, import { Select } from '@/components/Select';
AppShell, import { SettingsModal } from '@/components/settings/SettingsModal';
Tooltip, import { PRODUCT_NAME, TITLEBAR_HEIGHT } from '@/constants';
} from '@mantine/core'; import { useLogoClickSounds } from '@/hooks/useLogoClickSounds';
import { Minus, Square, X, Copy, Settings } from 'lucide-react';
import { useState, useEffect } from 'react';
import { useLaunchConfigStore } from '@/stores/launchConfig'; import { useLaunchConfigStore } from '@/stores/launchConfig';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { getAvailableInterfaceOptions } from '@/utils/interface';
import { useLogoClickSounds } from '@/hooks/useLogoClickSounds';
import { SettingsModal } from '@/components/settings/SettingsModal';
import { UpdateButton } from '@/components/App/UpdateButton';
import icon from '/icon.png';
import { PRODUCT_NAME, TITLEBAR_HEIGHT } from '@/constants';
import type { InterfaceTab, Screen, SelectOption } from '@/types'; import type { InterfaceTab, Screen, SelectOption } from '@/types';
import { Select } from '@/components/Select'; import { getAvailableInterfaceOptions } from '@/utils/interface';
import icon from '/icon.png';
interface TitleBarProps { interface TitleBarProps {
currentScreen: Screen; currentScreen: Screen;
@ -26,12 +19,7 @@ interface TitleBarProps {
onTabChange: (tab: InterfaceTab) => void; onTabChange: (tab: InterfaceTab) => void;
} }
export const TitleBar = ({ export const TitleBar = ({ currentScreen, currentTab, onEject, onTabChange }: TitleBarProps) => {
currentScreen,
currentTab,
onEject,
onTabChange,
}: TitleBarProps) => {
const { const {
resolvedColorScheme: colorScheme, resolvedColorScheme: colorScheme,
frontendPreference, frontendPreference,
@ -56,8 +44,7 @@ export const TitleBar = ({
<Box <Box
style={{ style={{
textAlign: 'center', textAlign: 'center',
color: color: option.value === 'eject' ? 'var(--mantine-color-red-6)' : undefined,
option.value === 'eject' ? 'var(--mantine-color-red-6)' : undefined,
fontWeight: option.value === 'eject' ? 600 : undefined, fontWeight: option.value === 'eject' ? 600 : undefined,
}} }}
> >
@ -81,9 +68,7 @@ export const TitleBar = ({
}, []); }, []);
return ( return (
<AppShell.Header <AppShell.Header style={{ display: 'flex', flexDirection: 'column', border: 'none' }}>
style={{ display: 'flex', flexDirection: 'column', border: 'none' }}
>
<Box <Box
style={{ style={{
height: TITLEBAR_HEIGHT, height: TITLEBAR_HEIGHT,
@ -92,20 +77,14 @@ export const TitleBar = ({
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
backgroundColor: backgroundColor:
colorScheme === 'dark' colorScheme === 'dark' ? 'var(--mantine-color-dark-6)' : 'var(--mantine-color-gray-1)',
? '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', WebkitAppRegion: isSelectOpen ? 'no-drag' : 'drag',
userSelect: 'none', userSelect: 'none',
position: 'relative', position: 'relative',
}} }}
> >
<Group <Group gap="0.5rem" align="center" style={{ WebkitAppRegion: 'no-drag' }}>
gap="0.5rem"
align="center"
style={{ WebkitAppRegion: 'no-drag' }}
>
<Image <Image
src={icon} src={icon}
alt={PRODUCT_NAME} alt={PRODUCT_NAME}

View file

@ -1,23 +1,14 @@
import { import { Anchor, Button, Card, Group, Loader, Progress, Stack, Text } from '@mantine/core';
Stack, import { Download, ExternalLink, X } from 'lucide-react';
Text,
Group,
Button,
Card,
Loader,
Anchor,
Progress,
} from '@mantine/core';
import { Download, X, ExternalLink } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import type { DownloadItem } from '@/types/electron'; import { Modal } from '@/components/Modal';
import { GITHUB_API } from '@/constants';
import type { BinaryUpdateInfo } from '@/hooks/useUpdateChecker'; import type { BinaryUpdateInfo } from '@/hooks/useUpdateChecker';
import { useKoboldBackendsStore } from '@/stores/koboldBackends'; import { useKoboldBackendsStore } from '@/stores/koboldBackends';
import type { DownloadItem } from '@/types/electron';
import { pretifyBinName } from '@/utils/assets'; import { pretifyBinName } from '@/utils/assets';
import { formatDownloadSize } from '@/utils/format'; import { formatDownloadSize } from '@/utils/format';
import { GITHUB_API } from '@/constants';
import { safeExecute } from '@/utils/logger'; import { safeExecute } from '@/utils/logger';
import { Modal } from '@/components/Modal';
interface UpdateAvailableModalProps { interface UpdateAvailableModalProps {
opened: boolean; opened: boolean;
@ -39,11 +30,8 @@ export const UpdateAvailableModal = ({
const availableUpdate = updateInfo?.availableUpdate; const availableUpdate = updateInfo?.availableUpdate;
const [isUpdating, setIsUpdating] = useState(false); const [isUpdating, setIsUpdating] = useState(false);
const isDownloading = const isDownloading = !!availableUpdate && downloading === availableUpdate.name;
!!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 () => {
if (availableUpdate) { if (availableUpdate) {
@ -99,8 +87,7 @@ export const UpdateAvailableModal = ({
{availableUpdate?.size && ( {availableUpdate?.size && (
<Text size="xs" c="dimmed"> <Text size="xs" c="dimmed">
Update Size:{' '} Update Size: {formatDownloadSize(availableUpdate.size, availableUpdate.url)}
{formatDownloadSize(availableUpdate.size, availableUpdate.url)}
</Text> </Text>
)} )}
@ -158,11 +145,7 @@ export const UpdateAvailableModal = ({
loading={isDownloading || isUpdating} loading={isDownloading || isUpdating}
disabled={isDownloading || isUpdating} disabled={isDownloading || isUpdating}
leftSection={ leftSection={
isDownloading || isUpdating ? ( isDownloading || isUpdating ? <Loader size="1rem" /> : <Download size={16} />
<Loader size="1rem" />
) : (
<Download size={16} />
)
} }
color="orange" color="orange"
> >

View file

@ -1,8 +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 { useAppUpdateChecker } from '@/hooks/useAppUpdateChecker'; import { type MouseEvent, useState } from 'react';
import { TITLEBAR_HEIGHT } from '@/constants'; import { TITLEBAR_HEIGHT } from '@/constants';
import { useState, type MouseEvent } from 'react'; import { useAppUpdateChecker } from '@/hooks/useAppUpdateChecker';
export const UpdateButton = () => { export const UpdateButton = () => {
const { const {

View file

@ -1,35 +1,27 @@
import { useState, useEffect, useMemo } from 'react'; import { AppShell, Center, Loader, Stack, Text, useMantineColorScheme } from '@mantine/core';
import { import { useCallback, useEffect, useMemo, useState } from 'react';
AppShell,
Loader,
Center,
Stack,
Text,
useMantineColorScheme,
} from '@mantine/core';
import { UpdateAvailableModal } from '@/components/App/UpdateAvailableModal';
import { EjectConfirmModal } from '@/components/App/EjectConfirmModal';
import { BackendCrashModal } from '@/components/App/BackendCrashModal'; import { BackendCrashModal } from '@/components/App/BackendCrashModal';
import { TitleBar } from '@/components/App/TitleBar'; import { EjectConfirmModal } from '@/components/App/EjectConfirmModal';
import { StatusBar } from '@/components/App/StatusBar';
import { ErrorBoundary } from '@/components/App/ErrorBoundary'; import { ErrorBoundary } from '@/components/App/ErrorBoundary';
import { AppRouter } from '@/components/App/Router'; import { AppRouter } from '@/components/App/Router';
import { StatusBar } from '@/components/App/StatusBar';
import { TitleBar } from '@/components/App/TitleBar';
import { UpdateAvailableModal } from '@/components/App/UpdateAvailableModal';
import { NotepadContainer } from '@/components/Notepad/Container'; import { NotepadContainer } from '@/components/Notepad/Container';
import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants';
import { useUpdateChecker } from '@/hooks/useUpdateChecker'; import { useUpdateChecker } from '@/hooks/useUpdateChecker';
import { useKoboldBackendsStore } from '@/stores/koboldBackends'; import { useKoboldBackendsStore } from '@/stores/koboldBackends';
import { usePreferencesStore } from '@/stores/preferences';
import { useLaunchConfigStore } from '@/stores/launchConfig'; import { useLaunchConfigStore } from '@/stores/launchConfig';
import { getDefaultInterfaceTab } from '@/utils/interface'; import { usePreferencesStore } from '@/stores/preferences';
import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants';
import type { DownloadItem } from '@/types/electron';
import type { InterfaceTab, Screen } from '@/types'; import type { InterfaceTab, Screen } from '@/types';
import type { DownloadItem } from '@/types/electron';
import type { KoboldCrashInfo } from '@/types/ipc'; import type { KoboldCrashInfo } from '@/types/ipc';
import { getDefaultInterfaceTab } from '@/utils/interface';
export const App = () => { export const App = () => {
const [currentScreen, setCurrentScreen] = useState<Screen | null>(null); const [currentScreen, setCurrentScreen] = useState<Screen | null>(null);
const [hasInitialized, setHasInitialized] = useState(false); const [hasInitialized, setHasInitialized] = useState(false);
const [activeInterfaceTab, setActiveInterfaceTab] = const [activeInterfaceTab, setActiveInterfaceTab] = useState<InterfaceTab>('terminal');
useState<InterfaceTab>('terminal');
const [isServerReady, setIsServerReady] = useState(false); const [isServerReady, setIsServerReady] = useState(false);
const [ejectConfirmModalOpen, setEjectConfirmModalOpen] = useState(false); const [ejectConfirmModalOpen, setEjectConfirmModalOpen] = useState(false);
const [crashInfo, setCrashInfo] = useState<KoboldCrashInfo | null>(null); const [crashInfo, setCrashInfo] = useState<KoboldCrashInfo | null>(null);
@ -42,8 +34,7 @@ export const App = () => {
imageGenerationFrontendPreference, imageGenerationFrontendPreference,
} = usePreferencesStore(); } = usePreferencesStore();
const { setColorScheme } = useMantineColorScheme(); const { setColorScheme } = useMantineColorScheme();
const { model, sdmodel, isTextMode, isImageGenerationMode } = const { model, sdmodel, isTextMode, isImageGenerationMode } = useLaunchConfigStore();
useLaunchConfigStore();
const defaultInterfaceTab = useMemo( const defaultInterfaceTab = useMemo(
() => () =>
@ -53,12 +44,7 @@ export const App = () => {
isTextMode, isTextMode,
isImageGenerationMode, isImageGenerationMode,
}), }),
[ [frontendPreference, imageGenerationFrontendPreference, isTextMode, isImageGenerationMode]
frontendPreference,
imageGenerationFrontendPreference,
isTextMode,
isImageGenerationMode,
]
); );
useEffect(() => { useEffect(() => {
@ -80,9 +66,7 @@ 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 const modelName = displayModel ? displayModel.split('/').pop() || displayModel : null;
? displayModel.split('/').pop() || displayModel
: null;
void window.electronAPI.app.updateTrayState({ void window.electronAPI.app.updateTrayState({
screen: currentScreen, screen: currentScreen,
@ -95,12 +79,12 @@ export const App = () => {
void updateTray(); void updateTray();
}, [currentScreen, model, sdmodel, systemMonitoringEnabled]); }, [currentScreen, model, sdmodel, systemMonitoringEnabled]);
const performEject = () => { const performEject = useCallback(() => {
window.electronAPI.kobold.stopKoboldCpp(); window.electronAPI.kobold.stopKoboldCpp();
setIsServerReady(false); setIsServerReady(false);
setActiveInterfaceTab('terminal'); setActiveInterfaceTab('terminal');
setCurrentScreen('launch'); setCurrentScreen('launch');
}; }, []);
useEffect(() => { useEffect(() => {
const ejectCleanup = window.electronAPI.app.onTrayEject(() => { const ejectCleanup = window.electronAPI.app.onTrayEject(() => {
@ -110,14 +94,12 @@ export const App = () => {
return () => { return () => {
ejectCleanup(); ejectCleanup();
}; };
}, []); }, [performEject]);
useEffect(() => { useEffect(() => {
const crashCleanup = window.electronAPI.kobold.onKoboldCrashed( const crashCleanup = window.electronAPI.kobold.onKoboldCrashed((crashData) => {
(crashData) => {
setCrashInfo(crashData); setCrashInfo(crashData);
} });
);
return () => { return () => {
crashCleanup(); crashCleanup();
@ -134,10 +116,7 @@ export const App = () => {
const { handleDownload, loadingRemote } = useKoboldBackendsStore(); const { handleDownload, loadingRemote } = useKoboldBackendsStore();
const determineScreen = ( const determineScreen = useCallback((currentVersion: unknown, hasSeenWelcome: boolean) => {
currentVersion: unknown,
hasSeenWelcome: boolean
) => {
if (!hasSeenWelcome) { if (!hasSeenWelcome) {
setCurrentScreen('welcome'); setCurrentScreen('welcome');
} else if (currentVersion) { } else if (currentVersion) {
@ -145,7 +124,7 @@ export const App = () => {
} else { } else {
setCurrentScreen('download'); setCurrentScreen('download');
} }
}; }, []);
useEffect(() => { useEffect(() => {
const checkInstallation = async () => { const checkInstallation = async () => {
@ -159,14 +138,13 @@ export const App = () => {
}; };
void checkInstallation(); void checkInstallation();
}, []); }, [determineScreen]);
useEffect(() => { useEffect(() => {
if (loadingRemote || !hasInitialized) return; if (loadingRemote || !hasInitialized) return;
const runUpdateCheck = async () => { const runUpdateCheck = async () => {
const currentBackend = const currentBackend = await window.electronAPI.kobold.getCurrentBackend();
await window.electronAPI.kobold.getCurrentBackend();
if (!currentBackend) return; if (!currentBackend) return;
void checkForUpdates(); void checkForUpdates();
@ -203,9 +181,7 @@ export const App = () => {
}; };
const handleEject = async () => { const handleEject = async () => {
const skipEjectConfirmation = await window.electronAPI.config.get( const skipEjectConfirmation = await window.electronAPI.config.get('skipEjectConfirmation');
'skipEjectConfirmation'
);
if (skipEjectConfirmation) { if (skipEjectConfirmation) {
performEject(); performEject();

View file

@ -1,20 +1,10 @@
import { import { Badge, Button, Card, Group, Loader, Progress, rem, Stack, Text } from '@mantine/core';
Card,
Stack,
Group,
Text,
Badge,
Button,
Loader,
Progress,
rem,
} from '@mantine/core';
import { Download, Trash2 } from 'lucide-react'; import { Download, Trash2 } from 'lucide-react';
import { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
import { pretifyBinName, isWindowsROCmBuild } from '@/utils/assets';
import { usePreferencesStore } from '@/stores/preferences';
import { useKoboldBackendsStore } from '@/stores/koboldBackends'; import { useKoboldBackendsStore } from '@/stores/koboldBackends';
import { usePreferencesStore } from '@/stores/preferences';
import type { BackendInfo } from '@/types'; import type { BackendInfo } from '@/types';
import { isWindowsROCmBuild, pretifyBinName } from '@/utils/assets';
interface DownloadCardProps { interface DownloadCardProps {
backend: BackendInfo; backend: BackendInfo;
@ -43,16 +33,11 @@ export const DownloadCard = ({
const { downloading, downloadProgress } = useKoboldBackendsStore(); const { downloading, downloadProgress } = useKoboldBackendsStore();
const isLoading = downloading === backend.name; const isLoading = downloading === backend.name;
const currentProgress = isLoading const currentProgress = isLoading ? Math.min(downloadProgress[backend.name], 100) || 0 : 0;
? Math.min(downloadProgress[backend.name], 100) || 0
: 0;
const hasVersionMismatch = Boolean( const hasVersionMismatch = Boolean(
backend.version && backend.version && backend.actualVersion && backend.version !== backend.actualVersion
backend.actualVersion &&
backend.version !== backend.actualVersion
); );
// eslint-disable-next-line sonarjs/cognitive-complexity
const renderActionButtons = () => { const renderActionButtons = () => {
const buttons = []; const buttons = [];

View file

@ -1,5 +1,5 @@
import { Anchor, Box, Text } from '@mantine/core';
import { useState } from 'react'; import { useState } from 'react';
import { Text, Anchor, Box } from '@mantine/core';
interface ImportBackendLinkProps { interface ImportBackendLinkProps {
disabled?: boolean; disabled?: boolean;

View file

@ -1,12 +1,4 @@
import { import { ActionIcon, Card, Group, rem, Stack, Text, Tooltip } from '@mantine/core';
rem,
ActionIcon,
Tooltip,
Card,
Stack,
Group,
Text,
} from '@mantine/core';
import { Copy } from 'lucide-react'; import { Copy } from 'lucide-react';
import { safeExecute } from '@/utils/logger'; import { safeExecute } from '@/utils/logger';

View file

@ -7,11 +7,7 @@ interface InfoTooltipProps {
width?: number; width?: number;
} }
export const InfoTooltip = ({ export const InfoTooltip = ({ label, multiline = true, width = 300 }: InfoTooltipProps) => (
label,
multiline = true,
width = 300,
}: InfoTooltipProps) => (
<Tooltip label={label} multiline={multiline} w={width}> <Tooltip label={label} multiline={multiline} w={width}>
<ActionIcon variant="subtle" size="xs" color="gray"> <ActionIcon variant="subtle" size="xs" color="gray">
<Info size={14} /> <Info size={14} />

View file

@ -1,5 +1,5 @@
import { Modal as MantineModal, Button, Box } from '@mantine/core'; import { Box, Button, Modal as MantineModal } from '@mantine/core';
import { ReactNode } from 'react'; import type { ReactNode } from 'react';
const TITLEBAR_HEIGHT = '2.5rem'; const TITLEBAR_HEIGHT = '2.5rem';

View file

@ -1,4 +1,4 @@
import { Text, Button, Group } from '@mantine/core'; import { Button, Group, Text } from '@mantine/core';
import { Modal } from '@/components/Modal'; import { Modal } from '@/components/Modal';
interface CloseConfirmModalProps { interface CloseConfirmModalProps {
@ -16,8 +16,8 @@ export const CloseConfirmModal = ({
}: CloseConfirmModalProps) => ( }: CloseConfirmModalProps) => (
<Modal opened={isOpen} onClose={onCancel} title="Confirm Close Tab" size="sm"> <Modal opened={isOpen} onClose={onCancel} title="Confirm Close Tab" size="sm">
<Text size="sm" mb="md"> <Text size="sm" mb="md">
The tab &ldquo;{tabTitle}&rdquo; contains content. Closing it will The tab &ldquo;{tabTitle}&rdquo; contains content. Closing it will permanently delete this
permanently delete this data. Are you sure you want to close it? data. Are you sure you want to close it?
</Text> </Text>
<Group justify="flex-end"> <Group justify="flex-end">
<Button variant="subtle" onClick={onCancel}> <Button variant="subtle" onClick={onCancel}>

View file

@ -1,12 +1,12 @@
import { useEffect, useRef, useState, type MouseEvent } from 'react'; import { ActionIcon, Box, Paper } from '@mantine/core';
import { Box, Paper, ActionIcon } from '@mantine/core';
import { Minus } from 'lucide-react'; import { Minus } from 'lucide-react';
import { type MouseEvent, useEffect, useRef, useState } from 'react';
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 { NOTEPAD_MIN_WIDTH, NOTEPAD_MIN_HEIGHT } from '@/constants/notepad';
import { NotepadTabs } from './Tabs.tsx';
import { NotepadEditor } from './Editor.tsx';
import { CloseConfirmModal } from './CloseConfirmModal.tsx'; import { CloseConfirmModal } from './CloseConfirmModal.tsx';
import { NotepadEditor } from './Editor.tsx';
import { NotepadTabs } from './Tabs.tsx';
export const NotepadContainer = () => { export const NotepadContainer = () => {
const { const {
@ -62,8 +62,7 @@ export const NotepadContainer = () => {
setConfirmCloseModal({ isOpen: false, title: '' }); setConfirmCloseModal({ isOpen: false, title: '' });
}; };
const handleResizeStart = const handleResizeStart = (direction: string) => (e: MouseEvent<HTMLDivElement>) => {
(direction: string) => (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation(); e.stopPropagation();
setResizeDirection(direction); setResizeDirection(direction);
}; };
@ -81,10 +80,7 @@ export const NotepadContainer = () => {
newWidth = Math.max(NOTEPAD_MIN_WIDTH, e.clientX - rect.left); newWidth = Math.max(NOTEPAD_MIN_WIDTH, e.clientX - rect.left);
} }
if (resizeDirection.includes('top')) { if (resizeDirection.includes('top')) {
newHeight = Math.max( newHeight = Math.max(NOTEPAD_MIN_HEIGHT, window.innerHeight - e.clientY - 5);
NOTEPAD_MIN_HEIGHT,
window.innerHeight - e.clientY - 5
);
} }
setPosition({ setPosition({
@ -179,8 +175,7 @@ export const NotepadContainer = () => {
zIndex: 9999, zIndex: 9999,
backgroundColor: 'transparent', backgroundColor: 'transparent',
cursor: cursor:
resizeDirection.includes('right') && resizeDirection.includes('right') && resizeDirection.includes('top')
resizeDirection.includes('top')
? 'ne-resize' ? 'ne-resize'
: resizeDirection.includes('right') : resizeDirection.includes('right')
? 'ew-resize' ? 'ew-resize'
@ -203,10 +198,7 @@ export const NotepadContainer = () => {
minHeight: 28, minHeight: 28,
}} }}
> >
<NotepadTabs <NotepadTabs onCreateNewTab={handleCreateNewTab} onCloseTab={handleTabCloseRequest} />
onCreateNewTab={handleCreateNewTab}
onCloseTab={handleTabCloseRequest}
/>
<Box <Box
style={{ style={{
@ -217,11 +209,7 @@ export const NotepadContainer = () => {
flexShrink: 0, flexShrink: 0,
}} }}
> >
<ActionIcon <ActionIcon variant="subtle" size="xs" onClick={() => setVisible(false)}>
variant="subtle"
size="xs"
onClick={() => setVisible(false)}
>
<Minus size="1rem" /> <Minus size="1rem" />
</ActionIcon> </ActionIcon>
</Box> </Box>

View file

@ -1,16 +1,10 @@
import { import { history, historyKeymap } from '@codemirror/commands';
useCallback, import { search } from '@codemirror/search';
useEffect, import { oneDark } from '@codemirror/theme-one-dark';
useState, import { EditorView, highlightActiveLine, keymap } from '@codemirror/view';
useRef,
type MouseEvent,
} from 'react';
import { Box } from '@mantine/core'; import { Box } from '@mantine/core';
import CodeMirror, { type ReactCodeMirrorRef } from '@uiw/react-codemirror'; import CodeMirror, { type ReactCodeMirrorRef } from '@uiw/react-codemirror';
import { search } from '@codemirror/search'; import { type MouseEvent, useCallback, useEffect, useRef, useState } from 'react';
import { EditorView, highlightActiveLine, keymap } from '@codemirror/view';
import { oneDark } from '@codemirror/theme-one-dark';
import { history, historyKeymap } from '@codemirror/commands';
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';
@ -20,13 +14,10 @@ interface NotepadEditorProps {
} }
export const NotepadEditor = ({ tab }: NotepadEditorProps) => { export const NotepadEditor = ({ tab }: NotepadEditorProps) => {
const { saveTabContent, showLineNumbers, setShowLineNumbers } = const { saveTabContent, showLineNumbers, setShowLineNumbers } = useNotepadStore();
useNotepadStore();
const { resolvedColorScheme } = usePreferencesStore(); const { resolvedColorScheme } = usePreferencesStore();
const [content, setContent] = useState(() => tab.content); const [content, setContent] = useState(() => tab.content);
const [saveTimeout, setSaveTimeout] = useState<ReturnType< const [saveTimeout, setSaveTimeout] = useState<ReturnType<typeof setTimeout> | null>(null);
typeof setTimeout
> | null>(null);
const editorRef = useRef<ReactCodeMirrorRef>(null); const editorRef = useRef<ReactCodeMirrorRef>(null);
const handleContentChange = useCallback( const handleContentChange = useCallback(

View file

@ -1,13 +1,13 @@
import { ActionIcon, Box, Text, TextInput } from '@mantine/core';
import { X } from 'lucide-react';
import { import {
type MouseEvent,
type DragEvent, type DragEvent,
type KeyboardEvent, type KeyboardEvent,
useState, type MouseEvent,
useRef,
useEffect, useEffect,
useRef,
useState,
} from 'react'; } from 'react';
import { Box, ActionIcon, Text, TextInput } from '@mantine/core';
import { X } from 'lucide-react';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
interface TabProps { interface TabProps {

View file

@ -1,6 +1,6 @@
import { type MouseEvent, type DragEvent, useState } from 'react'; import { ActionIcon, Box } from '@mantine/core';
import { Box, ActionIcon } from '@mantine/core';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
import { type DragEvent, type MouseEvent, 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,10 +10,7 @@ interface NotepadTabsProps {
onCloseTab: (title: string) => void; onCloseTab: (title: string) => void;
} }
export const NotepadTabs = ({ export const NotepadTabs = ({ onCreateNewTab, onCloseTab }: NotepadTabsProps) => {
onCreateNewTab,
onCloseTab,
}: NotepadTabsProps) => {
const { const {
tabs, tabs,
activeTabId, activeTabId,

View file

@ -1,5 +1,5 @@
import { Select as MantineSelect } from '@mantine/core';
import type { SelectProps } from '@mantine/core'; import type { SelectProps } from '@mantine/core';
import { Select as MantineSelect } from '@mantine/core';
export const Select = ({ export const Select = ({
allowDeselect = false, allowDeselect = false,
@ -7,10 +7,5 @@ export const Select = ({
size = 'sm', size = 'sm',
...props ...props
}: SelectProps) => ( }: SelectProps) => (
<MantineSelect <MantineSelect allowDeselect={allowDeselect} clearable={clearable} size={size} {...props} />
allowDeselect={allowDeselect}
clearable={clearable}
size={size}
{...props}
/>
); );

View file

@ -1,8 +1,8 @@
import type { CSSProperties } from 'react';
import type { ComboboxItem } from '@mantine/core'; import type { ComboboxItem } from '@mantine/core';
import type { CSSProperties } from 'react';
import { LabelWithTooltip } from '@/components/LabelWithTooltip'; import { LabelWithTooltip } from '@/components/LabelWithTooltip';
import type { SelectOption } from '@/types';
import { Select } from '@/components/Select'; import { Select } from '@/components/Select';
import type { SelectOption } from '@/types';
interface SelectWithTooltipProps { interface SelectWithTooltipProps {
label: string; label: string;

View file

@ -1,6 +1,6 @@
import { ReactNode } from 'react';
import { Group, List, Tooltip } from '@mantine/core'; import { Group, List, Tooltip } from '@mantine/core';
import { AlertTriangle, Info } from 'lucide-react'; import { AlertTriangle, Info } from 'lucide-react';
import type { ReactNode } from 'react';
interface WarningItem { interface WarningItem {
type: 'warning' | 'info'; type: 'warning' | 'info';
@ -38,11 +38,7 @@ export const WarningDisplay = ({ warnings, children }: WarningDisplayProps) => {
multiline multiline
maw={320} maw={320}
> >
<AlertTriangle <AlertTriangle size={18} color="var(--mantine-color-orange-6)" strokeWidth={2} />
size={18}
color="var(--mantine-color-orange-6)"
strokeWidth={2}
/>
</Tooltip> </Tooltip>
)} )}
{infoMessages.length > 0 && ( {infoMessages.length > 0 && (

View file

@ -1,12 +1,12 @@
import { useState, useCallback, useRef, useEffect } from 'react'; import { Card, Container, Loader, Stack, Text, Title } from '@mantine/core';
import { Card, Text, Title, Loader, Stack, Container } from '@mantine/core'; 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 { getPlatformDisplayName } from '@/utils/platform';
import { formatDownloadSize } from '@/utils/format';
import { getAssetDescription } from '@/utils/assets';
import { useKoboldBackendsStore } from '@/stores/koboldBackends'; import { useKoboldBackendsStore } from '@/stores/koboldBackends';
import type { DownloadItem } from '@/types/electron'; import type { DownloadItem } from '@/types/electron';
import { getAssetDescription } from '@/utils/assets';
import { formatDownloadSize } from '@/utils/format';
import { getPlatformDisplayName } from '@/utils/platform';
interface DownloadScreenProps { interface DownloadScreenProps {
onDownloadComplete: () => void; onDownloadComplete: () => void;
@ -73,14 +73,10 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
<Stack gap="sm"> <Stack gap="sm">
{availableDownloads.map((download) => { {availableDownloads.map((download) => {
const isDownloading = const isDownloading =
Boolean(downloading) && Boolean(downloading) && downloadingAsset === download.name;
downloadingAsset === download.name;
return ( return (
<div <div key={download.name} ref={isDownloading ? downloadingItemRef : null}>
key={download.name}
ref={isDownloading ? downloadingItemRef : null}
>
<DownloadCard <DownloadCard
backend={{ backend={{
name: download.name, name: download.name,
@ -95,8 +91,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
description={getAssetDescription(download.name)} description={getAssetDescription(download.name)}
disabled={ disabled={
importing || importing ||
(Boolean(downloading) && (Boolean(downloading) && downloadingAsset !== download.name)
downloadingAsset !== download.name)
} }
onDownload={(e) => { onDownload={(e) => {
e.stopPropagation(); e.stopPropagation();
@ -109,9 +104,8 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
</Stack> </Stack>
) : ( ) : (
<Text size="sm" c="dimmed" ta="center"> <Text size="sm" c="dimmed" ta="center">
Unable to fetch downloads for your platform ( Unable to fetch downloads for your platform ({getPlatformDisplayName(platform)}).
{getPlatformDisplayName(platform)}). Check your internet Check your internet connection and try again.
connection and try again.
</Text> </Text>
)} )}

View file

@ -1,9 +1,9 @@
import { Box, Text, Stack } 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 { useLaunchConfigStore } from '@/stores/launchConfig'; import { useLaunchConfigStore } from '@/stores/launchConfig';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { getServerInterfaceInfo } from '@/utils/interface'; import { getServerInterfaceInfo } from '@/utils/interface';
import { TITLEBAR_HEIGHT, STATUSBAR_HEIGHT } from '@/constants';
interface ServerTabProps { interface ServerTabProps {
isServerReady?: boolean; isServerReady?: boolean;
@ -12,15 +12,10 @@ interface ServerTabProps {
export const ServerTab = ({ isServerReady, activeTab }: ServerTabProps) => { export const ServerTab = ({ isServerReady, activeTab }: ServerTabProps) => {
const { isImageGenerationMode } = useLaunchConfigStore(); const { isImageGenerationMode } = useLaunchConfigStore();
const { frontendPreference, imageGenerationFrontendPreference } = const { frontendPreference, imageGenerationFrontendPreference } = usePreferencesStore();
usePreferencesStore();
const effectiveImageMode = const effectiveImageMode =
activeTab === 'chat-image' activeTab === 'chat-image' ? true : activeTab === 'chat-text' ? false : isImageGenerationMode;
? true
: activeTab === 'chat-text'
? false
: isImageGenerationMode;
const { url: iframeUrl, title } = useMemo( const { url: iframeUrl, title } = useMemo(
() => () =>
@ -48,9 +43,8 @@ export const ServerTab = ({ isServerReady, activeTab }: ServerTabProps) => {
Waiting for the server to start... Waiting for the server to start...
</Text> </Text>
<Text c="dimmed" size="sm"> <Text c="dimmed" size="sm">
The{' '} The {title.toLowerCase().includes('ui') ? 'image generation' : 'chat'} interface will
{title.toLowerCase().includes('ui') ? 'image generation' : 'chat'}{' '} load automatically when ready
interface will load automatically when ready
</Text> </Text>
</Stack> </Stack>
</Box> </Box>

View file

@ -1,15 +1,9 @@
import { import { ActionIcon, Box, ScrollArea } from '@mantine/core';
useState,
useEffect,
useRef,
forwardRef,
useImperativeHandle,
} from 'react';
import { Box, ScrollArea, ActionIcon } from '@mantine/core';
import { ChevronDown } from 'lucide-react'; import { ChevronDown } from 'lucide-react';
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants'; import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants';
import { handleTerminalOutput, processTerminalContent } from '@/utils/terminal';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { handleTerminalOutput, processTerminalContent } from '@/utils/terminal';
export interface TerminalTabRef { export interface TerminalTabRef {
scrollToBottom: () => void; scrollToBottom: () => void;
@ -55,7 +49,7 @@ export const TerminalTab = forwardRef<TerminalTabRef>((_props, ref) => {
const viewport = viewportRef.current; const viewport = viewportRef.current;
viewport.scrollTop = viewport.scrollHeight; viewport.scrollTop = viewport.scrollHeight;
} }
}, [terminalContent, shouldAutoScroll, isUserScrolling]); }, [shouldAutoScroll, isUserScrolling]);
useEffect(() => { useEffect(() => {
const cleanup = window.electronAPI.kobold.onKoboldOutput((data: string) => { const cleanup = window.electronAPI.kobold.onKoboldOutput((data: string) => {

View file

@ -1,9 +1,6 @@
import { useRef, useEffect } from 'react'; import { useEffect, useRef } from 'react';
import { ServerTab } from '@/components/screens/Interface/ServerTab'; import { ServerTab } from '@/components/screens/Interface/ServerTab';
import { import { TerminalTab, type TerminalTabRef } from '@/components/screens/Interface/TerminalTab';
TerminalTab,
type TerminalTabRef,
} from '@/components/screens/Interface/TerminalTab';
import type { InterfaceTab } from '@/types'; import type { InterfaceTab } from '@/types';
interface InterfaceScreenProps { interface InterfaceScreenProps {
@ -11,10 +8,7 @@ interface InterfaceScreenProps {
isServerReady: boolean; isServerReady: boolean;
} }
export const InterfaceScreen = ({ export const InterfaceScreen = ({ activeTab, isServerReady }: InterfaceScreenProps) => {
activeTab,
isServerReady,
}: InterfaceScreenProps) => {
const terminalTabRef = useRef<TerminalTabRef>(null); const terminalTabRef = useRef<TerminalTabRef>(null);
useEffect(() => { useEffect(() => {
@ -35,16 +29,10 @@ export const InterfaceScreen = ({
<div <div
style={{ style={{
flex: 1, flex: 1,
display: display: activeTab === 'chat-text' || activeTab === 'chat-image' ? 'block' : 'none',
activeTab === 'chat-text' || activeTab === 'chat-image'
? 'block'
: 'none',
}} }}
> >
<ServerTab <ServerTab isServerReady={isServerReady} activeTab={activeTab || undefined} />
isServerReady={isServerReady}
activeTab={activeTab || undefined}
/>
</div> </div>
<div <div
style={{ style={{

View file

@ -1,16 +1,8 @@
import { import { ActionIcon, Button, Group, SimpleGrid, Stack, Text, TextInput } from '@mantine/core';
Stack,
Group,
Text,
TextInput,
Button,
SimpleGrid,
ActionIcon,
} from '@mantine/core';
import { useState, useEffect } from 'react';
import { Plus, Trash2 } from 'lucide-react'; import { Plus, Trash2 } from 'lucide-react';
import { InfoTooltip } from '@/components/InfoTooltip'; import { useEffect, useState } from 'react';
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip'; import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
import { InfoTooltip } from '@/components/InfoTooltip';
import { CommandLineArgumentsModal } from '@/components/screens/Launch/CommandLineArgumentsModal'; import { CommandLineArgumentsModal } from '@/components/screens/Launch/CommandLineArgumentsModal';
import { useLaunchConfigStore } from '@/stores/launchConfig'; import { useLaunchConfigStore } from '@/stores/launchConfig';
@ -36,16 +28,13 @@ export const AdvancedTab = () => {
const handleAddArgument = (newArgument: string) => { const handleAddArgument = (newArgument: string) => {
const currentArgs = additionalArguments.trim(); const currentArgs = additionalArguments.trim();
const updatedArgs = currentArgs const updatedArgs = currentArgs ? `${currentArgs} ${newArgument}` : newArgument;
? `${currentArgs} ${newArgument}`
: newArgument;
setAdditionalArguments(updatedArgs); setAdditionalArguments(updatedArgs);
}; };
useEffect(() => { useEffect(() => {
const detectAccelerationSupport = async () => { const detectAccelerationSupport = async () => {
const support = const support = await window.electronAPI.kobold.detectAccelerationSupport();
await window.electronAPI.kobold.detectAccelerationSupport();
if (support) { if (support) {
setBackendSupport({ setBackendSupport({
@ -107,20 +96,14 @@ export const AdvancedTab = () => {
</Text> </Text>
<InfoTooltip label="Additional command line arguments to pass to the binary. Leave this empty if you don't know what they are." /> <InfoTooltip label="Additional command line arguments to pass to the binary. Leave this empty if you don't know what they are." />
</Group> </Group>
<Button <Button size="xs" variant="light" onClick={() => setCommandLineModalOpen(true)}>
size="xs"
variant="light"
onClick={() => setCommandLineModalOpen(true)}
>
View Available Arguments View Available Arguments
</Button> </Button>
</Group> </Group>
<TextInput <TextInput
placeholder="Additional command line arguments" placeholder="Additional command line arguments"
value={additionalArguments} value={additionalArguments}
onChange={(event) => onChange={(event) => setAdditionalArguments(event.currentTarget.value)}
setAdditionalArguments(event.currentTarget.value)
}
/> />
</div> </div>
@ -149,12 +132,8 @@ export const AdvancedTab = () => {
color="red" color="red"
disabled={preLaunchCommands.length === 1} disabled={preLaunchCommands.length === 1}
onClick={() => { onClick={() => {
const newCommands = preLaunchCommands.filter( const newCommands = preLaunchCommands.filter((_, i) => i !== index);
(_, i) => i !== index setPreLaunchCommands(newCommands.length === 0 ? [''] : newCommands);
);
setPreLaunchCommands(
newCommands.length === 0 ? [''] : newCommands
);
}} }}
> >
<Trash2 size={16} /> <Trash2 size={16} />

View file

@ -1,15 +1,6 @@
import { import { Accordion, Badge, Button, Code, Group, Stack, Text, TextInput } from '@mantine/core';
Text,
Stack,
Group,
Badge,
Accordion,
Code,
TextInput,
Button,
} from '@mantine/core';
import { useState } from 'react';
import { Code as CodeIcon } from 'lucide-react'; import { Code as CodeIcon } from 'lucide-react';
import { useState } from 'react';
import { Modal } from '@/components/Modal'; import { Modal } from '@/components/Modal';
interface CommandLineArgumentsModalProps { interface CommandLineArgumentsModalProps {
@ -108,8 +99,7 @@ const COMMAND_LINE_ARGUMENTS = [
}, },
{ {
flag: '--analyze', flag: '--analyze',
description: description: 'Reads the metadata, weight types and tensor names in any GGUF file.',
'Reads the metadata, weight types and tensor names in any GGUF file.',
metavar: '[filename]', metavar: '[filename]',
default: '', default: '',
category: 'Advanced', category: 'Advanced',
@ -130,18 +120,7 @@ const COMMAND_LINE_ARGUMENTS = [
description: 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.', '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', type: 'int',
choices: [ choices: ['-1', '16', '32', '64', '128', '256', '512', '1024', '2048', '4096'],
'-1',
'16',
'32',
'64',
'128',
'256',
'512',
'1024',
'2048',
'4096',
],
default: 512, default: 512,
category: 'Advanced', category: 'Advanced',
}, },
@ -212,8 +191,7 @@ const COMMAND_LINE_ARGUMENTS = [
}, },
{ {
flag: '--onready', flag: '--onready',
description: description: 'An optional shell command to execute after the model has been loaded.',
'An optional shell command to execute after the model has been loaded.',
metavar: '[shell command]', metavar: '[shell command]',
type: 'string', type: 'string',
default: '', default: '',
@ -310,8 +288,7 @@ const COMMAND_LINE_ARGUMENTS = [
}, },
{ {
flag: '--mmproj', flag: '--mmproj',
description: description: 'Select a multimodal projector file for vision models like LLaVA.',
'Select a multimodal projector file for vision models like LLaVA.',
metavar: '[filename]', metavar: '[filename]',
default: '', default: '',
category: 'Multimodal', category: 'Multimodal',
@ -353,8 +330,7 @@ const COMMAND_LINE_ARGUMENTS = [
{ {
flag: '--draftgpulayers', flag: '--draftgpulayers',
aliases: ['--gpu-layers-draft', '--n-gpu-layers-draft', '-ngld'], aliases: ['--gpu-layers-draft', '--n-gpu-layers-draft', '-ngld'],
description: description: 'How many layers to offload to GPU for the draft model (default=full offload)',
'How many layers to offload to GPU for the draft model (default=full offload)',
metavar: '[layers]', metavar: '[layers]',
type: 'int', type: 'int',
default: 999, default: 999,
@ -401,8 +377,7 @@ const COMMAND_LINE_ARGUMENTS = [
}, },
{ {
flag: '--ignoremissing', flag: '--ignoremissing',
description: description: 'Ignores all missing non-essential files, just skipping them instead.',
'Ignores all missing non-essential files, just skipping them instead.',
type: 'boolean', type: 'boolean',
category: 'Advanced', category: 'Advanced',
}, },
@ -438,8 +413,7 @@ const COMMAND_LINE_ARGUMENTS = [
}, },
{ {
flag: '--unpack', flag: '--unpack',
description: description: 'Extracts the file contents of the KoboldCpp binary into a target directory.',
'Extracts the file contents of the KoboldCpp binary into a target directory.',
metavar: 'destination', metavar: 'destination',
type: 'string', type: 'string',
default: '', default: '',
@ -447,8 +421,7 @@ const COMMAND_LINE_ARGUMENTS = [
}, },
{ {
flag: '--exportconfig', flag: '--exportconfig',
description: description: 'Exports the current selected arguments as a .kcpps settings file',
'Exports the current selected arguments as a .kcpps settings file',
metavar: '[filename]', metavar: '[filename]',
type: 'string', type: 'string',
default: '', default: '',
@ -456,8 +429,7 @@ const COMMAND_LINE_ARGUMENTS = [
}, },
{ {
flag: '--exporttemplate', flag: '--exporttemplate',
description: description: 'Exports the current selected arguments as a .kcppt template file',
'Exports the current selected arguments as a .kcppt template file',
metavar: '[filename]', metavar: '[filename]',
type: 'string', type: 'string',
default: '', default: '',
@ -465,8 +437,7 @@ const COMMAND_LINE_ARGUMENTS = [
}, },
{ {
flag: '--nomodel', flag: '--nomodel',
description: description: 'Allows you to launch the GUI alone, without selecting any model.',
'Allows you to launch the GUI alone, without selecting any model.',
type: 'boolean', type: 'boolean',
category: 'Advanced', category: 'Advanced',
}, },
@ -577,8 +548,7 @@ const COMMAND_LINE_ARGUMENTS = [
}, },
{ {
flag: '--sdquant', flag: '--sdquant',
description: description: 'If specified, loads the model quantized to save memory. 0=off, 1=q8, 2=q4',
'If specified, loads the model quantized to save memory. 0=off, 1=q8, 2=q4',
metavar: '[quantization level 0/1/2]', metavar: '[quantization level 0/1/2]',
type: 'int', type: 'int',
choices: ['0', '1', '2'], choices: ['0', '1', '2'],
@ -605,8 +575,7 @@ const COMMAND_LINE_ARGUMENTS = [
}, },
{ {
flag: '--sdvaeauto', flag: '--sdvaeauto',
description: description: 'Uses a built-in VAE via TAE SD, which is very fast and fixed bad VAEs.',
'Uses a built-in VAE via TAE SD, which is very fast and fixed bad VAEs.',
type: 'boolean', type: 'boolean',
category: 'Image Generation', category: 'Image Generation',
}, },
@ -629,23 +598,20 @@ const COMMAND_LINE_ARGUMENTS = [
}, },
{ {
flag: '--sdoffloadcpu', flag: '--sdoffloadcpu',
description: description: 'Offload image weights in RAM to save VRAM, swap into VRAM when needed.',
'Offload image weights in RAM to save VRAM, swap into VRAM when needed.',
type: 'boolean', type: 'boolean',
category: 'Image Generation', category: 'Image Generation',
}, },
{ {
flag: '--sdgendefaults', flag: '--sdgendefaults',
description: description: 'Sets default parameters for image generation, as a JSON string.',
'Sets default parameters for image generation, as a JSON string.',
metavar: '{"parameter":"value",...}', metavar: '{"parameter":"value",...}',
default: '', default: '',
category: 'Image Generation', category: 'Image Generation',
}, },
{ {
flag: '--whispermodel', flag: '--whispermodel',
description: description: 'Specify a Whisper .bin model to enable Speech-To-Text transcription.',
'Specify a Whisper .bin model to enable Speech-To-Text transcription.',
metavar: '[filename]', metavar: '[filename]',
default: '', default: '',
category: 'Audio', category: 'Audio',
@ -688,16 +654,14 @@ const COMMAND_LINE_ARGUMENTS = [
}, },
{ {
flag: '--embeddingsmodel', flag: '--embeddingsmodel',
description: description: 'Specify an embeddings model to be loaded for generating embedding vectors.',
'Specify an embeddings model to be loaded for generating embedding vectors.',
metavar: '[filename]', metavar: '[filename]',
default: '', default: '',
category: 'Embeddings', category: 'Embeddings',
}, },
{ {
flag: '--embeddingsgpu', flag: '--embeddingsgpu',
description: description: 'Attempts to offload layers of the embeddings model to GPU. Usually not needed.',
'Attempts to offload layers of the embeddings model to GPU. Usually not needed.',
type: 'boolean', type: 'boolean',
category: 'Embeddings', category: 'Embeddings',
}, },
@ -740,10 +704,7 @@ export const CommandLineArgumentsModal = ({
(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 && 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) {
@ -782,8 +743,8 @@ export const CommandLineArgumentsModal = ({
> >
<Stack gap="md"> <Stack gap="md">
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
These are additional command line arguments that can be added to the These are additional command line arguments that can be added to the &quot;Additional
&quot;Additional Arguments&quot; input field. Arguments&quot; input field.
</Text> </Text>
<TextInput <TextInput
@ -816,18 +777,9 @@ export const CommandLineArgumentsModal = ({
> >
<Group gap="xs" wrap="wrap" justify="space-between"> <Group gap="xs" wrap="wrap" justify="space-between">
<Group gap="xs" wrap="wrap" style={{ flex: 1 }}> <Group gap="xs" wrap="wrap" style={{ flex: 1 }}>
<Code <Code style={{ fontSize: '0.875em', fontWeight: 600 }}>{arg.flag}</Code>
style={{ fontSize: '0.875em', fontWeight: 600 }} {arg.aliases?.map((alias) => (
> <Code key={alias} style={{ fontSize: '0.75em' }} c="dimmed">
{arg.flag}
</Code>
{arg.aliases &&
arg.aliases.map((alias) => (
<Code
key={alias}
style={{ fontSize: '0.75em' }}
c="dimmed"
>
{alias} {alias}
</Code> </Code>
))} ))}
@ -839,11 +791,7 @@ export const CommandLineArgumentsModal = ({
</Group> </Group>
{onAddArgument && ( {onAddArgument && (
<Button <Button size="xs" variant="light" onClick={() => handleAddArgument(arg)}>
size="xs"
variant="light"
onClick={() => handleAddArgument(arg)}
>
Add Add
</Button> </Button>
)} )}
@ -853,9 +801,7 @@ export const CommandLineArgumentsModal = ({
{arg.description} {arg.description}
</Text> </Text>
{(arg.metavar || {(arg.metavar || arg.default !== undefined || arg.choices) && (
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,11 +1,11 @@
import { Stack, Text, Group, Button, Tooltip } from '@mantine/core'; import { Button, Group, Stack, Text, Tooltip } from '@mantine/core';
import { useState, useCallback } from 'react'; import { Check, File, Plus, Save, Trash2 } from 'lucide-react';
import { Save, File, Plus, Check, Trash2 } from 'lucide-react'; import { useCallback, useState } from 'react';
import { Select } from '@/components/Select';
import type { ConfigFile } from '@/types'; import type { ConfigFile } from '@/types';
import { stripFileExtension } from '@/utils/format';
import { CreateConfigModal } from './CreateConfigModal'; import { CreateConfigModal } from './CreateConfigModal';
import { DeleteConfigModal } from './DeleteConfigModal'; import { DeleteConfigModal } from './DeleteConfigModal';
import { Select } from '@/components/Select';
import { stripFileExtension } from '@/utils/format';
interface ConfigFileManagerProps { interface ConfigFileManagerProps {
configFiles: ConfigFile[]; configFiles: ConfigFile[];

View file

@ -1,6 +1,6 @@
import { TextInput, Group, Button, Stack } from '@mantine/core'; import { Button, Group, Stack, TextInput } from '@mantine/core';
import { Modal } from '@/components/Modal';
import { useState } from 'react'; import { useState } from 'react';
import { Modal } from '@/components/Modal';
interface CreateConfigModalProps { interface CreateConfigModalProps {
opened: boolean; opened: boolean;
@ -19,8 +19,7 @@ export const CreateConfigModal = ({
const trimmedConfigName = newConfigName.trim(); const trimmedConfigName = newConfigName.trim();
const configNameExists = const configNameExists =
trimmedConfigName && trimmedConfigName && existingConfigNames.includes(trimmedConfigName.toLowerCase());
existingConfigNames.includes(trimmedConfigName.toLowerCase());
const handleClose = () => { const handleClose = () => {
setNewConfigName(''); setNewConfigName('');
@ -35,12 +34,7 @@ export const CreateConfigModal = ({
}; };
return ( return (
<Modal <Modal opened={opened} onClose={handleClose} title="Create New Configuration" size="sm">
opened={opened}
onClose={handleClose}
title="Create New Configuration"
size="sm"
>
<Stack gap="md"> <Stack gap="md">
<TextInput <TextInput
label="Name" label="Name"
@ -48,11 +42,7 @@ export const CreateConfigModal = ({
value={newConfigName} value={newConfigName}
onChange={(event) => setNewConfigName(event.currentTarget.value)} onChange={(event) => setNewConfigName(event.currentTarget.value)}
data-autofocus data-autofocus
error={ error={configNameExists ? 'A configuration with this name already exists' : undefined}
configNameExists
? 'A configuration with this name already exists'
: undefined
}
/> />
<Group justify="flex-end" gap="sm"> <Group justify="flex-end" gap="sm">
<Button variant="outline" onClick={handleClose}> <Button variant="outline" onClick={handleClose}>

View file

@ -1,4 +1,4 @@
import { Text, Group, Button, Stack } 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';
@ -21,8 +21,7 @@ export const DeleteConfigModal = ({
<Modal opened={opened} onClose={onClose} title="Delete Configuration"> <Modal opened={opened} onClose={onClose} title="Delete Configuration">
<Stack gap="md"> <Stack gap="md">
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
Are you sure you want to delete &ldquo;{displayName}&rdquo;? This Are you sure you want to delete &ldquo;{displayName}&rdquo;? This action cannot be undone.
action cannot be undone.
</Text> </Text>
<Group justify="flex-end" gap="sm"> <Group justify="flex-end" gap="sm">

View file

@ -1,6 +1,6 @@
import { Text, Group, Badge, Box } from '@mantine/core'; import { Badge, Box, Group, Text } from '@mantine/core';
import type { AccelerationOption } from '@/types'; import type { AccelerationOption } from '@/types';
import { GPUDevice } from '@/types/hardware'; import type { GPUDevice } from '@/types/hardware';
type AccelerationSelectItemProps = Omit<AccelerationOption, 'value'>; type AccelerationSelectItemProps = Omit<AccelerationOption, 'value'>;
@ -11,9 +11,7 @@ export const AccelerationSelectItem = ({
}: AccelerationSelectItemProps) => { }: AccelerationSelectItemProps) => {
const renderDeviceName = (device: string | GPUDevice) => { const renderDeviceName = (device: string | GPUDevice) => {
const deviceName = typeof device === 'string' ? device : device.name; const deviceName = typeof device === 'string' ? device : device.name;
return deviceName.length > 25 return deviceName.length > 25 ? `${deviceName.slice(0, 25)}...` : deviceName;
? `${deviceName.slice(0, 25)}...`
: deviceName;
}; };
return ( return (

View file

@ -1,11 +1,11 @@
import { Text, Group, Checkbox, TextInput } from '@mantine/core'; import { Checkbox, Group, Text, TextInput } from '@mantine/core';
import { useState, useEffect, useRef } 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 { useLaunchConfigStore } from '@/stores/launchConfig'; import { useLaunchConfigStore } from '@/stores/launchConfig';
import type { Acceleration, AccelerationOption } from '@/types'; import type { Acceleration, AccelerationOption } from '@/types';
import { Select } from '@/components/Select';
export const AccelerationSelector = () => { export const AccelerationSelector = () => {
const { const {
@ -21,9 +21,7 @@ export const AccelerationSelector = () => {
setAutoGpuLayers, setAutoGpuLayers,
} = useLaunchConfigStore(); } = useLaunchConfigStore();
const [availableAccelerations, setAvailableAccelerations] = useState< const [availableAccelerations, setAvailableAccelerations] = useState<AccelerationOption[]>([]);
AccelerationOption[]
>([]);
const [isLoadingAccelerations, setIsLoadingAccelerations] = useState(false); const [isLoadingAccelerations, setIsLoadingAccelerations] = useState(false);
const [isCalculatingLayers, setIsCalculatingLayers] = useState(false); const [isCalculatingLayers, setIsCalculatingLayers] = useState(false);
const [isMac, setIsMac] = useState(false); const [isMac, setIsMac] = useState(false);
@ -63,9 +61,7 @@ export const AccelerationSelector = () => {
); );
if (!isAccelerationAvailable) { if (!isAccelerationAvailable) {
const fallbackAcceleration = availableAccelerations.find( const fallbackAcceleration = availableAccelerations.find((a) => !a.disabled);
(a) => !a.disabled
);
if (fallbackAcceleration) { if (fallbackAcceleration) {
setAcceleration(fallbackAcceleration.value as Acceleration); setAcceleration(fallbackAcceleration.value as Acceleration);
} }
@ -76,13 +72,7 @@ export const AccelerationSelector = () => {
useEffect(() => { useEffect(() => {
const calculateLayers = async () => { const calculateLayers = async () => {
const isCpuOnly = acceleration === 'cpu' && !isMac; const isCpuOnly = acceleration === 'cpu' && !isMac;
if ( if (!autoGpuLayers || !model || !contextSize || isCpuOnly || isLoadingAccelerations) {
!autoGpuLayers ||
!model ||
!contextSize ||
isCpuOnly ||
isLoadingAccelerations
) {
return; return;
} }
@ -97,18 +87,13 @@ export const AccelerationSelector = () => {
const selectedDeviceIndices = gpuDeviceSelection const selectedDeviceIndices = gpuDeviceSelection
.split(',') .split(',')
.map((d) => parseInt(d.trim(), 10)) .map((d) => parseInt(d.trim(), 10))
.filter((d) => !isNaN(d)); .filter((d) => !Number.isNaN(d));
const availableVramGB = selectedDeviceIndices.reduce( const availableVramGB = selectedDeviceIndices.reduce((total, deviceIndex) => {
(total, deviceIndex) => {
const device = gpuMemory[deviceIndex]; const device = gpuMemory[deviceIndex];
const vramGB = device?.totalMemoryGB const vramGB = device?.totalMemoryGB ? parseFloat(device.totalMemoryGB) : 0;
? parseFloat(device.totalMemoryGB)
: 0;
return total + vramGB; return total + vramGB;
}, }, 0);
0
);
if (availableVramGB === 0) { if (availableVramGB === 0) {
return; return;
@ -124,10 +109,7 @@ export const AccelerationSelector = () => {
setGpuLayers(result.recommendedLayers); setGpuLayers(result.recommendedLayers);
} catch (error) { } catch (error) {
window.electronAPI.logs.logError( window.electronAPI.logs.logError('Failed to calculate optimal GPU layers', error as Error);
'Failed to calculate optimal GPU layers',
error as Error
);
} finally { } finally {
setIsCalculatingLayers(false); setIsCalculatingLayers(false);
} }
@ -158,14 +140,10 @@ export const AccelerationSelector = () => {
</Group> </Group>
<Select <Select
placeholder={ placeholder={
isLoadingAccelerations isLoadingAccelerations ? 'Loading accelerations...' : 'Select acceleration'
? 'Loading accelerations...'
: 'Select acceleration'
} }
value={ value={
availableAccelerations.some( availableAccelerations.some((a) => a.value === acceleration && !a.disabled)
(a) => a.value === acceleration && !a.disabled
)
? acceleration ? acceleration
: null : null
} }
@ -179,13 +157,9 @@ export const AccelerationSelector = () => {
label: a.label, label: a.label,
disabled: a.disabled, disabled: a.disabled,
}))} }))}
disabled={ disabled={isLoadingAccelerations || availableAccelerations.length === 0}
isLoadingAccelerations || availableAccelerations.length === 0
}
renderOption={({ option }) => { renderOption={({ option }) => {
const accelerationData = availableAccelerations.find( const accelerationData = availableAccelerations.find((a) => a.value === option.value);
(a) => a.value === option.value
);
return ( return (
<AccelerationSelectItem <AccelerationSelectItem
@ -215,9 +189,7 @@ export const AccelerationSelector = () => {
: gpuLayers.toString() : gpuLayers.toString()
: undefined : undefined
} }
onChange={(event) => onChange={(event) => setGpuLayers(Number(event.target.value) || 0)}
setGpuLayers(Number(event.target.value) || 0)
}
type="number" type="number"
min={0} min={0}
max={100} max={100}
@ -230,9 +202,7 @@ export const AccelerationSelector = () => {
<Checkbox <Checkbox
label="Auto" label="Auto"
checked={autoGpuLayers} checked={autoGpuLayers}
onChange={(event) => onChange={(event) => setAutoGpuLayers(event.currentTarget.checked)}
setAutoGpuLayers(event.currentTarget.checked)
}
size="sm" size="sm"
disabled={acceleration === 'cpu' && !isMac} disabled={acceleration === 'cpu' && !isMac}
/> />

View file

@ -1,7 +1,7 @@
import { Text, Group, TextInput } from '@mantine/core'; import { Group, Text, TextInput } from '@mantine/core';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
import { useLaunchConfigStore } from '@/stores/launchConfig';
import { Select } from '@/components/Select'; import { Select } from '@/components/Select';
import { useLaunchConfigStore } from '@/stores/launchConfig';
import type { AccelerationOption } from '@/types'; import type { AccelerationOption } from '@/types';
const GPU_ACCELERATIONS = ['cuda', 'rocm', 'vulkan', 'clblast']; const GPU_ACCELERATIONS = ['cuda', 'rocm', 'vulkan', 'clblast'];
@ -11,29 +11,16 @@ interface GpuDeviceSelectorProps {
availableAccelerations: AccelerationOption[]; availableAccelerations: AccelerationOption[];
} }
export const GpuDeviceSelector = ({ export const GpuDeviceSelector = ({ availableAccelerations }: GpuDeviceSelectorProps) => {
availableAccelerations, const { acceleration, gpuDeviceSelection, tensorSplit, setGpuDeviceSelection, setTensorSplit } =
}: GpuDeviceSelectorProps) => { useLaunchConfigStore();
const {
acceleration,
gpuDeviceSelection,
tensorSplit,
setGpuDeviceSelection,
setTensorSplit,
} = useLaunchConfigStore();
const selectedAcceleration = availableAccelerations.find( const selectedAcceleration = availableAccelerations.find((a) => a.value === acceleration);
(a) => a.value === acceleration
);
const isGpuAcceleration = GPU_ACCELERATIONS.includes(acceleration); const isGpuAcceleration = GPU_ACCELERATIONS.includes(acceleration);
const getDiscreteDeviceCount = () => { const getDiscreteDeviceCount = () => {
if (!selectedAcceleration?.devices) return 0; if (!selectedAcceleration?.devices) return 0;
if ( if (acceleration === 'clblast' || acceleration === 'vulkan' || acceleration === 'rocm') {
acceleration === 'clblast' ||
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;
@ -68,9 +55,7 @@ export const GpuDeviceSelector = ({
label: `GPU ${index}: ${deviceName}`, label: `GPU ${index}: ${deviceName}`,
}; };
}) })
.filter( .filter((option): option is NonNullable<typeof option> => option !== null);
(option): option is NonNullable<typeof option> => option !== null
);
} }
if (acceleration === 'vulkan' || acceleration === 'rocm') { if (acceleration === 'vulkan' || acceleration === 'rocm') {
@ -85,9 +70,7 @@ export const GpuDeviceSelector = ({
label: `GPU ${index}: ${deviceName}`, label: `GPU ${index}: ${deviceName}`,
}; };
}) })
.filter( .filter((option): option is NonNullable<typeof option> => option !== null);
(option): option is NonNullable<typeof option> => option !== null
);
return [{ value: 'all', label: 'All GPUs' }, ...discreteDeviceOptions]; return [{ value: 'all', label: 'All GPUs' }, ...discreteDeviceOptions];
} }

View file

@ -1,11 +1,4 @@
import { import { Group, Slider, Stack, Text, TextInput, Transition } from '@mantine/core';
Stack,
Text,
Group,
TextInput,
Slider,
Transition,
} from '@mantine/core';
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';
@ -20,12 +13,7 @@ export const GeneralTab = ({ configLoaded = true }: GeneralTabProps) => {
useLaunchConfigStore(); useLaunchConfigStore();
return ( return (
<Transition <Transition mounted={configLoaded} transition="fade" duration={100} timingFunction="ease-out">
mounted={configLoaded}
transition="fade"
duration={100}
timingFunction="ease-out"
>
{(styles) => ( {(styles) => (
<Stack gap="md" style={styles}> <Stack gap="md" style={styles}>
<AccelerationSelector /> <AccelerationSelector />
@ -56,9 +44,7 @@ export const GeneralTab = ({ configLoaded = true }: GeneralTabProps) => {
</Group> </Group>
<TextInput <TextInput
value={contextSize?.toString() || ''} value={contextSize?.toString() || ''}
onChange={(event) => onChange={(event) => setContextSizeWithStep(Number(event.target.value) || 256)}
setContextSizeWithStep(Number(event.target.value) || 256)
}
type="number" type="number"
min={256} min={256}
max={131072} max={131072}

View file

@ -1,6 +1,6 @@
import { Table, Text, Group, Loader } from '@mantine/core'; import { Group, Loader, Table, Text } from '@mantine/core';
import { formatBytes } from '@/utils/format';
import type { HuggingFaceFileInfo } from '@/types'; import type { HuggingFaceFileInfo } from '@/types';
import { formatBytes } from '@/utils/format';
interface FilesTableProps { interface FilesTableProps {
files: HuggingFaceFileInfo[]; files: HuggingFaceFileInfo[];
@ -35,11 +35,7 @@ export const FilesTable = ({ files, loading, onSelect }: FilesTableProps) => {
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{files.map((file) => ( {files.map((file) => (
<Table.Tr <Table.Tr key={file.path} onClick={() => onSelect(file)} style={{ cursor: 'pointer' }}>
key={file.path}
onClick={() => onSelect(file)}
style={{ cursor: 'pointer' }}
>
<Table.Td> <Table.Td>
<Text size="sm" lineClamp={1}> <Text size="sm" lineClamp={1}>
{file.path} {file.path}

View file

@ -1,9 +1,9 @@
import { Accordion, Group, Loader, Paper, Text } from '@mantine/core';
import { useState } from 'react'; import { useState } from 'react';
import { Accordion, Paper, Group, Loader, Text } from '@mantine/core';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
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 { HUGGINGFACE_BASE_URL } from '@/constants'; import { HUGGINGFACE_BASE_URL } from '@/constants';
interface ModelCardProps { interface ModelCardProps {
@ -20,9 +20,7 @@ export const ModelCard = ({ modelId }: ModelCardProps) => {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const response = await fetch( const response = await fetch(`${HUGGINGFACE_BASE_URL}/${modelId}/raw/main/README.md`);
`${HUGGINGFACE_BASE_URL}/${modelId}/raw/main/README.md`
);
if (!response.ok) { if (!response.ok) {
throw new Error('README not available'); throw new Error('README not available');
} }
@ -42,9 +40,7 @@ export const ModelCard = ({ modelId }: ModelCardProps) => {
return ( return (
<Accordion> <Accordion>
<Accordion.Item value="readme"> <Accordion.Item value="readme">
<Accordion.Control onClick={() => void loadReadme()}> <Accordion.Control onClick={() => void loadReadme()}>Model Card</Accordion.Control>
Model Card
</Accordion.Control>
<Accordion.Panel> <Accordion.Panel>
{loading && ( {loading && (
<Group justify="center" py="md"> <Group justify="center" py="md">

View file

@ -1,16 +1,8 @@
import { import { Badge, Group, Loader, Stack, Table, Text, Tooltip } from '@mantine/core';
Table,
Text,
Group,
Loader,
Stack,
Badge,
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 { formatDownloads, formatDate } from '@/utils/format';
import type { HuggingFaceModelInfo, HuggingFaceSortOption } from '@/types'; import type { HuggingFaceModelInfo, HuggingFaceSortOption } from '@/types';
import { formatDate, formatDownloads } from '@/utils/format';
interface ModelsTableProps { interface ModelsTableProps {
models: HuggingFaceModelInfo[]; models: HuggingFaceModelInfo[];
@ -55,10 +47,7 @@ export const ModelsTable = ({
> >
Downloads{sortBy === 'downloads' && ' ↓'} Downloads{sortBy === 'downloads' && ' ↓'}
</Table.Th> </Table.Th>
<Table.Th <Table.Th style={{ width: 90, cursor: 'pointer' }} onClick={() => onSortChange('likes')}>
style={{ width: 90, cursor: 'pointer' }}
onClick={() => onSortChange('likes')}
>
Likes{sortBy === 'likes' && ' ↓'} Likes{sortBy === 'likes' && ' ↓'}
</Table.Th> </Table.Th>
</Table.Tr> </Table.Tr>
@ -69,9 +58,7 @@ export const ModelsTable = ({
key={model.id} key={model.id}
onClick={() => { onClick={() => {
if (model.gated) { if (model.gated) {
void window.electronAPI.app.openExternal( void window.electronAPI.app.openExternal(`${HUGGINGFACE_BASE_URL}/${model.id}`);
`${HUGGINGFACE_BASE_URL}/${model.id}`
);
} else { } else {
onSelect(model); onSelect(model);
} }
@ -88,11 +75,7 @@ export const ModelsTable = ({
</Text> </Text>
{model.gated && ( {model.gated && (
<Tooltip label="Gated model - click to open on HuggingFace"> <Tooltip label="Gated model - click to open on HuggingFace">
<Badge <Badge size="xs" color="yellow" leftSection={<Lock size={10} />}>
size="xs"
color="yellow"
leftSection={<Lock size={10} />}
>
Gated Gated
</Badge> </Badge>
</Tooltip> </Tooltip>

View file

@ -1,31 +1,31 @@
import { useEffect, useState, useRef } from 'react';
import { import {
Stack,
TextInput,
Text,
Group,
ActionIcon, ActionIcon,
Tooltip,
Badge,
Loader,
Alert, Alert,
Badge,
Group,
Loader,
ScrollArea, ScrollArea,
SegmentedControl, SegmentedControl,
Stack,
Text,
TextInput,
Tooltip,
} from '@mantine/core'; } from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks'; import { useDebouncedValue } from '@mantine/hooks';
import { Search, ArrowLeft, ExternalLink } from 'lucide-react'; import { ArrowLeft, ExternalLink, Search } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { Modal } from '@/components/Modal'; import { Modal } from '@/components/Modal';
import { useHuggingFaceSearch } from '@/hooks/useHuggingFaceSearch';
import { HUGGINGFACE_BASE_URL } from '@/constants'; import { HUGGINGFACE_BASE_URL } from '@/constants';
import { useHuggingFaceSearch } from '@/hooks/useHuggingFaceSearch';
import type { import type {
HuggingFaceModelInfo,
HuggingFaceFileInfo, HuggingFaceFileInfo,
HuggingFaceSortOption, HuggingFaceModelInfo,
HuggingFaceSearchParams, HuggingFaceSearchParams,
HuggingFaceSortOption,
} from '@/types'; } from '@/types';
import { ModelsTable } from './ModelsTable';
import { FilesTable } from './FilesTable'; import { FilesTable } from './FilesTable';
import { ModelCard } from './ModelCard'; import { ModelCard } from './ModelCard';
import { ModelsTable } from './ModelsTable';
interface HuggingFaceSearchModalProps { interface HuggingFaceSearchModalProps {
opened: boolean; opened: boolean;
@ -40,9 +40,7 @@ export const HuggingFaceSearchModal = ({
onSelect, onSelect,
searchParams: initialSearchParams, searchParams: initialSearchParams,
}: HuggingFaceSearchModalProps) => { }: HuggingFaceSearchModalProps) => {
const [searchQuery, setSearchQuery] = useState( const [searchQuery, setSearchQuery] = useState(initialSearchParams.search || '');
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);
@ -108,17 +106,14 @@ export const HuggingFaceSearchModal = ({
const handleOpenExternal = () => { const handleOpenExternal = () => {
if (selectedModel) { if (selectedModel) {
void window.electronAPI.app.openExternal( void window.electronAPI.app.openExternal(`${HUGGINGFACE_BASE_URL}/${selectedModel.id}`);
`${HUGGINGFACE_BASE_URL}/${selectedModel.id}`
);
} }
}; };
const getFilterBadges = () => { const getFilterBadges = () => {
const badges: string[] = []; const badges: string[] = [];
if (searchParams?.filter) badges.push(searchParams.filter.toUpperCase()); if (searchParams?.filter) badges.push(searchParams.filter.toUpperCase());
if (searchParams?.pipelineTag) if (searchParams?.pipelineTag) badges.push(searchParams.pipelineTag.replace('-', ' '));
badges.push(searchParams.pipelineTag.replace('-', ' '));
return badges; return badges;
}; };
@ -193,8 +188,7 @@ export const HuggingFaceSearchModal = ({
onScrollPositionChange={({ y }) => { onScrollPositionChange={({ y }) => {
const target = scrollAreaRef.current; const target = scrollAreaRef.current;
if (!target) return; if (!target) return;
const isNearBottom = const isNearBottom = target.scrollHeight - y <= target.clientHeight + 100;
target.scrollHeight - y <= target.clientHeight + 100;
if (isNearBottom && hasMore && !loading && !selectedModel) { if (isNearBottom && hasMore && !loading && !selectedModel) {
void loadMoreModels(); void loadMoreModels();
} }
@ -204,11 +198,7 @@ export const HuggingFaceSearchModal = ({
{selectedModel ? ( {selectedModel ? (
<Stack gap="md"> <Stack gap="md">
<ModelCard modelId={selectedModel.id} /> <ModelCard modelId={selectedModel.id} />
<FilesTable <FilesTable files={files} loading={loadingFiles} onSelect={handleFileSelect} />
files={files}
loading={loadingFiles}
onSelect={handleFileSelect}
/>
</Stack> </Stack>
) : ( ) : (
<ModelsTable <ModelsTable

View file

@ -1,8 +1,8 @@
import { Stack, Group } from '@mantine/core'; import { Group, Stack } from '@mantine/core';
import { useState } from 'react'; import { useState } from 'react';
import { ModelFileField } from '@/components/screens/Launch/ModelFileField';
import { SelectWithTooltip } from '@/components/SelectWithTooltip';
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip'; import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
import { SelectWithTooltip } from '@/components/SelectWithTooltip';
import { ModelFileField } from '@/components/screens/Launch/ModelFileField';
import { IMAGE_MODEL_PRESETS } from '@/constants/imageModelPresets'; import { IMAGE_MODEL_PRESETS } from '@/constants/imageModelPresets';
import { useLaunchConfigStore } from '@/stores/launchConfig'; import { useLaunchConfigStore } from '@/stores/launchConfig';
@ -129,9 +129,7 @@ export const ImageGenerationTab = () => {
placeholder="Select a PhotoMaker file or enter a direct URL" placeholder="Select a PhotoMaker file or enter a direct URL"
tooltip="PhotoMaker is a model that allows face cloning. Select a .safetensors PhotoMaker file to be loaded (SDXL only)." tooltip="PhotoMaker is a model that allows face cloning. Select a .safetensors PhotoMaker file to be loaded (SDXL only)."
onChange={setSdphotomaker} onChange={setSdphotomaker}
onSelectFile={() => onSelectFile={() => void selectFile('sdphotomaker', 'Select PhotoMaker Model')}
void selectFile('sdphotomaker', 'Select PhotoMaker Model')
}
searchParams={{ searchParams={{
search: 'photomaker', search: 'photomaker',
filter: 'safetensors', filter: 'safetensors',

View file

@ -1,6 +1,6 @@
import { Modal } from '@/components/Modal'; import { Alert, Group, rem, Stack, Text } from '@mantine/core';
import { Stack, Group, Text, Alert, rem } from '@mantine/core';
import { Info } from 'lucide-react'; import { Info } from 'lucide-react';
import { Modal } from '@/components/Modal';
import type { ModelAnalysis } from '@/types'; import type { ModelAnalysis } from '@/types';
interface ModelAnalysisModalProps { interface ModelAnalysisModalProps {
@ -63,9 +63,7 @@ export const ModelAnalysisModal = ({
{analysis && ( {analysis && (
<Stack gap="xs"> <Stack gap="xs">
{analysis.general.name && ( {analysis.general.name && <InfoRow label="Name" value={analysis.general.name} />}
<InfoRow label="Name" value={analysis.general.name} />
)}
<InfoRow label="Architecture" value={analysis.general.architecture} /> <InfoRow label="Architecture" value={analysis.general.architecture} />
{analysis.general.parameterCount && ( {analysis.general.parameterCount && (
<InfoRow label="Parameters" value={analysis.general.parameterCount} /> <InfoRow label="Parameters" value={analysis.general.parameterCount} />
@ -73,16 +71,10 @@ export const ModelAnalysisModal = ({
<InfoRow label="File Size" value={analysis.general.fileSize} /> <InfoRow label="File Size" value={analysis.general.fileSize} />
{analysis.architecture.expertCount && ( {analysis.architecture.expertCount && (
<InfoRow <InfoRow label="Expert Count (MoE)" value={analysis.architecture.expertCount} />
label="Expert Count (MoE)"
value={analysis.architecture.expertCount}
/>
)} )}
{analysis.context.maxContextLength && ( {analysis.context.maxContextLength && (
<InfoRow <InfoRow label="Max Context Length" value={analysis.context.maxContextLength} />
label="Max Context Length"
value={analysis.context.maxContextLength}
/>
)} )}
{analysis.architecture.layers && ( {analysis.architecture.layers && (
<InfoRow <InfoRow

View file

@ -1,25 +1,20 @@
import { useEffect, useState } from 'react';
import { import {
Group,
ActionIcon, ActionIcon,
Tooltip,
Button, Button,
Combobox, Combobox,
useCombobox, Group,
TextInput, TextInput,
Tooltip,
useCombobox,
} from '@mantine/core'; } from '@mantine/core';
import { File, Search, Info } from 'lucide-react'; import { File, Info, Search } from 'lucide-react';
import { useEffect, useState } from 'react';
import { LabelWithTooltip } from '@/components/LabelWithTooltip'; import { LabelWithTooltip } from '@/components/LabelWithTooltip';
import { ModelAnalysisModal } from '@/components/screens/Launch/ModelAnalysisModal';
import { HuggingFaceSearchModal } from '@/components/screens/Launch/HuggingFaceSearchModal'; import { HuggingFaceSearchModal } from '@/components/screens/Launch/HuggingFaceSearchModal';
import { getInputValidationState } from '@/utils/validation'; import { ModelAnalysisModal } from '@/components/screens/Launch/ModelAnalysisModal';
import type { CachedModel, HuggingFaceSearchParams, ModelAnalysis, ModelParamType } from '@/types';
import { logError } from '@/utils/logger'; import { logError } from '@/utils/logger';
import type { import { getInputValidationState } from '@/utils/validation';
ModelAnalysis,
ModelParamType,
CachedModel,
HuggingFaceSearchParams,
} from '@/types';
interface ModelFileFieldProps { interface ModelFileFieldProps {
label: string; label: string;
@ -46,9 +41,7 @@ export const ModelFileField = ({
}: ModelFileFieldProps) => { }: ModelFileFieldProps) => {
const validationState = getInputValidationState(value); const validationState = getInputValidationState(value);
const [analysisModalOpened, setAnalysisModalOpened] = useState(false); const [analysisModalOpened, setAnalysisModalOpened] = useState(false);
const [modelAnalysis, setModelAnalysis] = useState<ModelAnalysis | null>( const [modelAnalysis, setModelAnalysis] = useState<ModelAnalysis | null>(null);
null
);
const [analysisLoading, setAnalysisLoading] = useState(false); const [analysisLoading, setAnalysisLoading] = useState(false);
const [analysisError, setAnalysisError] = useState<string>(); const [analysisError, setAnalysisError] = useState<string>();
const [cachedModels, setCachedModels] = useState<CachedModel[]>([]); const [cachedModels, setCachedModels] = useState<CachedModel[]>([]);
@ -58,8 +51,7 @@ export const ModelFileField = ({
useEffect(() => { useEffect(() => {
void (async () => { void (async () => {
try { try {
const models = const models = await window.electronAPI.kobold.getLocalModels(paramType);
await window.electronAPI.kobold.getLocalModels(paramType);
setCachedModels(models); setCachedModels(models);
} catch (error) { } catch (error) {
logError('Failed to load cached models:', error as Error); logError('Failed to load cached models:', error as Error);
@ -95,8 +87,7 @@ export const ModelFileField = ({
const analysis = await window.electronAPI.kobold.analyzeModel(value); const analysis = await window.electronAPI.kobold.analyzeModel(value);
setModelAnalysis(analysis); setModelAnalysis(analysis);
} catch (error) { } catch (error) {
const errorMessage = const errorMessage = error instanceof Error ? error.message : 'Failed to analyze model';
error instanceof Error ? error.message : 'Failed to analyze model';
setAnalysisError(errorMessage); setAnalysisError(errorMessage);
logError('Failed to analyze model:', error as Error); logError('Failed to analyze model:', error as Error);
} finally { } finally {
@ -126,9 +117,7 @@ export const ModelFileField = ({
}} }}
onFocus={() => combobox.openDropdown()} onFocus={() => combobox.openDropdown()}
onBlur={() => combobox.closeDropdown()} onBlur={() => combobox.closeDropdown()}
error={ error={validationState === 'invalid' ? getHelperText() : undefined}
validationState === 'invalid' ? getHelperText() : undefined
}
rightSection={<Combobox.Chevron />} rightSection={<Combobox.Chevron />}
rightSectionPointerEvents="none" rightSectionPointerEvents="none"
/> />
@ -141,20 +130,12 @@ export const ModelFileField = ({
)} )}
</Combobox> </Combobox>
</div> </div>
<Button <Button onClick={onSelectFile} variant="light" leftSection={<File size={16} />}>
onClick={onSelectFile}
variant="light"
leftSection={<File size={16} />}
>
Browse Browse
</Button> </Button>
{searchParams && ( {searchParams && (
<Tooltip label="Search Hugging Face"> <Tooltip label="Search Hugging Face">
<ActionIcon <ActionIcon onClick={() => setSearchModalOpened(true)} variant="outline" size="lg">
onClick={() => setSearchModalOpened(true)}
variant="outline"
size="lg"
>
<Search size={16} /> <Search size={16} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
@ -172,9 +153,7 @@ export const ModelFileField = ({
variant="light" variant="light"
color="blue" color="blue"
size="lg" size="lg"
disabled={ disabled={validationState === 'neutral' || validationState === 'invalid'}
validationState === 'neutral' || validationState === 'invalid'
}
> >
<Info size={16} /> <Info size={16} />
</ActionIcon> </ActionIcon>

View file

@ -1,6 +1,6 @@
import { Stack, Text, TextInput, Group } from '@mantine/core'; import { Group, Stack, Text, TextInput } from '@mantine/core';
import { InfoTooltip } from '@/components/InfoTooltip';
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip'; import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
import { InfoTooltip } from '@/components/InfoTooltip';
import { useLaunchConfigStore } from '@/stores/launchConfig'; import { useLaunchConfigStore } from '@/stores/launchConfig';
export const NetworkTab = () => { export const NetworkTab = () => {
@ -57,7 +57,7 @@ export const NetworkTab = () => {
} }
const numValue = Number(value); const numValue = Number(value);
if (!isNaN(numValue) && numValue >= 1 && numValue <= 65535) { if (!Number.isNaN(numValue) && numValue >= 1 && numValue <= 65535) {
setPort(numValue); setPort(numValue);
} }
}} }}

View file

@ -1,6 +1,6 @@
import { Stack, Group, Text, NumberInput, SimpleGrid } from '@mantine/core'; import { Group, NumberInput, SimpleGrid, Stack, Text } from '@mantine/core';
import { InfoTooltip } from '@/components/InfoTooltip';
import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip'; import { CheckboxWithTooltip } from '@/components/CheckboxWithTooltip';
import { InfoTooltip } from '@/components/InfoTooltip';
import { useLaunchConfigStore } from '@/stores/launchConfig'; import { useLaunchConfigStore } from '@/stores/launchConfig';
export const PerformanceTab = () => { export const PerformanceTab = () => {

View file

@ -1,18 +1,18 @@
import { Card, Container, Stack, Tabs, Group, Button } from '@mantine/core'; import { Button, Card, Container, Group, Stack, Tabs } from '@mantine/core';
import { useState, useEffect, useCallback, useRef } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { logError } from '@/utils/logger'; import { AdvancedTab } from '@/components/screens/Launch/AdvancedTab';
import { useLaunchConfigStore } from '@/stores/launchConfig'; import { ConfigFileManager } from '@/components/screens/Launch/ConfigFileManager';
import { GeneralTab } from '@/components/screens/Launch/GeneralTab/index';
import { ImageGenerationTab } from '@/components/screens/Launch/ImageGenerationTab';
import { NetworkTab } from '@/components/screens/Launch/NetworkTab';
import { PerformanceTab } from '@/components/screens/Launch/PerformanceTab';
import { WarningDisplay } from '@/components/WarningDisplay';
import { DEFAULT_MODEL_URL } from '@/constants';
import { useLaunchLogic } from '@/hooks/useLaunchLogic'; import { useLaunchLogic } from '@/hooks/useLaunchLogic';
import { useWarnings } from '@/hooks/useWarnings'; import { useWarnings } from '@/hooks/useWarnings';
import { GeneralTab } from '@/components/screens/Launch/GeneralTab/index'; import { useLaunchConfigStore } from '@/stores/launchConfig';
import { AdvancedTab } from '@/components/screens/Launch/AdvancedTab';
import { PerformanceTab } from '@/components/screens/Launch/PerformanceTab';
import { NetworkTab } from '@/components/screens/Launch/NetworkTab';
import { ImageGenerationTab } from '@/components/screens/Launch/ImageGenerationTab';
import { WarningDisplay } from '@/components/WarningDisplay';
import { ConfigFileManager } from '@/components/screens/Launch/ConfigFileManager';
import { DEFAULT_MODEL_URL } from '@/constants';
import type { Acceleration, ConfigFile } from '@/types'; import type { Acceleration, ConfigFile } from '@/types';
import { logError } from '@/utils/logger';
interface LaunchScreenProps { interface LaunchScreenProps {
onLaunch: () => void; onLaunch: () => void;
@ -86,8 +86,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
}); });
const setHappyDefaults = useCallback(async () => { const setHappyDefaults = useCallback(async () => {
const accelerations = const accelerations = await window.electronAPI.kobold.getAvailableAccelerations();
await window.electronAPI.kobold.getAvailableAccelerations();
if (!acceleration && accelerations && accelerations.length > 0) { if (!acceleration && accelerations && accelerations.length > 0) {
setAcceleration(accelerations[0].value as Acceleration); setAcceleration(accelerations[0].value as Acceleration);
@ -96,11 +95,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
const setInitialDefaults = useCallback( const setInitialDefaults = useCallback(
(currentModel: string, currentSdModel: string) => { (currentModel: string, currentSdModel: string) => {
if ( if (!defaultsSetRef.current && !currentModel.trim() && !currentSdModel.trim()) {
!defaultsSetRef.current &&
!currentModel.trim() &&
!currentSdModel.trim()
) {
setModel(DEFAULT_MODEL_URL); setModel(DEFAULT_MODEL_URL);
defaultsSetRef.current = true; defaultsSetRef.current = true;
} }
@ -139,14 +134,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
} }
setConfigLoaded(true); setConfigLoaded(true);
}, [ }, [selectedFile, loadConfigFromFile]);
selectedFile,
loadConfigFromFile,
setConfigFiles,
setInstallDir,
setSelectedFile,
setConfigLoaded,
]);
const handleFileSelection = async (fileName: string) => { const handleFileSelection = async (fileName: string) => {
setSelectedFile(fileName); setSelectedFile(fileName);
@ -211,19 +199,13 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
setSelectedFile(fullConfigName); setSelectedFile(fullConfigName);
await window.electronAPI.kobold.setSelectedConfig(fullConfigName); await window.electronAPI.kobold.setSelectedConfig(fullConfigName);
} else { } else {
logError( logError('Failed to create new configuration', new Error('Save operation failed'));
'Failed to create new configuration',
new Error('Save operation failed')
);
} }
}; };
const handleSaveConfig = async () => { const handleSaveConfig = async () => {
if (!selectedFile) { if (!selectedFile) {
logError( logError('No configuration file selected for saving', new Error('Selected file is null'));
'No configuration file selected for saving',
new Error('Selected file is null')
);
return false; return false;
} }
@ -233,10 +215,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
); );
if (!saveSuccess) { if (!saveSuccess) {
logError( logError('Failed to save configuration', new Error('Save operation failed'));
'Failed to save configuration',
new Error('Save operation failed')
);
return false; return false;
} }
@ -244,8 +223,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
}; };
const handleDeleteConfig = async (fileName: string) => { const handleDeleteConfig = async (fileName: string) => {
const deleteSuccess = const deleteSuccess = await window.electronAPI.kobold.deleteConfigFile(fileName);
await window.electronAPI.kobold.deleteConfigFile(fileName);
if (deleteSuccess) { if (deleteSuccess) {
await loadConfigFiles(); await loadConfigFiles();
@ -270,16 +248,13 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
}; };
useEffect(() => { useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
void loadConfigFiles(); void loadConfigFiles();
const handleInstallDirChange = () => { const handleInstallDirChange = () => {
void loadConfigFiles(); void loadConfigFiles();
}; };
const cleanup = window.electronAPI.kobold.onInstallDirChanged( const cleanup = window.electronAPI.kobold.onInstallDirChanged(handleInstallDirChange);
handleInstallDirChange
);
return cleanup; return cleanup;
}, [loadConfigFiles]); }, [loadConfigFiles]);
@ -368,13 +343,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
return ( return (
<Container size="sm" mt="md"> <Container size="sm" mt="md">
<Stack gap="md"> <Stack gap="md">
<Card <Card withBorder radius="md" shadow="sm" p="lg" style={{ position: 'relative' }}>
withBorder
radius="md"
shadow="sm"
p="lg"
style={{ position: 'relative' }}
>
<Stack gap="lg"> <Stack gap="lg">
<ConfigFileManager <ConfigFileManager
configFiles={configFiles} configFiles={configFiles}

View file

@ -1,17 +1,17 @@
import { PRODUCT_NAME } from '@/constants';
import { import {
Container,
Card,
Stack,
Title,
Text,
Button, Button,
Card,
Container,
Group, Group,
List,
ThemeIcon,
Image, Image,
List,
Stack,
Text,
ThemeIcon,
Title,
} from '@mantine/core'; } from '@mantine/core';
import { Check } from 'lucide-react'; import { Check } from 'lucide-react';
import { PRODUCT_NAME } from '@/constants';
import iconUrl from '/icon.png'; import iconUrl from '/icon.png';
interface WelcomeScreenProps { interface WelcomeScreenProps {
@ -81,8 +81,8 @@ export const WelcomeScreen = ({ onGetStarted }: WelcomeScreenProps) => (
</List> </List>
<Text size="xs" c="dimmed" ta="center" mt="sm"> <Text size="xs" c="dimmed" ta="center" mt="sm">
Hardware acceleration requires appropriate drivers to be manually Hardware acceleration requires appropriate drivers to be manually installed (CUDA for
installed (CUDA for NVIDIA GPUs, ROCm for AMD GPUs, etc.) NVIDIA GPUs, ROCm for AMD GPUs, etc.)
</Text> </Text>
</Stack> </Stack>

View file

@ -1,26 +1,14 @@
import { useState, useEffect } from 'react'; import { Badge, Button, Card, Center, Group, Image, rem, Stack, Text } from '@mantine/core';
import {
Text,
Stack,
Group,
Card,
Image,
Center,
Badge,
Button,
rem,
} from '@mantine/core';
import { Github } from 'lucide-react'; import { Github } from 'lucide-react';
import { useEffect, useState } from 'react';
import { GITHUB_API, PRODUCT_NAME } from '@/constants';
import { useLogoClickSounds } from '@/hooks/useLogoClickSounds'; import { useLogoClickSounds } from '@/hooks/useLogoClickSounds';
import { PRODUCT_NAME, GITHUB_API } from '@/constants';
import type { SystemVersionInfo } from '@/types/electron'; import type { SystemVersionInfo } from '@/types/electron';
import icon from '/icon.png'; import icon from '/icon.png';
export const AboutTab = () => { export const AboutTab = () => {
const [versionInfo, setVersionInfo] = useState<SystemVersionInfo | null>( const [versionInfo, setVersionInfo] = useState<SystemVersionInfo | null>(null);
null
);
const { handleLogoClick, getLogoStyles } = useLogoClickSounds(); const { handleLogoClick, getLogoStyles } = useLogoClickSounds();
useEffect(() => { useEffect(() => {
@ -46,8 +34,7 @@ export const AboutTab = () => {
{ {
icon: Github, icon: Github,
label: 'GitHub', label: 'GitHub',
onClick: () => onClick: () => window.electronAPI.app.openExternal(GITHUB_API.GERBIL_GITHUB_URL),
window.electronAPI.app.openExternal(GITHUB_API.GERBIL_GITHUB_URL),
}, },
]; ];
@ -72,12 +59,7 @@ export const AboutTab = () => {
<Text size="xl" fw={600}> <Text size="xl" fw={600}>
{PRODUCT_NAME} {PRODUCT_NAME}
</Text> </Text>
<Badge <Badge variant="light" color="blue" size="lg" style={{ textTransform: 'none' }}>
variant="light"
color="blue"
size="lg"
style={{ textTransform: 'none' }}
>
v{versionInfo.appVersion} v{versionInfo.appVersion}
</Badge> </Badge>
</Group> </Group>
@ -90,15 +72,9 @@ export const AboutTab = () => {
key={button.label} key={button.label}
variant="light" variant="light"
size="compact-sm" size="compact-sm"
leftSection={ leftSection={<button.icon style={{ width: rem(16), height: rem(16) }} />}
<button.icon style={{ width: rem(16), height: rem(16) }} />
}
onClick={() => void button.onClick()} onClick={() => void button.onClick()}
style={ style={button.label === 'GitHub' ? { textDecoration: 'none' } : undefined}
button.label === 'GitHub'
? { textDecoration: 'none' }
: undefined
}
> >
{button.label} {button.label}
</Button> </Button>
@ -113,10 +89,9 @@ export const AboutTab = () => {
About {PRODUCT_NAME} About {PRODUCT_NAME}
</Text> </Text>
<Text size="sm" c="dimmed" mb="md"> <Text size="sm" c="dimmed" mb="md">
{PRODUCT_NAME} is a user-friendly desktop application that makes it {PRODUCT_NAME} is a user-friendly desktop application that makes it easy to run large
easy to run large language models locally on your machine. Whether language models locally on your machine. Whether you&apos;re looking to chat with AI
you&apos;re looking to chat with AI models, generate images, or models, generate images, or explore different interfaces like SillyTavern and Open WebUI,{' '}
explore different interfaces like SillyTavern and Open WebUI,{' '}
{PRODUCT_NAME} provides a streamlined experience for local AI. {PRODUCT_NAME} provides a streamlined experience for local AI.
</Text> </Text>
</Card> </Card>

View file

@ -1,31 +1,25 @@
import { import {
Group,
type MantineColorScheme,
rem,
SegmentedControl,
Slider,
Stack, Stack,
Text, Text,
Group,
SegmentedControl,
rem,
Slider,
TextInput, TextInput,
type MantineColorScheme,
} from '@mantine/core'; } from '@mantine/core';
import { Sun, Moon, Monitor } from 'lucide-react'; import { Monitor, Moon, Sun } from 'lucide-react';
import { useState, useEffect } from 'react'; import { useEffect, useState } from 'react';
import { usePreferencesStore } from '@/stores/preferences';
import { ZOOM } from '@/constants';
import { FrontendInterfaceSelector } from '@/components/settings/FrontendInterfaceSelector'; import { FrontendInterfaceSelector } from '@/components/settings/FrontendInterfaceSelector';
import { import { ZOOM } from '@/constants';
zoomLevelToPercentage, import { usePreferencesStore } from '@/stores/preferences';
percentageToZoomLevel, import { isValidZoomPercentage, percentageToZoomLevel, zoomLevelToPercentage } from '@/utils/zoom';
isValidZoomPercentage,
} from '@/utils/zoom';
interface AppearanceTabProps { interface AppearanceTabProps {
isOnInterfaceScreen?: boolean; isOnInterfaceScreen?: boolean;
} }
export const AppearanceTab = ({ export const AppearanceTab = ({ isOnInterfaceScreen = false }: AppearanceTabProps) => {
isOnInterfaceScreen = false,
}: AppearanceTabProps) => {
const { const {
rawColorScheme, rawColorScheme,
resolvedColorScheme, resolvedColorScheme,
@ -34,9 +28,7 @@ export const AppearanceTab = ({
const isDark = resolvedColorScheme === 'dark'; const isDark = resolvedColorScheme === 'dark';
const [zoomLevel, setZoomLevel] = useState<number>(ZOOM.DEFAULT_LEVEL); const [zoomLevel, setZoomLevel] = useState<number>(ZOOM.DEFAULT_LEVEL);
const [zoomPercentage, setZoomPercentage] = useState( const [zoomPercentage, setZoomPercentage] = useState(ZOOM.DEFAULT_PERCENTAGE.toString());
ZOOM.DEFAULT_PERCENTAGE.toString()
);
useEffect(() => { useEffect(() => {
const loadSettings = async () => { const loadSettings = async () => {
@ -67,7 +59,7 @@ export const AppearanceTab = ({
const handleZoomPercentageChange = (value: string) => { const handleZoomPercentageChange = (value: string) => {
setZoomPercentage(value); setZoomPercentage(value);
const numValue = Number(value); const numValue = Number(value);
if (!isNaN(numValue) && isValidZoomPercentage(numValue)) { if (!Number.isNaN(numValue) && isValidZoomPercentage(numValue)) {
const newZoomLevel = percentageToZoomLevel(numValue); const newZoomLevel = percentageToZoomLevel(numValue);
setZoomLevel(newZoomLevel); setZoomLevel(newZoomLevel);
@ -91,17 +83,11 @@ export const AppearanceTab = ({
onChange={handleColorSchemeChange} onChange={handleColorSchemeChange}
styles={(theme) => ({ styles={(theme) => ({
root: { root: {
border: `0.5px solid ${ border: `0.5px solid ${isDark ? theme.colors.dark[4] : theme.colors.gray[4]}`,
isDark ? theme.colors.dark[4] : theme.colors.gray[4]
}`,
}, },
indicator: { indicator: {
backgroundColor: isDark backgroundColor: isDark ? theme.colors.dark[5] : theme.colors.gray[2],
? theme.colors.dark[5] border: `1px solid ${isDark ? theme.colors.dark[4] : theme.colors.gray[4]}`,
: theme.colors.gray[2],
border: `1px solid ${
isDark ? theme.colors.dark[4] : theme.colors.gray[4]
}`,
}, },
})} })}
data={[ data={[
@ -145,13 +131,11 @@ export const AppearanceTab = ({
</Text> </Text>
<TextInput <TextInput
value={zoomPercentage} value={zoomPercentage}
onChange={(event) => onChange={(event) => handleZoomPercentageChange(event.currentTarget.value)}
handleZoomPercentageChange(event.currentTarget.value)
}
onBlur={(event) => { onBlur={(event) => {
const value = event.currentTarget.value; const value = event.currentTarget.value;
const numValue = Number(value); const numValue = Number(value);
if (isNaN(numValue) || !isValidZoomPercentage(numValue)) { if (Number.isNaN(numValue) || !isValidZoomPercentage(numValue)) {
setZoomPercentage(zoomLevelToPercentage(zoomLevel).toString()); setZoomPercentage(zoomLevelToPercentage(zoomLevel).toString());
} }
}} }}

View file

@ -1,27 +1,14 @@
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { Anchor, Card, Center, Group, Loader, Stack, Text } from '@mantine/core';
import {
Stack,
Text,
Group,
Card,
Loader,
Center,
Anchor,
} from '@mantine/core';
import { ExternalLink } from 'lucide-react'; import { ExternalLink } from 'lucide-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 { getAssetDescription } from '@/utils/assets';
import {
getDisplayNameFromPath,
stripAssetExtensions,
compareVersions,
} from '@/utils/version';
import { formatDownloadSize } from '@/utils/format';
import { useKoboldBackendsStore } from '@/stores/koboldBackends'; import { useKoboldBackendsStore } from '@/stores/koboldBackends';
import type { InstalledBackend, ReleaseWithStatus } from '@/types/electron';
import type { BackendInfo } from '@/types'; import type { BackendInfo } from '@/types';
import type { InstalledBackend, ReleaseWithStatus } from '@/types/electron';
import { getAssetDescription } from '@/utils/assets';
import { formatDownloadSize } from '@/utils/format';
import { compareVersions, getDisplayNameFromPath, stripAssetExtensions } from '@/utils/version';
export const BackendsTab = () => { export const BackendsTab = () => {
const { const {
@ -34,16 +21,10 @@ export const BackendsTab = () => {
refreshDownloads, refreshDownloads,
} = useKoboldBackendsStore(); } = useKoboldBackendsStore();
const [installedBackends, setInstalledBackends] = useState< const [installedBackends, setInstalledBackends] = useState<InstalledBackend[]>([]);
InstalledBackend[] const [currentBackend, setCurrentBackend] = useState<InstalledBackend | null>(null);
>([]);
const [currentBackend, setCurrentBackend] = useState<InstalledBackend | null>(
null
);
const [loadingInstalled, setLoadingInstalled] = useState(true); const [loadingInstalled, setLoadingInstalled] = useState(true);
const [latestRelease, setLatestRelease] = useState<ReleaseWithStatus | null>( const [latestRelease, setLatestRelease] = useState<ReleaseWithStatus | null>(null);
null
);
const [importing, setImporting] = useState(false); const [importing, setImporting] = useState(false);
const downloadingItemRef = useRef<HTMLDivElement>(null); const downloadingItemRef = useRef<HTMLDivElement>(null);
@ -94,19 +75,14 @@ export const BackendsTab = () => {
}); });
const isCurrent = Boolean( const isCurrent = Boolean(
installedBackend && installedBackend && currentBackend && currentBackend.path === installedBackend.path
currentBackend &&
currentBackend.path === installedBackend.path
); );
if (installedBackend) { if (installedBackend) {
processedInstalled.add(installedBackend.path); processedInstalled.add(installedBackend.path);
const hasUpdate = const hasUpdate =
compareVersions( compareVersions(download.version || 'unknown', installedBackend.version) > 0;
download.version || 'unknown',
installedBackend.version
) > 0;
backends.push({ backends.push({
name: download.name, name: download.name,
@ -135,9 +111,7 @@ export const BackendsTab = () => {
installedBackends.forEach((installed) => { installedBackends.forEach((installed) => {
if (!processedInstalled.has(installed.path)) { if (!processedInstalled.has(installed.path)) {
const displayName = getDisplayNameFromPath(installed); const displayName = getDisplayNameFromPath(installed);
const isCurrent = Boolean( const isCurrent = Boolean(currentBackend && currentBackend.path === installed.path);
currentBackend && currentBackend.path === installed.path
);
backends.push({ backends.push({
name: displayName, name: displayName,
@ -212,9 +186,7 @@ export const BackendsTab = () => {
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( const result = await window.electronAPI.kobold.deleteRelease(backend.installedPath);
backend.installedPath
);
if (result.success) { if (result.success) {
await loadInstalledBackends(); await loadInstalledBackends();
} }
@ -223,9 +195,7 @@ export const BackendsTab = () => {
const makeCurrent = (backend: BackendInfo) => { const makeCurrent = (backend: BackendInfo) => {
if (!backend.installedPath) return; if (!backend.installedPath) return;
const targetBackend = installedBackends.find( const targetBackend = installedBackends.find((b) => b.path === backend.installedPath);
(b) => b.path === backend.installedPath
);
if (targetBackend) { if (targetBackend) {
setCurrentBackend(targetBackend); setCurrentBackend(targetBackend);
} }
@ -292,11 +262,7 @@ export const BackendsTab = () => {
> >
<DownloadCard <DownloadCard
backend={backend} backend={backend}
size={ size={backend.size ? formatDownloadSize(backend.size, backend.downloadUrl) : ''}
backend.size
? formatDownloadSize(backend.size, backend.downloadUrl)
: ''
}
description={getAssetDescription(backend.name)} description={getAssetDescription(backend.name)}
disabled={isDisabled} disabled={isDisabled}
onDownload={(e) => { onDownload={(e) => {

View file

@ -1,14 +1,11 @@
import { useState, useEffect, useCallback, useMemo } from 'react'; import { Anchor, Box, Button, Group, rem, Stack, Text } from '@mantine/core';
import { Text, Box, Anchor, rem, Button, Group, Stack } from '@mantine/core'; import { Image, Monitor } from 'lucide-react';
import { Monitor, Image } from 'lucide-react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { usePreferencesStore } from '@/stores/preferences';
import type {
FrontendPreference,
ImageGenerationFrontendPreference,
} from '@/types';
import { FRONTENDS } from '@/constants';
import { Select } from '@/components/Select';
import { Modal } from '@/components/Modal'; import { Modal } from '@/components/Modal';
import { Select } from '@/components/Select';
import { FRONTENDS } from '@/constants';
import { usePreferencesStore } from '@/stores/preferences';
import type { FrontendPreference, ImageGenerationFrontendPreference } from '@/types';
interface FrontendRequirement { interface FrontendRequirement {
id: string; id: string;
@ -37,9 +34,7 @@ export const FrontendInterfaceSelector = ({
setImageGenerationFrontendPreference, setImageGenerationFrontendPreference,
} = usePreferencesStore(); } = usePreferencesStore();
const [frontendRequirements, setFrontendRequirements] = useState< const [frontendRequirements, setFrontendRequirements] = useState<Map<string, boolean>>(new Map());
Map<string, boolean>
>(new Map());
const [showClearDataModal, setShowClearDataModal] = useState(false); const [showClearDataModal, setShowClearDataModal] = useState(false);
@ -63,8 +58,7 @@ export const FrontendInterfaceSelector = ({
url: 'https://nodejs.org/', url: 'https://nodejs.org/',
}, },
], ],
requirementCheck: () => requirementCheck: () => window.electronAPI.dependencies.isNpxAvailable(),
window.electronAPI.dependencies.isNpxAvailable(),
}, },
{ {
value: 'openwebui', value: 'openwebui',
@ -131,9 +125,7 @@ export const FrontendInterfaceSelector = ({
}; };
const handleImageGenerationFrontendChange = (value: string | null) => { const handleImageGenerationFrontendChange = (value: string | null) => {
setImageGenerationFrontendPreference( setImageGenerationFrontendPreference(value as ImageGenerationFrontendPreference);
value as ImageGenerationFrontendPreference
);
}; };
const handleClearOpenWebUIData = async () => { const handleClearOpenWebUIData = async () => {
@ -158,13 +150,12 @@ export const FrontendInterfaceSelector = ({
if (!requirementGroups.has(reqKey)) { if (!requirementGroups.has(reqKey)) {
requirementGroups.set(reqKey, []); requirementGroups.set(reqKey, []);
} }
requirementGroups.get(reqKey)!.push(config.label); requirementGroups.get(reqKey)?.push(config.label);
}); });
return ( return (
<Box mt="sm"> <Box mt="sm">
{Array.from(requirementGroups.entries()).map( {Array.from(requirementGroups.entries()).map(([reqKey, frontendLabels]) => {
([reqKey, frontendLabels]) => {
const firstDisabledFrontend = disabledFrontends.find( const firstDisabledFrontend = disabledFrontends.find(
(config) => (config) =>
getUnmetRequirementsForFrontend(config.value) getUnmetRequirementsForFrontend(config.value)
@ -189,12 +180,7 @@ export const FrontendInterfaceSelector = ({
{frontendText} {isAre} disabled - requires{' '} {frontendText} {isAre} disabled - requires{' '}
{unmetReqs.map((req, index) => ( {unmetReqs.map((req, index) => (
<span key={req.id}> <span key={req.id}>
<Anchor <Anchor href={req.url} target="_blank" rel="noopener noreferrer" td="underline">
href={req.url}
target="_blank"
rel="noopener noreferrer"
td="underline"
>
{req.name} {req.name}
</Anchor> </Anchor>
{index < unmetReqs.length - 1 ? ', ' : ''} {index < unmetReqs.length - 1 ? ', ' : ''}
@ -202,8 +188,7 @@ export const FrontendInterfaceSelector = ({
))} ))}
</Text> </Text>
); );
} })}
)}
</Box> </Box>
); );
}; };
@ -224,7 +209,6 @@ export const FrontendInterfaceSelector = ({
useEffect(() => { useEffect(() => {
if (frontendPreference) { if (frontendPreference) {
// eslint-disable-next-line react-hooks/set-state-in-effect
void checkAllFrontendRequirements(); void checkAllFrontendRequirements();
} }
}, [frontendPreference, checkAllFrontendRequirements]); }, [frontendPreference, checkAllFrontendRequirements]);
@ -238,8 +222,7 @@ export const FrontendInterfaceSelector = ({
{getUnmetRequirements().length === 0 && ( {getUnmetRequirements().length === 0 && (
<Text size="sm" c="dimmed" mb="md"> <Text size="sm" c="dimmed" mb="md">
Choose which frontend interface to use for interacting with AI Choose which frontend interface to use for interacting with AI models
models
</Text> </Text>
)} )}
@ -274,9 +257,7 @@ export const FrontendInterfaceSelector = ({
label: config.label, label: config.label,
disabled: !isFrontendAvailable(config.value), disabled: !isFrontendAvailable(config.value),
}))} }))}
leftSection={ leftSection={<Monitor style={{ width: rem(16), height: rem(16) }} />}
<Monitor style={{ width: rem(16), height: rem(16) }} />
}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
@ -317,10 +298,7 @@ export const FrontendInterfaceSelector = ({
</Box> </Box>
<Group justify="flex-end" gap="sm"> <Group justify="flex-end" gap="sm">
<Button <Button variant="subtle" onClick={() => setShowClearDataModal(false)}>
variant="subtle"
onClick={() => setShowClearDataModal(false)}
>
Cancel Cancel
</Button> </Button>
<Button color="red" onClick={() => void handleClearOpenWebUIData()}> <Button color="red" onClick={() => void handleClearOpenWebUIData()}>

View file

@ -1,13 +1,12 @@
import { useState, useEffect } from 'react';
import { Stack, Text } from '@mantine/core'; import { Stack, Text } from '@mantine/core';
import { usePreferencesStore } from '@/stores/preferences'; import { useEffect, useState } from 'react';
import { Switch } from '@/components/Switch'; import { Switch } from '@/components/Switch';
import { usePreferencesStore } from '@/stores/preferences';
export const GeneralTab = () => { export const GeneralTab = () => {
const [enableSystemTray, setEnableSystemTray] = useState(false); const [enableSystemTray, setEnableSystemTray] = useState(false);
const [startMinimizedToTray, setStartMinimizedToTray] = useState(false); const [startMinimizedToTray, setStartMinimizedToTray] = useState(false);
const { systemMonitoringEnabled, setSystemMonitoringEnabled } = const { systemMonitoringEnabled, setSystemMonitoringEnabled } = usePreferencesStore();
usePreferencesStore();
useEffect(() => { useEffect(() => {
const loadSystemTrayPreference = async () => { const loadSystemTrayPreference = async () => {
@ -48,9 +47,7 @@ export const GeneralTab = () => {
<Switch <Switch
label="Show system metrics" label="Show system metrics"
checked={systemMonitoringEnabled} checked={systemMonitoringEnabled}
onChange={(event) => onChange={(event) => setSystemMonitoringEnabled(event.currentTarget.checked)}
setSystemMonitoringEnabled(event.currentTarget.checked)
}
/> />
</div> </div>
@ -59,15 +56,12 @@ export const GeneralTab = () => {
System Tray System Tray
</Text> </Text>
<Text size="sm" c="dimmed" mb="md"> <Text size="sm" c="dimmed" mb="md">
Add a system tray icon with quick access to launch, eject and monitor Add a system tray icon with quick access to launch, eject and monitor system metrics
system metrics
</Text> </Text>
<Switch <Switch
label="Enable system tray icon" label="Enable system tray icon"
checked={enableSystemTray} checked={enableSystemTray}
onChange={(event) => onChange={(event) => void handleSystemTrayToggle(event.currentTarget.checked)}
void handleSystemTrayToggle(event.currentTarget.checked)
}
/> />
</div> </div>
@ -82,9 +76,7 @@ export const GeneralTab = () => {
label="Start minimized to tray" label="Start minimized to tray"
checked={startMinimizedToTray} checked={startMinimizedToTray}
disabled={!enableSystemTray} disabled={!enableSystemTray}
onChange={(event) => onChange={(event) => void handleStartMinimizedToggle(event.currentTarget.checked)}
void handleStartMinimizedToggle(event.currentTarget.checked)
}
/> />
</div> </div>
</Stack> </Stack>

View file

@ -1,22 +1,22 @@
import { useState, useEffect } from 'react'; import { Group, rem, Tabs, Text } from '@mantine/core';
import { Tabs, Text, Group, rem } from '@mantine/core';
import { import {
Settings,
Palette,
SlidersHorizontal,
GitBranch, GitBranch,
Monitor,
Info, Info,
Monitor,
Palette,
Settings,
SlidersHorizontal,
Wrench, Wrench,
} from 'lucide-react'; } from 'lucide-react';
import { GeneralTab } from '@/components/settings/GeneralTab'; import { useEffect, useState } from 'react';
import { BackendsTab } from '@/components/settings/BackendsTab'; import { Modal } from '@/components/Modal';
import { AboutTab } from '@/components/settings/AboutTab';
import { AppearanceTab } from '@/components/settings/AppearanceTab'; import { AppearanceTab } from '@/components/settings/AppearanceTab';
import { BackendsTab } from '@/components/settings/BackendsTab';
import { GeneralTab } from '@/components/settings/GeneralTab';
import { SystemTab } from '@/components/settings/SystemTab'; import { SystemTab } from '@/components/settings/SystemTab';
import { TroubleshootingTab } from '@/components/settings/TroubleshootingTab'; import { TroubleshootingTab } from '@/components/settings/TroubleshootingTab';
import { AboutTab } from '@/components/settings/AboutTab';
import type { Screen } from '@/types'; import type { Screen } from '@/types';
import { Modal } from '@/components/Modal';
interface SettingsModalProps { interface SettingsModalProps {
opened: boolean; opened: boolean;
@ -33,17 +33,14 @@ export const SettingsModal = ({
}: SettingsModalProps) => { }: SettingsModalProps) => {
const [activeTab, setActiveTab] = useState('general'); const [activeTab, setActiveTab] = useState('general');
const showBackendsTab = const showBackendsTab = currentScreen !== 'download' && currentScreen !== 'welcome';
currentScreen !== 'download' && currentScreen !== 'welcome';
const effectiveActiveTab = const effectiveActiveTab = !showBackendsTab && activeTab === 'backends' ? 'general' : activeTab;
!showBackendsTab && activeTab === 'backends' ? 'general' : activeTab;
useEffect(() => { useEffect(() => {
if (opened) { if (opened) {
const originalOverflow = document.body.style.overflow; const originalOverflow = document.body.style.overflow;
const scrollbarWidth = const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
window.innerWidth - document.documentElement.clientWidth;
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
document.body.style.paddingRight = `${scrollbarWidth}px`; document.body.style.paddingRight = `${scrollbarWidth}px`;
@ -94,35 +91,27 @@ export const SettingsModal = ({
<Tabs.List> <Tabs.List>
<Tabs.Tab <Tabs.Tab
value="general" value="general"
leftSection={ leftSection={<SlidersHorizontal style={{ width: rem(16), height: rem(16) }} />}
<SlidersHorizontal style={{ width: rem(16), height: rem(16) }} />
}
> >
General General
</Tabs.Tab> </Tabs.Tab>
{showBackendsTab && ( {showBackendsTab && (
<Tabs.Tab <Tabs.Tab
value="backends" value="backends"
leftSection={ leftSection={<GitBranch style={{ width: rem(16), height: rem(16) }} />}
<GitBranch style={{ width: rem(16), height: rem(16) }} />
}
> >
Backends Backends
</Tabs.Tab> </Tabs.Tab>
)} )}
<Tabs.Tab <Tabs.Tab
value="appearance" value="appearance"
leftSection={ leftSection={<Palette style={{ width: rem(16), height: rem(16) }} />}
<Palette style={{ width: rem(16), height: rem(16) }} />
}
> >
Appearance Appearance
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab <Tabs.Tab
value="system" value="system"
leftSection={ leftSection={<Monitor style={{ width: rem(16), height: rem(16) }} />}
<Monitor style={{ width: rem(16), height: rem(16) }} />
}
> >
System System
</Tabs.Tab> </Tabs.Tab>

View file

@ -1,18 +1,12 @@
import { useState, useEffect } from 'react'; import { Center, Stack, Text } from '@mantine/core';
import { import { useEffect, useState } from 'react';
createSoftwareItems,
createDriverItems,
createHardwareItems,
} from '@/utils/systemInfo';
import { Stack, Text, Center } from '@mantine/core';
import { InfoCard } from '@/components/InfoCard'; import { InfoCard } from '@/components/InfoCard';
import type { HardwareInfo } from '@/types/hardware';
import type { SystemVersionInfo } from '@/types/electron'; import type { SystemVersionInfo } from '@/types/electron';
import type { HardwareInfo } from '@/types/hardware';
import { createDriverItems, createHardwareItems, createSoftwareItems } from '@/utils/systemInfo';
export const SystemTab = () => { export const SystemTab = () => {
const [versionInfo, setVersionInfo] = useState<SystemVersionInfo | null>( const [versionInfo, setVersionInfo] = useState<SystemVersionInfo | null>(null);
null
);
const [hardwareInfo, setHardwareInfo] = useState<HardwareInfo | null>(null); const [hardwareInfo, setHardwareInfo] = useState<HardwareInfo | null>(null);
const [koboldVersion, setKoboldVersion] = useState<string | null>(null); const [koboldVersion, setKoboldVersion] = useState<string | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -30,8 +24,7 @@ export const SystemTab = () => {
setVersionInfo(info); setVersionInfo(info);
setKoboldVersion(currentBackend?.version || null); setKoboldVersion(currentBackend?.version || null);
const [cpu, gpu, gpuCapabilities, gpuMemory, systemMemory] = const [cpu, gpu, gpuCapabilities, gpuMemory, systemMemory] = await Promise.all([
await Promise.all([
window.electronAPI.kobold.detectCPU(), window.electronAPI.kobold.detectCPU(),
window.electronAPI.kobold.detectGPU(), window.electronAPI.kobold.detectGPU(),
window.electronAPI.kobold.detectGPUCapabilities(), window.electronAPI.kobold.detectGPUCapabilities(),
@ -47,10 +40,7 @@ export const SystemTab = () => {
systemMemory, systemMemory,
}); });
} catch (error) { } catch (error) {
window.electronAPI.logs.logError( window.electronAPI.logs.logError('Failed to load system info', error as Error);
'Failed to load system info',
error as Error
);
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -77,11 +67,7 @@ export const SystemTab = () => {
<InfoCard title="Drivers" items={driverItems} loading={!hardwareInfo} /> <InfoCard title="Drivers" items={driverItems} loading={!hardwareInfo} />
<InfoCard <InfoCard title="Hardware" items={hardwareItems} loading={!hardwareInfo} />
title="Hardware"
items={hardwareItems}
loading={!hardwareInfo}
/>
</Stack> </Stack>
); );
}; };

View file

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'; import { Button, Group, rem, Stack, Text, TextInput } from '@mantine/core';
import { Stack, Text, Group, TextInput, Button, rem } from '@mantine/core'; import { ExternalLink, Folder, FolderOpen, Monitor } from 'lucide-react';
import { Folder, FolderOpen, Monitor, ExternalLink } from 'lucide-react'; import { useEffect, useState } from 'react';
export const TroubleshootingTab = () => { export const TroubleshootingTab = () => {
const [installDir, setInstallDir] = useState(''); const [installDir, setInstallDir] = useState('');
@ -17,8 +17,7 @@ export const TroubleshootingTab = () => {
}, []); }, []);
const handleSelectInstallDir = async () => { const handleSelectInstallDir = async () => {
const selectedDir = const selectedDir = await window.electronAPI.kobold.selectInstallDirectory();
await window.electronAPI.kobold.selectInstallDirectory();
if (selectedDir) { if (selectedDir) {
setInstallDir(selectedDir); setInstallDir(selectedDir);
} }
@ -50,9 +49,7 @@ export const TroubleshootingTab = () => {
<Button <Button
variant="outline" variant="outline"
onClick={() => void handleSelectInstallDir()} onClick={() => void handleSelectInstallDir()}
leftSection={ leftSection={<FolderOpen style={{ width: rem(16), height: rem(16) }} />}
<FolderOpen style={{ width: rem(16), height: rem(16) }} />
}
> >
Browse Browse
</Button> </Button>
@ -60,9 +57,7 @@ export const TroubleshootingTab = () => {
variant="outline" variant="outline"
onClick={() => void handleOpenInstallDir()} onClick={() => void handleOpenInstallDir()}
disabled={!installDir} disabled={!installDir}
leftSection={ leftSection={<ExternalLink style={{ width: rem(16), height: rem(16) }} />}
<ExternalLink style={{ width: rem(16), height: rem(16) }} />
}
> >
Open Open
</Button> </Button>
@ -80,9 +75,7 @@ export const TroubleshootingTab = () => {
<Button <Button
variant="outline" variant="outline"
size="compact-sm" size="compact-sm"
leftSection={ leftSection={<FolderOpen style={{ width: rem(16), height: rem(16) }} />}
<FolderOpen style={{ width: rem(16), height: rem(16) }} />
}
onClick={() => void window.electronAPI.app.showLogsFolder()} onClick={() => void window.electronAPI.app.showLogsFolder()}
> >
Show Logs Show Logs
@ -90,9 +83,7 @@ export const TroubleshootingTab = () => {
<Button <Button
variant="outline" variant="outline"
size="compact-sm" size="compact-sm"
leftSection={ leftSection={<Monitor style={{ width: rem(16), height: rem(16) }} />}
<Monitor style={{ width: rem(16), height: rem(16) }} />
}
onClick={() => void window.electronAPI.app.viewConfigFile()} onClick={() => void window.electronAPI.app.viewConfigFile()}
> >
View Config View Config

View file

@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { compareVersions } from '@/utils/version';
import { GITHUB_API } from '@/constants'; import { GITHUB_API } from '@/constants';
import { compareVersions } from '@/utils/version';
interface AppUpdateInfo { interface AppUpdateInfo {
currentVersion: string; currentVersion: string;

View file

@ -1,13 +1,12 @@
import { useState, useCallback, useRef } from 'react'; import { useCallback, useRef, useState } from 'react';
import { logError } from '@/utils/logger'; import { HUGGINGFACE_BASE_URL } from '@/constants';
import type { import type {
HuggingFaceModelInfo,
HuggingFaceFileInfo, HuggingFaceFileInfo,
HuggingFaceModelInfo,
HuggingFaceSearchParams, HuggingFaceSearchParams,
HuggingFaceSortOption, HuggingFaceSortOption,
} from '@/types'; } from '@/types';
import { logError } from '@/utils/logger';
import { HUGGINGFACE_BASE_URL } from '@/constants';
const MODELS_PER_PAGE = 20; const MODELS_PER_PAGE = 20;
const HF_API_BASE = `${HUGGINGFACE_BASE_URL}/api`; const HF_API_BASE = `${HUGGINGFACE_BASE_URL}/api`;
@ -78,9 +77,7 @@ const fetchBaseModelParams = async (baseModelId: string) => {
} }
}; };
export const useHuggingFaceSearch = ( export const useHuggingFaceSearch = (initialParams: HuggingFaceSearchParams) => {
initialParams: HuggingFaceSearchParams
) => {
const [models, setModels] = useState<HuggingFaceModelInfo[]>([]); const [models, setModels] = useState<HuggingFaceModelInfo[]>([]);
const [files, setFiles] = useState<HuggingFaceFileInfo[]>([]); const [files, setFiles] = useState<HuggingFaceFileInfo[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -88,19 +85,13 @@ export const useHuggingFaceSearch = (
const [error, setError] = useState<string>(); const [error, setError] = useState<string>();
const [hasMore, setHasMore] = useState(true); const [hasMore, setHasMore] = useState(true);
const [selectedModel, setSelectedModel] = useState<HuggingFaceModelInfo>(); const [selectedModel, setSelectedModel] = useState<HuggingFaceModelInfo>();
const [sortBy, setSortBy] = useState<HuggingFaceSortOption>( const [sortBy, setSortBy] = useState<HuggingFaceSortOption>(initialParams.sort);
initialParams.sort
);
const [searchParams] = useState<HuggingFaceSearchParams>(initialParams); const [searchParams] = useState<HuggingFaceSearchParams>(initialParams);
const pageRef = useRef(0); const pageRef = useRef(0);
const currentQueryRef = useRef<string | undefined>(undefined); const currentQueryRef = useRef<string | undefined>(undefined);
const searchModels = useCallback( const searchModels = useCallback(
async ( async (query?: string, reset = true, sort: HuggingFaceSortOption = sortBy) => {
query?: string,
reset = true,
sort: HuggingFaceSortOption = sortBy
) => {
if (reset) { if (reset) {
setModels([]); setModels([]);
setHasMore(true); setHasMore(true);
@ -115,21 +106,17 @@ export const useHuggingFaceSearch = (
currentQueryRef.current = query; currentQueryRef.current = query;
try { try {
const searchQuery = const searchQuery = query !== undefined ? query.trim() || undefined : searchParams.search;
query !== undefined ? query.trim() || undefined : searchParams.search;
const params = new URLSearchParams(); const params = new URLSearchParams();
if (searchQuery) params.set('search', searchQuery); if (searchQuery) params.set('search', searchQuery);
if (searchParams.pipelineTag) if (searchParams.pipelineTag) params.set('pipeline_tag', searchParams.pipelineTag);
params.set('pipeline_tag', searchParams.pipelineTag);
if (searchParams.filter) params.set('filter', searchParams.filter); 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');
const response = await fetch( const response = await fetch(`${HF_API_BASE}/models?${params.toString()}`);
`${HF_API_BASE}/models?${params.toString()}`
);
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch models: ${response.statusText}`); throw new Error(`Failed to fetch models: ${response.statusText}`);
@ -171,8 +158,7 @@ export const useHuggingFaceSearch = (
setHasMore(data.length === MODELS_PER_PAGE); setHasMore(data.length === MODELS_PER_PAGE);
pageRef.current = 1; pageRef.current = 1;
} catch (err) { } catch (err) {
const message = const message = err instanceof Error ? err.message : 'Failed to search models';
err instanceof Error ? err.message : 'Failed to search models';
setError(message); setError(message);
logError('HuggingFace search failed:', err as Error); logError('HuggingFace search failed:', err as Error);
} finally { } finally {
@ -202,17 +188,14 @@ export const useHuggingFaceSearch = (
const params = new URLSearchParams(); const params = new URLSearchParams();
if (searchQuery) params.set('search', searchQuery); if (searchQuery) params.set('search', searchQuery);
if (searchParams.pipelineTag) if (searchParams.pipelineTag) params.set('pipeline_tag', searchParams.pipelineTag);
params.set('pipeline_tag', searchParams.pipelineTag);
if (searchParams.filter) params.set('filter', searchParams.filter); 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));
params.set('full', 'false'); params.set('full', 'false');
const response = await fetch( const response = await fetch(`${HF_API_BASE}/models?${params.toString()}`);
`${HF_API_BASE}/models?${params.toString()}`
);
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch models: ${response.statusText}`); throw new Error(`Failed to fetch models: ${response.statusText}`);
} }
@ -253,8 +236,7 @@ export const useHuggingFaceSearch = (
setHasMore(validModels.length === MODELS_PER_PAGE); setHasMore(validModels.length === MODELS_PER_PAGE);
pageRef.current += 1; pageRef.current += 1;
} catch (err) { } catch (err) {
const message = const message = err instanceof Error ? err.message : 'Failed to load more models';
err instanceof Error ? err.message : 'Failed to load more models';
setError(message); setError(message);
logError('HuggingFace load more failed:', err as Error); logError('HuggingFace load more failed:', err as Error);
} finally { } finally {
@ -273,9 +255,7 @@ export const useHuggingFaceSearch = (
const filter = searchParams?.filter; const filter = searchParams?.filter;
const extensions = getExtensionsForLibrary(filter); const extensions = getExtensionsForLibrary(filter);
const response = await fetch( const response = await fetch(`${HF_API_BASE}/models/${model.id}/tree/main?recursive=true`);
`${HF_API_BASE}/models/${model.id}/tree/main?recursive=true`
);
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch files: ${response.statusText}`); throw new Error(`Failed to fetch files: ${response.statusText}`);
@ -285,10 +265,7 @@ export const useHuggingFaceSearch = (
const fileResults: HuggingFaceFileInfo[] = []; const fileResults: HuggingFaceFileInfo[] = [];
for (const file of items) { for (const file of items) {
if ( if (file.type === 'file' && extensions.some((ext) => file.path.endsWith(ext))) {
file.type === 'file' &&
extensions.some((ext) => file.path.endsWith(ext))
) {
fileResults.push({ fileResults.push({
path: file.path, path: file.path,
size: file.lfs?.size ?? file.size, size: file.lfs?.size ?? file.size,
@ -299,8 +276,7 @@ export const useHuggingFaceSearch = (
fileResults.sort((a, b) => b.size - a.size); fileResults.sort((a, b) => b.size - a.size);
setFiles(fileResults); setFiles(fileResults);
} catch (err) { } catch (err) {
const message = const message = err instanceof Error ? err.message : 'Failed to load model files';
err instanceof Error ? err.message : 'Failed to load model files';
setError(message); setError(message);
logError('HuggingFace list files failed:', err as Error); logError('HuggingFace list files failed:', err as Error);
} finally { } finally {

View file

@ -47,11 +47,7 @@ interface LaunchArgs {
pipelineparallel: boolean; pipelineparallel: boolean;
} }
const buildModelArgs = ( const buildModelArgs = (model: string, sdmodel: string, launchArgs: LaunchArgs) => {
model: string,
sdmodel: string,
launchArgs: LaunchArgs
) => {
const args: string[] = []; const args: string[] = [];
if (model.trim() !== '') { if (model.trim() !== '') {
@ -99,8 +95,7 @@ const buildModelArgs = (
const buildConfigArgs = (isImageMode: boolean, launchArgs: LaunchArgs) => { const buildConfigArgs = (isImageMode: boolean, launchArgs: LaunchArgs) => {
const args: string[] = []; const args: string[] = [];
const isGpuAcceleration = const isGpuAcceleration = launchArgs.acceleration && launchArgs.acceleration !== 'cpu';
launchArgs.acceleration && launchArgs.acceleration !== 'cpu';
if (isGpuAcceleration) { if (isGpuAcceleration) {
if (launchArgs.autoGpuLayers && launchArgs.gpuLayers > 0) { if (launchArgs.autoGpuLayers && launchArgs.gpuLayers > 0) {
@ -187,29 +182,23 @@ const buildClblastArgs = (launchArgs: LaunchArgs) => {
) { ) {
const parsed = parseCLBlastDevice(launchArgs.gpuDeviceSelection); const parsed = parseCLBlastDevice(launchArgs.gpuDeviceSelection);
if (parsed) { if (parsed) {
clblastArgs.push( clblastArgs.push(parsed.platformIndex.toString(), parsed.deviceIndex.toString());
parsed.platformIndex.toString(),
parsed.deviceIndex.toString()
);
} else { } else {
clblastArgs.push(launchArgs.gpuPlatform.toString(), '0'); clblastArgs.push(launchArgs.gpuPlatform.toString(), '0');
} }
} else { } else {
clblastArgs.push( clblastArgs.push(launchArgs.gpuPlatform.toString(), launchArgs.gpuDeviceSelection || '0');
launchArgs.gpuPlatform.toString(),
launchArgs.gpuDeviceSelection || '0'
);
} }
return clblastArgs; return clblastArgs;
}; };
const addTensorSplitArgs = (args: string[], launchArgs: LaunchArgs) => { const addTensorSplitArgs = (args: string[], launchArgs: LaunchArgs) => {
if (launchArgs.tensorSplit && launchArgs.tensorSplit.trim()) { if (launchArgs.tensorSplit?.trim()) {
const tensorValues = launchArgs.tensorSplit const tensorValues = launchArgs.tensorSplit
.split(',') .split(',')
.map((value) => value.trim()) .map((value) => value.trim())
.filter((value) => value !== '' && !isNaN(Number(value))); .filter((value) => value !== '' && !Number.isNaN(Number(value)));
if (tensorValues.length > 0) { if (tensorValues.length > 0) {
args.push('--tensorsplit', ...tensorValues); args.push('--tensorsplit', ...tensorValues);
@ -237,10 +226,7 @@ const buildBackendArgs = (launchArgs: LaunchArgs, platform: string) => {
launchArgs.acceleration === 'rocm' || launchArgs.acceleration === 'rocm' ||
launchArgs.acceleration === 'vulkan'; launchArgs.acceleration === 'vulkan';
if ( if (launchArgs.acceleration === 'cuda' || launchArgs.acceleration === 'rocm') {
launchArgs.acceleration === 'cuda' ||
launchArgs.acceleration === 'rocm'
) {
args.push(...buildCudaArgs(launchArgs)); args.push(...buildCudaArgs(launchArgs));
if (launchArgs.gpuDeviceSelection === 'all' && isTensorSplitSupported) { if (launchArgs.gpuDeviceSelection === 'all' && isTensorSplitSupported) {
@ -270,11 +256,7 @@ function parseCLBlastDevice(deviceString: string) {
return null; return null;
} }
export const useLaunchLogic = ({ export const useLaunchLogic = ({ model, sdmodel, onLaunch }: UseLaunchLogicProps) => {
model,
sdmodel,
onLaunch,
}: UseLaunchLogicProps) => {
const [isLaunching, setIsLaunching] = useState(false); const [isLaunching, setIsLaunching] = useState(false);
const handleLaunch = useCallback( const handleLaunch = useCallback(
@ -299,29 +281,19 @@ export const useLaunchLogic = ({
]; ];
if (launchArgs.additionalArguments.trim()) { if (launchArgs.additionalArguments.trim()) {
const additionalArgs = launchArgs.additionalArguments const additionalArgs = launchArgs.additionalArguments.trim().split(/\s+/);
.trim()
.split(/\s+/);
args.push(...additionalArgs); args.push(...additionalArgs);
} }
const preLaunchCommands = launchArgs.preLaunchCommands.filter( const preLaunchCommands = launchArgs.preLaunchCommands.filter((cmd) => cmd.trim() !== '');
(cmd) => cmd.trim() !== ''
);
const result = await window.electronAPI.kobold.launchKoboldCpp( const result = await window.electronAPI.kobold.launchKoboldCpp(args, preLaunchCommands);
args,
preLaunchCommands
);
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( window.electronAPI.logs.logError('Launch failed:', new Error(errorMessage));
'Launch failed:',
new Error(errorMessage)
);
} }
setIsLaunching(false); setIsLaunching(false);

View file

@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { soundAssets, playSound, initializeAudio } from '@/utils/sounds'; import { initializeAudio, playSound, soundAssets } from '@/utils/sounds';
export const useLogoClickSounds = () => { export const useLogoClickSounds = () => {
const [logoClickCount, setLogoClickCount] = useState(0); const [logoClickCount, setLogoClickCount] = useState(0);
@ -36,9 +36,7 @@ export const useLogoClickSounds = () => {
cursor: 'pointer', cursor: 'pointer',
userSelect: 'none' as const, userSelect: 'none' as const,
transition: 'transform 0.15s ease-in-out', transition: 'transform 0.15s ease-in-out',
transform: isElephantMode transform: isElephantMode ? 'scale(1.3) rotate(5deg)' : 'scale(1) rotate(0deg)',
? '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

View file

@ -1,13 +1,9 @@
import { useState, useCallback, useEffect } from 'react'; import { useCallback, useEffect, useState } from 'react';
import {
getDisplayNameFromPath,
compareVersions,
stripAssetExtensions,
} from '@/utils/version';
import { useKoboldBackendsStore } from '@/stores/koboldBackends'; import { useKoboldBackendsStore } from '@/stores/koboldBackends';
import { getROCmDownload } from '@/utils/rocm';
import type { InstalledBackend, DownloadItem } from '@/types/electron';
import type { DismissedUpdate } from '@/types'; import type { DismissedUpdate } from '@/types';
import type { DownloadItem, InstalledBackend } from '@/types/electron';
import { getROCmDownload } from '@/utils/rocm';
import { compareVersions, getDisplayNameFromPath, stripAssetExtensions } from '@/utils/version';
export interface BinaryUpdateInfo { export interface BinaryUpdateInfo {
currentBackend: InstalledBackend; currentBackend: InstalledBackend;
@ -18,17 +14,14 @@ export const useUpdateChecker = () => {
const [updateInfo, setUpdateInfo] = useState<BinaryUpdateInfo | null>(null); const [updateInfo, setUpdateInfo] = useState<BinaryUpdateInfo | null>(null);
const [isChecking, setIsChecking] = useState(false); const [isChecking, setIsChecking] = useState(false);
const [showUpdateModal, setShowUpdateModal] = useState(false); const [showUpdateModal, setShowUpdateModal] = useState(false);
const [dismissedUpdates, setDismissedUpdates] = useState<DismissedUpdate[]>( const [dismissedUpdates, setDismissedUpdates] = useState<DismissedUpdate[]>([]);
[]
);
const { availableDownloads: releases, loadingRemote } = const { availableDownloads: releases, loadingRemote } = useKoboldBackendsStore();
useKoboldBackendsStore();
useEffect(() => { useEffect(() => {
const loadDismissedUpdates = async () => { const loadDismissedUpdates = async () => {
const dismissed = (await window.electronAPI.config.get( const dismissed = (await window.electronAPI.config.get('dismissedUpdates')) as
'dismissedUpdates' | DismissedUpdate[]
)) as DismissedUpdate[] | undefined; | undefined;
if (dismissed) { if (dismissed) {
setDismissedUpdates(dismissed); setDismissedUpdates(dismissed);
@ -61,16 +54,13 @@ export const useUpdateChecker = () => {
const currentDisplayName = getDisplayNameFromPath(currentBackend); const currentDisplayName = getDisplayNameFromPath(currentBackend);
const matchingDownload = availableDownloads.find( const matchingDownload = availableDownloads.find((download: DownloadItem) => {
(download: DownloadItem) => {
const downloadBaseName = stripAssetExtensions(download.name); const downloadBaseName = stripAssetExtensions(download.name);
return downloadBaseName === currentDisplayName; return downloadBaseName === currentDisplayName;
} });
);
if (matchingDownload && matchingDownload.version) { if (matchingDownload?.version) {
const hasUpdate = const hasUpdate = compareVersions(matchingDownload.version, currentBackend.version) > 0;
compareVersions(matchingDownload.version, currentBackend.version) > 0;
if (hasUpdate) { if (hasUpdate) {
const isUpdateDismissed = dismissedUpdates.some( const isUpdateDismissed = dismissedUpdates.some(
@ -93,7 +83,7 @@ export const useUpdateChecker = () => {
}, [dismissedUpdates, releases, loadingRemote]); }, [dismissedUpdates, releases, loadingRemote]);
const skipUpdate = useCallback(() => { const skipUpdate = useCallback(() => {
if (updateInfo && updateInfo.availableUpdate.version) { if (updateInfo?.availableUpdate.version) {
const newDismissedUpdate: DismissedUpdate = { const newDismissedUpdate: DismissedUpdate = {
currentBackendPath: updateInfo.currentBackend.path, currentBackendPath: updateInfo.currentBackend.path,
targetVersion: updateInfo.availableUpdate.version, targetVersion: updateInfo.availableUpdate.version,

View file

@ -1,6 +1,6 @@
import { useEffect, useState, useCallback, useMemo } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { CPUCapabilities, GPUDevice } from '@/types/hardware';
import type { AccelerationOption, AccelerationSupport } from '@/types'; import type { AccelerationOption, AccelerationSupport } from '@/types';
import type { CPUCapabilities, GPUDevice } from '@/types/hardware';
export interface Warning { export interface Warning {
type: 'warning' | 'info'; type: 'warning' | 'info';
@ -14,11 +14,7 @@ interface UseWarningsProps {
configLoaded?: boolean; configLoaded?: boolean;
} }
const checkModelWarnings = ( const checkModelWarnings = (model: string, sdmodel: string, configLoaded: boolean) => {
model: string,
sdmodel: string,
configLoaded: boolean
) => {
const hasTextModel = model?.trim() !== ''; const hasTextModel = model?.trim() !== '';
const hasImageModel = sdmodel.trim() !== ''; const hasImageModel = sdmodel.trim() !== '';
const showModelPriorityWarning = hasTextModel && hasImageModel; const showModelPriorityWarning = hasTextModel && hasImageModel;
@ -37,8 +33,7 @@ const checkModelWarnings = (
if (showNoModelWarning) { if (showNoModelWarning) {
warnings.push({ warnings.push({
type: 'info', type: 'info',
message: message: 'Select a model in the General or Image Generation tab to enable launch.',
'Select a model in the General or Image Generation tab to enable launch.',
}); });
} }
@ -64,11 +59,7 @@ const checkGpuWarnings = async (
) => { ) => {
const warnings: Warning[] = []; const warnings: Warning[] = [];
if ( if (accelerationSupport.cuda && gpuCapabilities.cuda.devices.length === 0 && gpuInfo.hasNVIDIA) {
accelerationSupport.cuda &&
gpuCapabilities.cuda.devices.length === 0 &&
gpuInfo.hasNVIDIA
) {
warnings.push({ warnings.push({
type: 'warning', type: 'warning',
message: message:
@ -76,11 +67,7 @@ const checkGpuWarnings = async (
}); });
} }
if ( if (accelerationSupport.rocm && gpuCapabilities.rocm.devices.length === 0 && gpuInfo.hasAMD) {
accelerationSupport.rocm &&
gpuCapabilities.rocm.devices.length === 0 &&
gpuInfo.hasAMD
) {
const platform = await window.electronAPI.kobold.getPlatform(); const platform = await window.electronAPI.kobold.getPlatform();
const baseMessage = const baseMessage =
'Your binary supports ROCm and you have an AMD GPU, but ROCm runtime is not detected on your system.'; 'Your binary supports ROCm and you have an AMD GPU, but ROCm runtime is not detected on your system.';
@ -102,9 +89,7 @@ const checkGpuWarnings = async (
const checkVramWarnings = async (acceleration: string): Promise<Warning[]> => { const checkVramWarnings = async (acceleration: string): Promise<Warning[]> => {
const warnings: Warning[] = []; const warnings: Warning[] = [];
const isGpuAcceleration = ['cuda', 'rocm', 'vulkan', 'clblast'].includes( const isGpuAcceleration = ['cuda', 'rocm', 'vulkan', 'clblast'].includes(acceleration);
acceleration
);
if (isGpuAcceleration) { if (isGpuAcceleration) {
const gpuMemoryInfo = await window.electronAPI.kobold.detectGPUMemory(); const gpuMemoryInfo = await window.electronAPI.kobold.detectGPUMemory();
@ -115,12 +100,12 @@ const checkVramWarnings = async (acceleration: string): Promise<Warning[]> => {
(gpu) => gpu.totalMemoryGB !== null && gpu.totalMemoryGB !== '' (gpu) => gpu.totalMemoryGB !== null && gpu.totalMemoryGB !== ''
); );
const lowVramGpus = validGpus.filter( const lowVramGpus = validGpus.filter(
(gpu) => parseFloat(gpu.totalMemoryGB!) < lowVramThreshold (gpu) => parseFloat(gpu.totalMemoryGB ?? '0') < lowVramThreshold
); );
if (validGpus.length > 0 && lowVramGpus.length === validGpus.length) { if (validGpus.length > 0 && lowVramGpus.length === validGpus.length) {
const memoryDetails = lowVramGpus const memoryDetails = lowVramGpus
.map((gpu) => `${parseFloat(gpu.totalMemoryGB!).toFixed(1)}GB`) .map((gpu) => `${parseFloat(gpu.totalMemoryGB ?? '0').toFixed(1)}GB`)
.join(', '); .join(', ');
warnings.push({ warnings.push({
@ -134,20 +119,14 @@ const checkVramWarnings = async (acceleration: string): Promise<Warning[]> => {
return warnings; return warnings;
}; };
const checkCpuWarnings = ( const checkCpuWarnings = (acceleration: string, availableAccelerations: AccelerationOption[]) => {
acceleration: string,
availableAccelerations: AccelerationOption[]
) => {
const warnings: Warning[] = []; const warnings: Warning[] = [];
if (acceleration !== 'cpu') { if (acceleration !== 'cpu') {
return warnings; return warnings;
} }
if ( if (availableAccelerations.length > 0 && availableAccelerations.some((a) => a.value === 'cpu')) {
availableAccelerations.length > 0 &&
availableAccelerations.some((a) => a.value === 'cpu')
) {
warnings.push({ warnings.push({
type: 'info', type: 'info',
message: message:
@ -175,11 +154,7 @@ const checkBackendWarnings = async (params?: {
return warnings; return warnings;
} }
const gpuWarnings = await checkGpuWarnings( const gpuWarnings = await checkGpuWarnings(accelerationSupport, gpuCapabilities, gpuInfo);
accelerationSupport,
gpuCapabilities,
gpuInfo
);
warnings.push(...gpuWarnings); warnings.push(...gpuWarnings);
if (params) { if (params) {
@ -189,10 +164,7 @@ const checkBackendWarnings = async (params?: {
warnings.push(...vramWarnings); warnings.push(...vramWarnings);
if (cpuCapabilities) { if (cpuCapabilities) {
const cpuWarnings = checkCpuWarnings( const cpuWarnings = checkCpuWarnings(acceleration, availableAccelerations);
acceleration,
availableAccelerations
);
warnings.push(...cpuWarnings); warnings.push(...cpuWarnings);
} }
} }
@ -234,7 +206,6 @@ export const useWarnings = ({
}, [acceleration]); }, [acceleration]);
useEffect(() => { useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
void updateBackendWarnings(); void updateBackendWarnings();
}, [updateBackendWarnings]); }, [updateBackendWarnings]);

View file

@ -1,11 +1,14 @@
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 { MantineProvider } from '@mantine/core';
import { App } from '@/components/App'; import { App } from '@/components/App';
import { theme, cssVariablesResolver } from '@/theme'; import { cssVariablesResolver, theme } from '@/theme';
import '@/styles/index.css'; import '@/styles/index.css';
createRoot(document.getElementById('root')!).render( const rootElement = document.getElementById('root');
if (!rootElement) throw new Error('Root element not found');
createRoot(rootElement).render(
<StrictMode> <StrictMode>
<MantineProvider <MantineProvider
theme={theme} theme={theme}

View file

@ -1,10 +1,8 @@
/* eslint-disable no-console */ import { spawn } from 'node:child_process';
import { spawn } from 'child_process'; import { exit, on, platform, stderr, stdin, stdout } from 'node:process';
import { platform, exit, stdout, stderr, stdin, on } from 'process';
import { terminateProcess } from '@/utils/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';
async function getCurrentKoboldBinary() { async function getCurrentKoboldBinary() {
try { try {
@ -13,9 +11,7 @@ async function getCurrentKoboldBinary() {
return null; return null;
} }
const config = await readJsonFile<{ currentKoboldBinary?: string }>( const config = await readJsonFile<{ currentKoboldBinary?: string }>(configPath);
configPath
);
return config?.currentKoboldBinary || null; return config?.currentKoboldBinary || null;
} catch { } catch {
return null; return null;
@ -26,9 +22,7 @@ export async function handleCliMode(args: string[]) {
const currentBinary = await getCurrentKoboldBinary(); const currentBinary = await getCurrentKoboldBinary();
if (!currentBinary) { if (!currentBinary) {
console.error( console.error('Error: No binary found. Please run the GUI first to download the binary.');
'Error: No binary found. Please run the GUI first to download the binary.'
);
exit(1); exit(1);
} }

View file

@ -1,26 +1,21 @@
import { platform } from 'node:process';
import { app } from 'electron'; import { app } from 'electron';
import { platform } from 'process';
import {
createMainWindow,
cleanup as cleanupWindow,
getMainWindow,
} from '@/main/modules/window';
import {
initialize as initializeConfig,
getInstallDir,
getEnableSystemTray,
getStartMinimizedToTray,
} from '@/main/modules/config';
import { createTray } from '@/main/modules/tray';
import { safeExecute } from '@/utils/node/logging';
import { stopKoboldCpp } from '@/main/modules/koboldcpp/launcher';
import { stopFrontend as stopSillyTavern } from '@/main/modules/sillytavern';
import { stopFrontend as stopOpenWebUI } from '@/main/modules/openwebui';
import { stopStaticServer } from '@/main/modules/static-server';
import { setupIPCHandlers } from '@/main/ipc';
import { ensureDir } from '@/utils/node/fs';
import { PRODUCT_NAME } from '@/constants'; import { PRODUCT_NAME } from '@/constants';
import { setupIPCHandlers } from '@/main/ipc';
import {
getEnableSystemTray,
getInstallDir,
getStartMinimizedToTray,
initialize as initializeConfig,
} from '@/main/modules/config';
import { stopKoboldCpp } from '@/main/modules/koboldcpp/launcher';
import { stopFrontend as stopOpenWebUI } from '@/main/modules/openwebui';
import { stopFrontend as stopSillyTavern } from '@/main/modules/sillytavern';
import { stopStaticServer } from '@/main/modules/static-server';
import { createTray } from '@/main/modules/tray';
import { cleanup as cleanupWindow, createMainWindow, getMainWindow } from '@/main/modules/window';
import { ensureDir } from '@/utils/node/fs';
import { safeExecute } from '@/utils/node/logging';
export async function initializeApp(options?: { startMinimized?: boolean }) { export async function initializeApp(options?: { startMinimized?: boolean }) {
const gotTheLock = app.requestSingleInstanceLock(); const gotTheLock = app.requestSingleInstanceLock();

View file

@ -1,14 +1,12 @@
import { argv, exit } from 'process'; import { argv, exit } from 'node:process';
if (argv[1] === '--version') { if (argv[1] === '--version') {
void (async () => { void (async () => {
try { try {
const { app } = await import('electron'); const { app } = await import('electron');
const version = app.getVersion(); const version = app.getVersion();
// eslint-disable-next-line no-console
console.log(version); console.log(version);
} catch { } catch {
// eslint-disable-next-line no-console
console.log('unknown'); console.log('unknown');
} }
exit(0); exit(0);
@ -24,12 +22,10 @@ if (argv[1] === '--version') {
try { try {
await cliModule.handleCliMode(args); await cliModule.handleCliMode(args);
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console
console.error('CLI mode error:', error); console.error('CLI mode error:', error);
exit(1); exit(1);
} }
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console
console.error('Failed to load CLI module:', error); console.error('Failed to load CLI module:', error);
exit(1); exit(1);
} }
@ -37,11 +33,8 @@ if (argv[1] === '--version') {
try { try {
const guiModule = await import('./gui'); const guiModule = await import('./gui');
const startMinimized = argv.includes('--minimized'); const startMinimized = argv.includes('--minimized');
await guiModule.initializeApp( await guiModule.initializeApp(startMinimized ? { startMinimized } : undefined);
startMinimized ? { startMinimized } : undefined
);
} catch (error: unknown) { } catch (error: unknown) {
// eslint-disable-next-line no-console
console.error('Failed to initialize Gerbil:', error); console.error('Failed to initialize Gerbil:', error);
} }
} }

View file

@ -1,63 +1,30 @@
import { ipcMain, app } from 'electron'; import { join } from 'node:path';
import { join } from 'path'; import { platform } from 'node:process';
import { platform } from 'process'; import { app, ipcMain } from 'electron';
import type { Screen, Acceleration } from '@/types';
import { import {
stopKoboldCpp, canAutoUpdate,
launchKoboldCppWithCustomFrontends, checkForUpdates,
} from '@/main/modules/koboldcpp/launcher'; downloadUpdate,
isUpdateDownloaded,
quitAndInstall,
} from '@/main/modules/auto-updater';
import { import {
downloadRelease,
importLocalBackend,
} from '@/main/modules/koboldcpp/download';
import {
getInstalledBackends,
getCurrentBackend,
setCurrentBackend,
deleteRelease,
} from '@/main/modules/koboldcpp/backend';
import {
getConfigFiles,
saveConfigFile,
deleteConfigFile,
parseConfigFile,
selectModelFile,
selectInstallDirectory,
} from '@/main/modules/koboldcpp/config';
import { getLocalModelsForType } from '@/main/modules/koboldcpp/model-download';
import { analyzeGGUFModel } from '@/main/modules/koboldcpp/analyze';
import {
get as getConfig,
set as setConfig,
getSelectedConfig,
getInstallDir,
getColorScheme, getColorScheme,
get as getConfig,
getEnableSystemTray, getEnableSystemTray,
getInstallDir,
getSelectedConfig,
set as setConfig,
} from '@/main/modules/config'; } from '@/main/modules/config';
import { createTray, updateTrayState, destroyTray } from '@/main/modules/tray';
import { getConfigDir, openPathHandler, openUrl } from '@/utils/node/path';
import { logError } from '@/utils/node/logging';
import { stopFrontend as stopSillyTavernFrontend } from '@/main/modules/sillytavern';
import { stopFrontend as stopOpenWebUIFrontend } from '@/main/modules/openwebui';
import { import {
isUvAvailable,
isNpxAvailable,
getVersionInfo, getVersionInfo,
isAURInstallation, isAURInstallation,
isNpxAvailable,
isUvAvailable,
} from '@/main/modules/dependencies'; } from '@/main/modules/dependencies';
import { getMainWindow } from '@/main/modules/window';
import { import {
saveTabContent,
loadTabContent,
saveNotepadState,
loadNotepadState,
deleteTabFile,
createNewTab,
renameTab,
} from '@/main/modules/notepad';
import {
detectGPU,
detectCPU, detectCPU,
detectGPU,
detectGPUCapabilities, detectGPUCapabilities,
detectGPUMemory, detectGPUMemory,
detectROCm, detectROCm,
@ -67,18 +34,44 @@ import {
detectAccelerationSupport, detectAccelerationSupport,
getAvailableAccelerations, getAvailableAccelerations,
} from '@/main/modules/koboldcpp/acceleration'; } from '@/main/modules/koboldcpp/acceleration';
import { analyzeGGUFModel } from '@/main/modules/koboldcpp/analyze';
import { import {
openPerformanceManager, deleteRelease,
startMonitoring, getCurrentBackend,
stopMonitoring, getInstalledBackends,
} from '@/main/modules/monitoring'; setCurrentBackend,
} from '@/main/modules/koboldcpp/backend';
import { import {
checkForUpdates, deleteConfigFile,
downloadUpdate, getConfigFiles,
quitAndInstall, parseConfigFile,
isUpdateDownloaded, saveConfigFile,
canAutoUpdate, selectInstallDirectory,
} from '@/main/modules/auto-updater'; selectModelFile,
} from '@/main/modules/koboldcpp/config';
import { downloadRelease, importLocalBackend } from '@/main/modules/koboldcpp/download';
import {
launchKoboldCppWithCustomFrontends,
stopKoboldCpp,
} from '@/main/modules/koboldcpp/launcher';
import { getLocalModelsForType } from '@/main/modules/koboldcpp/model-download';
import { openPerformanceManager, startMonitoring, stopMonitoring } from '@/main/modules/monitoring';
import {
createNewTab,
deleteTabFile,
loadNotepadState,
loadTabContent,
renameTab,
saveNotepadState,
saveTabContent,
} from '@/main/modules/notepad';
import { stopFrontend as stopOpenWebUIFrontend } from '@/main/modules/openwebui';
import { stopFrontend as stopSillyTavernFrontend } from '@/main/modules/sillytavern';
import { createTray, destroyTray, updateTrayState } from '@/main/modules/tray';
import { getMainWindow } from '@/main/modules/window';
import type { Acceleration, Screen } from '@/types';
import { logError } from '@/utils/node/logging';
import { getConfigDir, openPathHandler, openUrl } from '@/utils/node/path';
import { calculateOptimalGpuLayers } from '@/utils/node/vram'; import { calculateOptimalGpuLayers } from '@/utils/node/vram';
export function setupIPCHandlers() { export function setupIPCHandlers() {
@ -98,9 +91,7 @@ export function setupIPCHandlers() {
saveConfigFile(configName, configData) saveConfigFile(configName, configData)
); );
ipcMain.handle('kobold:deleteConfigFile', async (_, configName) => ipcMain.handle('kobold:deleteConfigFile', async (_, configName) => deleteConfigFile(configName));
deleteConfigFile(configName)
);
ipcMain.handle('kobold:getSelectedConfig', () => getSelectedConfig()); ipcMain.handle('kobold:getSelectedConfig', () => getSelectedConfig());
@ -108,15 +99,11 @@ export function setupIPCHandlers() {
setConfig('selectedConfig', configName) setConfig('selectedConfig', configName)
); );
ipcMain.handle('kobold:setCurrentBackend', (_, version) => ipcMain.handle('kobold:setCurrentBackend', (_, version) => setCurrentBackend(version));
setCurrentBackend(version)
);
ipcMain.handle('kobold:getCurrentInstallDir', () => getInstallDir()); ipcMain.handle('kobold:getCurrentInstallDir', () => getInstallDir());
ipcMain.handle('kobold:selectInstallDirectory', () => ipcMain.handle('kobold:selectInstallDirectory', () => selectInstallDirectory());
selectInstallDirectory()
);
ipcMain.handle('kobold:detectGPU', () => detectGPU()); ipcMain.handle('kobold:detectGPU', () => detectGPU());
@ -130,13 +117,10 @@ export function setupIPCHandlers() {
ipcMain.handle('kobold:detectROCm', () => detectROCm()); ipcMain.handle('kobold:detectROCm', () => detectROCm());
ipcMain.handle('kobold:detectAccelerationSupport', () => ipcMain.handle('kobold:detectAccelerationSupport', () => detectAccelerationSupport());
detectAccelerationSupport()
);
ipcMain.handle( ipcMain.handle('kobold:getAvailableAccelerations', (_, includeDisabled = false) =>
'kobold:getAvailableAccelerations', getAvailableAccelerations(includeDisabled)
(_, includeDisabled = false) => getAvailableAccelerations(includeDisabled)
); );
ipcMain.handle('kobold:getPlatform', () => platform); ipcMain.handle('kobold:getPlatform', () => platform);
@ -145,9 +129,7 @@ export function setupIPCHandlers() {
launchKoboldCppWithCustomFrontends(args, preLaunchCommands) launchKoboldCppWithCustomFrontends(args, preLaunchCommands)
); );
ipcMain.handle('kobold:deleteRelease', (_, binaryPath) => ipcMain.handle('kobold:deleteRelease', (_, binaryPath) => deleteRelease(binaryPath));
deleteRelease(binaryPath)
);
ipcMain.handle('kobold:stopKoboldCpp', () => { ipcMain.handle('kobold:stopKoboldCpp', () => {
void stopKoboldCpp(); void stopKoboldCpp();
@ -155,25 +137,17 @@ export function setupIPCHandlers() {
void stopOpenWebUIFrontend(); void stopOpenWebUIFrontend();
}); });
ipcMain.handle('kobold:parseConfigFile', (_, filePath) => ipcMain.handle('kobold:parseConfigFile', (_, filePath) => parseConfigFile(filePath));
parseConfigFile(filePath)
);
ipcMain.handle('kobold:selectModelFile', (_, title) => ipcMain.handle('kobold:selectModelFile', (_, title) => selectModelFile(title));
selectModelFile(title)
);
ipcMain.handle('kobold:importLocalBackend', () => importLocalBackend()); ipcMain.handle('kobold:importLocalBackend', () => importLocalBackend());
ipcMain.handle('kobold:getLocalModels', (_, paramType: string) => ipcMain.handle('kobold:getLocalModels', (_, paramType: string) =>
getLocalModelsForType( getLocalModelsForType(paramType as Parameters<typeof getLocalModelsForType>[0])
paramType as Parameters<typeof getLocalModelsForType>[0]
)
); );
ipcMain.handle('kobold:analyzeModel', async (_, filePath: string) => ipcMain.handle('kobold:analyzeModel', async (_, filePath: string) => analyzeGGUFModel(filePath));
analyzeGGUFModel(filePath)
);
ipcMain.handle( ipcMain.handle(
'kobold:calculateOptimalLayers', 'kobold:calculateOptimalLayers',
@ -224,9 +198,7 @@ export function setupIPCHandlers() {
ipcMain.handle('app:isMaximized', () => mainWindow.isMaximized()); ipcMain.handle('app:isMaximized', () => mainWindow.isMaximized());
ipcMain.handle('app:getZoomLevel', () => ipcMain.handle('app:getZoomLevel', () => mainWindow.webContents.getZoomLevel());
mainWindow.webContents.getZoomLevel()
);
ipcMain.handle('app:setZoomLevel', (_, level) => { ipcMain.handle('app:setZoomLevel', (_, level) => {
mainWindow.webContents.setZoomLevel(level); mainWindow.webContents.setZoomLevel(level);
@ -250,9 +222,7 @@ export function setupIPCHandlers() {
} }
}); });
ipcMain.handle('app:getStartMinimizedToTray', () => ipcMain.handle('app:getStartMinimizedToTray', () => getConfig('startMinimizedToTray'));
getConfig('startMinimizedToTray')
);
ipcMain.handle('app:setStartMinimizedToTray', async (_, enabled: boolean) => { ipcMain.handle('app:setStartMinimizedToTray', async (_, enabled: boolean) => {
await setConfig('startMinimizedToTray', enabled); await setConfig('startMinimizedToTray', enabled);
@ -291,7 +261,7 @@ export function setupIPCHandlers() {
ipcMain.handle('dependencies:isUvAvailable', () => isUvAvailable()); ipcMain.handle('dependencies:isUvAvailable', () => isUvAvailable());
ipcMain.handle('dependencies:clearOpenWebUIData', async () => { ipcMain.handle('dependencies:clearOpenWebUIData', async () => {
const { rm } = await import('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, { recursive: true, force: true });
@ -318,15 +288,11 @@ export function setupIPCHandlers() {
ipcMain.handle('app:isAURInstallation', () => isAURInstallation()); ipcMain.handle('app:isAURInstallation', () => isAURInstallation());
ipcMain.handle('notepad:saveTabContent', (_, title, content) => ipcMain.handle('notepad:saveTabContent', (_, title, content) => saveTabContent(title, content));
saveTabContent(title, content)
);
ipcMain.handle('notepad:loadTabContent', (_, title) => loadTabContent(title)); ipcMain.handle('notepad:loadTabContent', (_, title) => loadTabContent(title));
ipcMain.handle('notepad:renameTab', (_, oldTitle, newTitle) => ipcMain.handle('notepad:renameTab', (_, oldTitle, newTitle) => renameTab(oldTitle, newTitle));
renameTab(oldTitle, newTitle)
);
ipcMain.handle('notepad:saveState', (_, state) => saveNotepadState(state)); ipcMain.handle('notepad:saveState', (_, state) => saveNotepadState(state));

View file

@ -1,8 +1,8 @@
import { autoUpdater } from 'electron-updater'; import { platform } from 'node:process';
import { app } from 'electron'; import { app } from 'electron';
import { platform } from 'process'; import { autoUpdater } from 'electron-updater';
import { logError, safeExecute } from '@/utils/node/logging';
import { isDevelopment } from '@/utils/node/environment'; import { isDevelopment } from '@/utils/node/environment';
import { logError, safeExecute } from '@/utils/node/logging';
import { getAURVersion, isWindowsPortableInstallation } from './dependencies'; import { getAURVersion, isWindowsPortableInstallation } from './dependencies';
export interface UpdateInfo { export interface UpdateInfo {
@ -42,10 +42,7 @@ export const checkForUpdates = async () => {
} }
return ( return (
(await safeExecute( (await safeExecute(() => autoUpdater.checkForUpdates(), 'Failed to check for updates')) !== null
() => autoUpdater.checkForUpdates(),
'Failed to check for updates'
)) !== null
); );
}; };
@ -55,10 +52,7 @@ export const downloadUpdate = async () => {
} }
return ( return (
(await safeExecute( (await safeExecute(() => autoUpdater.downloadUpdate(), 'Failed to download update')) !== null
() => autoUpdater.downloadUpdate(),
'Failed to download update'
)) !== null
); );
}; };

View file

@ -1,18 +1,18 @@
import { readJsonFile, writeJsonFile } from '@/utils/node/fs'; import { homedir } from 'node:os';
import { safeExecute } from '@/utils/node/logging'; import { join } from 'node:path';
import { getConfigDir } from '@/utils/node/path'; import { platform } from 'node:process';
import { homedir } from 'os'; import type { MantineColorScheme } from '@mantine/core';
import { join } from 'path';
import { platform } from 'process';
import { nativeTheme } from 'electron'; import { nativeTheme } from 'electron';
import { PRODUCT_NAME } from '@/constants'; import { PRODUCT_NAME } from '@/constants';
import type { import type {
FrontendPreference,
DismissedUpdate, DismissedUpdate,
FrontendPreference,
ImageGenerationFrontendPreference, ImageGenerationFrontendPreference,
} from '@/types'; } from '@/types';
import type { MantineColorScheme } from '@mantine/core';
import type { SavedNotepadState } from '@/types/electron'; import type { SavedNotepadState } from '@/types/electron';
import { readJsonFile, writeJsonFile } from '@/utils/node/fs';
import { safeExecute } from '@/utils/node/logging';
import { getConfigDir } from '@/utils/node/path';
export interface WindowBounds { export interface WindowBounds {
x?: number; x?: number;
@ -51,10 +51,7 @@ async function loadConfig() {
} }
async function saveConfig() { async function saveConfig() {
const success = await safeExecute( const success = await safeExecute(() => writeJsonFile(configPath, config), 'Error saving config');
() => writeJsonFile(configPath, config),
'Error saving config'
);
return success !== null; return success !== null;
} }
@ -65,10 +62,7 @@ export async function initialize() {
export const get = <K extends keyof AppConfig>(key: K) => config[key]; export const get = <K extends keyof AppConfig>(key: K) => config[key];
export async function set<K extends keyof AppConfig>( export async function set<K extends keyof AppConfig>(key: K, value: AppConfig[K]) {
key: K,
value: AppConfig[K]
) {
config[key] = value; config[key] = value;
await saveConfig(); await saveConfig();
} }
@ -123,5 +117,4 @@ export const getWindowBounds = () => config.windowBounds;
export const getEnableSystemTray = () => config.enableSystemTray ?? false; export const getEnableSystemTray = () => config.enableSystemTray ?? false;
export const getStartMinimizedToTray = () => export const getStartMinimizedToTray = () => config.startMinimizedToTray ?? false;
config.startMinimizedToTray ?? false;

View file

@ -1,9 +1,9 @@
import { access, readdir, readlink } from 'fs/promises'; import { access, readdir, readlink } from 'node:fs/promises';
import { homedir, release } from 'os'; import { homedir, release } from 'node:os';
import { join } from 'path'; import { join } from 'node:path';
import { platform, env as processEnv, versions, arch } from 'process'; import { arch, platform, env as processEnv, versions } from 'node:process';
import { execa } from 'execa';
import { app } from 'electron'; import { app } from 'electron';
import { execa } from 'execa';
import { PRODUCT_NAME } from '@/constants'; import { PRODUCT_NAME } from '@/constants';
const PATH_SEPARATOR = platform === 'win32' ? ';' : ':'; const PATH_SEPARATOR = platform === 'win32' ? ';' : ':';
@ -75,10 +75,7 @@ export async function isUvAvailable() {
export async function getUvEnvironment() { export async function getUvEnvironment() {
const env = { ...processEnv }; const env = { ...processEnv };
const uvPaths = [ const uvPaths = [join(homedir(), '.cargo', 'bin'), join(homedir(), '.local', 'bin')];
join(homedir(), '.cargo', 'bin'),
join(homedir(), '.local', 'bin'),
];
const existingPaths: string[] = []; const existingPaths: string[] = [];
for (const path of uvPaths) { for (const path of uvPaths) {
@ -149,10 +146,7 @@ async function findNodeBinPath(baseDir: string) {
return null; return null;
} }
async function tryVersionManagerPath( async function tryVersionManagerPath(basePath: string, env: Record<string, string | undefined>) {
basePath: string,
env: Record<string, string | undefined>
) {
if (!(await pathExists(basePath))) { if (!(await pathExists(basePath))) {
return false; return false;
} }
@ -224,9 +218,11 @@ export async function isAURInstallation() {
} }
export async function getVersionInfo() { export async function getVersionInfo() {
const [nodeJsSystemVersion, uvVersion, aurPackageVersion] = await Promise.all( const [nodeJsSystemVersion, uvVersion, aurPackageVersion] = await Promise.all([
[getSystemNodeVersion(), getUvVersion(), getAURVersion()] getSystemNodeVersion(),
); getUvVersion(),
getAURVersion(),
]);
return { return {
appVersion: app.getVersion(), appVersion: app.getVersion(),
@ -243,10 +239,7 @@ export async function getVersionInfo() {
}; };
} }
function tryAddPathToEnv( function tryAddPathToEnv(env: Record<string, string | undefined>, path: string) {
env: Record<string, string | undefined>,
path: string
) {
if (!env.PATH?.includes(path)) { if (!env.PATH?.includes(path)) {
env.PATH = `${path}${PATH_SEPARATOR}${env.PATH}`; env.PATH = `${path}${PATH_SEPARATOR}${env.PATH}`;
return true; return true;
@ -269,16 +262,12 @@ export function isWindowsPortableInstallation() {
const execPath = app.getPath('exe'); const execPath = app.getPath('exe');
const isInTemp = const isInTemp =
execPath.toLowerCase().includes('\\temp\\') || execPath.toLowerCase().includes('\\temp\\') || execPath.toLowerCase().includes('\\tmp\\');
execPath.toLowerCase().includes('\\tmp\\');
const isInProgramFiles = execPath.toLowerCase().includes('program files'); const isInProgramFiles = execPath.toLowerCase().includes('program files');
const isInAppDataPrograms = execPath const isInAppDataPrograms = execPath.toLowerCase().includes('appdata\\local\\programs');
.toLowerCase()
.includes('appdata\\local\\programs');
windowsPortableInstallationCache = windowsPortableInstallationCache = isInTemp && !isInProgramFiles && !isInAppDataPrograms;
isInTemp && !isInProgramFiles && !isInAppDataPrograms;
return windowsPortableInstallationCache; return windowsPortableInstallationCache;
} catch { } catch {

View file

@ -1,21 +1,17 @@
import { import { platform } from 'node:process';
cpu as siCpu, import { execa } from 'execa';
mem as siMem, import { cpu as siCpu, mem as siMem, memLayout as siMemLayout } from 'systeminformation';
memLayout as siMemLayout,
} from 'systeminformation';
import { safeExecute } from '@/utils/node/logging';
import { getGPUData } from '@/utils/node/gpu';
import type { import type {
BasicGPUInfo,
CPUCapabilities, CPUCapabilities,
GPUCapabilities, GPUCapabilities,
BasicGPUInfo,
GPUMemoryInfo,
GPUDevice, GPUDevice,
GPUMemoryInfo,
} from '@/types/hardware'; } from '@/types/hardware';
import { execa } from 'execa';
import { formatDeviceName } from '@/utils/format'; import { formatDeviceName } from '@/utils/format';
import { platform } from 'process'; import { getGPUData } from '@/utils/node/gpu';
import { getVulkanInfo, detectGPUViaVulkan } from '@/utils/node/vulkan'; import { safeExecute } from '@/utils/node/logging';
import { detectGPUViaVulkan, getVulkanInfo } from '@/utils/node/vulkan';
const COMMON_EXEC_OPTIONS = { const COMMON_EXEC_OPTIONS = {
timeout: 3000, timeout: 3000,
@ -65,10 +61,7 @@ export async function detectGPU() {
return basicGPUInfoCache; return basicGPUInfoCache;
} }
const result = await safeExecute( const result = await safeExecute(() => detectGPUViaVulkan(), 'GPU detection failed');
() => detectGPUViaVulkan(),
'GPU detection failed'
);
const fallbackGPUInfo = { const fallbackGPUInfo = {
hasAMD: false, hasAMD: false,
@ -172,7 +165,6 @@ async function detectCUDA() {
} }
} }
// eslint-disable-next-line sonarjs/cognitive-complexity
export async function detectROCm() { export async function detectROCm() {
try { try {
const rocminfoCommand = platform === 'win32' ? 'hipInfo' : 'rocminfo'; const rocminfoCommand = platform === 'win32' ? 'hipInfo' : 'rocminfo';
@ -188,9 +180,7 @@ export async function detectROCm() {
try { try {
if (hipccOutput.trim()) { if (hipccOutput.trim()) {
const hipVersionMatch = hipccOutput.match( const hipVersionMatch = hipccOutput.match(/HIP version:\s*(\d+\.\d+(?:\.\d+)?)/i);
/HIP version:\s*(\d+\.\d+(?:\.\d+)?)/i
);
if (hipVersionMatch) { if (hipVersionMatch) {
version = hipVersionMatch[1]; version = hipVersionMatch[1];
@ -273,11 +263,7 @@ function parseClInfoOutput(output: string) {
} }
function findDeviceNameInClInfo(lines: string[], startIndex: number) { function findDeviceNameInClInfo(lines: string[], startIndex: number) {
for ( for (let j = startIndex + 1; j < Math.min(startIndex + 50, lines.length); j++) {
let j = startIndex + 1;
j < Math.min(startIndex + 50, lines.length);
j++
) {
const nextLine = lines[j].trim(); const nextLine = lines[j].trim();
if (nextLine.includes('Device Board Name (AMD)')) { if (nextLine.includes('Device Board Name (AMD)')) {
return nextLine.split('Device Board Name (AMD)')[1]?.trim() || ''; return nextLine.split('Device Board Name (AMD)')[1]?.trim() || '';
@ -287,11 +273,7 @@ function findDeviceNameInClInfo(lines: string[], startIndex: number) {
} }
} }
for ( for (let j = startIndex + 1; j < Math.min(startIndex + 100, lines.length); j++) {
let j = startIndex + 1;
j < Math.min(startIndex + 100, lines.length);
j++
) {
const nextLine = lines[j].trim(); const nextLine = lines[j].trim();
if (nextLine.startsWith('Device Name:')) { if (nextLine.startsWith('Device Name:')) {
return nextLine.split('Device Name:')[1]?.trim() || ''; return nextLine.split('Device Name:')[1]?.trim() || '';
@ -305,11 +287,7 @@ function findDeviceNameInClInfo(lines: string[], startIndex: number) {
} }
function findComputeUnitsInClInfo(lines: string[], startIndex: number) { function findComputeUnitsInClInfo(lines: string[], startIndex: number) {
for ( for (let j = startIndex + 1; j < Math.min(startIndex + 50, lines.length); j++) {
let j = startIndex + 1;
j < Math.min(startIndex + 50, lines.length);
j++
) {
const nextLine = lines[j].trim(); const nextLine = lines[j].trim();
if (nextLine.includes('Max compute units')) { if (nextLine.includes('Max compute units')) {
const units = nextLine const units = nextLine
@ -347,9 +325,7 @@ function parseRocmOutput(output: string, vulkanInfo: { allGPUs: GPUDevice[] }) {
} }
if (currentDevice?.name) { if (currentDevice?.name) {
devices.push( devices.push(createDevice(currentDevice.name, currentDevice.isIntegrated || false));
createDevice(currentDevice.name, currentDevice.isIntegrated || false)
);
} }
return devices; return devices;
@ -362,9 +338,7 @@ function handleHipInfoLine(
) { ) {
if (trimmedLine.startsWith('device#')) { if (trimmedLine.startsWith('device#')) {
if (currentDevice?.name) { if (currentDevice?.name) {
devices.push( devices.push(createDevice(currentDevice.name, currentDevice.isIntegrated || false));
createDevice(currentDevice.name, currentDevice.isIntegrated || false)
);
} }
return true; return true;
} }
@ -408,11 +382,7 @@ function findDeviceType(lines: string[], startIndex: number) {
const searchStartIndex = Math.max(0, startIndex - searchRangeLines); const searchStartIndex = Math.max(0, startIndex - searchRangeLines);
const searchEndIndex = Math.min(lines.length, startIndex + searchRangeLines); const searchEndIndex = Math.min(lines.length, startIndex + searchRangeLines);
for ( for (let searchIndex = searchStartIndex; searchIndex < searchEndIndex; searchIndex++) {
let searchIndex = searchStartIndex;
searchIndex < searchEndIndex;
searchIndex++
) {
if (lines[searchIndex].includes('Device Type:')) { if (lines[searchIndex].includes('Device Type:')) {
return lines[searchIndex].split('Device Type:')[1]?.trim() || ''; return lines[searchIndex].split('Device Type:')[1]?.trim() || '';
} }
@ -420,10 +390,7 @@ function findDeviceType(lines: string[], startIndex: number) {
return ''; return '';
} }
function determineIfIntegrated( function determineIfIntegrated(name: string, vulkanInfo: { allGPUs: GPUDevice[] }) {
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)
@ -492,8 +459,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.clockSpeed && slot.clockSpeed > 0) || (slot.type && slot.type !== ''))
(slot.type && slot.type !== ''))
); );
if (populatedSlot) { if (populatedSlot) {
@ -501,10 +467,7 @@ export const detectSystemMemory = async () => {
populatedSlot.clockSpeed && populatedSlot.clockSpeed > 0 populatedSlot.clockSpeed && populatedSlot.clockSpeed > 0
? populatedSlot.clockSpeed ? populatedSlot.clockSpeed
: undefined; : undefined;
type = type = populatedSlot.type && populatedSlot.type !== '' ? populatedSlot.type : undefined;
populatedSlot.type && populatedSlot.type !== ''
? populatedSlot.type
: undefined;
} }
} }

View file

@ -1,10 +1,10 @@
import { join, dirname } from 'path'; import { dirname, join } from 'node:path';
import { platform } from 'process'; import { platform } from 'node:process';
import { pathExists } from '@/utils/node/fs';
import { getCurrentBinaryInfo } from './backend';
import { detectGPUCapabilities, detectCPU } from '../hardware';
import { tryExecute, safeExecute } from '@/utils/node/logging';
import type { AccelerationOption, AccelerationSupport } from '@/types'; import type { AccelerationOption, AccelerationSupport } from '@/types';
import { pathExists } from '@/utils/node/fs';
import { safeExecute, tryExecute } from '@/utils/node/logging';
import { detectCPU, detectGPUCapabilities } from '../hardware';
import { getCurrentBinaryInfo } from './backend';
const accelerationSupportCache = new Map<string, AccelerationSupport>(); const accelerationSupportCache = new Map<string, AccelerationSupport>();
const availableAccelerationsCache = new Map<string, AccelerationOption[]>(); const availableAccelerationsCache = new Map<string, AccelerationOption[]>();
@ -12,8 +12,9 @@ const availableAccelerationsCache = new Map<string, AccelerationOption[]>();
const CPU_LABEL = platform === 'darwin' ? 'Metal' : 'CPU'; const CPU_LABEL = platform === 'darwin' ? 'Metal' : 'CPU';
async function detectAccelerationSupportFromPath(koboldBinaryPath: string) { async function detectAccelerationSupportFromPath(koboldBinaryPath: string) {
if (accelerationSupportCache.has(koboldBinaryPath)) { const cached = accelerationSupportCache.get(koboldBinaryPath);
return accelerationSupportCache.get(koboldBinaryPath)!; if (cached) {
return cached;
} }
const support: AccelerationSupport = { const support: AccelerationSupport = {
@ -84,10 +85,8 @@ export async function getAvailableAccelerations(includeDisabled = false) {
return [{ value: 'cpu', label: CPU_LABEL }]; return [{ value: 'cpu', label: CPU_LABEL }];
} }
// eslint-disable-next-line sonarjs/cognitive-complexity
const result = await safeExecute(async () => { const result = await safeExecute(async () => {
const [currentBinaryInfo, hardwareCapabilities, cpuCapabilities] = const [currentBinaryInfo, hardwareCapabilities, cpuCapabilities] = await Promise.all([
await Promise.all([
getCurrentBinaryInfo(), getCurrentBinaryInfo(),
detectGPUCapabilities(), detectGPUCapabilities(),
includeDisabled ? detectCPU() : Promise.resolve(null), includeDisabled ? detectCPU() : Promise.resolve(null),
@ -99,8 +98,9 @@ export async function getAvailableAccelerations(includeDisabled = false) {
const cacheKey = `${currentBinaryInfo.path}:${includeDisabled}`; const cacheKey = `${currentBinaryInfo.path}:${includeDisabled}`;
if (availableAccelerationsCache.has(cacheKey)) { const cached = availableAccelerationsCache.get(cacheKey);
return availableAccelerationsCache.get(cacheKey)!; if (cached) {
return cached;
} }
const accelerationSupport = await detectAccelerationSupport(); const accelerationSupport = await detectAccelerationSupport();

View file

@ -1,7 +1,7 @@
import { stat } from 'node:fs/promises';
import { gguf } from '@huggingface/gguf'; import { gguf } from '@huggingface/gguf';
import { stat } from 'fs/promises';
import { logError } from '@/utils/node/logging';
import { formatBytes } from '@/utils/format'; import { formatBytes } from '@/utils/format';
import { logError } from '@/utils/node/logging';
function estimateMemoryRequirements(fileSize: number) { function estimateMemoryRequirements(fileSize: number) {
const vramOverhead = 1.1; const vramOverhead = 1.1;
@ -43,8 +43,7 @@ function getMetadataValue(metadata: Record<string, unknown>, key: string) {
export async function analyzeGGUFModel(filePath: string) { export async function analyzeGGUFModel(filePath: string) {
try { try {
const isUrl = const isUrl = filePath.startsWith('http://') || filePath.startsWith('https://');
filePath.startsWith('http://') || filePath.startsWith('https://');
let fileSize: number; let fileSize: number;
if (isUrl) { if (isUrl) {
@ -62,31 +61,22 @@ export async function analyzeGGUFModel(filePath: string) {
const metadataRecord = metadata as Record<string, unknown>; const metadataRecord = metadata as Record<string, unknown>;
const architecture = getMetadataValue( const architecture = getMetadataValue(metadataRecord, 'general.architecture') as string;
metadataRecord, const name = getMetadataValue(metadataRecord, 'general.name') as string | undefined;
'general.architecture' const paramCount = getMetadataValue(metadataRecord, 'general.parameter_count') as
) as string; | number
const name = getMetadataValue(metadataRecord, 'general.name') as
| string
| undefined; | undefined;
const paramCount = getMetadataValue(
metadataRecord,
'general.parameter_count'
) as number | undefined;
const contextLength = getMetadataValue( const contextLength = getMetadataValue(metadataRecord, `${architecture}.context_length`) as
metadataRecord, | number
`${architecture}.context_length` | undefined;
) as number | undefined;
const blockCount = getMetadataValue( const blockCount = getMetadataValue(metadataRecord, `${architecture}.block_count`) as
metadataRecord, | number
`${architecture}.block_count` | undefined;
) as number | undefined; const expertCount = getMetadataValue(metadataRecord, `${architecture}.expert_count`) as
const expertCount = getMetadataValue( | number
metadataRecord, | undefined;
`${architecture}.expert_count`
) as number | undefined;
const memoryEstimates = estimateMemoryRequirements(fileSize); const memoryEstimates = estimateMemoryRequirements(fileSize);
const vramPerLayer = estimateVramPerLayer(fileSize, blockCount); const vramPerLayer = estimateVramPerLayer(fileSize, blockCount);

View file

@ -1,22 +1,14 @@
import { readdir, stat, rm } from 'fs/promises'; import { readdir, rm, stat } from 'node:fs/promises';
import { join } from 'path'; import { join } from 'node:path';
import { execa } from 'execa'; import { execa } from 'execa';
import type { InstalledBackend } from '@/types/electron';
import {
getCurrentKoboldBinary,
setCurrentKoboldBinary,
getInstallDir,
} from '../config';
import { sendToRenderer } from '../window';
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 type { InstalledBackend } from '@/types/electron'; import { getCurrentKoboldBinary, getInstallDir, setCurrentKoboldBinary } from '../config';
import { sendToRenderer } from '../window';
const backendVersionCache = new Map< const backendVersionCache = new Map<string, { version: string; actualVersion?: string } | null>();
string,
{ version: string; actualVersion?: string } | null
>();
export function clearBackendVersionCache(path?: string) { export function clearBackendVersionCache(path?: string) {
if (path) { if (path) {
@ -70,18 +62,13 @@ export async function getInstalledBackends() {
actualVersion: versionInfo.actualVersion, actualVersion: versionInfo.actualVersion,
} as InstalledBackend; } as InstalledBackend;
} catch (error) { } catch (error) {
logError( logError(`Could not detect version for ${launcher.filename}:`, error as Error);
`Could not detect version for ${launcher.filename}:`,
error as Error
);
return null; return null;
} }
}); });
const results = await Promise.all(versionPromises); const results = await Promise.all(versionPromises);
return results.filter( return results.filter((version): version is InstalledBackend => version !== null);
(version): version is InstalledBackend => version !== null
);
} catch (error) { } catch (error) {
logError('Error scanning install directory:', error as Error); logError('Error scanning install directory:', error as Error);
return []; return [];
@ -93,9 +80,7 @@ export async function getCurrentBackend() {
const backends = await getInstalledBackends(); const backends = await getInstalledBackends();
if (currentBinaryPath && (await pathExists(currentBinaryPath))) { if (currentBinaryPath && (await pathExists(currentBinaryPath))) {
const currentBackend = backends.find( const currentBackend = backends.find((b: InstalledBackend) => b.path === currentBinaryPath);
(b: InstalledBackend) => b.path === currentBinaryPath
);
if (currentBackend) { if (currentBackend) {
return currentBackend; return currentBackend;
} }

View file

@ -1,13 +1,12 @@
import { join } from 'path'; import { readdir, stat, unlink } from 'node:fs/promises';
import { readdir, stat, unlink } from 'fs/promises'; import { join } from 'node:path';
import { dialog } from 'electron'; import { dialog } from 'electron';
import { getInstallDir, setInstallDir } from '../config';
import { logError, safeExecute, tryExecute } from '@/utils/node/logging';
import { pathExists, readJsonFile, writeJsonFile } from '@/utils/node/fs';
import { getMainWindow, sendToRenderer } from '../window';
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 { logError, safeExecute, tryExecute } from '@/utils/node/logging';
import { getInstallDir, setInstallDir } from '../config';
import { getMainWindow, sendToRenderer } from '../window';
export async function getConfigFiles() { export async function getConfigFiles() {
const configFiles: { name: string; path: string; size: number }[] = []; const configFiles: { name: string; path: string; size: number }[] = [];
@ -23,9 +22,7 @@ export async function getConfigFiles() {
const stats = await stat(filePath); const stats = await stat(filePath);
if ( if (
stats.isFile() && stats.isFile() &&
(file.endsWith('.kcpps') || (file.endsWith('.kcpps') || file.endsWith('.kcppt') || file.endsWith('.json'))
file.endsWith('.kcppt') ||
file.endsWith('.json'))
) { ) {
configFiles.push({ configFiles.push({
name: file, name: file,
@ -53,10 +50,7 @@ export async function parseConfigFile(filePath: string) {
}, 'Error parsing config file'); }, 'Error parsing config file');
} }
export async function saveConfigFile( export async function saveConfigFile(configFileName: string, configData: KoboldConfig) {
configFileName: string,
configData: KoboldConfig
) {
return tryExecute(async () => { return tryExecute(async () => {
const installDir = getInstallDir(); const installDir = getInstallDir();
const configPath = join(installDir, configFileName); const configPath = join(installDir, configFileName);

View file

@ -1,40 +1,28 @@
import { createWriteStream } from 'fs'; import { createWriteStream } from 'node:fs';
import { join, basename } from 'path'; import { chmod, copyFile, mkdir, rename, rm, unlink } from 'node:fs/promises';
import { platform } from 'process'; import { basename, join } from 'node:path';
import { rm, unlink, rename, mkdir, chmod, copyFile } from 'fs/promises'; import { platform } from 'node:process';
import { execa } from 'execa';
import { dialog } from 'electron'; import { dialog } from 'electron';
import { execa } from 'execa';
import {
getInstallDir,
getCurrentKoboldBinary,
setCurrentKoboldBinary,
} from '../config';
import { logError } from '@/utils/node/logging';
import { getMainWindow, sendToRenderer } from '../window';
import { pathExists } from '@/utils/node/fs';
import { stripAssetExtensions } from '@/utils/version';
import { getLauncherPath } from '@/utils/node/path';
import type { DownloadReleaseOptions, GitHubAsset } from '@/types/electron'; import type { DownloadReleaseOptions, GitHubAsset } from '@/types/electron';
import { pathExists } from '@/utils/node/fs';
import { logError } from '@/utils/node/logging';
import { getLauncherPath } from '@/utils/node/path';
import { stripAssetExtensions } from '@/utils/version';
import { getCurrentKoboldBinary, getInstallDir, setCurrentKoboldBinary } from '../config';
import { getMainWindow, sendToRenderer } from '../window';
import { clearBackendVersionCache, getVersionFromBinary } from './backend'; import { clearBackendVersionCache, getVersionFromBinary } from './backend';
async function removeDirectoryWithRetry( async function removeDirectoryWithRetry(dirPath: string, maxRetries = 3, currentRetry = 0) {
dirPath: string,
maxRetries = 3,
currentRetry = 0
) {
try { try {
await rm(dirPath, { recursive: true, force: true }); await rm(dirPath, { recursive: true, force: true });
} catch (error) { } catch (error) {
if (currentRetry < maxRetries) { if (currentRetry < maxRetries) {
const delay = Math.pow(2, currentRetry) * 1000; const delay = 2 ** currentRetry * 1000;
await new Promise((resolve) => setTimeout(resolve, delay)); await new Promise((resolve) => setTimeout(resolve, delay));
return removeDirectoryWithRetry(dirPath, maxRetries, currentRetry + 1); return removeDirectoryWithRetry(dirPath, maxRetries, currentRetry + 1);
} else { } else {
logError( logError(`Failed to remove directory after ${maxRetries} retries:`, error as Error);
`Failed to remove directory after ${maxRetries} retries:`,
error as Error
);
} }
} }
} }
@ -51,10 +39,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( const totalBytes = parseInt(response.headers.get('content-length') || '0', 10);
response.headers.get('content-length') || '0',
10
);
const reader = response.body.getReader(); const reader = response.body.getReader();
const pump = async () => { const pump = async () => {
@ -102,10 +87,7 @@ async function downloadFile(asset: GitHubAsset, tempPackedFilePath: string) {
}); });
} }
async function setupLauncher( async function setupLauncher(tempPackedFilePath: string, unpackedDirPath: string) {
tempPackedFilePath: string,
unpackedDirPath: string
) {
let launcherPath = await getLauncherPath(unpackedDirPath); let launcherPath = await getLauncherPath(unpackedDirPath);
if (!launcherPath || !(await pathExists(launcherPath))) { if (!launcherPath || !(await pathExists(launcherPath))) {
@ -149,8 +131,7 @@ async function unpackKoboldCpp(packedPath: string, unpackDir: string) {
stdout?: string; stdout?: string;
message: string; message: string;
}; };
const errorMessage = const errorMessage = execaError.stderr || execaError.stdout || execaError.message;
execaError.stderr || execaError.stdout || execaError.message;
throw new Error(`Unpack failed: ${errorMessage}`); throw new Error(`Unpack failed: ${errorMessage}`);
} }
} }
@ -208,15 +189,10 @@ async function installBackend({
return launcherPath; return launcherPath;
} }
export async function downloadRelease( export async function downloadRelease(asset: GitHubAsset, options: DownloadReleaseOptions) {
asset: GitHubAsset,
options: DownloadReleaseOptions
) {
const tempPackedFilePath = join(getInstallDir(), `${asset.name}.packed`); const tempPackedFilePath = join(getInstallDir(), `${asset.name}.packed`);
const baseFilename = stripAssetExtensions(asset.name); const baseFilename = stripAssetExtensions(asset.name);
const folderName = asset.version const folderName = asset.version ? `${baseFilename}-${asset.version}` : baseFilename;
? `${baseFilename}-${asset.version}`
: baseFilename;
const unpackedDirPath = join(getInstallDir(), folderName); const unpackedDirPath = join(getInstallDir(), folderName);
try { try {
@ -264,8 +240,7 @@ export async function importLocalBackend() {
if (!backendVersion || backendVersion.version === 'unknown') { if (!backendVersion || backendVersion.version === 'unknown') {
return { return {
success: false, success: false,
error: error: 'Invalid backend executable. Could not determine version information.',
'Invalid backend executable. Could not determine version information.',
}; };
} }

View file

@ -1,35 +1,25 @@
import { spawn, ChildProcess } from 'child_process'; import { type ChildProcess, spawn } from 'node:child_process';
import { platform } from 'process'; import { platform } from 'node:process';
import { terminateProcess } from '@/utils/node/process';
import { logError, safeExecute } from '@/utils/node/logging';
import { sendKoboldOutput, sendToRenderer } from '@/main/modules/window';
import { SERVER_READY_SIGNALS } from '@/constants'; import { SERVER_READY_SIGNALS } from '@/constants';
import type { KoboldCrashInfo } from '@/types/ipc'; import { get as getConfig, getCurrentKoboldBinary, getInstallDir } from '@/main/modules/config';
import { pathExists } from '@/utils/node/fs';
import { parseKoboldConfig } from '@/utils/node/kobold';
import { getCurrentBackend } from '../backend';
import {
getCurrentKoboldBinary,
get as getConfig,
getInstallDir,
} from '@/main/modules/config';
import { startFrontend as startSillyTavernFrontend } from '@/main/modules/sillytavern';
import { startFrontend as startOpenWebUIFrontend } from '@/main/modules/openwebui'; import { startFrontend as startOpenWebUIFrontend } from '@/main/modules/openwebui';
import { import { startFrontend as startSillyTavernFrontend } from '@/main/modules/sillytavern';
patchKliteEmbd, import { sendKoboldOutput, sendToRenderer } from '@/main/modules/window';
patchKcppSduiEmbd,
patchLcppGzEmbd,
filterSpam,
} from './patches';
import { startProxy, stopProxy } from '../proxy';
import { startTunnel, stopTunnel } from '../tunnel';
import { resolveModelPath, abortActiveDownloads } from '../model-download';
import type { import type {
FrontendPreference, FrontendPreference,
ImageGenerationFrontendPreference, ImageGenerationFrontendPreference,
ModelParamType, ModelParamType,
} from '@/types'; } from '@/types';
import type { KoboldCrashInfo } from '@/types/ipc';
import { pathExists } from '@/utils/node/fs';
import { parseKoboldConfig } from '@/utils/node/kobold';
import { logError, safeExecute } from '@/utils/node/logging';
import { terminateProcess } from '@/utils/node/process';
import { getCurrentBackend } from '../backend';
import { abortActiveDownloads, resolveModelPath } from '../model-download';
import { startProxy, stopProxy } from '../proxy';
import { startTunnel, stopTunnel } from '../tunnel';
import { filterSpam, patchKcppSduiEmbd, patchKliteEmbd, patchLcppGzEmbd } from './patches';
let koboldProcess: ChildProcess | null = null; let koboldProcess: ChildProcess | null = null;
let isIntentionalStop = false; let isIntentionalStop = false;
@ -73,9 +63,7 @@ function spawnPreLaunchCommands(commands: string[]) {
if (code !== 0 && code !== null) { if (code !== 0 && code !== null) {
sendKoboldOutput(`Command "${command}" exited with code ${code}\n`); sendKoboldOutput(`Command "${command}" exited with code ${code}\n`);
} else if (signal) { } else if (signal) {
sendKoboldOutput( sendKoboldOutput(`Command "${command}" terminated with signal ${signal}\n`);
`Command "${command}" terminated with signal ${signal}\n`
);
} }
}); });
} catch (error) { } catch (error) {
@ -87,9 +75,7 @@ function spawnPreLaunchCommands(commands: string[]) {
} }
async function stopPreLaunchProcesses() { async function stopPreLaunchProcesses() {
const terminations = Array.from(preLaunchProcesses).map((process) => const terminations = Array.from(preLaunchProcesses).map((process) => terminateProcess(process));
terminateProcess(process)
);
await Promise.all(terminations); await Promise.all(terminations);
preLaunchProcesses.clear(); preLaunchProcesses.clear();
@ -126,8 +112,7 @@ async function resolveModelPaths(args: string[]) {
resolvedArgs.push(resolvedPath); resolvedArgs.push(resolvedPath);
i++; i++;
} catch (error) { } catch (error) {
const errorMessage = const errorMessage = error instanceof Error ? error.message : String(error);
error instanceof Error ? error.message : String(error);
if (errorMessage.includes('aborted')) { if (errorMessage.includes('aborted')) {
throw error; throw error;
} }
@ -225,9 +210,8 @@ export async function launchKoboldCpp(
void startTunnel(frontendPreference); void startTunnel(frontendPreference);
} }
let readyResolve: let readyResolve: ((value: { success: boolean; pid?: number; error?: string }) => void) | null =
| ((value: { success: boolean; pid?: number; error?: string }) => void) null;
| null = null;
let readyReject: ((error: Error) => void) | null = null; let readyReject: ((error: Error) => void) | null = null;
let isReady = false; let isReady = false;
@ -275,7 +259,7 @@ export async function launchKoboldCpp(
const displayMessage = signal const displayMessage = signal
? `\nProcess terminated with signal ${signal}` ? `\nProcess terminated with signal ${signal}`
: code === 0 : code === 0
? `\nProcess exited successfully` ? '\nProcess exited successfully'
: code && (code > 1 || code < 0) : code && (code > 1 || code < 0)
? `\nProcess exited with code ${code}` ? `\nProcess exited with code ${code}`
: `\nProcess exited with code ${code}`; : `\nProcess exited with code ${code}`;
@ -297,9 +281,7 @@ export async function launchKoboldCpp(
if (!isReady) { if (!isReady) {
readyReject?.( readyReject?.(
new Error( new Error(`Process exited before ready signal (code: ${code}, signal: ${signal})`)
`Process exited before ready signal (code: ${code}, signal: ${signal})`
)
); );
} }
}); });
@ -347,9 +329,7 @@ export const launchKoboldCppWithCustomFrontends = async (
) => ) =>
safeExecute(async () => { safeExecute(async () => {
const frontendPreference = getConfig('frontendPreference'); const frontendPreference = getConfig('frontendPreference');
const imageGenerationFrontendPreference = getConfig( const imageGenerationFrontendPreference = getConfig('imageGenerationFrontendPreference');
'imageGenerationFrontendPreference'
);
const { isTextMode } = parseKoboldConfig(args); const { isTextMode } = parseKoboldConfig(args);

View file

@ -1,8 +1,7 @@
import { readFile, writeFile, copyFile } from 'fs/promises'; import { copyFile, readFile, writeFile } from 'node:fs/promises';
import { join } from 'path'; import { join } from 'node:path';
import { tryExecute } from '@/utils/node/logging';
import { pathExists } from '@/utils/node/fs'; import { pathExists } from '@/utils/node/fs';
import { tryExecute } from '@/utils/node/logging';
import { getAssetPath } from '@/utils/node/path'; import { getAssetPath } from '@/utils/node/path';
const KLITE_CSS_OVERRIDE = ` const KLITE_CSS_OVERRIDE = `
@ -86,10 +85,7 @@ export const patchKliteEmbd = (unpackedDir: string) =>
); );
} }
patchedContent = patchedContent.replace( patchedContent = patchedContent.replace('</head>', `${KLITE_CSS_OVERRIDE}\n</head>`);
'</head>',
`${KLITE_CSS_OVERRIDE}\n</head>`
);
await writeFile(kliteEmbdPath, patchedContent, 'utf8'); await writeFile(kliteEmbdPath, patchedContent, 'utf8');
} }

View file

@ -1,14 +1,14 @@
import { join, basename, dirname } from 'path'; import { createWriteStream } from 'node:fs';
import { mkdir, readdir, stat, rename, unlink } from 'fs/promises'; import { mkdir, readdir, rename, stat, unlink } from 'node:fs/promises';
import { createWriteStream } from 'fs'; import type { IncomingMessage } from 'node:http';
import { get as httpGet } from 'http'; import { get as httpGet } from 'node:http';
import { get as httpsGet } from 'https'; import { get as httpsGet } from 'node:https';
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 type { CachedModel, ModelParamType } from '@/types';
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 { sendKoboldOutput, sendToRenderer } from '@/main/modules/window';
import type { ModelParamType, CachedModel } from '@/types';
import type { IncomingMessage } from 'http';
const activeDownloads = new Set<{ const activeDownloads = new Set<{
abort: () => void; abort: () => void;
@ -27,9 +27,7 @@ interface DownloadProgress {
} }
function parseHuggingFaceUrl(url: string) { function parseHuggingFaceUrl(url: string) {
const hfMatch = url.match( const hfMatch = url.match(/huggingface\.co\/([^/]+)\/([^/]+)\/(?:resolve|blob)\/[^/]+\/(.+)/);
/huggingface\.co\/([^/]+)\/([^/]+)\/(?:resolve|blob)\/[^/]+\/(.+)/
);
if (hfMatch) { if (hfMatch) {
const pathWithQuery = hfMatch[3]; const pathWithQuery = hfMatch[3];
@ -131,13 +129,11 @@ async function downloadFile(
if (response.statusCode !== 200) { if (response.statusCode !== 200) {
activeDownloads.delete(abortController); activeDownloads.delete(abortController);
reject( reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`)
);
return; return;
} }
const totalBytes = parseInt(response.headers['content-length'] || '0'); 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;
@ -154,23 +150,17 @@ async function downloadFile(
if (timeDiff >= 0.5) { if (timeDiff >= 0.5) {
const bytesDiff = downloadedBytes - lastReportedBytes; const bytesDiff = downloadedBytes - lastReportedBytes;
const speedBytesPerSec = bytesDiff / timeDiff; const speedBytesPerSec = bytesDiff / timeDiff;
const percent = totalBytes const percent = totalBytes ? Math.round((downloadedBytes / totalBytes) * 100) : 0;
? Math.round((downloadedBytes / totalBytes) * 100)
: 0;
const downloadedMB = (downloadedBytes / 1024 / 1024).toFixed(2); const downloadedMB = (downloadedBytes / 1024 / 1024).toFixed(2);
const totalMB = (totalBytes / 1024 / 1024).toFixed(2); const totalMB = (totalBytes / 1024 / 1024).toFixed(2);
const speedMBPerSec = (speedBytesPerSec / 1024 / 1024).toFixed(2); const speedMBPerSec = (speedBytesPerSec / 1024 / 1024).toFixed(2);
const remainingBytes = totalBytes - downloadedBytes; const remainingBytes = totalBytes - downloadedBytes;
const etaSeconds = speedBytesPerSec const etaSeconds = speedBytesPerSec ? Math.round(remainingBytes / speedBytesPerSec) : 0;
? Math.round(remainingBytes / speedBytesPerSec)
: 0;
const etaMinutes = Math.floor(etaSeconds / 60); const etaMinutes = Math.floor(etaSeconds / 60);
const etaSecondsRemainder = etaSeconds % 60; const etaSecondsRemainder = etaSeconds % 60;
const etaStr = const etaStr =
etaMinutes > 0 etaMinutes > 0 ? `${etaMinutes}m${etaSecondsRemainder}s` : `${etaSeconds}s`;
? `${etaMinutes}m${etaSecondsRemainder}s`
: `${etaSeconds}s`;
const progressMsg = totalBytes const progressMsg = totalBytes
? `Downloaded ${downloadedMB}MB / ${totalMB}MB (${percent}%) - ${speedMBPerSec}MB/s - ETA: ${etaStr}` ? `Downloaded ${downloadedMB}MB / ${totalMB}MB (${percent}%) - ${speedMBPerSec}MB/s - ETA: ${etaStr}`
@ -366,6 +356,8 @@ export async function getLocalModelsForType(paramType: ModelParamType) {
export function abortActiveDownloads() { export function abortActiveDownloads() {
const downloads = Array.from(activeDownloads); const downloads = Array.from(activeDownloads);
downloads.forEach((controller) => controller.abort()); for (const controller of downloads) {
controller.abort();
}
activeDownloads.clear(); activeDownloads.clear();
} }

View file

@ -1,9 +1,8 @@
import http from 'http'; import type { IncomingMessage, ServerResponse } from 'node:http';
import type { IncomingMessage, ServerResponse } from 'http'; import http from 'node:http';
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';
import { PROXY } from '@/constants/proxy';
let proxyServer: http.Server | null = null; let proxyServer: http.Server | null = null;
let koboldCppHost = 'localhost'; let koboldCppHost = 'localhost';
@ -17,10 +16,7 @@ const replaceKoboldWithGerbil = (data: string) => {
} }
}; };
const proxyRequest = ( const proxyRequest = (clientReq: IncomingMessage, clientRes: ServerResponse) => {
clientReq: IncomingMessage,
clientRes: ServerResponse
) => {
const options = { const options = {
hostname: koboldCppHost, hostname: koboldCppHost,
port: koboldCppPort, port: koboldCppPort,
@ -30,8 +26,7 @@ const proxyRequest = (
}; };
const proxyReq = http.request(options, (proxyRes) => { const proxyReq = http.request(options, (proxyRes) => {
const isJson = const isJson = proxyRes.headers['content-type']?.includes('application/json');
proxyRes.headers['content-type']?.includes('application/json');
if (isJson) { if (isJson) {
let body = ''; let body = '';

View file

@ -1,17 +1,16 @@
import { createWriteStream } from 'fs'; import { createWriteStream } from 'node:fs';
import { chmod, access } from 'fs/promises'; import { access, chmod } from 'node:fs/promises';
import path from 'path'; import path from 'node:path';
import { platform, arch } from 'process'; import { arch, platform } from 'node:process';
import { pipeline } from 'stream/promises'; import { Readable } from 'node:stream';
import { Readable } from 'stream'; import { pipeline } from 'node:stream/promises';
import { execa, type ResultPromise } from 'execa'; import { execa, type ResultPromise } from 'execa';
import { GITHUB_API, OPENWEBUI, SILLYTAVERN } from '@/constants';
import { logError } from '@/utils/node/logging';
import { sendKoboldOutput, sendToRenderer } from '../window';
import { PROXY } from '@/constants/proxy'; import { PROXY } from '@/constants/proxy';
import { SILLYTAVERN, OPENWEBUI, GITHUB_API } from '@/constants';
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 { sendKoboldOutput, sendToRenderer } from '../window';
let activeTunnel: ResultPromise | null = null; let activeTunnel: ResultPromise | null = null;
let tunnelUrl: string | null = null; let tunnelUrl: string | null = null;
@ -36,16 +35,11 @@ const getCloudflaredAssetName = () => {
const getCloudflaredDownloadUrl = async () => { const getCloudflaredDownloadUrl = async () => {
const response = await fetch(GITHUB_API.CLOUDFLARED_LATEST_RELEASE_URL); const response = await fetch(GITHUB_API.CLOUDFLARED_LATEST_RELEASE_URL);
if (!response.ok) { if (!response.ok) {
throw new Error( throw new Error(`Failed to fetch latest cloudflared release: ${response.statusText}`);
`Failed to fetch latest cloudflared release: ${response.statusText}`
);
} }
const release = (await response.json()) as { tag_name: string }; const release = (await response.json()) as { tag_name: string };
return GITHUB_API.getCloudflaredDownloadUrl( return GITHUB_API.getCloudflaredDownloadUrl(release.tag_name, getCloudflaredAssetName());
release.tag_name,
getCloudflaredAssetName()
);
}; };
const downloadCloudflared = async (binPath: string) => { const downloadCloudflared = async (binPath: string) => {
@ -57,11 +51,8 @@ const downloadCloudflared = async (binPath: string) => {
throw new Error(`Failed to download cloudflared: ${response.statusText}`); throw new Error(`Failed to download cloudflared: ${response.statusText}`);
} }
await pipeline( // biome-ignore lint/suspicious/noExplicitAny: Node.js stream types are incompatible with web streams
// eslint-disable-next-line @typescript-eslint/no-explicit-any await pipeline(Readable.fromWeb(response.body as any), createWriteStream(binPath));
Readable.fromWeb(response.body as any),
createWriteStream(binPath)
);
if (platform !== 'win32') { if (platform !== 'win32') {
await chmod(binPath, 0o755); await chmod(binPath, 0o755);
@ -100,9 +91,7 @@ const waitForBackend = async (url: string, timeoutMs = 30000) => {
return false; return false;
}; };
export const startTunnel = async ( export const startTunnel = async (frontendPreference: FrontendPreference = 'koboldcpp') => {
frontendPreference: FrontendPreference = 'koboldcpp'
) => {
if (activeTunnel) { if (activeTunnel) {
return tunnelUrl; return tunnelUrl;
} }
@ -131,12 +120,7 @@ export const startTunnel = async (
await downloadCloudflared(bin); await downloadCloudflared(bin);
} }
const tunnel = execa(bin, [ const tunnel = execa(bin, ['tunnel', '--url', tunnelTarget, '--no-autoupdate']);
'tunnel',
'--url',
tunnelTarget,
'--no-autoupdate',
]);
activeTunnel = tunnel; activeTunnel = tunnel;
@ -204,9 +188,7 @@ export const startTunnel = async (
}); });
tunnel.on('exit', (code: number, signal: string) => { tunnel.on('exit', (code: number, signal: string) => {
sendKoboldOutput( sendKoboldOutput(`Tunnel process exited (code: ${code}, signal: ${signal})`);
`Tunnel process exited (code: ${code}, signal: ${signal})`
);
activeTunnel = null; activeTunnel = null;
tunnelUrl = null; tunnelUrl = null;
sendToRenderer('tunnel-url-changed', null); sendToRenderer('tunnel-url-changed', null);

View file

@ -1,10 +1,10 @@
import { mem, cpuTemperature, currentLoad } from 'systeminformation'; import { spawn } from 'node:child_process';
import { BrowserWindow } from 'electron'; import { platform } from 'node:process';
import { platform } from 'process'; import type { BrowserWindow } from 'electron';
import { spawn } from 'child_process'; import { cpuTemperature, currentLoad, mem } from 'systeminformation';
import { getGPUData } from '@/utils/node/gpu'; import { getGPUData } from '@/utils/node/gpu';
import { safeExecute, tryExecute } from '@/utils/node/logging';
import { detectGPU } from './hardware'; import { detectGPU } from './hardware';
import { tryExecute, safeExecute } from '@/utils/node/logging';
import { isTrayActive, updateMetrics } from './tray'; import { isTrayActive, updateMetrics } from './tray';
export interface CpuMetrics { export interface CpuMetrics {
@ -115,9 +115,7 @@ async function collectAndSendCpuMetrics() {
try { try {
const tempData = await cpuTemperature(); const tempData = await cpuTemperature();
metrics.temperature = metrics.temperature =
tempData.main && tempData.main > 0 tempData.main && tempData.main > 0 ? Math.round(tempData.main) : undefined;
? Math.round(tempData.main)
: undefined;
} catch {} } catch {}
} }
@ -244,10 +242,7 @@ export const openPerformanceManager = async () =>
(await safeExecute(async () => { (await safeExecute(async () => {
switch (platform) { switch (platform) {
case 'darwin': { case 'darwin': {
const success = await tryLaunchCommand('open', [ const success = await tryLaunchCommand('open', ['-a', 'Activity Monitor']);
'-a',
'Activity Monitor',
]);
if (success) { if (success) {
return { success: true, app: 'Activity Monitor' }; return { success: true, app: 'Activity Monitor' };
} }

View file

@ -1,12 +1,9 @@
import { get, set, getInstallDir } from './config'; import { readdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { DEFAULT_NOTEPAD_POSITION, DEFAULT_TAB_CONTENT } from '@/constants/notepad';
import type { SavedNotepadState } from '@/types/electron'; import type { SavedNotepadState } from '@/types/electron';
import {
DEFAULT_NOTEPAD_POSITION,
DEFAULT_TAB_CONTENT,
} from '@/constants/notepad';
import { pathExists } from '@/utils/node/fs'; import { pathExists } from '@/utils/node/fs';
import { join } from 'path'; import { get, getInstallDir, set } from './config';
import { readFile, readdir, writeFile, unlink, rename } from 'fs/promises';
const DEFAULT_NOTEPAD_STATE: SavedNotepadState = { const DEFAULT_NOTEPAD_STATE: SavedNotepadState = {
activeTabId: null, activeTabId: null,
@ -41,10 +38,7 @@ export const renameTab = async (oldTitle: string, newTitle: string) => {
try { try {
const notepadDir = getNotepadDir(); const notepadDir = getNotepadDir();
await rename( await rename(join(notepadDir, `${oldTitle}.txt`), join(notepadDir, `${newTitle}.txt`));
join(notepadDir, `${oldTitle}.txt`),
join(notepadDir, `${newTitle}.txt`)
);
return true; return true;
} catch { } catch {
return false; return false;

View file

@ -1,27 +1,21 @@
import { spawn } from 'child_process'; import type { ChildProcess } from 'node:child_process';
import { join } from 'path'; import { spawn } from 'node:child_process';
import { on } from 'process'; import { join } from 'node:path';
import { on } from 'node:process';
import { app } from 'electron'; import { app } from 'electron';
import type { ChildProcess } from 'child_process';
import { logError } from '@/utils/node/logging';
import { sendKoboldOutput, sendToRenderer } from './window';
import { getInstallDir } from './config';
import { OPENWEBUI, SERVER_READY_SIGNALS } from '@/constants'; import { OPENWEBUI, SERVER_READY_SIGNALS } from '@/constants';
import { terminateProcess } from '@/utils/node/process';
import { parseKoboldConfig } from '@/utils/node/kobold';
import { getUvEnvironment } from './dependencies';
import { PROXY } from '@/constants/proxy'; import { PROXY } from '@/constants/proxy';
import { parseKoboldConfig } from '@/utils/node/kobold';
import { logError } from '@/utils/node/logging';
import { terminateProcess } from '@/utils/node/process';
import { getInstallDir } from './config';
import { getUvEnvironment } from './dependencies';
import { sendKoboldOutput, sendToRenderer } from './window';
let openWebUIProcess: ChildProcess | null = null; let openWebUIProcess: ChildProcess | null = null;
const OPENWEBUI_VERSION = '0.6.41'; const OPENWEBUI_VERSION = '0.6.41';
const OPENWEBUI_BASE_ARGS = [ const OPENWEBUI_BASE_ARGS = ['--python', '3.11', `open-webui@${OPENWEBUI_VERSION}`, 'serve'];
'--python',
'3.11',
`open-webui@${OPENWEBUI_VERSION}`,
'serve',
];
on('SIGINT', () => { on('SIGINT', () => {
void stopFrontend(); void stopFrontend();
@ -76,11 +70,7 @@ export async function startFrontend(args: string[]) {
name: 'openwebui', name: 'openwebui',
port: OPENWEBUI.PORT, port: OPENWEBUI.PORT,
}; };
const { const { host: koboldHost, port: koboldPort, isImageMode } = parseKoboldConfig(args);
host: koboldHost,
port: koboldPort,
isImageMode,
} = parseKoboldConfig(args);
await stopFrontend(); await stopFrontend();
const appVersion = app.getVersion(); const appVersion = app.getVersion();
@ -92,11 +82,7 @@ export async function startFrontend(args: string[]) {
const koboldUrl = `http://${koboldHost}:${koboldPort}`; const koboldUrl = `http://${koboldHost}:${koboldPort}`;
const proxyUrl = PROXY.URL; const proxyUrl = PROXY.URL;
const openWebUIArgs = [ const openWebUIArgs = [...OPENWEBUI_BASE_ARGS, '--port', config.port.toString()];
...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)' : ''}...`
@ -143,16 +129,13 @@ export async function startFrontend(args: string[]) {
}); });
} }
openWebUIProcess.on( openWebUIProcess.on('exit', (code: number | null, signal: string | null) => {
'exit',
(code: number | null, signal: string | null) => {
const message = signal const message = signal
? `Open WebUI terminated with signal ${signal}` ? `Open WebUI terminated with signal ${signal}`
: `Open WebUI exited with code ${code}`; : `Open WebUI exited with code ${code}`;
sendKoboldOutput(message); sendKoboldOutput(message);
openWebUIProcess = null; openWebUIProcess = null;
} });
);
openWebUIProcess.on('error', (error) => { openWebUIProcess.on('error', (error) => {
logError('Open WebUI process error:', error); logError('Open WebUI process error:', error);
@ -162,7 +145,7 @@ export async function startFrontend(args: string[]) {
await waitForOpenWebUIToStart(); await waitForOpenWebUIToStart();
sendKoboldOutput(`Open WebUI is ready and auto-configured!`); sendKoboldOutput('Open WebUI is ready and auto-configured!');
sendKoboldOutput(`Access Open WebUI at: http://localhost:${config.port}`); sendKoboldOutput(`Access Open WebUI at: http://localhost:${config.port}`);
sendKoboldOutput(`Text Generation: ${proxyUrl}/v1 (auto-configured)`); sendKoboldOutput(`Text Generation: ${proxyUrl}/v1 (auto-configured)`);
if (isImageMode) { if (isImageMode) {

View file

@ -1,6 +1,6 @@
import { platform } from 'process'; import { platform } from 'node:process';
import { detectGPU } from './hardware';
import { safeExecute } from '@/utils/node/logging'; import { safeExecute } from '@/utils/node/logging';
import { detectGPU } from './hardware';
export interface PlatformGPUInfo { export interface PlatformGPUInfo {
hasAMD: boolean; hasAMD: boolean;

View file

@ -1,28 +1,22 @@
import { spawn } from 'child_process'; import type { ChildProcess } from 'node:child_process';
import { createServer, request, type Server } from 'http'; import { spawn } from 'node:child_process';
import { join } from 'path'; import { createServer, request, type Server } from 'node:http';
import { platform, on } from 'process'; import { join } from 'node:path';
import type { ChildProcess } from 'child_process'; import { on, platform } from 'node:process';
import { SERVER_READY_SIGNALS, SILLYTAVERN } from '@/constants';
import { logError, tryExecute } from '@/utils/node/logging';
import { sendKoboldOutput, sendToRenderer } from './window';
import { SILLYTAVERN, SERVER_READY_SIGNALS } from '@/constants';
import { PROXY } from '@/constants/proxy'; import { PROXY } from '@/constants/proxy';
import { terminateProcess } from '@/utils/node/process';
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 { getNodeEnvironment } from './dependencies'; import { logError, tryExecute } from '@/utils/node/logging';
import { terminateProcess } from '@/utils/node/process';
import { getInstallDir } from './config'; import { getInstallDir } from './config';
import { getNodeEnvironment } from './dependencies';
import { sendKoboldOutput, sendToRenderer } from './window';
let sillyTavernProcess: ChildProcess | null = null; let sillyTavernProcess: ChildProcess | null = null;
let proxyServer: Server | null = null; let proxyServer: Server | null = null;
const SILLYTAVERN_BASE_ARGS = [ const SILLYTAVERN_BASE_ARGS = ['--listen', '--browserLaunchEnabled', 'false', '--disableCsrf'];
'--listen',
'--browserLaunchEnabled',
'false',
'--disableCsrf',
];
on('SIGINT', () => { on('SIGINT', () => {
void stopFrontend(); void stopFrontend();
@ -34,8 +28,7 @@ on('SIGTERM', () => {
const getSillyTavernDataDir = () => join(getInstallDir(), 'sillytavern-data'); const getSillyTavernDataDir = () => join(getInstallDir(), 'sillytavern-data');
const getSillyTavernInstallDir = () => const getSillyTavernInstallDir = () => join(getInstallDir(), 'sillytavern-server');
join(getInstallDir(), 'sillytavern-server');
const getSillyTavernServerPath = () => const getSillyTavernServerPath = () =>
join(getSillyTavernInstallDir(), 'node_modules', 'sillytavern', 'server.js'); join(getSillyTavernInstallDir(), 'node_modules', 'sillytavern', 'server.js');
@ -57,18 +50,14 @@ async function ensureSillyTavernInstalled() {
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const rmCmd = platform === 'win32' ? 'rmdir' : 'rm'; const rmCmd = platform === 'win32' ? 'rmdir' : 'rm';
const rmArgs = const rmArgs =
platform === 'win32' platform === 'win32' ? ['/s', '/q', nodeModulesPath] : ['-rf', nodeModulesPath];
? ['/s', '/q', nodeModulesPath]
: ['-rf', nodeModulesPath];
spawn(rmCmd, rmArgs, { spawn(rmCmd, rmArgs, {
stdio: 'inherit', stdio: 'inherit',
shell: true, shell: true,
}) })
.on('exit', (code) => .on('exit', (code) =>
code === 0 code === 0 ? resolve() : reject(new Error(`Failed with code ${code}`))
? resolve()
: reject(new Error(`Failed with code ${code}`))
) )
.on('error', reject); .on('error', reject);
}); });
@ -195,9 +184,7 @@ async function ensureSillyTavernSettings() {
await terminateProcess(initProcess); await terminateProcess(initProcess);
sendKoboldOutput( sendKoboldOutput('SillyTavern settings should now be generated');
'SillyTavern settings should now be generated'
);
resolve(); resolve();
} }
@ -223,15 +210,14 @@ async function setupSillyTavernConfig(isImageMode: boolean) {
if (await pathExists(configPath)) { if (await pathExists(configPath)) {
try { try {
const existingSettings = const existingSettings = await readJsonFile<Record<string, unknown>>(configPath);
await readJsonFile<Record<string, unknown>>(configPath);
if (existingSettings) { if (existingSettings) {
settings = existingSettings; settings = existingSettings;
sendKoboldOutput(`Loaded existing SillyTavern settings`); sendKoboldOutput('Loaded existing SillyTavern settings');
} }
} catch { } catch {
sendKoboldOutput(`Could not read existing settings, creating new ones`); sendKoboldOutput('Could not read existing settings, creating new ones');
} }
} }
@ -241,12 +227,8 @@ async function setupSillyTavernConfig(isImageMode: boolean) {
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) 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 = {}; if (!textgenSettings.server_urls) textgenSettings.server_urls = {};
const serverUrls = textgenSettings.server_urls as Record<string, unknown>; const serverUrls = textgenSettings.server_urls as Record<string, unknown>;
@ -255,14 +237,12 @@ async function setupSillyTavernConfig(isImageMode: boolean) {
settings.main_api = 'textgenerationwebui'; settings.main_api = 'textgenerationwebui';
textgenSettings.type = 'koboldcpp'; textgenSettings.type = 'koboldcpp';
sendKoboldOutput( sendKoboldOutput(`Configured SillyTavern for text generation at ${proxyUrl}`);
`Configured SillyTavern for text generation at ${proxyUrl}`
);
if (isImageMode) { if (isImageMode) {
sendKoboldOutput( sendKoboldOutput(
`Image generation mode detected. Configure SillyTavern manually:\n` + 'Image generation mode detected. Configure SillyTavern manually:\n' +
`1. Open SillyTavern Settings (top-right gear icon)\n` + '1. Open SillyTavern Settings (top-right gear icon)\n' +
`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` +
@ -272,13 +252,11 @@ async function setupSillyTavernConfig(isImageMode: boolean) {
await writeJsonFile(configPath, settings); await writeJsonFile(configPath, settings);
sendKoboldOutput(`SillyTavern configuration updated successfully!`); sendKoboldOutput('SillyTavern configuration updated successfully!');
}, 'Failed to setup SillyTavern config'); }, 'Failed to setup SillyTavern config');
if (!success) { if (!success) {
sendKoboldOutput( sendKoboldOutput('Failed to configure SillyTavern. Check logs for details.');
`Failed to configure SillyTavern. Check logs for details.`
);
} }
} }
@ -352,15 +330,13 @@ export async function startFrontend(args: string[]) {
await stopFrontend(); await stopFrontend();
sendKoboldOutput(`Preparing SillyTavern to connect via proxy...`); sendKoboldOutput('Preparing SillyTavern to connect via proxy...');
await ensureSillyTavernInstalled(); await ensureSillyTavernInstalled();
await ensureSillyTavernSettings(); await ensureSillyTavernSettings();
await setupSillyTavernConfig(isImageMode); await setupSillyTavernConfig(isImageMode);
sendKoboldOutput( sendKoboldOutput(`Starting ${config.name} frontend on port ${config.port}...`);
`Starting ${config.name} frontend on port ${config.port}...`
);
const sillyTavernDataDir = getSillyTavernDataDir(); const sillyTavernDataDir = getSillyTavernDataDir();
@ -386,16 +362,13 @@ export async function startFrontend(args: string[]) {
}); });
} }
sillyTavernProcess.on( sillyTavernProcess.on('exit', (code: number | null, signal: string | null) => {
'exit',
(code: number | null, signal: string | null) => {
const message = signal const message = signal
? `SillyTavern terminated with signal ${signal}` ? `SillyTavern terminated with signal ${signal}`
: `SillyTavern exited with code ${code}`; : `SillyTavern exited with code ${code}`;
sendKoboldOutput(message); sendKoboldOutput(message);
sillyTavernProcess = null; sillyTavernProcess = null;
} });
);
sillyTavernProcess.on('error', (error) => { sillyTavernProcess.on('error', (error) => {
logError('SillyTavern process error:', error); logError('SillyTavern process error:', error);

View file

@ -1,6 +1,6 @@
import { createServer, Server } from 'http'; import { readFile } from 'node:fs/promises';
import { readFile } from 'fs/promises'; import { createServer, type Server } from 'node:http';
import { join, normalize, resolve as resolvePath, sep } from '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';
@ -12,7 +12,7 @@ export const startStaticServer = (distPath: string) =>
server = createServer( server = createServer(
(req, res) => (req, res) =>
void (async () => { void (async () => {
const requestPath = req.url === '/' ? 'index.html' : req.url!; const requestPath = req.url === '/' ? 'index.html' : (req.url ?? 'index.html');
const normalizedPath = normalize(join(distPath, requestPath)); const normalizedPath = normalize(join(distPath, requestPath));
const resolvedDistPath = resolvePath(distPath); const resolvedDistPath = resolvePath(distPath);
@ -32,8 +32,7 @@ export const startStaticServer = (distPath: string) =>
try { try {
const content = await readFile(safeFilePath); const content = await readFile(safeFilePath);
const contentType = const contentType = lookup(safeFilePath) || 'application/octet-stream';
lookup(safeFilePath) || 'application/octet-stream';
res.writeHead(200, { 'Content-Type': contentType }); res.writeHead(200, { 'Content-Type': contentType });
res.end(content); res.end(content);
@ -45,7 +44,7 @@ export const startStaticServer = (distPath: string) =>
); );
server.listen(0, 'localhost', () => { server.listen(0, 'localhost', () => {
const address = server!.address(); const address = server?.address();
if (address && typeof address !== 'string') { if (address && typeof address !== 'string') {
serverPort = address.port; serverPort = address.port;
resolve(`http://localhost:${serverPort}`); resolve(`http://localhost:${serverPort}`);

View file

@ -1,18 +1,12 @@
import { import { join } from 'node:path';
app, import { platform, resourcesPath } from 'node:process';
Tray, import { app, Menu, type MenuItemConstructorOptions, nativeImage, Tray } from 'electron';
Menu,
nativeImage,
MenuItemConstructorOptions,
} from 'electron';
import { join } from 'path';
import { platform, resourcesPath } from 'process';
import { getEnableSystemTray } from './config';
import { getMainWindow } from './window';
import type { CpuMetrics, MemoryMetrics, GpuMetrics } from './monitoring';
import type { Screen } from '@/types';
import { PRODUCT_NAME } from '@/constants'; import { PRODUCT_NAME } from '@/constants';
import type { Screen } from '@/types';
import { stripFileExtension } from '@/utils/format'; import { stripFileExtension } from '@/utils/format';
import { getEnableSystemTray } from './config';
import type { CpuMetrics, GpuMetrics, MemoryMetrics } from './monitoring';
import { getMainWindow } from './window';
let tray: Tray | null = null; let tray: Tray | null = null;
let currentMetrics: { let currentMetrics: {
@ -82,11 +76,7 @@ function showAndFocusWindow() {
function buildTooltipText() { function buildTooltipText() {
const parts: string[] = []; const parts: string[] = [];
if ( if (appState.monitoringEnabled && currentMetrics.cpu && currentMetrics.memory) {
appState.monitoringEnabled &&
currentMetrics.cpu &&
currentMetrics.memory
) {
const metrics: string[] = []; const metrics: string[] = [];
const cpuText = `CPU: ${currentMetrics.cpu.usage}%${currentMetrics.cpu.temperature ? `${currentMetrics.cpu.temperature}°C` : ''}`; const cpuText = `CPU: ${currentMetrics.cpu.usage}%${currentMetrics.cpu.temperature ? `${currentMetrics.cpu.temperature}°C` : ''}`;
@ -97,13 +87,12 @@ function buildTooltipText() {
if (currentMetrics.gpu?.gpus) { if (currentMetrics.gpu?.gpus) {
currentMetrics.gpu.gpus.forEach((gpu, index) => { currentMetrics.gpu.gpus.forEach((gpu, index) => {
const gpuLabel = const gpuLabel = (currentMetrics.gpu?.gpus?.length ?? 0) > 1 ? `GPU ${index + 1}` : 'GPU';
currentMetrics.gpu!.gpus.length > 1 ? `GPU ${index + 1}` : 'GPU';
const gpuText = `${gpuLabel}: ${gpu.usage}%${gpu.temperature ? `${gpu.temperature}°C` : ''}`; const gpuText = `${gpuLabel}: ${gpu.usage}%${gpu.temperature ? `${gpu.temperature}°C` : ''}`;
metrics.push(gpuText); metrics.push(gpuText);
const vramLabel = const vramLabel =
currentMetrics.gpu!.gpus.length > 1 ? `VRAM ${index + 1}` : 'VRAM'; (currentMetrics.gpu?.gpus?.length ?? 0) > 1 ? `VRAM ${index + 1}` : 'VRAM';
const vramText = `${vramLabel}: ${gpu.memoryUsage}% • ${gpu.memoryUsed.toFixed(2)} GB`; const vramText = `${vramLabel}: ${gpu.memoryUsage}% • ${gpu.memoryUsed.toFixed(2)} GB`;
metrics.push(vramText); metrics.push(vramText);
}); });

View file

@ -1,19 +1,19 @@
import { BrowserWindow, app, shell, screen, Menu, clipboard } from 'electron'; import { join } from 'node:path';
import { join } from 'path'; import { stripVTControlCharacters } from 'node:util';
import { stripVTControlCharacters } from 'util'; import type { BrowserWindowConstructorOptions } from 'electron';
import { app, BrowserWindow, clipboard, Menu, screen, shell } from 'electron';
import { PRODUCT_NAME } from '@/constants'; import { PRODUCT_NAME } from '@/constants';
import type { IPCChannel, IPCChannelPayloads } from '@/types/ipc';
import { isDevelopment } from '@/utils/node/environment'; import { isDevelopment } from '@/utils/node/environment';
import { startStaticServer } from './static-server';
import { import {
getBackgroundColor, getBackgroundColor,
getEnableSystemTray,
getWindowBounds, getWindowBounds,
set as setConfig, set as setConfig,
WindowBounds, type WindowBounds,
getEnableSystemTray,
} from './config'; } from './config';
import { startStaticServer } from './static-server';
import { isTrayActive } from './tray'; import { isTrayActive } from './tray';
import type { IPCChannel, IPCChannelPayloads } from '@/types/ipc';
import type { BrowserWindowConstructorOptions } from 'electron';
let mainWindow: BrowserWindow | null = null; let mainWindow: BrowserWindow | null = null;
@ -23,10 +23,7 @@ export async function createMainWindow(options?: { startHidden?: boolean }) {
const defaultWidth = 800; const defaultWidth = 800;
const minHeight = 600; const minHeight = 600;
const defaultHeight = Math.max( const defaultHeight = Math.max(minHeight, Math.min(Math.floor(size.height * 0.75), 1000));
minHeight,
Math.min(Math.floor(size.height * 0.75), 1000)
);
const windowOptions = { const windowOptions = {
minWidth: 600, minWidth: 600,
@ -61,12 +58,8 @@ export async function createMainWindow(options?: { startHidden?: boolean }) {
windowOptions.x = savedBounds.x; windowOptions.x = savedBounds.x;
windowOptions.y = savedBounds.y; windowOptions.y = savedBounds.y;
} else { } else {
windowOptions.x = Math.floor( windowOptions.x = Math.floor((size.width - (savedBounds.width || defaultWidth)) / 2);
(size.width - (savedBounds.width || defaultWidth)) / 2 windowOptions.y = Math.floor((size.height - (savedBounds.height || defaultHeight)) / 2);
);
windowOptions.y = Math.floor(
(size.height - (savedBounds.height || defaultHeight)) / 2
);
} }
} else { } else {
windowOptions.x = Math.floor((size.width - defaultWidth) / 2); windowOptions.x = Math.floor((size.width - defaultWidth) / 2);
@ -165,7 +158,6 @@ export async function createMainWindow(options?: { startHidden?: boolean }) {
} }
function setupContextMenu(mainWindow: BrowserWindow) { function setupContextMenu(mainWindow: BrowserWindow) {
// eslint-disable-next-line sonarjs/cognitive-complexity
mainWindow.webContents.on('context-menu', (_, params) => { mainWindow.webContents.on('context-menu', (_, params) => {
const hasLinkURL = !!params.linkURL; const hasLinkURL = !!params.linkURL;
const hasSelection = !!params.selectionText; const hasSelection = !!params.selectionText;
@ -179,8 +171,7 @@ function setupContextMenu(mainWindow: BrowserWindow) {
const canSelectAll = isEditable || params.mediaType === 'none'; const canSelectAll = isEditable || params.mediaType === 'none';
const canUndo = isEditable && params.editFlags?.canUndo; const canUndo = isEditable && params.editFlags?.canUndo;
const canRedo = isEditable && params.editFlags?.canRedo; const canRedo = isEditable && params.editFlags?.canRedo;
const hasEditOperations = const hasEditOperations = canCut || canCopy || canPaste || canSelectAll || canUndo || canRedo;
canCut || canCopy || canPaste || canSelectAll || canUndo || canRedo;
const menuItems = []; const menuItems = [];
@ -211,9 +202,7 @@ function setupContextMenu(mainWindow: BrowserWindow) {
menuItems.push({ menuItems.push({
label: 'Add to Dictionary', label: 'Add to Dictionary',
click: () => { click: () => {
mainWindow.webContents.session.addWordToSpellCheckerDictionary( mainWindow.webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord);
params.misspelledWord
);
}, },
}); });
@ -234,10 +223,7 @@ function setupContextMenu(mainWindow: BrowserWindow) {
menuItems.push({ label: 'Redo', role: 'redo' as const }); menuItems.push({ label: 'Redo', role: 'redo' as const });
} }
if ( if ((canUndo || canRedo) && (canCut || canCopy || canPaste || canSelectAll)) {
(canUndo || canRedo) &&
(canCut || canCopy || canPaste || canSelectAll)
) {
menuItems.push({ type: 'separator' as const }); menuItems.push({ type: 'separator' as const });
} }

View file

@ -1,71 +1,49 @@
import { contextBridge, ipcRenderer, type IpcRendererEvent } from 'electron'; import { contextBridge, type IpcRendererEvent, ipcRenderer } from 'electron';
import type { CpuMetrics, GpuMetrics, MemoryMetrics } from '@/main/modules/monitoring';
import type { import type {
KoboldAPI,
AppAPI, AppAPI,
ConfigAPI, ConfigAPI,
LogsAPI,
DependenciesAPI, DependenciesAPI,
KoboldAPI,
LogsAPI,
MonitoringAPI, MonitoringAPI,
UpdaterAPI,
NotepadAPI, NotepadAPI,
UpdaterAPI,
} from '@/types/electron'; } from '@/types/electron';
import type {
CpuMetrics,
MemoryMetrics,
GpuMetrics,
} from '@/main/modules/monitoring';
import type { KoboldCrashInfo } from '@/types/ipc'; import type { KoboldCrashInfo } from '@/types/ipc';
const koboldAPI: KoboldAPI = { const koboldAPI: KoboldAPI = {
getInstalledBackends: () => ipcRenderer.invoke('kobold:getInstalledBackends'), getInstalledBackends: () => ipcRenderer.invoke('kobold:getInstalledBackends'),
getCurrentBackend: () => ipcRenderer.invoke('kobold:getCurrentBackend'), getCurrentBackend: () => ipcRenderer.invoke('kobold:getCurrentBackend'),
setCurrentBackend: (backend) => setCurrentBackend: (backend) => ipcRenderer.invoke('kobold:setCurrentBackend', backend),
ipcRenderer.invoke('kobold:setCurrentBackend', backend),
getPlatform: () => ipcRenderer.invoke('kobold:getPlatform'), getPlatform: () => ipcRenderer.invoke('kobold:getPlatform'),
detectGPU: () => ipcRenderer.invoke('kobold:detectGPU'), detectGPU: () => ipcRenderer.invoke('kobold:detectGPU'),
detectCPU: () => ipcRenderer.invoke('kobold:detectCPU'), detectCPU: () => ipcRenderer.invoke('kobold:detectCPU'),
detectGPUCapabilities: () => detectGPUCapabilities: () => ipcRenderer.invoke('kobold:detectGPUCapabilities'),
ipcRenderer.invoke('kobold:detectGPUCapabilities'),
detectGPUMemory: () => ipcRenderer.invoke('kobold:detectGPUMemory'), detectGPUMemory: () => ipcRenderer.invoke('kobold:detectGPUMemory'),
detectSystemMemory: () => ipcRenderer.invoke('kobold:detectSystemMemory'), detectSystemMemory: () => ipcRenderer.invoke('kobold:detectSystemMemory'),
detectROCm: () => ipcRenderer.invoke('kobold:detectROCm'), detectROCm: () => ipcRenderer.invoke('kobold:detectROCm'),
detectAccelerationSupport: () => detectAccelerationSupport: () => ipcRenderer.invoke('kobold:detectAccelerationSupport'),
ipcRenderer.invoke('kobold:detectAccelerationSupport'),
getAvailableAccelerations: (includeDisabled) => getAvailableAccelerations: (includeDisabled) =>
ipcRenderer.invoke('kobold:getAvailableAccelerations', includeDisabled), ipcRenderer.invoke('kobold:getAvailableAccelerations', includeDisabled),
getCurrentInstallDir: () => ipcRenderer.invoke('kobold:getCurrentInstallDir'), getCurrentInstallDir: () => ipcRenderer.invoke('kobold:getCurrentInstallDir'),
selectInstallDirectory: () => selectInstallDirectory: () => ipcRenderer.invoke('kobold:selectInstallDirectory'),
ipcRenderer.invoke('kobold:selectInstallDirectory'), downloadRelease: (asset, options) => ipcRenderer.invoke('kobold:downloadRelease', asset, options),
downloadRelease: (asset, options) => deleteRelease: (binaryPath) => ipcRenderer.invoke('kobold:deleteRelease', binaryPath),
ipcRenderer.invoke('kobold:downloadRelease', asset, options),
deleteRelease: (binaryPath) =>
ipcRenderer.invoke('kobold:deleteRelease', binaryPath),
launchKoboldCpp: (args, preLaunchCommands) => launchKoboldCpp: (args, preLaunchCommands) =>
ipcRenderer.invoke('kobold:launchKoboldCpp', args, preLaunchCommands), ipcRenderer.invoke('kobold:launchKoboldCpp', args, preLaunchCommands),
getConfigFiles: () => ipcRenderer.invoke('kobold:getConfigFiles'), getConfigFiles: () => ipcRenderer.invoke('kobold:getConfigFiles'),
saveConfigFile: (configName, configData) => saveConfigFile: (configName, configData) =>
ipcRenderer.invoke('kobold:saveConfigFile', configName, configData), ipcRenderer.invoke('kobold:saveConfigFile', configName, configData),
deleteConfigFile: (configName) => deleteConfigFile: (configName) => ipcRenderer.invoke('kobold:deleteConfigFile', configName),
ipcRenderer.invoke('kobold:deleteConfigFile', configName),
getSelectedConfig: () => ipcRenderer.invoke('kobold:getSelectedConfig'), getSelectedConfig: () => ipcRenderer.invoke('kobold:getSelectedConfig'),
setSelectedConfig: (configName) => setSelectedConfig: (configName) => ipcRenderer.invoke('kobold:setSelectedConfig', configName),
ipcRenderer.invoke('kobold:setSelectedConfig', configName), parseConfigFile: (filePath) => ipcRenderer.invoke('kobold:parseConfigFile', filePath),
parseConfigFile: (filePath) => selectModelFile: (title) => ipcRenderer.invoke('kobold:selectModelFile', title),
ipcRenderer.invoke('kobold:parseConfigFile', filePath),
selectModelFile: (title) =>
ipcRenderer.invoke('kobold:selectModelFile', title),
importLocalBackend: () => ipcRenderer.invoke('kobold:importLocalBackend'), importLocalBackend: () => ipcRenderer.invoke('kobold:importLocalBackend'),
getLocalModels: (paramType) => getLocalModels: (paramType) => ipcRenderer.invoke('kobold:getLocalModels', paramType),
ipcRenderer.invoke('kobold:getLocalModels', paramType), analyzeModel: (filePath) => ipcRenderer.invoke('kobold:analyzeModel', filePath),
analyzeModel: (filePath) => calculateOptimalLayers: (modelPath, contextSize, availableVramGB, flashAttention, acceleration) =>
ipcRenderer.invoke('kobold:analyzeModel', filePath),
calculateOptimalLayers: (
modelPath,
contextSize,
availableVramGB,
flashAttention,
acceleration
) =>
ipcRenderer.invoke( ipcRenderer.invoke(
'kobold:calculateOptimalLayers', 'kobold:calculateOptimalLayers',
modelPath, modelPath,
@ -76,8 +54,7 @@ const koboldAPI: KoboldAPI = {
), ),
stopKoboldCpp: () => void ipcRenderer.invoke('kobold:stopKoboldCpp'), stopKoboldCpp: () => void ipcRenderer.invoke('kobold:stopKoboldCpp'),
onDownloadProgress: (callback) => { onDownloadProgress: (callback) => {
const handler = (_: IpcRendererEvent, progress: number) => const handler = (_: IpcRendererEvent, progress: number) => callback(progress);
callback(progress);
ipcRenderer.on('download-progress', handler); ipcRenderer.on('download-progress', handler);
return () => { return () => {
@ -109,8 +86,7 @@ const koboldAPI: KoboldAPI = {
}; };
}, },
onKoboldCrashed: (callback) => { onKoboldCrashed: (callback) => {
const handler = (_: IpcRendererEvent, crashInfo: KoboldCrashInfo) => const handler = (_: IpcRendererEvent, crashInfo: KoboldCrashInfo) => callback(crashInfo);
callback(crashInfo);
ipcRenderer.on('kobold-crashed', handler); ipcRenderer.on('kobold-crashed', handler);
return () => { return () => {
@ -148,15 +124,11 @@ const appAPI: AppAPI = {
getZoomLevel: () => ipcRenderer.invoke('app:getZoomLevel'), getZoomLevel: () => ipcRenderer.invoke('app:getZoomLevel'),
setZoomLevel: async (level) => ipcRenderer.invoke('app:setZoomLevel', level), setZoomLevel: async (level) => ipcRenderer.invoke('app:setZoomLevel', level),
getColorScheme: () => ipcRenderer.invoke('app:getColorScheme'), getColorScheme: () => ipcRenderer.invoke('app:getColorScheme'),
setColorScheme: async (colorScheme) => setColorScheme: async (colorScheme) => ipcRenderer.invoke('app:setColorScheme', colorScheme),
ipcRenderer.invoke('app:setColorScheme', colorScheme),
getEnableSystemTray: () => ipcRenderer.invoke('app:getEnableSystemTray'), getEnableSystemTray: () => ipcRenderer.invoke('app:getEnableSystemTray'),
setEnableSystemTray: (enabled) => setEnableSystemTray: (enabled) => ipcRenderer.invoke('app:setEnableSystemTray', enabled),
ipcRenderer.invoke('app:setEnableSystemTray', enabled), getStartMinimizedToTray: () => ipcRenderer.invoke('app:getStartMinimizedToTray'),
getStartMinimizedToTray: () => setStartMinimizedToTray: (enabled) => ipcRenderer.invoke('app:setStartMinimizedToTray', enabled),
ipcRenderer.invoke('app:getStartMinimizedToTray'),
setStartMinimizedToTray: (enabled) =>
ipcRenderer.invoke('app:setStartMinimizedToTray', enabled),
updateTrayState: (state) => ipcRenderer.invoke('app:updateTrayState', state), updateTrayState: (state) => ipcRenderer.invoke('app:updateTrayState', state),
onTrayEject: (callback) => { onTrayEject: (callback) => {
const handler = () => callback(); const handler = () => callback();
@ -166,8 +138,7 @@ const appAPI: AppAPI = {
}; };
}, },
openExternal: (url) => ipcRenderer.invoke('app:openExternal', url), openExternal: (url) => ipcRenderer.invoke('app:openExternal', url),
openPerformanceManager: () => openPerformanceManager: () => ipcRenderer.invoke('app:openPerformanceManager'),
ipcRenderer.invoke('app:openPerformanceManager'),
checkForUpdates: () => ipcRenderer.invoke('app:checkForUpdates'), checkForUpdates: () => ipcRenderer.invoke('app:checkForUpdates'),
downloadUpdate: () => ipcRenderer.invoke('app:downloadUpdate'), downloadUpdate: () => ipcRenderer.invoke('app:downloadUpdate'),
quitAndInstall: () => ipcRenderer.invoke('app:quitAndInstall'), quitAndInstall: () => ipcRenderer.invoke('app:quitAndInstall'),
@ -184,8 +155,7 @@ const appAPI: AppAPI = {
}; };
}, },
onLineNumbersChanged: (callback) => { onLineNumbersChanged: (callback) => {
const handler = (_: IpcRendererEvent, showLineNumbers: boolean) => const handler = (_: IpcRendererEvent, showLineNumbers: boolean) => callback(showLineNumbers);
callback(showLineNumbers);
ipcRenderer.on('line-numbers-changed', handler); ipcRenderer.on('line-numbers-changed', handler);
@ -201,15 +171,13 @@ const configAPI: ConfigAPI = {
}; };
const logsAPI: LogsAPI = { const logsAPI: LogsAPI = {
logError: (message, error) => logError: (message, error) => ipcRenderer.send('logs:logError', message, error),
ipcRenderer.send('logs:logError', message, error),
}; };
const dependenciesAPI: DependenciesAPI = { const dependenciesAPI: DependenciesAPI = {
isUvAvailable: () => ipcRenderer.invoke('dependencies:isUvAvailable'), isUvAvailable: () => ipcRenderer.invoke('dependencies:isUvAvailable'),
isNpxAvailable: () => ipcRenderer.invoke('dependencies:isNpxAvailable'), isNpxAvailable: () => ipcRenderer.invoke('dependencies:isNpxAvailable'),
clearOpenWebUIData: () => clearOpenWebUIData: () => ipcRenderer.invoke('dependencies:clearOpenWebUIData'),
ipcRenderer.invoke('dependencies:clearOpenWebUIData'),
}; };
const monitoringAPI: MonitoringAPI = { const monitoringAPI: MonitoringAPI = {
@ -221,8 +189,7 @@ const monitoringAPI: MonitoringAPI = {
}; };
}, },
onCpuMetrics: (callback) => { onCpuMetrics: (callback) => {
const handler = (_: IpcRendererEvent, metrics: CpuMetrics) => const handler = (_: IpcRendererEvent, metrics: CpuMetrics) => callback(metrics);
callback(metrics);
ipcRenderer.on('cpu-metrics', handler); ipcRenderer.on('cpu-metrics', handler);
return () => { return () => {
@ -230,8 +197,7 @@ const monitoringAPI: MonitoringAPI = {
}; };
}, },
onMemoryMetrics: (callback) => { onMemoryMetrics: (callback) => {
const handler = (_: IpcRendererEvent, metrics: MemoryMetrics) => const handler = (_: IpcRendererEvent, metrics: MemoryMetrics) => callback(metrics);
callback(metrics);
ipcRenderer.on('memory-metrics', handler); ipcRenderer.on('memory-metrics', handler);
return () => { return () => {
@ -239,8 +205,7 @@ const monitoringAPI: MonitoringAPI = {
}; };
}, },
onGpuMetrics: (callback) => { onGpuMetrics: (callback) => {
const handler = (_: IpcRendererEvent, metrics: GpuMetrics) => const handler = (_: IpcRendererEvent, metrics: GpuMetrics) => callback(metrics);
callback(metrics);
ipcRenderer.on('gpu-metrics', handler); ipcRenderer.on('gpu-metrics', handler);
return () => { return () => {
@ -259,12 +224,9 @@ const updaterAPI: UpdaterAPI = {
}; };
const notepadAPI: NotepadAPI = { const notepadAPI: NotepadAPI = {
saveTabContent: (title, content) => saveTabContent: (title, content) => ipcRenderer.invoke('notepad:saveTabContent', title, content),
ipcRenderer.invoke('notepad:saveTabContent', title, content), loadTabContent: (title) => ipcRenderer.invoke('notepad:loadTabContent', title),
loadTabContent: (title) => renameTab: (oldTitle, newTitle) => ipcRenderer.invoke('notepad:renameTab', oldTitle, newTitle),
ipcRenderer.invoke('notepad:loadTabContent', title),
renameTab: (oldTitle, newTitle) =>
ipcRenderer.invoke('notepad:renameTab', oldTitle, newTitle),
saveState: (state) => ipcRenderer.invoke('notepad:saveState', state), saveState: (state) => ipcRenderer.invoke('notepad:saveState', state),
loadState: () => ipcRenderer.invoke('notepad:loadState'), loadState: () => ipcRenderer.invoke('notepad:loadState'),
deleteTab: (title) => ipcRenderer.invoke('notepad:deleteTab', title), deleteTab: (title) => ipcRenderer.invoke('notepad:deleteTab', title),

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