mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 19:54:44 -07:00
windows rocm support, warn users from suboptimal setups
This commit is contained in:
parent
867ced21f9
commit
01058a8d02
26 changed files with 670 additions and 657 deletions
|
|
@ -2,6 +2,13 @@
|
|||
|
||||
A koboldcpp manager.
|
||||
|
||||
## Core Features
|
||||
|
||||
- modern UI with full support for Linux Wayland
|
||||
- download and keep up-to-date your [koboldcpp](https://github.com/LostRuins/koboldcpp/releases) binary
|
||||
- better surface the ROCm-specific builds of koboldcpp from YellowRoseCx and from [koboldai.org](https://koboldai.org/cpplinuxrocm)
|
||||
- manage the koboldcpp binary to prevent it from running in the background indefinitely
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@
|
|||
"flexbox",
|
||||
"flexdir",
|
||||
"flexwrap",
|
||||
"flashattention",
|
||||
"Flashattention",
|
||||
"fontsize",
|
||||
"fontweight",
|
||||
"forwardRef",
|
||||
|
|
@ -125,6 +127,8 @@
|
|||
"nocuda",
|
||||
"nodeIntegration",
|
||||
"noheader",
|
||||
"noshift",
|
||||
"Noshift",
|
||||
"nsis",
|
||||
"nvidia",
|
||||
"oldpc",
|
||||
|
|
@ -210,6 +214,9 @@
|
|||
"useReducer",
|
||||
"useRef",
|
||||
"useState",
|
||||
"useclblast",
|
||||
"usecuda",
|
||||
"usevulkan",
|
||||
"util",
|
||||
"utils",
|
||||
"var",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import react from 'eslint-plugin-react';
|
|||
import importPlugin from 'eslint-plugin-import';
|
||||
import sonarjs from 'eslint-plugin-sonarjs';
|
||||
import cspell from '@cspell/eslint-plugin';
|
||||
import noComments from 'eslint-plugin-no-comments';
|
||||
|
||||
const config = [
|
||||
js.configs.recommended,
|
||||
|
|
@ -39,6 +40,7 @@ const config = [
|
|||
import: importPlugin,
|
||||
sonarjs: sonarjs,
|
||||
'@cspell': cspell,
|
||||
'no-comments': noComments,
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
|
|
@ -46,11 +48,9 @@ const config = [
|
|||
},
|
||||
},
|
||||
rules: {
|
||||
// Use recommended rules from plugins
|
||||
...reactHooks.configs.recommended.rules,
|
||||
...react.configs.recommended.rules,
|
||||
|
||||
// Essential TypeScript rules
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
|
|
@ -59,10 +59,9 @@ const config = [
|
|||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
'no-unused-vars': 'off', // Turn off base rule to use TypeScript version
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
|
||||
// React-specific rules you wanted
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
|
|
@ -74,9 +73,13 @@ const config = [
|
|||
unnamedComponents: 'arrow-function',
|
||||
},
|
||||
],
|
||||
'react/react-in-jsx-scope': 'off', // Not needed with new JSX transform
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/jsx-boolean-value': ['error', 'never'],
|
||||
'react/jsx-curly-brace-presence': [
|
||||
'error',
|
||||
{ props: 'never', children: 'never' },
|
||||
],
|
||||
|
||||
// No default React imports - force specific imports
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
|
|
@ -91,41 +94,33 @@ const config = [
|
|||
},
|
||||
],
|
||||
|
||||
// Import rules - enforce named exports
|
||||
'import/no-default-export': 'error',
|
||||
'import/prefer-default-export': 'off',
|
||||
|
||||
// Enforce arrow function shorthand when possible
|
||||
'arrow-body-style': ['error', 'as-needed'],
|
||||
'prefer-arrow-callback': ['error', { allowNamedFunctions: false }],
|
||||
|
||||
// Forbid console.log usage
|
||||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||
|
||||
// TypeScript rules
|
||||
'@typescript-eslint/no-inferrable-types': 'warn',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
|
||||
// SonarJS rules - keep cognitive complexity reasonable
|
||||
'sonarjs/cognitive-complexity': ['warn', 25],
|
||||
|
||||
// Spell checking for code
|
||||
'@cspell/spellchecker': ['warn'],
|
||||
|
||||
'no-comments/disallowComments': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
// TypeScript definition files should have relaxed rules
|
||||
files: ['**/*.d.ts'],
|
||||
rules: {
|
||||
// Allow unused variables in type definitions
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
// Allow any types in definitions
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Allow default exports for config files
|
||||
files: [
|
||||
'*.config.*',
|
||||
'vite.config.*',
|
||||
|
|
|
|||
105
package-lock.json
generated
105
package-lock.json
generated
|
|
@ -23,7 +23,7 @@
|
|||
"@types/node": "^24.2.1",
|
||||
"@types/react": "^19.1.10",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@types/systeminformation": "^3.23.1",
|
||||
"@types/systeminformation": "^3.54.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.39.1",
|
||||
"@typescript-eslint/parser": "^8.39.1",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
|
|
@ -34,6 +34,7 @@
|
|||
"electron-vite": "^4.0.0",
|
||||
"eslint": "^9.33.0",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-no-comments": "^1.1.10",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
|
|
@ -87,22 +88,22 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz",
|
||||
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz",
|
||||
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.0",
|
||||
"@babel/generator": "^7.28.3",
|
||||
"@babel/helper-compilation-targets": "^7.27.2",
|
||||
"@babel/helper-module-transforms": "^7.27.3",
|
||||
"@babel/helpers": "^7.27.6",
|
||||
"@babel/parser": "^7.28.0",
|
||||
"@babel/helper-module-transforms": "^7.28.3",
|
||||
"@babel/helpers": "^7.28.3",
|
||||
"@babel/parser": "^7.28.3",
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/traverse": "^7.28.0",
|
||||
"@babel/types": "^7.28.0",
|
||||
"@babel/traverse": "^7.28.3",
|
||||
"@babel/types": "^7.28.2",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
|
|
@ -135,13 +136,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz",
|
||||
"integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==",
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
|
||||
"integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.0",
|
||||
"@babel/types": "^7.28.0",
|
||||
"@babel/parser": "^7.28.3",
|
||||
"@babel/types": "^7.28.2",
|
||||
"@jridgewell/gen-mapping": "^0.3.12",
|
||||
"@jridgewell/trace-mapping": "^0.3.28",
|
||||
"jsesc": "^3.0.2"
|
||||
|
|
@ -200,15 +201,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-transforms": {
|
||||
"version": "7.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz",
|
||||
"integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==",
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
|
||||
"integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
"@babel/traverse": "^7.27.3"
|
||||
"@babel/traverse": "^7.28.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
|
@ -256,9 +257,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz",
|
||||
"integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==",
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz",
|
||||
"integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -270,12 +271,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
|
||||
"integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz",
|
||||
"integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.28.0"
|
||||
"@babel/types": "^7.28.2"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
|
|
@ -333,9 +334,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz",
|
||||
"integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==",
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz",
|
||||
"integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
|
@ -356,17 +357,17 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz",
|
||||
"integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==",
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz",
|
||||
"integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.0",
|
||||
"@babel/generator": "^7.28.3",
|
||||
"@babel/helper-globals": "^7.28.0",
|
||||
"@babel/parser": "^7.28.0",
|
||||
"@babel/parser": "^7.28.3",
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/types": "^7.28.0",
|
||||
"@babel/types": "^7.28.2",
|
||||
"debug": "^4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -3284,11 +3285,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@types/systeminformation": {
|
||||
"version": "3.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/systeminformation/-/systeminformation-3.23.1.tgz",
|
||||
"integrity": "sha512-0/y5m3PQLhQL7UM8MEvwFuLoTT6k5bamcZyjap12S0iHb33ngKfDDqCSnIaCaKWvRE41R/9BsZov1aE6hvcpEg==",
|
||||
"version": "3.54.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/systeminformation/-/systeminformation-3.54.1.tgz",
|
||||
"integrity": "sha512-vvisj2mdWygyc0jk/5XtSVq9gtxCmF3nrGwv8wVway8pwNRhtPji/MU9dc1L0F6rl0F/NFIHa4ScRU7wmNaHmg==",
|
||||
"deprecated": "This is a stub types definition. systeminformation provides its own type definitions, so you do not need this installed.",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"systeminformation": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/verror": {
|
||||
"version": "1.10.11",
|
||||
|
|
@ -5787,9 +5792,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.200",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.200.tgz",
|
||||
"integrity": "sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w==",
|
||||
"version": "1.5.201",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.201.tgz",
|
||||
"integrity": "sha512-ZG65vsrLClodGqywuigc+7m0gr4ISoTQttfVh7nfpLv0M7SIwF4WbFNEOywcqTiujs12AUeeXbFyQieDICAIxg==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
|
|
@ -6386,6 +6391,13 @@
|
|||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-no-comments": {
|
||||
"version": "1.1.10",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-no-comments/-/eslint-plugin-no-comments-1.1.10.tgz",
|
||||
"integrity": "sha512-hJohtfKNKDDAmhQ/VsvaN7Q41npFVBeAQISf54ywRQx7EhknF+068SPHj5g3njKMoxYAuk62ahJvJQQvMYeL7g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eslint-plugin-react": {
|
||||
"version": "7.37.5",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz",
|
||||
|
|
@ -6829,11 +6841,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.4.6",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
||||
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@
|
|||
"@types/node": "^24.2.1",
|
||||
"@types/react": "^19.1.10",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@types/systeminformation": "^3.23.1",
|
||||
"@types/systeminformation": "^3.54.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.39.1",
|
||||
"@typescript-eslint/parser": "^8.39.1",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
|
|
@ -62,6 +62,7 @@
|
|||
"electron-vite": "^4.0.0",
|
||||
"eslint": "^9.33.0",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-no-comments": "^1.1.10",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export const UpdateDialog = ({
|
|||
onAccept,
|
||||
}: UpdateDialogProps) => (
|
||||
<Modal
|
||||
opened={true}
|
||||
opened
|
||||
onClose={onIgnore}
|
||||
title="Update Available"
|
||||
centered
|
||||
|
|
|
|||
|
|
@ -4,15 +4,23 @@ import { InfoTooltip } from '@/components/InfoTooltip';
|
|||
interface AdvancedTabProps {
|
||||
additionalArguments: string;
|
||||
serverOnly: boolean;
|
||||
noshift: boolean;
|
||||
flashattention: boolean;
|
||||
onAdditionalArgumentsChange: (args: string) => void;
|
||||
onServerOnlyChange: (serverOnly: boolean) => void;
|
||||
onNoshiftChange: (noshift: boolean) => void;
|
||||
onFlashattentionChange: (flashattention: boolean) => void;
|
||||
}
|
||||
|
||||
export const AdvancedTab = ({
|
||||
additionalArguments,
|
||||
serverOnly,
|
||||
noshift,
|
||||
flashattention,
|
||||
onAdditionalArgumentsChange,
|
||||
onServerOnlyChange,
|
||||
onNoshiftChange,
|
||||
onFlashattentionChange,
|
||||
}: AdvancedTabProps) => (
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
|
|
@ -41,5 +49,29 @@ export const AdvancedTab = ({
|
|||
<InfoTooltip label="In server-only mode, the KoboldAI Lite web UI won't be displayed. Use this if you'll be using your own frontend." />
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Group gap="xs" align="center">
|
||||
<Checkbox
|
||||
checked={!noshift}
|
||||
onChange={(event) => onNoshiftChange(!event.currentTarget.checked)}
|
||||
label="Use ContextShift"
|
||||
/>
|
||||
<InfoTooltip label="Use Context Shifting to reduce reprocessing. Recommended" />
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Group gap="xs" align="center">
|
||||
<Checkbox
|
||||
checked={flashattention}
|
||||
onChange={(event) =>
|
||||
onFlashattentionChange(event.currentTarget.checked)
|
||||
}
|
||||
label="Use FlashAttention"
|
||||
/>
|
||||
<InfoTooltip label="Enable flash attention for GGUF models." />
|
||||
</Group>
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,21 +6,32 @@ import {
|
|||
Button,
|
||||
Checkbox,
|
||||
Slider,
|
||||
Select,
|
||||
Badge,
|
||||
Card,
|
||||
} from '@mantine/core';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { File, Search } from 'lucide-react';
|
||||
import { InfoTooltip } from '@/components/InfoTooltip';
|
||||
import { getInputValidationState } from '@/utils/validation';
|
||||
import {
|
||||
isOldPcBinary,
|
||||
isNoCudaBinary,
|
||||
isRocmBinary,
|
||||
} from '@/utils/binaryUtils';
|
||||
|
||||
interface GeneralTabProps {
|
||||
modelPath: string;
|
||||
gpuLayers: number;
|
||||
autoGpuLayers: boolean;
|
||||
contextSize: number;
|
||||
backend: string;
|
||||
onModelPathChange: (path: string) => void;
|
||||
onSelectModelFile: () => void;
|
||||
onGpuLayersChange: (layers: number) => void;
|
||||
onAutoGpuLayersChange: (auto: boolean) => void;
|
||||
onContextSizeChange: (size: number) => void;
|
||||
onBackendChange: (backend: string) => void;
|
||||
}
|
||||
|
||||
export const GeneralTab = ({
|
||||
|
|
@ -28,12 +39,98 @@ export const GeneralTab = ({
|
|||
gpuLayers,
|
||||
autoGpuLayers,
|
||||
contextSize,
|
||||
backend,
|
||||
onModelPathChange,
|
||||
onSelectModelFile,
|
||||
onGpuLayersChange,
|
||||
onAutoGpuLayersChange,
|
||||
onContextSizeChange,
|
||||
onBackendChange,
|
||||
}: GeneralTabProps) => {
|
||||
const [availableBackends, setAvailableBackends] = useState<
|
||||
Array<{ value: string; label: string; devices?: string[] }>
|
||||
>([]);
|
||||
const [isLoadingBackends, setIsLoadingBackends] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const detectAvailableBackends = async () => {
|
||||
setIsLoadingBackends(true);
|
||||
|
||||
try {
|
||||
const [currentVersion, cpuCapabilities, gpuCapabilities] =
|
||||
await Promise.all([
|
||||
window.electronAPI.kobold.getCurrentVersion(),
|
||||
window.electronAPI.kobold.detectCPU(),
|
||||
window.electronAPI.kobold.detectGPUCapabilities(),
|
||||
]);
|
||||
|
||||
const backends: Array<{
|
||||
value: string;
|
||||
label: string;
|
||||
devices?: string[];
|
||||
}> = [];
|
||||
|
||||
if (currentVersion?.filename) {
|
||||
const filename = currentVersion.filename;
|
||||
|
||||
if (!isOldPcBinary(filename) && cpuCapabilities.avx2) {
|
||||
backends.push({ value: 'cpu', label: 'CPU' });
|
||||
}
|
||||
|
||||
if (!isNoCudaBinary(filename) && gpuCapabilities.cuda.supported) {
|
||||
backends.push({
|
||||
value: 'cuda',
|
||||
label: 'CUDA',
|
||||
devices: gpuCapabilities.cuda.devices,
|
||||
});
|
||||
}
|
||||
|
||||
if (isRocmBinary(filename) && gpuCapabilities.rocm.supported) {
|
||||
backends.push({
|
||||
value: 'rocm',
|
||||
label: 'ROCm',
|
||||
devices: gpuCapabilities.rocm.devices,
|
||||
});
|
||||
}
|
||||
|
||||
if (gpuCapabilities.vulkan.supported) {
|
||||
backends.push({
|
||||
value: 'vulkan',
|
||||
label: 'Vulkan',
|
||||
devices: gpuCapabilities.vulkan.devices,
|
||||
});
|
||||
}
|
||||
|
||||
if (gpuCapabilities.clblast.supported) {
|
||||
backends.push({
|
||||
value: 'clblast',
|
||||
label: 'CLBlast',
|
||||
devices: gpuCapabilities.clblast.devices,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
backends.push({ value: 'failsafe', label: 'Failsafe' });
|
||||
|
||||
setAvailableBackends(backends);
|
||||
|
||||
if (
|
||||
backends.length > 0 &&
|
||||
(!backend || !backends.some((b) => b.value === backend))
|
||||
) {
|
||||
onBackendChange(backends[0].value);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to detect available backends:', error);
|
||||
setAvailableBackends([]);
|
||||
} finally {
|
||||
setIsLoadingBackends(false);
|
||||
}
|
||||
};
|
||||
|
||||
void detectAvailableBackends();
|
||||
}, [backend, onBackendChange]);
|
||||
|
||||
const validationState = getInputValidationState(modelPath);
|
||||
|
||||
const getInputColor = () => {
|
||||
|
|
@ -97,6 +194,56 @@ export const GeneralTab = ({
|
|||
</Group>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Group gap="xs" align="center" mb="xs">
|
||||
<Text size="sm" fw={500}>
|
||||
Backend
|
||||
</Text>
|
||||
<InfoTooltip label="Select a backend to use. CUDA runs on Nvidia GPUs, and is much faster. ROCm is the AMD equivalent. Vulkan and CLBlast works on all GPUs but are somewhat slower." />
|
||||
</Group>
|
||||
<Select
|
||||
placeholder={
|
||||
isLoadingBackends
|
||||
? 'Detecting available backends...'
|
||||
: 'Select backend'
|
||||
}
|
||||
value={backend}
|
||||
onChange={(value) => value && onBackendChange(value)}
|
||||
data={availableBackends.map((b) => ({
|
||||
value: b.value,
|
||||
label: b.label,
|
||||
}))}
|
||||
disabled={isLoadingBackends || availableBackends.length === 0}
|
||||
/>
|
||||
{backend &&
|
||||
availableBackends.find((b) => b.value === backend)?.devices && (
|
||||
<Group gap="xs" mt="xs">
|
||||
<Text size="xs" c="dimmed">
|
||||
Devices:
|
||||
</Text>
|
||||
{availableBackends
|
||||
.find((b) => b.value === backend)
|
||||
?.devices?.map((device, index) => (
|
||||
<Badge key={index} variant="light" size="sm">
|
||||
{device}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
)}
|
||||
{backend === 'cpu' && (
|
||||
<Card withBorder p="sm" mt="xs" bg="blue.0">
|
||||
<Text size="sm" c="blue.8">
|
||||
<Text component="span" fw={600}>
|
||||
Performance Note:
|
||||
</Text>{' '}
|
||||
LLMs run significantly faster on GPU-accelerated systems. Consider
|
||||
using NVIDIA's CUDA or AMD's ROCm backends for optimal
|
||||
performance.
|
||||
</Text>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Group justify="space-between" align="center" mb="xs">
|
||||
<Group gap="xs" align="center">
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
import { Title, Text, Group, Button } from '@mantine/core';
|
||||
|
||||
interface LaunchHeaderProps {
|
||||
selectedFile: string | null;
|
||||
hasUnsavedChanges: boolean;
|
||||
modelPath: string;
|
||||
isLaunching: boolean;
|
||||
onLaunch: () => void;
|
||||
}
|
||||
|
||||
export const LaunchHeader = ({
|
||||
selectedFile,
|
||||
hasUnsavedChanges,
|
||||
modelPath,
|
||||
isLaunching,
|
||||
onLaunch,
|
||||
}: LaunchHeaderProps) => (
|
||||
<Group justify="space-between" align="center">
|
||||
<div>
|
||||
<Title order={3}>Launch Configuration</Title>
|
||||
<Text size="sm" c="dimmed">
|
||||
{selectedFile
|
||||
? `Using: ${selectedFile}`
|
||||
: 'No configuration file selected'}
|
||||
{hasUnsavedChanges && (
|
||||
<Text span c="orange">
|
||||
{' '}
|
||||
• Unsaved changes
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
<Button
|
||||
radius="md"
|
||||
disabled={!modelPath || isLaunching}
|
||||
onClick={onLaunch}
|
||||
loading={isLaunching}
|
||||
size="lg"
|
||||
variant="filled"
|
||||
>
|
||||
{isLaunching
|
||||
? 'Launching...'
|
||||
: modelPath
|
||||
? 'Launch KoboldCpp'
|
||||
: 'Select a model file to launch'}
|
||||
</Button>
|
||||
</Group>
|
||||
);
|
||||
|
|
@ -73,11 +73,7 @@ export const NetworkTab = ({
|
|||
</Group>
|
||||
|
||||
<div>
|
||||
<Text size="sm" fw={500} mb="md">
|
||||
Network Options
|
||||
</Text>
|
||||
<Stack gap="md">
|
||||
{/* First row: Multiuser and Multiplayer */}
|
||||
<Group gap="lg" align="flex-start" wrap="nowrap">
|
||||
<div style={{ minWidth: '250px' }}>
|
||||
<Group gap="xs" align="center">
|
||||
|
|
@ -106,7 +102,6 @@ export const NetworkTab = ({
|
|||
</div>
|
||||
</Group>
|
||||
|
||||
{/* Second row: Remote Tunnel and No Certify */}
|
||||
<Group gap="lg" align="flex-start" wrap="nowrap">
|
||||
<div style={{ minWidth: '250px' }}>
|
||||
<Group gap="xs" align="center">
|
||||
|
|
@ -135,7 +130,6 @@ export const NetworkTab = ({
|
|||
</div>
|
||||
</Group>
|
||||
|
||||
{/* Third row: WebSearch (single item) */}
|
||||
<Group gap="xs" align="center">
|
||||
<Checkbox
|
||||
checked={websearch}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Card, Text, Title, Loader, Stack, Container } from '@mantine/core';
|
||||
import {
|
||||
Card,
|
||||
Text,
|
||||
Title,
|
||||
Loader,
|
||||
Stack,
|
||||
Container,
|
||||
Badge,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { DownloadOptionCard } from '@/components/DownloadOptionCard';
|
||||
import {
|
||||
getPlatformDisplayName,
|
||||
|
|
@ -27,6 +36,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
|||
const [selectedROCm, setSelectedROCm] = useState<boolean>(false);
|
||||
const [userPlatform, setUserPlatform] = useState<string>('');
|
||||
const [hasAMDGPU, setHasAMDGPU] = useState<boolean>(false);
|
||||
const [hasROCm, setHasROCm] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [downloadProgress, setDownloadProgress] = useState(0);
|
||||
|
|
@ -57,12 +67,18 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
|||
try {
|
||||
const gpuInfo = await window.electronAPI.kobold.detectGPU();
|
||||
setHasAMDGPU(gpuInfo.hasAMD);
|
||||
|
||||
if (gpuInfo.hasAMD) {
|
||||
const rocmInfo = await window.electronAPI.kobold.detectROCm();
|
||||
setHasROCm(rocmInfo.supported);
|
||||
}
|
||||
} catch (gpuError) {
|
||||
console.warn(
|
||||
'GPU detection failed, proceeding without GPU info:',
|
||||
gpuError
|
||||
);
|
||||
setHasAMDGPU(false);
|
||||
setHasROCm(false);
|
||||
}
|
||||
|
||||
if (releaseData) {
|
||||
|
|
@ -196,6 +212,41 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
|||
|
||||
{filteredAssets.length > 0 || rocmDownload ? (
|
||||
<Stack gap="sm">
|
||||
{hasAMDGPU && !hasROCm && (
|
||||
<Card withBorder p="md" bg="orange.0">
|
||||
<Stack gap="xs">
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<Text fw={600} c="orange.9">
|
||||
AMD GPU Detected
|
||||
</Text>
|
||||
<Tooltip
|
||||
label="ROCm is not installed. Install ROCm for optimal AMD GPU performance with KoboldCpp."
|
||||
multiline
|
||||
w={220}
|
||||
>
|
||||
<Badge
|
||||
size="sm"
|
||||
color="orange"
|
||||
variant="filled"
|
||||
>
|
||||
ROCm Not Found
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Text size="sm" c="orange.8">
|
||||
For best performance with your AMD GPU, consider
|
||||
installing ROCm support.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{rocmDownload && hasAMDGPU && renderROCmCard()}
|
||||
|
||||
{sortAssetsByRecommendation(
|
||||
|
|
|
|||
|
|
@ -44,6 +44,9 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
|||
remotetunnel,
|
||||
nocertify,
|
||||
websearch,
|
||||
noshift,
|
||||
flashattention,
|
||||
backend,
|
||||
parseAndApplyConfigFile,
|
||||
loadSavedSettings,
|
||||
loadConfigFromFile,
|
||||
|
|
@ -61,6 +64,9 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
|||
handleRemotetunnelChange,
|
||||
handleNocertifyChange,
|
||||
handleWebsearchChange,
|
||||
handleNoshiftChange,
|
||||
handleFlashattentionChange,
|
||||
handleBackendChange,
|
||||
} = useLaunchConfig();
|
||||
|
||||
const loadConfigFiles = useCallback(async () => {
|
||||
|
|
@ -96,11 +102,9 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
|||
await parseAndApplyConfigFile(selectedConfig.path);
|
||||
}
|
||||
|
||||
// Reset unsaved changes when loading a new config
|
||||
setHasUnsavedChanges(false);
|
||||
};
|
||||
|
||||
// Wrapper functions to track changes
|
||||
const handleModelPathChangeWithTracking = (path: string) => {
|
||||
handleModelPathChange(path);
|
||||
setHasUnsavedChanges(true);
|
||||
|
|
@ -141,6 +145,16 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
|||
setHasUnsavedChanges(true);
|
||||
};
|
||||
|
||||
const handleNoshiftChangeWithTracking = (noshift: boolean) => {
|
||||
handleNoshiftChange(noshift);
|
||||
setHasUnsavedChanges(true);
|
||||
};
|
||||
|
||||
const handleFlashattentionChangeWithTracking = (flashattention: boolean) => {
|
||||
handleFlashattentionChange(flashattention);
|
||||
setHasUnsavedChanges(true);
|
||||
};
|
||||
|
||||
const handleMultiuserChangeWithTracking = (multiuser: boolean) => {
|
||||
handleMultiuserChange(multiuser);
|
||||
setHasUnsavedChanges(true);
|
||||
|
|
@ -166,6 +180,11 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
|||
setHasUnsavedChanges(true);
|
||||
};
|
||||
|
||||
const handleBackendChangeWithTracking = (backend: string) => {
|
||||
handleBackendChange(backend);
|
||||
setHasUnsavedChanges(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void loadConfigFiles();
|
||||
|
||||
|
|
@ -180,6 +199,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
|||
return cleanup;
|
||||
}, [loadConfigFiles]);
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const handleLaunch = async () => {
|
||||
if (isLaunching || !modelPath) {
|
||||
return;
|
||||
|
|
@ -234,6 +254,28 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
|||
args.push('--websearch');
|
||||
}
|
||||
|
||||
if (noshift) {
|
||||
args.push('--noshift');
|
||||
}
|
||||
|
||||
if (flashattention) {
|
||||
args.push('--flashattention');
|
||||
}
|
||||
|
||||
if (backend && backend !== 'cpu') {
|
||||
if (backend === 'cuda') {
|
||||
args.push('--usecuda');
|
||||
} else if (backend === 'rocm') {
|
||||
args.push('--usecuda');
|
||||
} else if (backend === 'vulkan') {
|
||||
args.push('--usevulkan');
|
||||
} else if (backend === 'clblast') {
|
||||
args.push('--useclblast');
|
||||
} else if (backend === 'failsafe') {
|
||||
args.push('--failsafe');
|
||||
}
|
||||
}
|
||||
|
||||
if (additionalArguments.trim()) {
|
||||
const additionalArgs = additionalArguments.trim().split(/\s+/);
|
||||
args.push(...additionalArgs);
|
||||
|
|
@ -285,11 +327,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
|||
size="lg"
|
||||
variant="filled"
|
||||
>
|
||||
{isLaunching
|
||||
? 'Launching...'
|
||||
: modelPath
|
||||
? 'Launch KoboldCpp'
|
||||
: 'Select a model file to launch'}
|
||||
{isLaunching ? 'Launching...' : 'Launch'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Card>
|
||||
|
|
@ -301,9 +339,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
|||
onFileSelection={handleFileSelection}
|
||||
onRefresh={loadConfigFiles}
|
||||
onSaveAsNew={() => setSaveModalOpened(true)}
|
||||
onUpdateCurrent={() => {
|
||||
// TODO: Implement update current configuration
|
||||
}}
|
||||
onUpdateCurrent={() => {}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
|
|
@ -323,11 +359,13 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
|||
gpuLayers={gpuLayers}
|
||||
autoGpuLayers={autoGpuLayers}
|
||||
contextSize={contextSize}
|
||||
backend={backend}
|
||||
onModelPathChange={handleModelPathChangeWithTracking}
|
||||
onSelectModelFile={handleSelectModelFile}
|
||||
onGpuLayersChange={handleGpuLayersChangeWithTracking}
|
||||
onAutoGpuLayersChange={handleAutoGpuLayersChangeWithTracking}
|
||||
onContextSizeChange={handleContextSizeChangeWithTracking}
|
||||
onBackendChange={handleBackendChangeWithTracking}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
|
|
@ -335,10 +373,16 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
|||
<AdvancedTab
|
||||
additionalArguments={additionalArguments}
|
||||
serverOnly={serverOnly}
|
||||
noshift={noshift}
|
||||
flashattention={flashattention}
|
||||
onAdditionalArgumentsChange={
|
||||
handleAdditionalArgumentsChangeWithTracking
|
||||
}
|
||||
onServerOnlyChange={handleServerOnlyChangeWithTracking}
|
||||
onNoshiftChange={handleNoshiftChangeWithTracking}
|
||||
onFlashattentionChange={
|
||||
handleFlashattentionChangeWithTracking
|
||||
}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
|
|
@ -379,7 +423,6 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
|||
configName={newConfigName}
|
||||
onConfigNameChange={setNewConfigName}
|
||||
onSave={() => {
|
||||
// TODO: Implement save configuration
|
||||
setSaveModalOpened(false);
|
||||
setNewConfigName('');
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ export const VersionsTab = () => {
|
|||
installedVersion?.version ||
|
||||
latestRelease?.tag_name.replace(/^v/, '') ||
|
||||
'unknown',
|
||||
size: asset.size,
|
||||
size: installedVersion?.size || asset.size,
|
||||
isInstalled: Boolean(installedVersion),
|
||||
isCurrent,
|
||||
downloadUrl: asset.browser_download_url,
|
||||
|
|
@ -175,6 +175,7 @@ export const VersionsTab = () => {
|
|||
versions.push({
|
||||
name: installed.filename,
|
||||
version: installed.version,
|
||||
size: installed.size,
|
||||
isInstalled: true,
|
||||
isCurrent,
|
||||
installedPath: installed.path,
|
||||
|
|
@ -207,21 +208,26 @@ export const VersionsTab = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleVersionSelect = async (version: VersionInfo) => {
|
||||
const makeCurrent = async (version: VersionInfo) => {
|
||||
if (!version.installedPath) return;
|
||||
|
||||
try {
|
||||
const filename = version.installedPath.split('/').pop() || '';
|
||||
const success =
|
||||
await window.electronAPI.kobold.setCurrentVersion(filename);
|
||||
const success = await window.electronAPI.kobold.setCurrentVersion(
|
||||
version.installedPath
|
||||
);
|
||||
|
||||
if (success) {
|
||||
await window.electronAPI.config.set('currentKoboldBinary', filename);
|
||||
await window.electronAPI.config.set(
|
||||
'currentKoboldVersion',
|
||||
version.version
|
||||
'currentKoboldBinary',
|
||||
version.installedPath
|
||||
);
|
||||
await loadInstalledVersions();
|
||||
|
||||
const newCurrentVersion = installedVersions.find(
|
||||
(v) => v.path === version.installedPath
|
||||
);
|
||||
if (newCurrentVersion) {
|
||||
setCurrentVersion(newCurrentVersion);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to set current version:', error);
|
||||
|
|
@ -299,11 +305,6 @@ export const VersionsTab = () => {
|
|||
Current
|
||||
</Badge>
|
||||
)}
|
||||
{version.isInstalled && (
|
||||
<Badge variant="light" color="green" size="sm">
|
||||
Installed
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
<Text size="xs" c="dimmed">
|
||||
Version {version.version}
|
||||
|
|
@ -339,10 +340,7 @@ export const VersionsTab = () => {
|
|||
<Button
|
||||
variant="light"
|
||||
size="xs"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleVersionSelect(version);
|
||||
}}
|
||||
onClick={() => makeCurrent(version)}
|
||||
>
|
||||
Make Current
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -5,12 +5,16 @@ export const CONFIG_FILE_NAME = 'config.json';
|
|||
export const GITHUB_API = {
|
||||
BASE_URL: 'https://api.github.com',
|
||||
KOBOLDCPP_REPO: 'LostRuins/koboldcpp',
|
||||
KOBOLDCPP_ROCM_REPO: 'YellowRoseCx/koboldcpp-rocm',
|
||||
get LATEST_RELEASE_URL() {
|
||||
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_REPO}/releases/latest`;
|
||||
},
|
||||
get ALL_RELEASES_URL() {
|
||||
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_REPO}/releases`;
|
||||
},
|
||||
get ROCM_LATEST_RELEASE_URL() {
|
||||
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_ROCM_REPO}/releases/latest`;
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const ASSET_SUFFIXES = {
|
||||
|
|
@ -27,6 +31,5 @@ export const KOBOLDAI_URLS = {
|
|||
export const ROCM = {
|
||||
BINARY_NAME: 'koboldcpp-linux-x64-rocm',
|
||||
DOWNLOAD_URL: KOBOLDAI_URLS.ROCM_DOWNLOAD,
|
||||
ERROR_MESSAGE: 'ROCm version is only available for Linux',
|
||||
SIZE_BYTES: 1024 * 1024 * 1024, // 1GB
|
||||
SIZE_BYTES: 1024 * 1024 * 1024,
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ export const useLaunchConfig = () => {
|
|||
const [remotetunnel, setRemotetunnel] = useState<boolean>(false);
|
||||
const [nocertify, setNocertify] = useState<boolean>(false);
|
||||
const [websearch, setWebsearch] = useState<boolean>(false);
|
||||
const [noshift, setNoshift] = useState<boolean>(false);
|
||||
const [flashattention, setFlashattention] = useState<boolean>(false);
|
||||
const [backend, setBackend] = useState<string>('');
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const parseAndApplyConfigFile = useCallback(async (configPath: string) => {
|
||||
|
|
@ -78,6 +81,24 @@ export const useLaunchConfig = () => {
|
|||
} else {
|
||||
setWebsearch(false);
|
||||
}
|
||||
|
||||
if (typeof configData.noshift === 'boolean') {
|
||||
setNoshift(configData.noshift);
|
||||
} else {
|
||||
setNoshift(false);
|
||||
}
|
||||
|
||||
if (typeof configData.flashattention === 'boolean') {
|
||||
setFlashattention(configData.flashattention);
|
||||
} else {
|
||||
setFlashattention(false);
|
||||
}
|
||||
|
||||
if (typeof configData.backend === 'string') {
|
||||
setBackend(configData.backend);
|
||||
} else {
|
||||
setBackend('');
|
||||
}
|
||||
} else {
|
||||
setGpuLayers(0);
|
||||
setContextSize(2048);
|
||||
|
|
@ -88,6 +109,9 @@ export const useLaunchConfig = () => {
|
|||
setRemotetunnel(false);
|
||||
setNocertify(false);
|
||||
setWebsearch(false);
|
||||
setNoshift(false);
|
||||
setFlashattention(false);
|
||||
setBackend('');
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
|
@ -95,7 +119,7 @@ export const useLaunchConfig = () => {
|
|||
const savedServerOnly = await window.electronAPI.config.getServerOnly();
|
||||
|
||||
setServerOnly(savedServerOnly);
|
||||
setModelPath(''); // Model path comes from config file, not saved settings
|
||||
setModelPath('');
|
||||
setGpuLayers(0);
|
||||
setContextSize(2048);
|
||||
setPort(5001);
|
||||
|
|
@ -105,6 +129,9 @@ export const useLaunchConfig = () => {
|
|||
setRemotetunnel(false);
|
||||
setNocertify(false);
|
||||
setWebsearch(false);
|
||||
setNoshift(false);
|
||||
setFlashattention(false);
|
||||
setBackend('');
|
||||
}, []);
|
||||
|
||||
const loadConfigFromFile = useCallback(
|
||||
|
|
@ -202,6 +229,18 @@ export const useLaunchConfig = () => {
|
|||
setWebsearch(checked);
|
||||
}, []);
|
||||
|
||||
const handleNoshiftChange = useCallback((checked: boolean) => {
|
||||
setNoshift(checked);
|
||||
}, []);
|
||||
|
||||
const handleFlashattentionChange = useCallback((checked: boolean) => {
|
||||
setFlashattention(checked);
|
||||
}, []);
|
||||
|
||||
const handleBackendChange = useCallback((backend: string) => {
|
||||
setBackend(backend);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
serverOnly,
|
||||
gpuLayers,
|
||||
|
|
@ -216,6 +255,9 @@ export const useLaunchConfig = () => {
|
|||
remotetunnel,
|
||||
nocertify,
|
||||
websearch,
|
||||
noshift,
|
||||
flashattention,
|
||||
backend,
|
||||
|
||||
parseAndApplyConfigFile,
|
||||
loadSavedSettings,
|
||||
|
|
@ -234,5 +276,8 @@ export const useLaunchConfig = () => {
|
|||
handleRemotetunnelChange,
|
||||
handleNocertifyChange,
|
||||
handleWebsearchChange,
|
||||
handleNoshiftChange,
|
||||
handleFlashattentionChange,
|
||||
handleBackendChange,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { dialog } from 'electron';
|
|||
import { GitHubService } from '@/main/services/GitHubService';
|
||||
import { ConfigManager } from '@/main/managers/ConfigManager';
|
||||
import { WindowManager } from '@/main/managers/WindowManager';
|
||||
import { ROCM } from '@/constants';
|
||||
import { ROCM, GITHUB_API } from '@/constants';
|
||||
|
||||
interface GitHubAsset {
|
||||
name: string;
|
||||
|
|
@ -49,6 +49,7 @@ export interface InstalledVersion {
|
|||
version: string;
|
||||
path: string;
|
||||
filename: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export class KoboldCppManager {
|
||||
|
|
@ -161,17 +162,23 @@ export class KoboldCppManager {
|
|||
});
|
||||
|
||||
if (!includeVersions) {
|
||||
return koboldFiles.map((file) => ({
|
||||
version: 'unknown',
|
||||
path: join(this.installDir, file),
|
||||
filename: file,
|
||||
}));
|
||||
return koboldFiles.map((file) => {
|
||||
const filePath = join(this.installDir, file);
|
||||
const stats = statSync(filePath);
|
||||
return {
|
||||
version: 'unknown',
|
||||
path: filePath,
|
||||
filename: file,
|
||||
size: stats.size,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const versionPromises = koboldFiles.map(async (file) => {
|
||||
const filePath = join(this.installDir, file);
|
||||
|
||||
try {
|
||||
const stats = statSync(filePath);
|
||||
const detectedVersion = await this.getVersionFromBinary(filePath);
|
||||
const version = detectedVersion || 'unknown';
|
||||
|
||||
|
|
@ -179,6 +186,7 @@ export class KoboldCppManager {
|
|||
version,
|
||||
path: filePath,
|
||||
filename: file,
|
||||
size: stats.size,
|
||||
} as InstalledVersion;
|
||||
} catch (error) {
|
||||
console.warn(`Could not detect version for ${file}:`, error);
|
||||
|
|
@ -195,6 +203,7 @@ export class KoboldCppManager {
|
|||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getConfigFiles(): Promise<
|
||||
Array<{ name: string; path: string; size: number }>
|
||||
> {
|
||||
|
|
@ -306,12 +315,9 @@ export class KoboldCppManager {
|
|||
return null;
|
||||
}
|
||||
|
||||
async setCurrentVersion(version: string): Promise<boolean> {
|
||||
const versions = await this.getInstalledVersions();
|
||||
const targetVersion = versions.find((v) => v.version === version);
|
||||
|
||||
if (targetVersion && existsSync(targetVersion.path)) {
|
||||
this.configManager.setCurrentKoboldBinary(targetVersion.path);
|
||||
async setCurrentVersion(binaryPath: string): Promise<boolean> {
|
||||
if (existsSync(binaryPath)) {
|
||||
this.configManager.setCurrentKoboldBinary(binaryPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -376,7 +382,7 @@ export class KoboldCppManager {
|
|||
try {
|
||||
process.kill('SIGTERM');
|
||||
} catch {
|
||||
// ignore
|
||||
void 0;
|
||||
}
|
||||
resolve(null);
|
||||
}, 10000);
|
||||
|
|
@ -479,19 +485,45 @@ export class KoboldCppManager {
|
|||
version?: string;
|
||||
} | null> {
|
||||
const platform = process.platform;
|
||||
if (platform !== 'linux') {
|
||||
return null;
|
||||
|
||||
if (platform === 'linux') {
|
||||
const latestRelease = await this.githubService.getLatestRelease();
|
||||
const version = latestRelease?.tag_name?.replace(/^v/, '') || 'unknown';
|
||||
|
||||
return {
|
||||
name: ROCM.BINARY_NAME,
|
||||
url: ROCM.DOWNLOAD_URL,
|
||||
size: ROCM.SIZE_BYTES,
|
||||
version,
|
||||
};
|
||||
} else if (platform === 'win32') {
|
||||
try {
|
||||
const response = await fetch(GITHUB_API.ROCM_LATEST_RELEASE_URL);
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const release = await response.json();
|
||||
const rocmAsset = release.assets?.find(
|
||||
(asset: GitHubAsset) =>
|
||||
asset.name.endsWith('rocm.exe') &&
|
||||
!asset.name.includes('rocm_b2.exe')
|
||||
);
|
||||
|
||||
if (rocmAsset) {
|
||||
return {
|
||||
name: rocmAsset.name,
|
||||
url: rocmAsset.browser_download_url,
|
||||
size: rocmAsset.size,
|
||||
version: release.tag_name?.replace(/^v/, '') || 'unknown',
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch Windows ROCm release:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const latestRelease = await this.githubService.getLatestRelease();
|
||||
const version = latestRelease?.tag_name?.replace(/^v/, '') || 'unknown';
|
||||
|
||||
return {
|
||||
name: ROCM.BINARY_NAME,
|
||||
url: ROCM.DOWNLOAD_URL,
|
||||
size: ROCM.SIZE_BYTES,
|
||||
version,
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
async downloadROCm(onProgress?: (progress: number) => void): Promise<{
|
||||
|
|
@ -500,15 +532,15 @@ export class KoboldCppManager {
|
|||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const platform = process.platform;
|
||||
if (platform !== 'linux') {
|
||||
const rocmInfo = await this.getROCmDownload();
|
||||
if (!rocmInfo) {
|
||||
return {
|
||||
success: false,
|
||||
error: ROCM.ERROR_MESSAGE,
|
||||
error: 'ROCm version not available for this platform',
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch(ROCM.DOWNLOAD_URL);
|
||||
const response = await fetch(rocmInfo.url);
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
|
|
@ -516,7 +548,7 @@ export class KoboldCppManager {
|
|||
};
|
||||
}
|
||||
|
||||
const totalBytes = ROCM.SIZE_BYTES;
|
||||
const totalBytes = rocmInfo.size;
|
||||
let downloadedBytes = 0;
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
|
|
@ -527,7 +559,7 @@ export class KoboldCppManager {
|
|||
};
|
||||
}
|
||||
|
||||
const filePath = join(this.installDir, ROCM.BINARY_NAME);
|
||||
const filePath = join(this.installDir, rocmInfo.name);
|
||||
const writer = createWriteStream(filePath);
|
||||
|
||||
try {
|
||||
|
|
@ -676,13 +708,11 @@ export class KoboldCppManager {
|
|||
|
||||
const child = spawn(currentVersion.path, finalArgs, {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
detached: true,
|
||||
detached: false,
|
||||
});
|
||||
|
||||
this.koboldProcess = child;
|
||||
|
||||
child.unref();
|
||||
|
||||
const mainWindow = this.windowManager.getMainWindow();
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
const commandLine = `$ ${currentVersion.path} ${finalArgs.join(' ')}\n${'─'.repeat(60)}\n`;
|
||||
|
|
@ -737,23 +767,12 @@ export class KoboldCppManager {
|
|||
stopKoboldCpp(): void {
|
||||
if (this.koboldProcess) {
|
||||
try {
|
||||
// For detached processes, we need to kill the process group
|
||||
if (process.platform !== 'win32') {
|
||||
// On Unix-like systems, kill the process group
|
||||
process.kill(-this.koboldProcess.pid!, 'SIGTERM');
|
||||
} else {
|
||||
// On Windows, just kill the process
|
||||
this.koboldProcess.kill('SIGTERM');
|
||||
}
|
||||
this.koboldProcess.kill('SIGTERM');
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.koboldProcess && !this.koboldProcess.killed) {
|
||||
try {
|
||||
if (process.platform !== 'win32') {
|
||||
process.kill(-this.koboldProcess.pid!, 'SIGKILL');
|
||||
} else {
|
||||
this.koboldProcess.kill('SIGKILL');
|
||||
}
|
||||
this.koboldProcess.kill('SIGKILL');
|
||||
} catch (error) {
|
||||
console.warn('Error force-killing KoboldCpp process:', error);
|
||||
}
|
||||
|
|
@ -785,25 +804,14 @@ export class KoboldCppManager {
|
|||
this.koboldProcess.once('error', cleanup);
|
||||
|
||||
try {
|
||||
// For detached processes, we need to handle cleanup differently
|
||||
if (process.platform !== 'win32') {
|
||||
// On Unix-like systems, kill the process group
|
||||
process.kill(-this.koboldProcess.pid!, 'SIGTERM');
|
||||
} else {
|
||||
// On Windows, just kill the process
|
||||
this.koboldProcess.kill('SIGTERM');
|
||||
}
|
||||
this.koboldProcess.kill('SIGTERM');
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.koboldProcess && !this.koboldProcess.killed) {
|
||||
try {
|
||||
if (process.platform !== 'win32') {
|
||||
process.kill(-this.koboldProcess.pid!, 'SIGKILL');
|
||||
} else {
|
||||
this.koboldProcess.kill('SIGKILL');
|
||||
}
|
||||
this.koboldProcess.kill('SIGKILL');
|
||||
} catch {
|
||||
// Ignore errors on force kill
|
||||
void 0;
|
||||
}
|
||||
}
|
||||
cleanup();
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import { ConfigManager } from './ConfigManager';
|
|||
export class WindowManager {
|
||||
private mainWindow: BrowserWindow | null = null;
|
||||
private tray: Tray | null = null;
|
||||
private isQuitting = false;
|
||||
private configManager: ConfigManager;
|
||||
|
||||
constructor(configManager: ConfigManager) {
|
||||
|
|
@ -46,10 +45,13 @@ export class WindowManager {
|
|||
this.mainWindow = null;
|
||||
});
|
||||
|
||||
// Allow navigation to localhost URLs for iframe content
|
||||
const minimizeToTray = this.configManager.get('minimizeToTray') === true;
|
||||
if (minimizeToTray && !this.tray) {
|
||||
this.createSystemTray();
|
||||
}
|
||||
|
||||
this.mainWindow.webContents.on('will-navigate', (event, navigationUrl) => {
|
||||
const url = new URL(navigationUrl);
|
||||
// Only allow navigation to localhost or the app's origin
|
||||
if (
|
||||
url.hostname !== 'localhost' &&
|
||||
url.hostname !== '127.0.0.1' &&
|
||||
|
|
@ -59,46 +61,39 @@ export class WindowManager {
|
|||
}
|
||||
});
|
||||
|
||||
// Handle iframe navigation permissions
|
||||
this.mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
const parsedUrl = new URL(url);
|
||||
// Allow localhost URLs to open in the same window/iframe
|
||||
|
||||
if (
|
||||
parsedUrl.hostname === 'localhost' ||
|
||||
parsedUrl.hostname === '127.0.0.1'
|
||||
) {
|
||||
return { action: 'allow' };
|
||||
}
|
||||
// For other URLs, open in external browser
|
||||
|
||||
shell.openExternal(url);
|
||||
|
||||
return { action: 'deny' };
|
||||
});
|
||||
|
||||
this.mainWindow.on('close', async (event) => {
|
||||
if (!this.isQuitting) {
|
||||
const minimizeToTray =
|
||||
this.configManager.get('minimizeToTray') === true;
|
||||
|
||||
if (minimizeToTray) {
|
||||
event.preventDefault();
|
||||
this.mainWindow?.hide();
|
||||
|
||||
if (!this.tray) {
|
||||
this.createSystemTray();
|
||||
}
|
||||
}
|
||||
}
|
||||
this.mainWindow.on('close', () => {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
this.mainWindow.on('minimize', () => {
|
||||
const minimizeToTray = this.configManager.get('minimizeToTray') === true;
|
||||
|
||||
if (minimizeToTray) {
|
||||
this.mainWindow?.hide();
|
||||
|
||||
if (!this.tray) {
|
||||
this.createSystemTray();
|
||||
}
|
||||
|
||||
if (this.tray) {
|
||||
setTimeout(() => {
|
||||
this.mainWindow?.setSkipTaskbar(true);
|
||||
this.mainWindow?.hide();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -111,41 +106,59 @@ export class WindowManager {
|
|||
}
|
||||
|
||||
private createSystemTray() {
|
||||
const iconPath = join(app.getAppPath(), 'assets', 'icon.png');
|
||||
this.tray = new Tray(nativeImage.createFromPath(iconPath));
|
||||
try {
|
||||
const iconPath = join(app.getAppPath(), 'assets', 'icon.png');
|
||||
|
||||
this.tray.setToolTip('Friendly Kobold');
|
||||
|
||||
const trayMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Show',
|
||||
click: () => {
|
||||
this.mainWindow?.show();
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Quit',
|
||||
click: () => {
|
||||
this.isQuitting = true;
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
this.tray.setContextMenu(trayMenu);
|
||||
|
||||
this.tray.on('click', () => {
|
||||
this.mainWindow?.show();
|
||||
});
|
||||
|
||||
this.tray.on('double-click', () => {
|
||||
if (this.mainWindow?.isVisible()) {
|
||||
this.mainWindow.hide();
|
||||
const image = nativeImage.createFromPath(iconPath);
|
||||
if (!image.isEmpty()) {
|
||||
const resizedImage = image.resize({ width: 16, height: 16 });
|
||||
this.tray = new Tray(resizedImage);
|
||||
} else {
|
||||
this.mainWindow?.show();
|
||||
this.tray = new Tray(image);
|
||||
}
|
||||
});
|
||||
|
||||
this.tray.setToolTip('Friendly Kobold');
|
||||
|
||||
const trayMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Show',
|
||||
click: () => {
|
||||
this.mainWindow?.setSkipTaskbar(false);
|
||||
this.mainWindow?.show();
|
||||
this.mainWindow?.focus();
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Quit',
|
||||
click: () => {
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
this.tray.setContextMenu(trayMenu);
|
||||
|
||||
this.tray.on('click', () => {
|
||||
this.mainWindow?.setSkipTaskbar(false);
|
||||
this.mainWindow?.show();
|
||||
this.mainWindow?.focus();
|
||||
});
|
||||
|
||||
this.tray.on('double-click', () => {
|
||||
if (this.mainWindow?.isVisible()) {
|
||||
this.mainWindow.setSkipTaskbar(true);
|
||||
this.mainWindow.hide();
|
||||
} else {
|
||||
this.mainWindow?.setSkipTaskbar(false);
|
||||
this.mainWindow?.show();
|
||||
this.mainWindow?.focus();
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Failed to create system tray:', error);
|
||||
this.tray = null;
|
||||
}
|
||||
}
|
||||
|
||||
public cleanup() {
|
||||
|
|
@ -160,6 +173,17 @@ export class WindowManager {
|
|||
}
|
||||
}
|
||||
|
||||
public ensureTrayExists() {
|
||||
const minimizeToTray = this.configManager.get('minimizeToTray') === true;
|
||||
if (minimizeToTray && !this.tray) {
|
||||
this.createSystemTray();
|
||||
} else if (!minimizeToTray && this.tray) {
|
||||
this.tray.removeAllListeners();
|
||||
this.tray.destroy();
|
||||
this.tray = null;
|
||||
}
|
||||
}
|
||||
|
||||
private setupContextMenu() {
|
||||
if (!this.mainWindow) return;
|
||||
|
||||
|
|
@ -204,7 +228,6 @@ export class WindowManager {
|
|||
label: 'Quit',
|
||||
accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Ctrl+Q',
|
||||
click: () => {
|
||||
this.isQuitting = true;
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
|
|
@ -265,18 +288,27 @@ export class WindowManager {
|
|||
{
|
||||
label: 'Help',
|
||||
submenu: [
|
||||
{
|
||||
label: 'About',
|
||||
click: async () => {
|
||||
await this.showAboutDialog();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'KoboldCpp Wiki',
|
||||
click: () => {
|
||||
shell.openExternal('https://github.com/LostRuins/koboldcpp/wiki');
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'View on GitHub',
|
||||
click: () => {
|
||||
shell.openExternal(
|
||||
'https://github.com/lone-cloud/friendly-kobold'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'About',
|
||||
click: async () => {
|
||||
await this.showAboutDialog();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
import si from 'systeminformation';
|
||||
|
||||
export interface CPUCapabilities {
|
||||
avx: boolean;
|
||||
avx2: boolean;
|
||||
cpuInfo: string[];
|
||||
}
|
||||
|
||||
export class CPUService {
|
||||
async detectCPUCapabilities(): Promise<CPUCapabilities> {
|
||||
try {
|
||||
const [cpu, flags] = await Promise.all([si.cpu(), si.cpuFlags()]);
|
||||
|
||||
const cpuInfo: string[] = [];
|
||||
if (cpu.brand) {
|
||||
cpuInfo.push(cpu.brand);
|
||||
}
|
||||
if (cpu.cores) {
|
||||
cpuInfo.push(`${cpu.cores} cores`);
|
||||
}
|
||||
if (cpu.speed) {
|
||||
cpuInfo.push(`${cpu.speed}GHz`);
|
||||
}
|
||||
|
||||
const avx = flags.includes('avx') || flags.includes('AVX');
|
||||
const avx2 = flags.includes('avx2') || flags.includes('AVX2');
|
||||
|
||||
return {
|
||||
avx,
|
||||
avx2,
|
||||
cpuInfo: cpuInfo.length > 0 ? cpuInfo : ['CPU information unavailable'],
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('CPU detection failed:', error);
|
||||
return {
|
||||
avx: false,
|
||||
avx2: false,
|
||||
cpuInfo: ['CPU detection failed'],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,335 +0,0 @@
|
|||
import si from 'systeminformation';
|
||||
|
||||
export interface GPUCapabilities {
|
||||
cuda: {
|
||||
supported: boolean;
|
||||
devices: string[];
|
||||
};
|
||||
rocm: {
|
||||
supported: boolean;
|
||||
devices: string[];
|
||||
};
|
||||
vulkan: {
|
||||
supported: boolean;
|
||||
devices: string[];
|
||||
};
|
||||
clblast: {
|
||||
supported: boolean;
|
||||
devices: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export class GPUService {
|
||||
async detectGPU(): Promise<{
|
||||
hasAMD: boolean;
|
||||
hasNVIDIA: boolean;
|
||||
gpuInfo: string[];
|
||||
}> {
|
||||
try {
|
||||
const graphics = await si.graphics();
|
||||
|
||||
let hasAMD = false;
|
||||
let hasNVIDIA = false;
|
||||
const gpuInfo: string[] = [];
|
||||
|
||||
for (const controller of graphics.controllers) {
|
||||
if (controller.model) {
|
||||
gpuInfo.push(controller.model);
|
||||
}
|
||||
|
||||
const vendor = controller.vendor?.toLowerCase() || '';
|
||||
const model = controller.model?.toLowerCase() || '';
|
||||
|
||||
if (
|
||||
vendor.includes('amd') ||
|
||||
vendor.includes('ati') ||
|
||||
model.includes('radeon') ||
|
||||
model.includes('amd')
|
||||
) {
|
||||
hasAMD = true;
|
||||
}
|
||||
|
||||
if (
|
||||
vendor.includes('nvidia') ||
|
||||
model.includes('nvidia') ||
|
||||
model.includes('geforce') ||
|
||||
model.includes('gtx') ||
|
||||
model.includes('rtx')
|
||||
) {
|
||||
hasNVIDIA = true;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
hasAMD,
|
||||
hasNVIDIA,
|
||||
gpuInfo:
|
||||
gpuInfo.length > 0 ? gpuInfo : ['No GPU information available'],
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('GPU detection failed:', error);
|
||||
return {
|
||||
hasAMD: false,
|
||||
hasNVIDIA: false,
|
||||
gpuInfo: ['GPU detection failed'],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async detectGPUCapabilities(): Promise<GPUCapabilities> {
|
||||
const [cuda, rocm, vulkan, clblast] = await Promise.all([
|
||||
this.detectCUDA(),
|
||||
this.detectROCm(),
|
||||
this.detectVulkan(),
|
||||
this.detectCLBlast(),
|
||||
]);
|
||||
|
||||
return { cuda, rocm, vulkan, clblast };
|
||||
}
|
||||
|
||||
private async detectCUDA(): Promise<{
|
||||
supported: boolean;
|
||||
devices: string[];
|
||||
}> {
|
||||
try {
|
||||
const { spawn } = await import('child_process');
|
||||
const nvidia = spawn(
|
||||
'nvidia-smi',
|
||||
['--query-gpu=name,memory.total,memory.free', '--format=csv,noheader'],
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
|
||||
let output = '';
|
||||
nvidia.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
nvidia.on('close', (code) => {
|
||||
if (code === 0 && output.trim()) {
|
||||
const devices = output
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
const parts = line.split(',');
|
||||
return parts[0]?.trim() || 'Unknown NVIDIA GPU';
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
resolve({
|
||||
supported: devices.length > 0,
|
||||
devices,
|
||||
});
|
||||
} else {
|
||||
resolve({ supported: false, devices: [] });
|
||||
}
|
||||
});
|
||||
|
||||
nvidia.on('error', () => {
|
||||
resolve({ supported: false, devices: [] });
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
nvidia.kill('SIGTERM');
|
||||
} catch {
|
||||
// Process already terminated
|
||||
}
|
||||
resolve({ supported: false, devices: [] });
|
||||
}, 5000);
|
||||
});
|
||||
} catch {
|
||||
return { supported: false, devices: [] };
|
||||
}
|
||||
}
|
||||
|
||||
private async detectROCm(): Promise<{
|
||||
supported: boolean;
|
||||
devices: string[];
|
||||
}> {
|
||||
try {
|
||||
const { spawn } = await import('child_process');
|
||||
const rocminfo = spawn('rocminfo', [], { timeout: 5000 });
|
||||
|
||||
let output = '';
|
||||
rocminfo.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
rocminfo.on('close', (code) => {
|
||||
if (code === 0 && output.trim()) {
|
||||
const devices: string[] = [];
|
||||
const lines = output.split('\n');
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (line.includes('Marketing Name:')) {
|
||||
const name = line.split('Marketing Name:')[1]?.trim();
|
||||
if (name && !name.includes('CPU')) {
|
||||
devices.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolve({
|
||||
supported: devices.length > 0,
|
||||
devices,
|
||||
});
|
||||
} else {
|
||||
resolve({ supported: false, devices: [] });
|
||||
}
|
||||
});
|
||||
|
||||
rocminfo.on('error', () => {
|
||||
resolve({ supported: false, devices: [] });
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
rocminfo.kill('SIGTERM');
|
||||
} catch {
|
||||
// Process already terminated
|
||||
}
|
||||
resolve({ supported: false, devices: [] });
|
||||
}, 5000);
|
||||
});
|
||||
} catch {
|
||||
return { supported: false, devices: [] };
|
||||
}
|
||||
}
|
||||
|
||||
private async detectVulkan(): Promise<{
|
||||
supported: boolean;
|
||||
devices: string[];
|
||||
}> {
|
||||
try {
|
||||
const { spawn } = await import('child_process');
|
||||
const vulkaninfo = spawn('vulkaninfo', ['--summary'], { timeout: 5000 });
|
||||
|
||||
let output = '';
|
||||
vulkaninfo.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
vulkaninfo.on('close', (code) => {
|
||||
if (code === 0 && output.trim()) {
|
||||
const devices: string[] = [];
|
||||
const lines = output.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes('deviceName')) {
|
||||
const name = line.split('=')[1]?.trim();
|
||||
if (name) {
|
||||
devices.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolve({
|
||||
supported: devices.length > 0,
|
||||
devices,
|
||||
});
|
||||
} else {
|
||||
resolve({ supported: false, devices: [] });
|
||||
}
|
||||
});
|
||||
|
||||
vulkaninfo.on('error', () => {
|
||||
resolve({ supported: false, devices: [] });
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
vulkaninfo.kill('SIGTERM');
|
||||
} catch {
|
||||
// Process already terminated
|
||||
}
|
||||
resolve({ supported: false, devices: [] });
|
||||
}, 5000);
|
||||
});
|
||||
} catch {
|
||||
return { supported: false, devices: [] };
|
||||
}
|
||||
}
|
||||
|
||||
private async detectCLBlast(): Promise<{
|
||||
supported: boolean;
|
||||
devices: string[];
|
||||
}> {
|
||||
try {
|
||||
const { spawn } = await import('child_process');
|
||||
const clinfo = spawn('clinfo', ['--json'], { timeout: 5000 });
|
||||
|
||||
let output = '';
|
||||
clinfo.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
clinfo.on('close', (code) => {
|
||||
if (code === 0 && output.trim()) {
|
||||
try {
|
||||
const data = JSON.parse(output);
|
||||
const devices: string[] = [];
|
||||
|
||||
if (data.platforms) {
|
||||
for (const platform of data.platforms) {
|
||||
if (platform.devices) {
|
||||
for (const device of platform.devices) {
|
||||
if (device.name && device.type !== 'CPU') {
|
||||
devices.push(device.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolve({
|
||||
supported: devices.length > 0,
|
||||
devices,
|
||||
});
|
||||
} catch {
|
||||
// Failed to parse JSON, but clinfo ran successfully
|
||||
// Try to extract device names from text output
|
||||
const lines = output.split('\n');
|
||||
const devices: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes('Device Name') && !line.includes('CPU')) {
|
||||
const name = line.split(':')[1]?.trim();
|
||||
if (name) {
|
||||
devices.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolve({
|
||||
supported: devices.length > 0,
|
||||
devices,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
resolve({ supported: false, devices: [] });
|
||||
}
|
||||
});
|
||||
|
||||
clinfo.on('error', () => {
|
||||
resolve({ supported: false, devices: [] });
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
clinfo.kill('SIGTERM');
|
||||
} catch {
|
||||
// Process already terminated
|
||||
}
|
||||
resolve({ supported: false, devices: [] });
|
||||
}, 5000);
|
||||
});
|
||||
} catch {
|
||||
return { supported: false, devices: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ import { GITHUB_API } from '@/constants';
|
|||
|
||||
export class GitHubService {
|
||||
private lastApiCall = 0;
|
||||
private apiCooldown = 60000; // 1 minute cooldown
|
||||
private apiCooldown = 60000;
|
||||
private cachedRelease: GitHubRelease | null = null;
|
||||
private cachedReleases: GitHubRelease[] = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ export class HardwareService {
|
|||
try {
|
||||
nvidia.kill('SIGTERM');
|
||||
} catch {
|
||||
// Process already terminated
|
||||
void 0;
|
||||
}
|
||||
resolve({ supported: false, devices: [] });
|
||||
}, 5000);
|
||||
|
|
@ -175,7 +175,7 @@ export class HardwareService {
|
|||
}
|
||||
}
|
||||
|
||||
private async detectROCm(): Promise<{
|
||||
async detectROCm(): Promise<{
|
||||
supported: boolean;
|
||||
devices: string[];
|
||||
}> {
|
||||
|
|
@ -189,6 +189,7 @@ export class HardwareService {
|
|||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
rocminfo.on('close', (code) => {
|
||||
if (code === 0 && output.trim()) {
|
||||
const devices: string[] = [];
|
||||
|
|
@ -196,10 +197,27 @@ export class HardwareService {
|
|||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
if (line.includes('Marketing Name:')) {
|
||||
const name = line.split('Marketing Name:')[1]?.trim();
|
||||
if (name && !name.includes('CPU')) {
|
||||
devices.push(name);
|
||||
if (name) {
|
||||
let deviceType = '';
|
||||
|
||||
for (
|
||||
let j = Math.max(0, i - 10);
|
||||
j < Math.min(lines.length, i + 10);
|
||||
j++
|
||||
) {
|
||||
if (lines[j].includes('Device Type:')) {
|
||||
deviceType =
|
||||
lines[j].split('Device Type:')[1]?.trim() || '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (deviceType !== 'CPU') {
|
||||
devices.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -221,7 +239,7 @@ export class HardwareService {
|
|||
try {
|
||||
rocminfo.kill('SIGTERM');
|
||||
} catch {
|
||||
// Process already terminated
|
||||
void 0;
|
||||
}
|
||||
resolve({ supported: false, devices: [] });
|
||||
}, 5000);
|
||||
|
|
@ -276,7 +294,7 @@ export class HardwareService {
|
|||
try {
|
||||
vulkaninfo.kill('SIGTERM');
|
||||
} catch {
|
||||
// Process already terminated
|
||||
void 0;
|
||||
}
|
||||
resolve({ supported: false, devices: [] });
|
||||
}, 5000);
|
||||
|
|
@ -324,7 +342,6 @@ export class HardwareService {
|
|||
devices,
|
||||
});
|
||||
} catch {
|
||||
// Failed to parse JSON, try text parsing
|
||||
const lines = output.split('\n');
|
||||
const devices: string[] = [];
|
||||
|
||||
|
|
@ -355,7 +372,7 @@ export class HardwareService {
|
|||
try {
|
||||
clinfo.kill('SIGTERM');
|
||||
} catch {
|
||||
// Process already terminated
|
||||
void 0;
|
||||
}
|
||||
resolve({ supported: false, devices: [] });
|
||||
}, 5000);
|
||||
|
|
|
|||
|
|
@ -112,6 +112,10 @@ export class IPCHandlers {
|
|||
this.hardwareService.detectGPUCapabilities()
|
||||
);
|
||||
|
||||
ipcMain.handle('kobold:detectROCm', () =>
|
||||
this.hardwareService.detectROCm()
|
||||
);
|
||||
|
||||
ipcMain.handle('kobold:detectHardware', () =>
|
||||
this.hardwareService.detectAll()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ const koboldAPI: KoboldAPI = {
|
|||
detectCPU: () => ipcRenderer.invoke('kobold:detectCPU'),
|
||||
detectGPUCapabilities: () =>
|
||||
ipcRenderer.invoke('kobold:detectGPUCapabilities'),
|
||||
detectROCm: () => ipcRenderer.invoke('kobold:detectROCm'),
|
||||
detectHardware: () => ipcRenderer.invoke('kobold:detectHardware'),
|
||||
detectAllCapabilities: () =>
|
||||
ipcRenderer.invoke('kobold:detectAllCapabilities'),
|
||||
|
|
|
|||
2
src/types/electron.d.ts
vendored
2
src/types/electron.d.ts
vendored
|
|
@ -43,6 +43,7 @@ interface InstalledVersion {
|
|||
path: string;
|
||||
type: 'github' | 'rocm';
|
||||
filename: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
interface ROCmDownload {
|
||||
|
|
@ -66,6 +67,7 @@ export interface KoboldAPI {
|
|||
detectGPU: () => Promise<BasicGPUInfo>;
|
||||
detectCPU: () => Promise<CPUCapabilities>;
|
||||
detectGPUCapabilities: () => Promise<GPUCapabilities>;
|
||||
detectROCm: () => Promise<{ supported: boolean; devices: string[] }>;
|
||||
detectHardware: () => Promise<HardwareInfo>;
|
||||
detectAllCapabilities: () => Promise<HardwareInfo>;
|
||||
getCurrentInstallDir: () => Promise<string>;
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ export interface InstalledVersion {
|
|||
version: string;
|
||||
path: string;
|
||||
filename: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export interface ROCmDownload {
|
||||
|
|
|
|||
35
src/utils/binaryUtils.ts
Normal file
35
src/utils/binaryUtils.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
export const removeBinaryExtension = (filename: string): string => {
|
||||
if (filename.endsWith('.exe')) {
|
||||
return filename.slice(0, -4);
|
||||
}
|
||||
return filename;
|
||||
};
|
||||
|
||||
export const getBinaryBaseName = (filename: string): string => {
|
||||
const baseName = filename.split(/[/\\]/).pop() || filename;
|
||||
return removeBinaryExtension(baseName).toLowerCase();
|
||||
};
|
||||
|
||||
export const isOldPcBinary = (filename: string): boolean => {
|
||||
const baseName = getBinaryBaseName(filename);
|
||||
return baseName.endsWith('oldpc');
|
||||
};
|
||||
|
||||
export const isNoCudaBinary = (filename: string): boolean => {
|
||||
const baseName = getBinaryBaseName(filename);
|
||||
return baseName.endsWith('nocuda');
|
||||
};
|
||||
|
||||
export const isRocmBinary = (filename: string): boolean => {
|
||||
const baseName = getBinaryBaseName(filename);
|
||||
return baseName.endsWith('rocm');
|
||||
};
|
||||
|
||||
export const getBinaryType = (
|
||||
filename: string
|
||||
): 'oldpc' | 'nocuda' | 'rocm' | 'standard' => {
|
||||
if (isOldPcBinary(filename)) return 'oldpc';
|
||||
if (isNoCudaBinary(filename)) return 'nocuda';
|
||||
if (isRocmBinary(filename)) return 'rocm';
|
||||
return 'standard';
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue