windows rocm support, warn users from suboptimal setups

This commit is contained in:
Egor 2025-08-14 17:59:27 -07:00
parent 867ced21f9
commit 01058a8d02
26 changed files with 670 additions and 657 deletions

View file

@ -2,6 +2,13 @@
A koboldcpp manager. 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 ### Prerequisites
- Node.js 18+ - Node.js 18+

View file

@ -63,6 +63,8 @@
"flexbox", "flexbox",
"flexdir", "flexdir",
"flexwrap", "flexwrap",
"flashattention",
"Flashattention",
"fontsize", "fontsize",
"fontweight", "fontweight",
"forwardRef", "forwardRef",
@ -125,6 +127,8 @@
"nocuda", "nocuda",
"nodeIntegration", "nodeIntegration",
"noheader", "noheader",
"noshift",
"Noshift",
"nsis", "nsis",
"nvidia", "nvidia",
"oldpc", "oldpc",
@ -210,6 +214,9 @@
"useReducer", "useReducer",
"useRef", "useRef",
"useState", "useState",
"useclblast",
"usecuda",
"usevulkan",
"util", "util",
"utils", "utils",
"var", "var",

View file

@ -8,6 +8,7 @@ import react from 'eslint-plugin-react';
import importPlugin from 'eslint-plugin-import'; import importPlugin from 'eslint-plugin-import';
import sonarjs from 'eslint-plugin-sonarjs'; import sonarjs from 'eslint-plugin-sonarjs';
import cspell from '@cspell/eslint-plugin'; import cspell from '@cspell/eslint-plugin';
import noComments from 'eslint-plugin-no-comments';
const config = [ const config = [
js.configs.recommended, js.configs.recommended,
@ -39,6 +40,7 @@ const config = [
import: importPlugin, import: importPlugin,
sonarjs: sonarjs, sonarjs: sonarjs,
'@cspell': cspell, '@cspell': cspell,
'no-comments': noComments,
}, },
settings: { settings: {
react: { react: {
@ -46,11 +48,9 @@ const config = [
}, },
}, },
rules: { rules: {
// Use recommended rules from plugins
...reactHooks.configs.recommended.rules, ...reactHooks.configs.recommended.rules,
...react.configs.recommended.rules, ...react.configs.recommended.rules,
// Essential TypeScript rules
'@typescript-eslint/no-unused-vars': [ '@typescript-eslint/no-unused-vars': [
'error', 'error',
{ {
@ -59,10 +59,9 @@ const config = [
ignoreRestSiblings: true, ignoreRestSiblings: true,
}, },
], ],
'no-unused-vars': 'off', // Turn off base rule to use TypeScript version 'no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-explicit-any': 'warn',
// React-specific rules you wanted
'react-refresh/only-export-components': [ 'react-refresh/only-export-components': [
'warn', 'warn',
{ allowConstantExport: true }, { allowConstantExport: true },
@ -74,9 +73,13 @@ const config = [
unnamedComponents: 'arrow-function', 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': [ 'no-restricted-imports': [
'error', 'error',
{ {
@ -91,41 +94,33 @@ const config = [
}, },
], ],
// Import rules - enforce named exports
'import/no-default-export': 'error', 'import/no-default-export': 'error',
'import/prefer-default-export': 'off', 'import/prefer-default-export': 'off',
// Enforce arrow function shorthand when possible
'arrow-body-style': ['error', 'as-needed'], 'arrow-body-style': ['error', 'as-needed'],
'prefer-arrow-callback': ['error', { allowNamedFunctions: false }], 'prefer-arrow-callback': ['error', { allowNamedFunctions: false }],
// Forbid console.log usage
'no-console': ['error', { allow: ['warn', 'error'] }], 'no-console': ['error', { allow: ['warn', 'error'] }],
// TypeScript rules
'@typescript-eslint/no-inferrable-types': 'warn', '@typescript-eslint/no-inferrable-types': 'warn',
'@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-function-return-type': 'off',
// SonarJS rules - keep cognitive complexity reasonable
'sonarjs/cognitive-complexity': ['warn', 25], 'sonarjs/cognitive-complexity': ['warn', 25],
// Spell checking for code
'@cspell/spellchecker': ['warn'], '@cspell/spellchecker': ['warn'],
'no-comments/disallowComments': 'error',
}, },
}, },
{ {
// TypeScript definition files should have relaxed rules
files: ['**/*.d.ts'], files: ['**/*.d.ts'],
rules: { rules: {
// Allow unused variables in type definitions
'no-unused-vars': 'off', 'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': 'off',
// Allow any types in definitions
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'off',
}, },
}, },
{ {
// Allow default exports for config files
files: [ files: [
'*.config.*', '*.config.*',
'vite.config.*', 'vite.config.*',

105
package-lock.json generated
View file

@ -23,7 +23,7 @@
"@types/node": "^24.2.1", "@types/node": "^24.2.1",
"@types/react": "^19.1.10", "@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7", "@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/eslint-plugin": "^8.39.1",
"@typescript-eslint/parser": "^8.39.1", "@typescript-eslint/parser": "^8.39.1",
"@vitejs/plugin-react": "^5.0.0", "@vitejs/plugin-react": "^5.0.0",
@ -34,6 +34,7 @@
"electron-vite": "^4.0.0", "electron-vite": "^4.0.0",
"eslint": "^9.33.0", "eslint": "^9.33.0",
"eslint-plugin-import": "^2.32.0", "eslint-plugin-import": "^2.32.0",
"eslint-plugin-no-comments": "^1.1.10",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.20",
@ -87,22 +88,22 @@
} }
}, },
"node_modules/@babel/core": { "node_modules/@babel/core": {
"version": "7.28.0", "version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz",
"integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.0", "@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1", "@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-compilation-targets": "^7.27.2",
"@babel/helper-module-transforms": "^7.27.3", "@babel/helper-module-transforms": "^7.28.3",
"@babel/helpers": "^7.27.6", "@babel/helpers": "^7.28.3",
"@babel/parser": "^7.28.0", "@babel/parser": "^7.28.3",
"@babel/template": "^7.27.2", "@babel/template": "^7.27.2",
"@babel/traverse": "^7.28.0", "@babel/traverse": "^7.28.3",
"@babel/types": "^7.28.0", "@babel/types": "^7.28.2",
"convert-source-map": "^2.0.0", "convert-source-map": "^2.0.0",
"debug": "^4.1.0", "debug": "^4.1.0",
"gensync": "^1.0.0-beta.2", "gensync": "^1.0.0-beta.2",
@ -135,13 +136,13 @@
} }
}, },
"node_modules/@babel/generator": { "node_modules/@babel/generator": {
"version": "7.28.0", "version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
"integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/parser": "^7.28.0", "@babel/parser": "^7.28.3",
"@babel/types": "^7.28.0", "@babel/types": "^7.28.2",
"@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28", "@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2" "jsesc": "^3.0.2"
@ -200,15 +201,15 @@
} }
}, },
"node_modules/@babel/helper-module-transforms": { "node_modules/@babel/helper-module-transforms": {
"version": "7.27.3", "version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
"integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-module-imports": "^7.27.1", "@babel/helper-module-imports": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1",
"@babel/traverse": "^7.27.3" "@babel/traverse": "^7.28.3"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@ -256,9 +257,9 @@
} }
}, },
"node_modules/@babel/helpers": { "node_modules/@babel/helpers": {
"version": "7.28.2", "version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz",
"integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -270,12 +271,12 @@
} }
}, },
"node_modules/@babel/parser": { "node_modules/@babel/parser": {
"version": "7.28.0", "version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz",
"integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/types": "^7.28.0" "@babel/types": "^7.28.2"
}, },
"bin": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
@ -333,9 +334,9 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.28.2", "version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz",
"integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@ -356,17 +357,17 @@
} }
}, },
"node_modules/@babel/traverse": { "node_modules/@babel/traverse": {
"version": "7.28.0", "version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz",
"integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.0", "@babel/generator": "^7.28.3",
"@babel/helper-globals": "^7.28.0", "@babel/helper-globals": "^7.28.0",
"@babel/parser": "^7.28.0", "@babel/parser": "^7.28.3",
"@babel/template": "^7.27.2", "@babel/template": "^7.27.2",
"@babel/types": "^7.28.0", "@babel/types": "^7.28.2",
"debug": "^4.3.1" "debug": "^4.3.1"
}, },
"engines": { "engines": {
@ -3284,11 +3285,15 @@
} }
}, },
"node_modules/@types/systeminformation": { "node_modules/@types/systeminformation": {
"version": "3.23.1", "version": "3.54.1",
"resolved": "https://registry.npmjs.org/@types/systeminformation/-/systeminformation-3.23.1.tgz", "resolved": "https://registry.npmjs.org/@types/systeminformation/-/systeminformation-3.54.1.tgz",
"integrity": "sha512-0/y5m3PQLhQL7UM8MEvwFuLoTT6k5bamcZyjap12S0iHb33ngKfDDqCSnIaCaKWvRE41R/9BsZov1aE6hvcpEg==", "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, "dev": true,
"license": "MIT" "license": "MIT",
"dependencies": {
"systeminformation": "*"
}
}, },
"node_modules/@types/verror": { "node_modules/@types/verror": {
"version": "1.10.11", "version": "1.10.11",
@ -5787,9 +5792,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.200", "version": "1.5.201",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.200.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.201.tgz",
"integrity": "sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w==", "integrity": "sha512-ZG65vsrLClodGqywuigc+7m0gr4ISoTQttfVh7nfpLv0M7SIwF4WbFNEOywcqTiujs12AUeeXbFyQieDICAIxg==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@ -6386,6 +6391,13 @@
"semver": "bin/semver.js" "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": { "node_modules/eslint-plugin-react": {
"version": "7.37.5", "version": "7.37.5",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz",
@ -6829,11 +6841,14 @@
} }
}, },
"node_modules/fdir": { "node_modules/fdir": {
"version": "6.4.6", "version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": { "peerDependencies": {
"picomatch": "^3 || ^4" "picomatch": "^3 || ^4"
}, },

View file

@ -51,7 +51,7 @@
"@types/node": "^24.2.1", "@types/node": "^24.2.1",
"@types/react": "^19.1.10", "@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7", "@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/eslint-plugin": "^8.39.1",
"@typescript-eslint/parser": "^8.39.1", "@typescript-eslint/parser": "^8.39.1",
"@vitejs/plugin-react": "^5.0.0", "@vitejs/plugin-react": "^5.0.0",
@ -62,6 +62,7 @@
"electron-vite": "^4.0.0", "electron-vite": "^4.0.0",
"eslint": "^9.33.0", "eslint": "^9.33.0",
"eslint-plugin-import": "^2.32.0", "eslint-plugin-import": "^2.32.0",
"eslint-plugin-no-comments": "^1.1.10",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.20",

View file

@ -17,7 +17,7 @@ export const UpdateDialog = ({
onAccept, onAccept,
}: UpdateDialogProps) => ( }: UpdateDialogProps) => (
<Modal <Modal
opened={true} opened
onClose={onIgnore} onClose={onIgnore}
title="Update Available" title="Update Available"
centered centered

View file

@ -4,15 +4,23 @@ import { InfoTooltip } from '@/components/InfoTooltip';
interface AdvancedTabProps { interface AdvancedTabProps {
additionalArguments: string; additionalArguments: string;
serverOnly: boolean; serverOnly: boolean;
noshift: boolean;
flashattention: boolean;
onAdditionalArgumentsChange: (args: string) => void; onAdditionalArgumentsChange: (args: string) => void;
onServerOnlyChange: (serverOnly: boolean) => void; onServerOnlyChange: (serverOnly: boolean) => void;
onNoshiftChange: (noshift: boolean) => void;
onFlashattentionChange: (flashattention: boolean) => void;
} }
export const AdvancedTab = ({ export const AdvancedTab = ({
additionalArguments, additionalArguments,
serverOnly, serverOnly,
noshift,
flashattention,
onAdditionalArgumentsChange, onAdditionalArgumentsChange,
onServerOnlyChange, onServerOnlyChange,
onNoshiftChange,
onFlashattentionChange,
}: AdvancedTabProps) => ( }: AdvancedTabProps) => (
<Stack gap="lg"> <Stack gap="lg">
<div> <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." /> <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> </Group>
</div> </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> </Stack>
); );

View file

@ -6,21 +6,32 @@ import {
Button, Button,
Checkbox, Checkbox,
Slider, Slider,
Select,
Badge,
Card,
} from '@mantine/core'; } from '@mantine/core';
import { useState, useEffect } from 'react';
import { File, Search } from 'lucide-react'; import { File, Search } from 'lucide-react';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
import { getInputValidationState } from '@/utils/validation'; import { getInputValidationState } from '@/utils/validation';
import {
isOldPcBinary,
isNoCudaBinary,
isRocmBinary,
} from '@/utils/binaryUtils';
interface GeneralTabProps { interface GeneralTabProps {
modelPath: string; modelPath: string;
gpuLayers: number; gpuLayers: number;
autoGpuLayers: boolean; autoGpuLayers: boolean;
contextSize: number; contextSize: number;
backend: string;
onModelPathChange: (path: string) => void; onModelPathChange: (path: string) => void;
onSelectModelFile: () => void; onSelectModelFile: () => void;
onGpuLayersChange: (layers: number) => void; onGpuLayersChange: (layers: number) => void;
onAutoGpuLayersChange: (auto: boolean) => void; onAutoGpuLayersChange: (auto: boolean) => void;
onContextSizeChange: (size: number) => void; onContextSizeChange: (size: number) => void;
onBackendChange: (backend: string) => void;
} }
export const GeneralTab = ({ export const GeneralTab = ({
@ -28,12 +39,98 @@ export const GeneralTab = ({
gpuLayers, gpuLayers,
autoGpuLayers, autoGpuLayers,
contextSize, contextSize,
backend,
onModelPathChange, onModelPathChange,
onSelectModelFile, onSelectModelFile,
onGpuLayersChange, onGpuLayersChange,
onAutoGpuLayersChange, onAutoGpuLayersChange,
onContextSizeChange, onContextSizeChange,
onBackendChange,
}: GeneralTabProps) => { }: 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 validationState = getInputValidationState(modelPath);
const getInputColor = () => { const getInputColor = () => {
@ -97,6 +194,56 @@ export const GeneralTab = ({
</Group> </Group>
</div> </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&apos;s CUDA or AMD&apos;s ROCm backends for optimal
performance.
</Text>
</Card>
)}
</div>
<div> <div>
<Group justify="space-between" align="center" mb="xs"> <Group justify="space-between" align="center" mb="xs">
<Group gap="xs" align="center"> <Group gap="xs" align="center">

View file

@ -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>
);

View file

@ -73,11 +73,7 @@ export const NetworkTab = ({
</Group> </Group>
<div> <div>
<Text size="sm" fw={500} mb="md">
Network Options
</Text>
<Stack gap="md"> <Stack gap="md">
{/* First row: Multiuser and Multiplayer */}
<Group gap="lg" align="flex-start" wrap="nowrap"> <Group gap="lg" align="flex-start" wrap="nowrap">
<div style={{ minWidth: '250px' }}> <div style={{ minWidth: '250px' }}>
<Group gap="xs" align="center"> <Group gap="xs" align="center">
@ -106,7 +102,6 @@ export const NetworkTab = ({
</div> </div>
</Group> </Group>
{/* Second row: Remote Tunnel and No Certify */}
<Group gap="lg" align="flex-start" wrap="nowrap"> <Group gap="lg" align="flex-start" wrap="nowrap">
<div style={{ minWidth: '250px' }}> <div style={{ minWidth: '250px' }}>
<Group gap="xs" align="center"> <Group gap="xs" align="center">
@ -135,7 +130,6 @@ export const NetworkTab = ({
</div> </div>
</Group> </Group>
{/* Third row: WebSearch (single item) */}
<Group gap="xs" align="center"> <Group gap="xs" align="center">
<Checkbox <Checkbox
checked={websearch} checked={websearch}

View file

@ -1,5 +1,14 @@
import { useState, useEffect, useCallback } from 'react'; 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 { DownloadOptionCard } from '@/components/DownloadOptionCard';
import { import {
getPlatformDisplayName, getPlatformDisplayName,
@ -27,6 +36,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
const [selectedROCm, setSelectedROCm] = useState<boolean>(false); const [selectedROCm, setSelectedROCm] = useState<boolean>(false);
const [userPlatform, setUserPlatform] = useState<string>(''); const [userPlatform, setUserPlatform] = useState<string>('');
const [hasAMDGPU, setHasAMDGPU] = useState<boolean>(false); const [hasAMDGPU, setHasAMDGPU] = useState<boolean>(false);
const [hasROCm, setHasROCm] = useState<boolean>(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [downloading, setDownloading] = useState(false); const [downloading, setDownloading] = useState(false);
const [downloadProgress, setDownloadProgress] = useState(0); const [downloadProgress, setDownloadProgress] = useState(0);
@ -57,12 +67,18 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
try { try {
const gpuInfo = await window.electronAPI.kobold.detectGPU(); const gpuInfo = await window.electronAPI.kobold.detectGPU();
setHasAMDGPU(gpuInfo.hasAMD); setHasAMDGPU(gpuInfo.hasAMD);
if (gpuInfo.hasAMD) {
const rocmInfo = await window.electronAPI.kobold.detectROCm();
setHasROCm(rocmInfo.supported);
}
} catch (gpuError) { } catch (gpuError) {
console.warn( console.warn(
'GPU detection failed, proceeding without GPU info:', 'GPU detection failed, proceeding without GPU info:',
gpuError gpuError
); );
setHasAMDGPU(false); setHasAMDGPU(false);
setHasROCm(false);
} }
if (releaseData) { if (releaseData) {
@ -196,6 +212,41 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
{filteredAssets.length > 0 || rocmDownload ? ( {filteredAssets.length > 0 || rocmDownload ? (
<Stack gap="sm"> <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()} {rocmDownload && hasAMDGPU && renderROCmCard()}
{sortAssetsByRecommendation( {sortAssetsByRecommendation(

View file

@ -44,6 +44,9 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
remotetunnel, remotetunnel,
nocertify, nocertify,
websearch, websearch,
noshift,
flashattention,
backend,
parseAndApplyConfigFile, parseAndApplyConfigFile,
loadSavedSettings, loadSavedSettings,
loadConfigFromFile, loadConfigFromFile,
@ -61,6 +64,9 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
handleRemotetunnelChange, handleRemotetunnelChange,
handleNocertifyChange, handleNocertifyChange,
handleWebsearchChange, handleWebsearchChange,
handleNoshiftChange,
handleFlashattentionChange,
handleBackendChange,
} = useLaunchConfig(); } = useLaunchConfig();
const loadConfigFiles = useCallback(async () => { const loadConfigFiles = useCallback(async () => {
@ -96,11 +102,9 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
await parseAndApplyConfigFile(selectedConfig.path); await parseAndApplyConfigFile(selectedConfig.path);
} }
// Reset unsaved changes when loading a new config
setHasUnsavedChanges(false); setHasUnsavedChanges(false);
}; };
// Wrapper functions to track changes
const handleModelPathChangeWithTracking = (path: string) => { const handleModelPathChangeWithTracking = (path: string) => {
handleModelPathChange(path); handleModelPathChange(path);
setHasUnsavedChanges(true); setHasUnsavedChanges(true);
@ -141,6 +145,16 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
setHasUnsavedChanges(true); setHasUnsavedChanges(true);
}; };
const handleNoshiftChangeWithTracking = (noshift: boolean) => {
handleNoshiftChange(noshift);
setHasUnsavedChanges(true);
};
const handleFlashattentionChangeWithTracking = (flashattention: boolean) => {
handleFlashattentionChange(flashattention);
setHasUnsavedChanges(true);
};
const handleMultiuserChangeWithTracking = (multiuser: boolean) => { const handleMultiuserChangeWithTracking = (multiuser: boolean) => {
handleMultiuserChange(multiuser); handleMultiuserChange(multiuser);
setHasUnsavedChanges(true); setHasUnsavedChanges(true);
@ -166,6 +180,11 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
setHasUnsavedChanges(true); setHasUnsavedChanges(true);
}; };
const handleBackendChangeWithTracking = (backend: string) => {
handleBackendChange(backend);
setHasUnsavedChanges(true);
};
useEffect(() => { useEffect(() => {
void loadConfigFiles(); void loadConfigFiles();
@ -180,6 +199,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
return cleanup; return cleanup;
}, [loadConfigFiles]); }, [loadConfigFiles]);
// eslint-disable-next-line sonarjs/cognitive-complexity
const handleLaunch = async () => { const handleLaunch = async () => {
if (isLaunching || !modelPath) { if (isLaunching || !modelPath) {
return; return;
@ -234,6 +254,28 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
args.push('--websearch'); 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()) { if (additionalArguments.trim()) {
const additionalArgs = additionalArguments.trim().split(/\s+/); const additionalArgs = additionalArguments.trim().split(/\s+/);
args.push(...additionalArgs); args.push(...additionalArgs);
@ -285,11 +327,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
size="lg" size="lg"
variant="filled" variant="filled"
> >
{isLaunching {isLaunching ? 'Launching...' : 'Launch'}
? 'Launching...'
: modelPath
? 'Launch KoboldCpp'
: 'Select a model file to launch'}
</Button> </Button>
</Group> </Group>
</Card> </Card>
@ -301,9 +339,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
onFileSelection={handleFileSelection} onFileSelection={handleFileSelection}
onRefresh={loadConfigFiles} onRefresh={loadConfigFiles}
onSaveAsNew={() => setSaveModalOpened(true)} onSaveAsNew={() => setSaveModalOpened(true)}
onUpdateCurrent={() => { onUpdateCurrent={() => {}}
// TODO: Implement update current configuration
}}
/> />
</Card> </Card>
@ -323,11 +359,13 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
gpuLayers={gpuLayers} gpuLayers={gpuLayers}
autoGpuLayers={autoGpuLayers} autoGpuLayers={autoGpuLayers}
contextSize={contextSize} contextSize={contextSize}
backend={backend}
onModelPathChange={handleModelPathChangeWithTracking} onModelPathChange={handleModelPathChangeWithTracking}
onSelectModelFile={handleSelectModelFile} onSelectModelFile={handleSelectModelFile}
onGpuLayersChange={handleGpuLayersChangeWithTracking} onGpuLayersChange={handleGpuLayersChangeWithTracking}
onAutoGpuLayersChange={handleAutoGpuLayersChangeWithTracking} onAutoGpuLayersChange={handleAutoGpuLayersChangeWithTracking}
onContextSizeChange={handleContextSizeChangeWithTracking} onContextSizeChange={handleContextSizeChangeWithTracking}
onBackendChange={handleBackendChangeWithTracking}
/> />
</Tabs.Panel> </Tabs.Panel>
@ -335,10 +373,16 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
<AdvancedTab <AdvancedTab
additionalArguments={additionalArguments} additionalArguments={additionalArguments}
serverOnly={serverOnly} serverOnly={serverOnly}
noshift={noshift}
flashattention={flashattention}
onAdditionalArgumentsChange={ onAdditionalArgumentsChange={
handleAdditionalArgumentsChangeWithTracking handleAdditionalArgumentsChangeWithTracking
} }
onServerOnlyChange={handleServerOnlyChangeWithTracking} onServerOnlyChange={handleServerOnlyChangeWithTracking}
onNoshiftChange={handleNoshiftChangeWithTracking}
onFlashattentionChange={
handleFlashattentionChangeWithTracking
}
/> />
</Tabs.Panel> </Tabs.Panel>
@ -379,7 +423,6 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
configName={newConfigName} configName={newConfigName}
onConfigNameChange={setNewConfigName} onConfigNameChange={setNewConfigName}
onSave={() => { onSave={() => {
// TODO: Implement save configuration
setSaveModalOpened(false); setSaveModalOpened(false);
setNewConfigName(''); setNewConfigName('');
}} }}

View file

@ -154,7 +154,7 @@ export const VersionsTab = () => {
installedVersion?.version || installedVersion?.version ||
latestRelease?.tag_name.replace(/^v/, '') || latestRelease?.tag_name.replace(/^v/, '') ||
'unknown', 'unknown',
size: asset.size, size: installedVersion?.size || asset.size,
isInstalled: Boolean(installedVersion), isInstalled: Boolean(installedVersion),
isCurrent, isCurrent,
downloadUrl: asset.browser_download_url, downloadUrl: asset.browser_download_url,
@ -175,6 +175,7 @@ export const VersionsTab = () => {
versions.push({ versions.push({
name: installed.filename, name: installed.filename,
version: installed.version, version: installed.version,
size: installed.size,
isInstalled: true, isInstalled: true,
isCurrent, isCurrent,
installedPath: installed.path, installedPath: installed.path,
@ -207,21 +208,26 @@ export const VersionsTab = () => {
} }
}; };
const handleVersionSelect = async (version: VersionInfo) => { const makeCurrent = async (version: VersionInfo) => {
if (!version.installedPath) return; if (!version.installedPath) return;
try { try {
const filename = version.installedPath.split('/').pop() || ''; const success = await window.electronAPI.kobold.setCurrentVersion(
const success = version.installedPath
await window.electronAPI.kobold.setCurrentVersion(filename); );
if (success) { if (success) {
await window.electronAPI.config.set('currentKoboldBinary', filename);
await window.electronAPI.config.set( await window.electronAPI.config.set(
'currentKoboldVersion', 'currentKoboldBinary',
version.version version.installedPath
); );
await loadInstalledVersions();
const newCurrentVersion = installedVersions.find(
(v) => v.path === version.installedPath
);
if (newCurrentVersion) {
setCurrentVersion(newCurrentVersion);
}
} }
} catch (error) { } catch (error) {
console.error('Failed to set current version:', error); console.error('Failed to set current version:', error);
@ -299,11 +305,6 @@ export const VersionsTab = () => {
Current Current
</Badge> </Badge>
)} )}
{version.isInstalled && (
<Badge variant="light" color="green" size="sm">
Installed
</Badge>
)}
</Group> </Group>
<Text size="xs" c="dimmed"> <Text size="xs" c="dimmed">
Version {version.version} Version {version.version}
@ -339,10 +340,7 @@ export const VersionsTab = () => {
<Button <Button
variant="light" variant="light"
size="xs" size="xs"
onClick={(e) => { onClick={() => makeCurrent(version)}
e.stopPropagation();
handleVersionSelect(version);
}}
> >
Make Current Make Current
</Button> </Button>

View file

@ -5,12 +5,16 @@ export const CONFIG_FILE_NAME = 'config.json';
export const GITHUB_API = { export const GITHUB_API = {
BASE_URL: 'https://api.github.com', BASE_URL: 'https://api.github.com',
KOBOLDCPP_REPO: 'LostRuins/koboldcpp', KOBOLDCPP_REPO: 'LostRuins/koboldcpp',
KOBOLDCPP_ROCM_REPO: 'YellowRoseCx/koboldcpp-rocm',
get LATEST_RELEASE_URL() { get LATEST_RELEASE_URL() {
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_REPO}/releases/latest`; return `${this.BASE_URL}/repos/${this.KOBOLDCPP_REPO}/releases/latest`;
}, },
get ALL_RELEASES_URL() { get ALL_RELEASES_URL() {
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_REPO}/releases`; 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; } as const;
export const ASSET_SUFFIXES = { export const ASSET_SUFFIXES = {
@ -27,6 +31,5 @@ export const KOBOLDAI_URLS = {
export const ROCM = { export const ROCM = {
BINARY_NAME: 'koboldcpp-linux-x64-rocm', BINARY_NAME: 'koboldcpp-linux-x64-rocm',
DOWNLOAD_URL: KOBOLDAI_URLS.ROCM_DOWNLOAD, DOWNLOAD_URL: KOBOLDAI_URLS.ROCM_DOWNLOAD,
ERROR_MESSAGE: 'ROCm version is only available for Linux', SIZE_BYTES: 1024 * 1024 * 1024,
SIZE_BYTES: 1024 * 1024 * 1024, // 1GB
} as const; } as const;

View file

@ -15,6 +15,9 @@ export const useLaunchConfig = () => {
const [remotetunnel, setRemotetunnel] = useState<boolean>(false); const [remotetunnel, setRemotetunnel] = useState<boolean>(false);
const [nocertify, setNocertify] = useState<boolean>(false); const [nocertify, setNocertify] = useState<boolean>(false);
const [websearch, setWebsearch] = 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 // eslint-disable-next-line sonarjs/cognitive-complexity
const parseAndApplyConfigFile = useCallback(async (configPath: string) => { const parseAndApplyConfigFile = useCallback(async (configPath: string) => {
@ -78,6 +81,24 @@ export const useLaunchConfig = () => {
} else { } else {
setWebsearch(false); 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 { } else {
setGpuLayers(0); setGpuLayers(0);
setContextSize(2048); setContextSize(2048);
@ -88,6 +109,9 @@ export const useLaunchConfig = () => {
setRemotetunnel(false); setRemotetunnel(false);
setNocertify(false); setNocertify(false);
setWebsearch(false); setWebsearch(false);
setNoshift(false);
setFlashattention(false);
setBackend('');
} }
}, []); }, []);
@ -95,7 +119,7 @@ export const useLaunchConfig = () => {
const savedServerOnly = await window.electronAPI.config.getServerOnly(); const savedServerOnly = await window.electronAPI.config.getServerOnly();
setServerOnly(savedServerOnly); setServerOnly(savedServerOnly);
setModelPath(''); // Model path comes from config file, not saved settings setModelPath('');
setGpuLayers(0); setGpuLayers(0);
setContextSize(2048); setContextSize(2048);
setPort(5001); setPort(5001);
@ -105,6 +129,9 @@ export const useLaunchConfig = () => {
setRemotetunnel(false); setRemotetunnel(false);
setNocertify(false); setNocertify(false);
setWebsearch(false); setWebsearch(false);
setNoshift(false);
setFlashattention(false);
setBackend('');
}, []); }, []);
const loadConfigFromFile = useCallback( const loadConfigFromFile = useCallback(
@ -202,6 +229,18 @@ export const useLaunchConfig = () => {
setWebsearch(checked); setWebsearch(checked);
}, []); }, []);
const handleNoshiftChange = useCallback((checked: boolean) => {
setNoshift(checked);
}, []);
const handleFlashattentionChange = useCallback((checked: boolean) => {
setFlashattention(checked);
}, []);
const handleBackendChange = useCallback((backend: string) => {
setBackend(backend);
}, []);
return { return {
serverOnly, serverOnly,
gpuLayers, gpuLayers,
@ -216,6 +255,9 @@ export const useLaunchConfig = () => {
remotetunnel, remotetunnel,
nocertify, nocertify,
websearch, websearch,
noshift,
flashattention,
backend,
parseAndApplyConfigFile, parseAndApplyConfigFile,
loadSavedSettings, loadSavedSettings,
@ -234,5 +276,8 @@ export const useLaunchConfig = () => {
handleRemotetunnelChange, handleRemotetunnelChange,
handleNocertifyChange, handleNocertifyChange,
handleWebsearchChange, handleWebsearchChange,
handleNoshiftChange,
handleFlashattentionChange,
handleBackendChange,
}; };
}; };

View file

@ -12,7 +12,7 @@ import { dialog } from 'electron';
import { GitHubService } from '@/main/services/GitHubService'; import { GitHubService } from '@/main/services/GitHubService';
import { ConfigManager } from '@/main/managers/ConfigManager'; import { ConfigManager } from '@/main/managers/ConfigManager';
import { WindowManager } from '@/main/managers/WindowManager'; import { WindowManager } from '@/main/managers/WindowManager';
import { ROCM } from '@/constants'; import { ROCM, GITHUB_API } from '@/constants';
interface GitHubAsset { interface GitHubAsset {
name: string; name: string;
@ -49,6 +49,7 @@ export interface InstalledVersion {
version: string; version: string;
path: string; path: string;
filename: string; filename: string;
size?: number;
} }
export class KoboldCppManager { export class KoboldCppManager {
@ -161,17 +162,23 @@ export class KoboldCppManager {
}); });
if (!includeVersions) { if (!includeVersions) {
return koboldFiles.map((file) => ({ return koboldFiles.map((file) => {
version: 'unknown', const filePath = join(this.installDir, file);
path: join(this.installDir, file), const stats = statSync(filePath);
filename: file, return {
})); version: 'unknown',
path: filePath,
filename: file,
size: stats.size,
};
});
} }
const versionPromises = koboldFiles.map(async (file) => { const versionPromises = koboldFiles.map(async (file) => {
const filePath = join(this.installDir, file); const filePath = join(this.installDir, file);
try { try {
const stats = statSync(filePath);
const detectedVersion = await this.getVersionFromBinary(filePath); const detectedVersion = await this.getVersionFromBinary(filePath);
const version = detectedVersion || 'unknown'; const version = detectedVersion || 'unknown';
@ -179,6 +186,7 @@ export class KoboldCppManager {
version, version,
path: filePath, path: filePath,
filename: file, filename: file,
size: stats.size,
} as InstalledVersion; } as InstalledVersion;
} catch (error) { } catch (error) {
console.warn(`Could not detect version for ${file}:`, error); console.warn(`Could not detect version for ${file}:`, error);
@ -195,6 +203,7 @@ export class KoboldCppManager {
return []; return [];
} }
} }
async getConfigFiles(): Promise< async getConfigFiles(): Promise<
Array<{ name: string; path: string; size: number }> Array<{ name: string; path: string; size: number }>
> { > {
@ -306,12 +315,9 @@ export class KoboldCppManager {
return null; return null;
} }
async setCurrentVersion(version: string): Promise<boolean> { async setCurrentVersion(binaryPath: string): Promise<boolean> {
const versions = await this.getInstalledVersions(); if (existsSync(binaryPath)) {
const targetVersion = versions.find((v) => v.version === version); this.configManager.setCurrentKoboldBinary(binaryPath);
if (targetVersion && existsSync(targetVersion.path)) {
this.configManager.setCurrentKoboldBinary(targetVersion.path);
return true; return true;
} }
@ -376,7 +382,7 @@ export class KoboldCppManager {
try { try {
process.kill('SIGTERM'); process.kill('SIGTERM');
} catch { } catch {
// ignore void 0;
} }
resolve(null); resolve(null);
}, 10000); }, 10000);
@ -479,19 +485,45 @@ export class KoboldCppManager {
version?: string; version?: string;
} | null> { } | null> {
const platform = process.platform; 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(); return null;
const version = latestRelease?.tag_name?.replace(/^v/, '') || 'unknown';
return {
name: ROCM.BINARY_NAME,
url: ROCM.DOWNLOAD_URL,
size: ROCM.SIZE_BYTES,
version,
};
} }
async downloadROCm(onProgress?: (progress: number) => void): Promise<{ async downloadROCm(onProgress?: (progress: number) => void): Promise<{
@ -500,15 +532,15 @@ export class KoboldCppManager {
error?: string; error?: string;
}> { }> {
try { try {
const platform = process.platform; const rocmInfo = await this.getROCmDownload();
if (platform !== 'linux') { if (!rocmInfo) {
return { return {
success: false, 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) { if (!response.ok) {
return { return {
success: false, success: false,
@ -516,7 +548,7 @@ export class KoboldCppManager {
}; };
} }
const totalBytes = ROCM.SIZE_BYTES; const totalBytes = rocmInfo.size;
let downloadedBytes = 0; let downloadedBytes = 0;
const reader = response.body?.getReader(); 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); const writer = createWriteStream(filePath);
try { try {
@ -676,13 +708,11 @@ export class KoboldCppManager {
const child = spawn(currentVersion.path, finalArgs, { const child = spawn(currentVersion.path, finalArgs, {
stdio: ['pipe', 'pipe', 'pipe'], stdio: ['pipe', 'pipe', 'pipe'],
detached: true, detached: false,
}); });
this.koboldProcess = child; this.koboldProcess = child;
child.unref();
const mainWindow = this.windowManager.getMainWindow(); const mainWindow = this.windowManager.getMainWindow();
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
const commandLine = `$ ${currentVersion.path} ${finalArgs.join(' ')}\n${'─'.repeat(60)}\n`; const commandLine = `$ ${currentVersion.path} ${finalArgs.join(' ')}\n${'─'.repeat(60)}\n`;
@ -737,23 +767,12 @@ export class KoboldCppManager {
stopKoboldCpp(): void { stopKoboldCpp(): void {
if (this.koboldProcess) { if (this.koboldProcess) {
try { try {
// For detached processes, we need to kill the process group this.koboldProcess.kill('SIGTERM');
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');
}
setTimeout(() => { setTimeout(() => {
if (this.koboldProcess && !this.koboldProcess.killed) { if (this.koboldProcess && !this.koboldProcess.killed) {
try { try {
if (process.platform !== 'win32') { this.koboldProcess.kill('SIGKILL');
process.kill(-this.koboldProcess.pid!, 'SIGKILL');
} else {
this.koboldProcess.kill('SIGKILL');
}
} catch (error) { } catch (error) {
console.warn('Error force-killing KoboldCpp process:', error); console.warn('Error force-killing KoboldCpp process:', error);
} }
@ -785,25 +804,14 @@ export class KoboldCppManager {
this.koboldProcess.once('error', cleanup); this.koboldProcess.once('error', cleanup);
try { try {
// For detached processes, we need to handle cleanup differently this.koboldProcess.kill('SIGTERM');
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');
}
setTimeout(() => { setTimeout(() => {
if (this.koboldProcess && !this.koboldProcess.killed) { if (this.koboldProcess && !this.koboldProcess.killed) {
try { try {
if (process.platform !== 'win32') { this.koboldProcess.kill('SIGKILL');
process.kill(-this.koboldProcess.pid!, 'SIGKILL');
} else {
this.koboldProcess.kill('SIGKILL');
}
} catch { } catch {
// Ignore errors on force kill void 0;
} }
} }
cleanup(); cleanup();

View file

@ -15,7 +15,6 @@ import { ConfigManager } from './ConfigManager';
export class WindowManager { export class WindowManager {
private mainWindow: BrowserWindow | null = null; private mainWindow: BrowserWindow | null = null;
private tray: Tray | null = null; private tray: Tray | null = null;
private isQuitting = false;
private configManager: ConfigManager; private configManager: ConfigManager;
constructor(configManager: ConfigManager) { constructor(configManager: ConfigManager) {
@ -46,10 +45,13 @@ export class WindowManager {
this.mainWindow = null; 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) => { this.mainWindow.webContents.on('will-navigate', (event, navigationUrl) => {
const url = new URL(navigationUrl); const url = new URL(navigationUrl);
// Only allow navigation to localhost or the app's origin
if ( if (
url.hostname !== 'localhost' && url.hostname !== 'localhost' &&
url.hostname !== '127.0.0.1' && url.hostname !== '127.0.0.1' &&
@ -59,46 +61,39 @@ export class WindowManager {
} }
}); });
// Handle iframe navigation permissions
this.mainWindow.webContents.setWindowOpenHandler(({ url }) => { this.mainWindow.webContents.setWindowOpenHandler(({ url }) => {
const parsedUrl = new URL(url); const parsedUrl = new URL(url);
// Allow localhost URLs to open in the same window/iframe
if ( if (
parsedUrl.hostname === 'localhost' || parsedUrl.hostname === 'localhost' ||
parsedUrl.hostname === '127.0.0.1' parsedUrl.hostname === '127.0.0.1'
) { ) {
return { action: 'allow' }; return { action: 'allow' };
} }
// For other URLs, open in external browser
shell.openExternal(url); shell.openExternal(url);
return { action: 'deny' }; return { action: 'deny' };
}); });
this.mainWindow.on('close', async (event) => { this.mainWindow.on('close', () => {
if (!this.isQuitting) { app.quit();
const minimizeToTray =
this.configManager.get('minimizeToTray') === true;
if (minimizeToTray) {
event.preventDefault();
this.mainWindow?.hide();
if (!this.tray) {
this.createSystemTray();
}
}
}
}); });
this.mainWindow.on('minimize', () => { this.mainWindow.on('minimize', () => {
const minimizeToTray = this.configManager.get('minimizeToTray') === true; const minimizeToTray = this.configManager.get('minimizeToTray') === true;
if (minimizeToTray) { if (minimizeToTray) {
this.mainWindow?.hide();
if (!this.tray) { if (!this.tray) {
this.createSystemTray(); this.createSystemTray();
} }
if (this.tray) {
setTimeout(() => {
this.mainWindow?.setSkipTaskbar(true);
this.mainWindow?.hide();
}, 100);
}
} }
}); });
@ -111,41 +106,59 @@ export class WindowManager {
} }
private createSystemTray() { private createSystemTray() {
const iconPath = join(app.getAppPath(), 'assets', 'icon.png'); try {
this.tray = new Tray(nativeImage.createFromPath(iconPath)); const iconPath = join(app.getAppPath(), 'assets', 'icon.png');
this.tray.setToolTip('Friendly Kobold'); const image = nativeImage.createFromPath(iconPath);
if (!image.isEmpty()) {
const trayMenu = Menu.buildFromTemplate([ const resizedImage = image.resize({ width: 16, height: 16 });
{ this.tray = new Tray(resizedImage);
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();
} else { } 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() { 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() { private setupContextMenu() {
if (!this.mainWindow) return; if (!this.mainWindow) return;
@ -204,7 +228,6 @@ export class WindowManager {
label: 'Quit', label: 'Quit',
accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Ctrl+Q', accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Ctrl+Q',
click: () => { click: () => {
this.isQuitting = true;
app.quit(); app.quit();
}, },
}, },
@ -265,18 +288,27 @@ export class WindowManager {
{ {
label: 'Help', label: 'Help',
submenu: [ submenu: [
{
label: 'About',
click: async () => {
await this.showAboutDialog();
},
},
{ {
label: 'KoboldCpp Wiki', label: 'KoboldCpp Wiki',
click: () => { click: () => {
shell.openExternal('https://github.com/LostRuins/koboldcpp/wiki'); 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();
},
},
], ],
}, },
]; ];

View file

@ -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'],
};
}
}
}

View file

@ -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: [] };
}
}
}

View file

@ -3,7 +3,7 @@ import { GITHUB_API } from '@/constants';
export class GitHubService { export class GitHubService {
private lastApiCall = 0; private lastApiCall = 0;
private apiCooldown = 60000; // 1 minute cooldown private apiCooldown = 60000;
private cachedRelease: GitHubRelease | null = null; private cachedRelease: GitHubRelease | null = null;
private cachedReleases: GitHubRelease[] = []; private cachedReleases: GitHubRelease[] = [];

View file

@ -165,7 +165,7 @@ export class HardwareService {
try { try {
nvidia.kill('SIGTERM'); nvidia.kill('SIGTERM');
} catch { } catch {
// Process already terminated void 0;
} }
resolve({ supported: false, devices: [] }); resolve({ supported: false, devices: [] });
}, 5000); }, 5000);
@ -175,7 +175,7 @@ export class HardwareService {
} }
} }
private async detectROCm(): Promise<{ async detectROCm(): Promise<{
supported: boolean; supported: boolean;
devices: string[]; devices: string[];
}> { }> {
@ -189,6 +189,7 @@ export class HardwareService {
}); });
return new Promise((resolve) => { return new Promise((resolve) => {
// eslint-disable-next-line sonarjs/cognitive-complexity
rocminfo.on('close', (code) => { rocminfo.on('close', (code) => {
if (code === 0 && output.trim()) { if (code === 0 && output.trim()) {
const devices: string[] = []; const devices: string[] = [];
@ -196,10 +197,27 @@ export class HardwareService {
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const line = lines[i]; const line = lines[i];
if (line.includes('Marketing Name:')) { if (line.includes('Marketing Name:')) {
const name = line.split('Marketing Name:')[1]?.trim(); const name = line.split('Marketing Name:')[1]?.trim();
if (name && !name.includes('CPU')) { if (name) {
devices.push(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 { try {
rocminfo.kill('SIGTERM'); rocminfo.kill('SIGTERM');
} catch { } catch {
// Process already terminated void 0;
} }
resolve({ supported: false, devices: [] }); resolve({ supported: false, devices: [] });
}, 5000); }, 5000);
@ -276,7 +294,7 @@ export class HardwareService {
try { try {
vulkaninfo.kill('SIGTERM'); vulkaninfo.kill('SIGTERM');
} catch { } catch {
// Process already terminated void 0;
} }
resolve({ supported: false, devices: [] }); resolve({ supported: false, devices: [] });
}, 5000); }, 5000);
@ -324,7 +342,6 @@ export class HardwareService {
devices, devices,
}); });
} catch { } catch {
// Failed to parse JSON, try text parsing
const lines = output.split('\n'); const lines = output.split('\n');
const devices: string[] = []; const devices: string[] = [];
@ -355,7 +372,7 @@ export class HardwareService {
try { try {
clinfo.kill('SIGTERM'); clinfo.kill('SIGTERM');
} catch { } catch {
// Process already terminated void 0;
} }
resolve({ supported: false, devices: [] }); resolve({ supported: false, devices: [] });
}, 5000); }, 5000);

View file

@ -112,6 +112,10 @@ export class IPCHandlers {
this.hardwareService.detectGPUCapabilities() this.hardwareService.detectGPUCapabilities()
); );
ipcMain.handle('kobold:detectROCm', () =>
this.hardwareService.detectROCm()
);
ipcMain.handle('kobold:detectHardware', () => ipcMain.handle('kobold:detectHardware', () =>
this.hardwareService.detectAll() this.hardwareService.detectAll()
); );

View file

@ -26,6 +26,7 @@ const koboldAPI: KoboldAPI = {
detectCPU: () => ipcRenderer.invoke('kobold:detectCPU'), detectCPU: () => ipcRenderer.invoke('kobold:detectCPU'),
detectGPUCapabilities: () => detectGPUCapabilities: () =>
ipcRenderer.invoke('kobold:detectGPUCapabilities'), ipcRenderer.invoke('kobold:detectGPUCapabilities'),
detectROCm: () => ipcRenderer.invoke('kobold:detectROCm'),
detectHardware: () => ipcRenderer.invoke('kobold:detectHardware'), detectHardware: () => ipcRenderer.invoke('kobold:detectHardware'),
detectAllCapabilities: () => detectAllCapabilities: () =>
ipcRenderer.invoke('kobold:detectAllCapabilities'), ipcRenderer.invoke('kobold:detectAllCapabilities'),

View file

@ -43,6 +43,7 @@ interface InstalledVersion {
path: string; path: string;
type: 'github' | 'rocm'; type: 'github' | 'rocm';
filename: string; filename: string;
size?: number;
} }
interface ROCmDownload { interface ROCmDownload {
@ -66,6 +67,7 @@ export interface KoboldAPI {
detectGPU: () => Promise<BasicGPUInfo>; detectGPU: () => Promise<BasicGPUInfo>;
detectCPU: () => Promise<CPUCapabilities>; detectCPU: () => Promise<CPUCapabilities>;
detectGPUCapabilities: () => Promise<GPUCapabilities>; detectGPUCapabilities: () => Promise<GPUCapabilities>;
detectROCm: () => Promise<{ supported: boolean; devices: string[] }>;
detectHardware: () => Promise<HardwareInfo>; detectHardware: () => Promise<HardwareInfo>;
detectAllCapabilities: () => Promise<HardwareInfo>; detectAllCapabilities: () => Promise<HardwareInfo>;
getCurrentInstallDir: () => Promise<string>; getCurrentInstallDir: () => Promise<string>;

View file

@ -39,6 +39,7 @@ export interface InstalledVersion {
version: string; version: string;
path: string; path: string;
filename: string; filename: string;
size?: number;
} }
export interface ROCmDownload { export interface ROCmDownload {

35
src/utils/binaryUtils.ts Normal file
View 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';
};