mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 19:54:44 -07:00
better dummy icon, fix close to tray, implement launch tabs for more configurations, kobold lite iframe view, UX improvements and bug fixes
This commit is contained in:
parent
ab6ec3ffe7
commit
105e5cfbf7
33 changed files with 1658 additions and 970 deletions
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
|
|
@ -16,4 +16,4 @@
|
||||||
- Use proper TypeScript types (avoid `any` when possible)
|
- Use proper TypeScript types (avoid `any` when possible)
|
||||||
- Follow the ESLint configuration (includes SonarJS and security rules)
|
- Follow the ESLint configuration (includes SonarJS and security rules)
|
||||||
- Never create tests, docs or github workflows
|
- Never create tests, docs or github workflows
|
||||||
- don't log anything to console
|
- Stop asking me to run the "dev" script to test changes
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,10 @@ A koboldcpp manager.
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Linux Wayland support
|
||||||
|
|
||||||
|
Additional configurations have been written to help with ideal Wayland support, but as per current Electron guidelines, the user should set `ELECTRON_OZONE_PLATFORM_HINT` to `wayland` in their environment variable according to the [Electron Environment Variables documentation](https://www.electronjs.org/docs/latest/api/environment-variables#electron_ozone_platform_hint-linux).
|
||||||
|
|
||||||
### Future considerations
|
### Future considerations
|
||||||
|
|
||||||
It would make a lot of sense to transition this project to Tauri from Electron. The app size should drop from ~80MB to ~10MB; however, users on obsolete OSes (with outdated WebViews) will very likely encounter issues. In addition, I would need to learn Rust to rewrite the BE (Electron main code), but at least we can re-use all the React code. The app would be much smaller, faster and memory efficient, but not work for some users. I think it's a worthy tradeoff.
|
It would make a lot of sense to transition this project to Tauri from Electron. The app size should drop from ~80MB to ~10MB; however, users on obsolete OSes (with outdated WebViews) will very likely encounter issues. In addition, I would need to learn Rust to rewrite the BE (Electron main code), but at least we can re-use all the React code. The app would be much smaller, faster and memory efficient, but not work for some users. I think it's a worthy tradeoff.
|
||||||
|
|
|
||||||
9
assets/friendly-kobold.desktop
Normal file
9
assets/friendly-kobold.desktop
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=FriendlyKobold
|
||||||
|
Comment=A modern Electron shell for KoboldCpp
|
||||||
|
Exec=friendly-kobold %U
|
||||||
|
Icon=/home/eggy/Projects/friendly-kobold/assets/icon_512.png
|
||||||
|
Type=Application
|
||||||
|
Categories=Development;TextEditor;
|
||||||
|
StartupWMClass=FriendlyKobold
|
||||||
|
StartupNotify=true
|
||||||
BIN
assets/icon_256.png
Normal file
BIN
assets/icon_256.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
assets/icon_512.png
Normal file
BIN
assets/icon_512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
|
|
@ -205,6 +205,7 @@
|
||||||
"vite",
|
"vite",
|
||||||
"vitejs",
|
"vitejs",
|
||||||
"vitest",
|
"vitest",
|
||||||
|
"wayland",
|
||||||
"webContents",
|
"webContents",
|
||||||
"webkit",
|
"webkit",
|
||||||
"webp",
|
"webp",
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ export default defineConfig({
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: './index.html',
|
input: './index.html',
|
||||||
},
|
},
|
||||||
|
chunkSizeWarningLimit: 1000,
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src 'self' http://localhost:*; script-src 'self' 'unsafe-inline' 'unsafe-eval';"
|
content="default-src 'self' 'unsafe-inline'; connect-src 'self' http://localhost:*; script-src 'self' 'unsafe-inline'; frame-src 'self' http://localhost:*;"
|
||||||
/>
|
/>
|
||||||
<title>Friendly Kobold</title>
|
<title>Friendly Kobold</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
||||||
528
package-lock.json
generated
528
package-lock.json
generated
|
|
@ -11,8 +11,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@mantine/core": "^8.2.4",
|
"@mantine/core": "^8.2.4",
|
||||||
"@mantine/hooks": "^8.2.4",
|
"jiti": "^2.5.1",
|
||||||
"@mantine/notifications": "^8.2.4",
|
|
||||||
"lucide-react": "^0.539.0",
|
"lucide-react": "^0.539.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1"
|
"react-dom": "^19.1.1"
|
||||||
|
|
@ -26,7 +25,7 @@
|
||||||
"@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",
|
||||||
"concurrently": "^9.2.0",
|
"cross-env": "^10.0.0",
|
||||||
"cspell": "^9.2.0",
|
"cspell": "^9.2.0",
|
||||||
"electron": "^37.2.6",
|
"electron": "^37.2.6",
|
||||||
"electron-builder": "^26.0.12",
|
"electron-builder": "^26.0.12",
|
||||||
|
|
@ -39,7 +38,6 @@
|
||||||
"eslint-plugin-sonarjs": "^3.0.4",
|
"eslint-plugin-sonarjs": "^3.0.4",
|
||||||
"globals": "^16.3.0",
|
"globals": "^16.3.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"jiti": "^2.5.1",
|
|
||||||
"lint-staged": "^16.1.5",
|
"lint-staged": "^16.1.5",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"rollup-plugin-visualizer": "^6.0.3",
|
"rollup-plugin-visualizer": "^6.0.3",
|
||||||
|
|
@ -1078,6 +1076,23 @@
|
||||||
"electron-fuses": "dist/bin.js"
|
"electron-fuses": "dist/bin.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@electron/fuses/node_modules/chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@electron/fuses/node_modules/fs-extra": {
|
"node_modules/@electron/fuses/node_modules/fs-extra": {
|
||||||
"version": "9.1.0",
|
"version": "9.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
||||||
|
|
@ -1228,41 +1243,6 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@electron/node-gyp/node_modules/minipass": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@electron/node-gyp/node_modules/tar": {
|
|
||||||
"version": "6.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
|
||||||
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"chownr": "^2.0.0",
|
|
||||||
"fs-minipass": "^2.0.0",
|
|
||||||
"minipass": "^5.0.0",
|
|
||||||
"minizlib": "^2.1.1",
|
|
||||||
"mkdirp": "^1.0.3",
|
|
||||||
"yallist": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@electron/node-gyp/node_modules/yallist": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/@electron/notarize": {
|
"node_modules/@electron/notarize": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz",
|
||||||
|
|
@ -1419,6 +1399,23 @@
|
||||||
"node": ">=12.13.0"
|
"node": ">=12.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@electron/rebuild/node_modules/chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@electron/rebuild/node_modules/fs-extra": {
|
"node_modules/@electron/rebuild/node_modules/fs-extra": {
|
||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||||
|
|
@ -1447,34 +1444,6 @@
|
||||||
"graceful-fs": "^4.1.6"
|
"graceful-fs": "^4.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@electron/rebuild/node_modules/minipass": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@electron/rebuild/node_modules/tar": {
|
|
||||||
"version": "6.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
|
||||||
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"chownr": "^2.0.0",
|
|
||||||
"fs-minipass": "^2.0.0",
|
|
||||||
"minipass": "^5.0.0",
|
|
||||||
"minizlib": "^2.1.1",
|
|
||||||
"mkdirp": "^1.0.3",
|
|
||||||
"yallist": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@electron/rebuild/node_modules/universalify": {
|
"node_modules/@electron/rebuild/node_modules/universalify": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||||
|
|
@ -1485,13 +1454,6 @@
|
||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@electron/rebuild/node_modules/yallist": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/@electron/universal": {
|
"node_modules/@electron/universal": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz",
|
||||||
|
|
@ -1663,6 +1625,13 @@
|
||||||
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
|
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@epic-web/invariant": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.25.9",
|
"version": "0.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
|
||||||
|
|
@ -2681,31 +2650,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.2.4.tgz",
|
||||||
"integrity": "sha512-ZXQ0uk6UtJa6Gl+ZWMBh4wC7UqCAoWWvau1TOoe05sBrkyGcXSVrTfoCVzjEQaB/h8VEOUWLGtJokkiMQKcMzA==",
|
"integrity": "sha512-ZXQ0uk6UtJa6Gl+ZWMBh4wC7UqCAoWWvau1TOoe05sBrkyGcXSVrTfoCVzjEQaB/h8VEOUWLGtJokkiMQKcMzA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peer": true,
|
||||||
"react": "^18.x || ^19.x"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@mantine/notifications": {
|
|
||||||
"version": "8.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-8.2.4.tgz",
|
|
||||||
"integrity": "sha512-CPyYM1Y9oXxlJl5zTJN0mgJGZh8ZrhdIsA4ZktnpmJMKvGHWQdmtzTcPDu4gwzDNdANsN0f9DtMSp68kNiD1xA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@mantine/store": "8.2.4",
|
|
||||||
"react-transition-group": "4.4.5"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@mantine/core": "8.2.4",
|
|
||||||
"@mantine/hooks": "8.2.4",
|
|
||||||
"react": "^18.x || ^19.x",
|
|
||||||
"react-dom": "^18.x || ^19.x"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@mantine/store": {
|
|
||||||
"version": "8.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@mantine/store/-/store-8.2.4.tgz",
|
|
||||||
"integrity": "sha512-NYbhSy6UkVXsCDDHau+ZmGuuLgQ1laNINhKRHYabRvH5aSuU9drbgIlraNgzoF/+LeoTSQ8LylsdWNQRq0hqqA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^18.x || ^19.x"
|
"react": "^18.x || ^19.x"
|
||||||
}
|
}
|
||||||
|
|
@ -3867,34 +3812,6 @@
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/app-builder-lib/node_modules/minipass": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/app-builder-lib/node_modules/tar": {
|
|
||||||
"version": "6.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
|
||||||
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"chownr": "^2.0.0",
|
|
||||||
"fs-minipass": "^2.0.0",
|
|
||||||
"minipass": "^5.0.0",
|
|
||||||
"minizlib": "^2.1.1",
|
|
||||||
"mkdirp": "^1.0.3",
|
|
||||||
"yallist": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/app-builder-lib/node_modules/universalify": {
|
"node_modules/app-builder-lib/node_modules/universalify": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||||
|
|
@ -3905,13 +3822,6 @@
|
||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/app-builder-lib/node_modules/yallist": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/argparse": {
|
"node_modules/argparse": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
|
|
@ -4382,6 +4292,23 @@
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/builder-util/node_modules/chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/builder-util/node_modules/fs-extra": {
|
"node_modules/builder-util/node_modules/fs-extra": {
|
||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||||
|
|
@ -4527,41 +4454,6 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cacache/node_modules/tar": {
|
|
||||||
"version": "6.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
|
||||||
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"chownr": "^2.0.0",
|
|
||||||
"fs-minipass": "^2.0.0",
|
|
||||||
"minipass": "^5.0.0",
|
|
||||||
"minizlib": "^2.1.1",
|
|
||||||
"mkdirp": "^1.0.3",
|
|
||||||
"yallist": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cacache/node_modules/tar/node_modules/minipass": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cacache/node_modules/yallist": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/cacheable-lookup": {
|
"node_modules/cacheable-lookup": {
|
||||||
"version": "5.0.4",
|
"version": "5.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
|
||||||
|
|
@ -4651,9 +4543,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001734",
|
"version": "1.0.30001735",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz",
|
||||||
"integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==",
|
"integrity": "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -4672,17 +4564,13 @@
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "5.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz",
|
||||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
"integrity": "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
|
||||||
"ansi-styles": "^4.1.0",
|
|
||||||
"supports-color": "^7.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
|
@ -4704,32 +4592,6 @@
|
||||||
"url": "https://github.com/chalk/chalk-template?sponsor=1"
|
"url": "https://github.com/chalk/chalk-template?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chalk-template/node_modules/chalk": {
|
|
||||||
"version": "5.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz",
|
|
||||||
"integrity": "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/chalk/node_modules/supports-color": {
|
|
||||||
"version": "7.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
|
||||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"has-flag": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/chownr": {
|
"node_modules/chownr": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||||
|
|
@ -5009,32 +4871,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/concurrently": {
|
|
||||||
"version": "9.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.0.tgz",
|
|
||||||
"integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"chalk": "^4.1.2",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"rxjs": "^7.8.1",
|
|
||||||
"shell-quote": "^1.8.1",
|
|
||||||
"supports-color": "^8.1.1",
|
|
||||||
"tree-kill": "^1.2.2",
|
|
||||||
"yargs": "^17.7.2"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"conc": "dist/bin/concurrently.js",
|
|
||||||
"concurrently": "dist/bin/concurrently.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/config-file-ts": {
|
"node_modules/config-file-ts": {
|
||||||
"version": "0.2.8-rc1",
|
"version": "0.2.8-rc1",
|
||||||
"resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.8-rc1.tgz",
|
"resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.8-rc1.tgz",
|
||||||
|
|
@ -5126,6 +4962,24 @@
|
||||||
"buffer": "^5.1.0"
|
"buffer": "^5.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cross-env": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-aU8qlEK/nHYtVuN4p7UQgAwVljzMg8hB4YK5ThRqD2l/ziSnryncPNn7bMLt5cFYsKVKBh8HqLqyCoTupEUu7Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@epic-web/invariant": "^1.0.0",
|
||||||
|
"cross-spawn": "^7.0.6"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"cross-env": "dist/bin/cross-env.js",
|
||||||
|
"cross-env-shell": "dist/bin/cross-env-shell.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
|
|
@ -5324,19 +5178,6 @@
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cspell/node_modules/chalk": {
|
|
||||||
"version": "5.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz",
|
|
||||||
"integrity": "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
|
|
@ -5684,16 +5525,6 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dom-helpers": {
|
|
||||||
"version": "5.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
|
||||||
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.8.7",
|
|
||||||
"csstype": "^3.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.6.1",
|
"version": "16.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||||
|
|
@ -5819,6 +5650,23 @@
|
||||||
"electron-winstaller": "5.4.0"
|
"electron-winstaller": "5.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/electron-builder/node_modules/chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron-builder/node_modules/fs-extra": {
|
"node_modules/electron-builder/node_modules/fs-extra": {
|
||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||||
|
|
@ -5874,6 +5722,23 @@
|
||||||
"mime": "^2.5.2"
|
"mime": "^2.5.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/electron-publish/node_modules/chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron-publish/node_modules/fs-extra": {
|
"node_modules/electron-publish/node_modules/fs-extra": {
|
||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||||
|
|
@ -6683,6 +6548,23 @@
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eslint/node_modules/chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eslint/node_modules/eslint-visitor-keys": {
|
"node_modules/eslint/node_modules/eslint-visitor-keys": {
|
||||||
"version": "4.2.1",
|
"version": "4.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
|
||||||
|
|
@ -8487,7 +8369,6 @@
|
||||||
"version": "2.5.1",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
|
||||||
"integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
|
"integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
|
|
@ -8690,19 +8571,6 @@
|
||||||
"url": "https://opencollective.com/lint-staged"
|
"url": "https://opencollective.com/lint-staged"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lint-staged/node_modules/chalk": {
|
|
||||||
"version": "5.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz",
|
|
||||||
"integrity": "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/listr2": {
|
"node_modules/listr2": {
|
||||||
"version": "9.0.1",
|
"version": "9.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.1.tgz",
|
||||||
|
|
@ -8853,6 +8721,23 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/log-symbols/node_modules/chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/log-update": {
|
"node_modules/log-update": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
|
||||||
|
|
@ -8923,6 +8808,7 @@
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
|
|
@ -9456,6 +9342,7 @@
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
|
|
@ -9670,6 +9557,23 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ora/node_modules/chalk": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ora/node_modules/cli-cursor": {
|
"node_modules/ora/node_modules/cli-cursor": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||||
|
|
@ -10105,6 +10009,7 @@
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.4.0",
|
"loose-envify": "^1.4.0",
|
||||||
|
|
@ -10307,22 +10212,6 @@
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-transition-group": {
|
|
||||||
"version": "4.4.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
|
||||||
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.5.5",
|
|
||||||
"dom-helpers": "^5.0.1",
|
|
||||||
"loose-envify": "^1.4.0",
|
|
||||||
"prop-types": "^15.6.2"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">=16.6.0",
|
|
||||||
"react-dom": ">=16.6.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/read-binary-file-arch": {
|
"node_modules/read-binary-file-arch": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz",
|
||||||
|
|
@ -10951,19 +10840,6 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/shell-quote": {
|
|
||||||
"version": "1.8.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
|
|
||||||
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/side-channel": {
|
"node_modules/side-channel": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||||
|
|
@ -11512,19 +11388,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/supports-color": {
|
"node_modules/supports-color": {
|
||||||
"version": "8.1.1",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-flag": "^4.0.0"
|
"has-flag": "^4.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=8"
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/supports-preserve-symlinks-flag": {
|
"node_modules/supports-preserve-symlinks-flag": {
|
||||||
|
|
@ -11561,6 +11434,41 @@
|
||||||
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
|
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tar": {
|
||||||
|
"version": "6.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||||
|
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"chownr": "^2.0.0",
|
||||||
|
"fs-minipass": "^2.0.0",
|
||||||
|
"minipass": "^5.0.0",
|
||||||
|
"minizlib": "^2.1.1",
|
||||||
|
"mkdirp": "^1.0.3",
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tar/node_modules/minipass": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tar/node_modules/yallist": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/temp": {
|
"node_modules/temp": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz",
|
||||||
|
|
@ -11724,16 +11632,6 @@
|
||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tree-kill": {
|
|
||||||
"version": "1.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
|
||||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"tree-kill": "cli.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/truncate-utf8-bytes": {
|
"node_modules/truncate-utf8-bytes": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
|
||||||
|
|
|
||||||
25
package.json
25
package.json
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "friendly-kobold",
|
"name": "friendly-kobold",
|
||||||
"productName": "friendly-kobold",
|
"productName": "Friendly Kobold",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "A modern Electron shell for KoboldCpp",
|
"description": "A modern Electron shell for KoboldCpp",
|
||||||
"main": "out/main/index.js",
|
"main": "out/main/index.js",
|
||||||
|
|
@ -8,10 +8,10 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "electron-vite dev",
|
"dev": "electron-vite dev",
|
||||||
"build": "electron-vite build && electron-builder",
|
"build": "electron-vite build && electron-builder",
|
||||||
"build:analyze": "ANALYZE=true electron-vite build",
|
"build:analyze": "cross-env ANALYZE=true electron-vite build && npx open-cli dist/stats.html",
|
||||||
"build:electron": "electron-vite build",
|
"build:electron": "electron-vite build",
|
||||||
"analyze": "npm run build:analyze",
|
"analyze": "npm run build:analyze",
|
||||||
"analyze:server": "ANALYZE=server electron-vite build",
|
"analyze:server": "cross-env ANALYZE=server electron-vite build && npx open-cli dist/stats.html",
|
||||||
"preview": "electron-vite preview",
|
"preview": "electron-vite preview",
|
||||||
"preb": "electron-vite build",
|
"preb": "electron-vite build",
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
"@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",
|
||||||
"concurrently": "^9.2.0",
|
"cross-env": "^10.0.0",
|
||||||
"cspell": "^9.2.0",
|
"cspell": "^9.2.0",
|
||||||
"electron": "^37.2.6",
|
"electron": "^37.2.6",
|
||||||
"electron-builder": "^26.0.12",
|
"electron-builder": "^26.0.12",
|
||||||
|
|
@ -67,7 +67,6 @@
|
||||||
"eslint-plugin-sonarjs": "^3.0.4",
|
"eslint-plugin-sonarjs": "^3.0.4",
|
||||||
"globals": "^16.3.0",
|
"globals": "^16.3.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"jiti": "^2.5.1",
|
|
||||||
"lint-staged": "^16.1.5",
|
"lint-staged": "^16.1.5",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"rollup-plugin-visualizer": "^6.0.3",
|
"rollup-plugin-visualizer": "^6.0.3",
|
||||||
|
|
@ -78,16 +77,16 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@mantine/core": "^8.2.4",
|
"@mantine/core": "^8.2.4",
|
||||||
"@mantine/hooks": "^8.2.4",
|
"jiti": "^2.5.1",
|
||||||
"@mantine/notifications": "^8.2.4",
|
|
||||||
"lucide-react": "^0.539.0",
|
"lucide-react": "^0.539.0",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1"
|
"react-dom": "^19.1.1"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.friendlykobold.app",
|
"appId": "com.friendly-kobold.app",
|
||||||
"productName": "Friendly Kobold",
|
"productName": "Friendly Kobold",
|
||||||
"compression": "maximum",
|
"compression": "maximum",
|
||||||
|
"icon": "assets/icon_512.png",
|
||||||
"directories": {
|
"directories": {
|
||||||
"output": "release"
|
"output": "release"
|
||||||
},
|
},
|
||||||
|
|
@ -118,7 +117,6 @@
|
||||||
{
|
{
|
||||||
"target": "dmg",
|
"target": "dmg",
|
||||||
"arch": [
|
"arch": [
|
||||||
"x64",
|
|
||||||
"arm64"
|
"arm64"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -135,6 +133,15 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
|
"category": "Development",
|
||||||
|
"desktop": {
|
||||||
|
"entry": {
|
||||||
|
"Name": "Friendly Kobold",
|
||||||
|
"Comment": "A modern Electron shell for KoboldCpp",
|
||||||
|
"Categories": "Development;Utility;",
|
||||||
|
"StartupWMClass": "Friendly Kobold"
|
||||||
|
}
|
||||||
|
},
|
||||||
"target": [
|
"target": [
|
||||||
{
|
{
|
||||||
"target": "AppImage",
|
"target": "AppImage",
|
||||||
|
|
|
||||||
139
src/App.tsx
139
src/App.tsx
|
|
@ -1,73 +1,28 @@
|
||||||
import { useState, useEffect, ReactNode } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
AppShell,
|
AppShell,
|
||||||
Group,
|
Group,
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
rem,
|
rem,
|
||||||
Transition,
|
|
||||||
Loader,
|
Loader,
|
||||||
Center,
|
Center,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
Button,
|
Button,
|
||||||
|
Select,
|
||||||
useMantineColorScheme,
|
useMantineColorScheme,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Notifications } from '@mantine/notifications';
|
|
||||||
import { Settings, ArrowLeft } from 'lucide-react';
|
import { Settings, ArrowLeft } from 'lucide-react';
|
||||||
import { DownloadScreen } from '@/screens/DownloadScreen';
|
import { DownloadScreen } from '@/screens/DownloadScreen';
|
||||||
import { LaunchScreen } from '@/screens/LaunchScreen';
|
import { LaunchScreen } from '@/screens/LaunchScreen';
|
||||||
import { TerminalScreen } from '@/screens/TerminalScreen';
|
import { InterfaceScreen } from '@/screens/InterfaceScreen';
|
||||||
import { UpdateDialog } from '@/components/UpdateDialog';
|
import { UpdateDialog } from '@/components/UpdateDialog';
|
||||||
import { SettingsModal } from '@/components/SettingsModal';
|
import { SettingsModal } from '@/components/SettingsModal';
|
||||||
import { useNotifications } from '@/hooks/useNotifications';
|
import { ScreenTransition } from '@/components/ScreenTransition';
|
||||||
import type { UpdateInfo } from '@/types';
|
import type { UpdateInfo } from '@/types';
|
||||||
|
|
||||||
type Screen = 'download' | 'launch' | 'terminal';
|
type Screen = 'download' | 'launch' | 'interface';
|
||||||
|
|
||||||
interface ScreenTransitionProps {
|
|
||||||
isActive: boolean;
|
|
||||||
shouldAnimate: boolean;
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ScreenTransition = ({
|
|
||||||
isActive,
|
|
||||||
shouldAnimate,
|
|
||||||
children,
|
|
||||||
}: ScreenTransitionProps) => {
|
|
||||||
const getTransform = () => {
|
|
||||||
if (!shouldAnimate) return undefined;
|
|
||||||
const scale = isActive ? 1 : 0.98;
|
|
||||||
return `scale(${scale})`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition
|
|
||||||
mounted={isActive}
|
|
||||||
transition="fade"
|
|
||||||
duration={shouldAnimate ? 350 : 0}
|
|
||||||
timingFunction="ease-out"
|
|
||||||
>
|
|
||||||
{(styles) => (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
...styles,
|
|
||||||
position: isActive ? 'static' : 'absolute',
|
|
||||||
width: '100%',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
zIndex: isActive ? 1 : 0,
|
|
||||||
transform: `${styles.transform || ''} ${getTransform() || ''}`,
|
|
||||||
transition: shouldAnimate ? 'all 350ms ease-out' : undefined,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Transition>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const [currentScreen, setCurrentScreen] = useState<Screen | null>(null);
|
const [currentScreen, setCurrentScreen] = useState<Screen | null>(null);
|
||||||
|
|
@ -75,14 +30,27 @@ export const App = () => {
|
||||||
const [showUpdateDialog, setShowUpdateDialog] = useState(false);
|
const [showUpdateDialog, setShowUpdateDialog] = useState(false);
|
||||||
const [settingsOpened, setSettingsOpened] = useState(false);
|
const [settingsOpened, setSettingsOpened] = useState(false);
|
||||||
const [hasInitialized, setHasInitialized] = useState(false);
|
const [hasInitialized, setHasInitialized] = useState(false);
|
||||||
|
const [activeInterfaceTab, setActiveInterfaceTab] = useState<string | null>(
|
||||||
|
'terminal'
|
||||||
|
);
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
const notify = useNotifications();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkInstallation = async () => {
|
const checkInstallation = async () => {
|
||||||
try {
|
try {
|
||||||
const installed = await window.electronAPI.kobold.isInstalled();
|
const startTime = Date.now();
|
||||||
setCurrentScreen(installed ? 'launch' : 'download');
|
const installedVersions =
|
||||||
|
await window.electronAPI.kobold.getInstalledVersions(false);
|
||||||
|
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
const minDelay = 500;
|
||||||
|
if (elapsed < minDelay) {
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, minDelay - elapsed)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentScreen(installedVersions.length > 0 ? 'launch' : 'download');
|
||||||
setHasInitialized(true);
|
setHasInitialized(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking installation:', error);
|
console.error('Error checking installation:', error);
|
||||||
|
|
@ -109,18 +77,13 @@ export const App = () => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDownloadComplete = () => {
|
const handleDownloadComplete = () => {
|
||||||
notify.success(
|
|
||||||
'Download Complete',
|
|
||||||
'KoboldCpp has been successfully installed'
|
|
||||||
);
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setCurrentScreen('launch');
|
setCurrentScreen('launch');
|
||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLaunch = () => {
|
const handleLaunch = () => {
|
||||||
setCurrentScreen('terminal');
|
setCurrentScreen('interface');
|
||||||
notify.success('Launch Started', 'KoboldCpp is starting up...');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBackToLaunch = () => {
|
const handleBackToLaunch = () => {
|
||||||
|
|
@ -142,7 +105,6 @@ export const App = () => {
|
||||||
await window.electronAPI.kobold.stopKoboldCpp();
|
await window.electronAPI.kobold.stopKoboldCpp();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error stopping KoboldCpp:', error);
|
console.error('Error stopping KoboldCpp:', error);
|
||||||
notify.error('Stop Failed', 'Failed to stop KoboldCpp process');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBackToLaunch();
|
handleBackToLaunch();
|
||||||
|
|
@ -159,25 +121,20 @@ export const App = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Notifications
|
|
||||||
position="bottom-right"
|
|
||||||
zIndex={1000}
|
|
||||||
containerWidth={320}
|
|
||||||
/>
|
|
||||||
<AppShell header={{ height: 60 }} padding="md">
|
<AppShell header={{ height: 60 }} padding="md">
|
||||||
<AppShell.Header
|
<AppShell.Header
|
||||||
style={{
|
style={{
|
||||||
borderBottom: `1px solid var(--mantine-color-${colorScheme === 'dark' ? 'dark-4' : 'gray-3'})`,
|
borderBottom: `1px solid var(--mantine-color-${colorScheme === 'dark' ? 'dark-4' : 'gray-3'})`,
|
||||||
backdropFilter: 'blur(10px)',
|
|
||||||
background:
|
background:
|
||||||
colorScheme === 'dark'
|
colorScheme === 'dark'
|
||||||
? 'rgba(26, 27, 30, 0.8)'
|
? 'var(--mantine-color-dark-7)'
|
||||||
: 'rgba(255, 255, 255, 0.8)',
|
: 'var(--mantine-color-white)',
|
||||||
transition: 'all 200ms ease',
|
transition: 'all 200ms ease',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Group h="100%" px="md" justify="space-between">
|
<Group h="100%" px="md" justify="space-between" align="center">
|
||||||
{currentScreen === 'terminal' && (
|
<div style={{ minWidth: '100px' }}>
|
||||||
|
{currentScreen === 'interface' && (
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
color="red"
|
color="red"
|
||||||
|
|
@ -187,8 +144,39 @@ export const App = () => {
|
||||||
Eject
|
Eject
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<Group ml="auto">
|
{currentScreen === 'interface' && (
|
||||||
|
<Select
|
||||||
|
value={activeInterfaceTab}
|
||||||
|
onChange={setActiveInterfaceTab}
|
||||||
|
data={[
|
||||||
|
{ value: 'chat', label: 'KoboldAI Lite' },
|
||||||
|
{ value: 'terminal', label: 'Terminal' },
|
||||||
|
]}
|
||||||
|
placeholder="Select view"
|
||||||
|
styles={{
|
||||||
|
input: {
|
||||||
|
minWidth: '150px',
|
||||||
|
textAlign: 'center',
|
||||||
|
border: 'none',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
dropdown: {
|
||||||
|
minWidth: '150px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
minWidth: '100px',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Tooltip label="Settings" position="bottom">
|
<Tooltip label="Settings" position="bottom">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
|
|
@ -203,7 +191,7 @@ export const App = () => {
|
||||||
<Settings style={{ width: rem(20), height: rem(20) }} />
|
<Settings style={{ width: rem(20), height: rem(20) }} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Group>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
</AppShell.Header>
|
</AppShell.Header>
|
||||||
<AppShell.Main
|
<AppShell.Main
|
||||||
|
|
@ -239,10 +227,13 @@ export const App = () => {
|
||||||
</ScreenTransition>
|
</ScreenTransition>
|
||||||
|
|
||||||
<ScreenTransition
|
<ScreenTransition
|
||||||
isActive={currentScreen === 'terminal'}
|
isActive={currentScreen === 'interface'}
|
||||||
shouldAnimate={hasInitialized}
|
shouldAnimate={hasInitialized}
|
||||||
>
|
>
|
||||||
<TerminalScreen />
|
<InterfaceScreen
|
||||||
|
activeTab={activeInterfaceTab}
|
||||||
|
onTabChange={setActiveInterfaceTab}
|
||||||
|
/>
|
||||||
</ScreenTransition>
|
</ScreenTransition>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
36
src/components/ScreenTransition.tsx
Normal file
36
src/components/ScreenTransition.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { Transition } from '@mantine/core';
|
||||||
|
|
||||||
|
interface ScreenTransitionProps {
|
||||||
|
isActive: boolean;
|
||||||
|
shouldAnimate: boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ScreenTransition = ({
|
||||||
|
isActive,
|
||||||
|
shouldAnimate,
|
||||||
|
children,
|
||||||
|
}: ScreenTransitionProps) => (
|
||||||
|
<Transition
|
||||||
|
mounted={isActive}
|
||||||
|
transition="fade"
|
||||||
|
duration={shouldAnimate ? 100 : 0}
|
||||||
|
timingFunction="ease-in-out"
|
||||||
|
>
|
||||||
|
{(styles) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
...styles,
|
||||||
|
position: isActive ? 'static' : 'absolute',
|
||||||
|
width: '100%',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
zIndex: isActive ? 1 : 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Transition>
|
||||||
|
);
|
||||||
51
src/components/interface/ChatTab.tsx
Normal file
51
src/components/interface/ChatTab.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import { Box, Text, Stack } from '@mantine/core';
|
||||||
|
|
||||||
|
interface ChatTabProps {
|
||||||
|
serverUrl?: string;
|
||||||
|
isServerReady?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChatTab = ({ serverUrl, isServerReady }: ChatTabProps) => {
|
||||||
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
|
|
||||||
|
if (!isServerReady || !serverUrl) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack align="center" gap="md">
|
||||||
|
<Text c="dimmed" size="lg">
|
||||||
|
Waiting for KoboldCpp server to start...
|
||||||
|
</Text>
|
||||||
|
<Text c="dimmed" size="sm">
|
||||||
|
The chat interface will load automatically when ready
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box style={{ width: '100%', height: '100%', overflow: 'hidden' }}>
|
||||||
|
<iframe
|
||||||
|
ref={iframeRef}
|
||||||
|
src={serverUrl}
|
||||||
|
title="KoboldAI Lite Interface"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 'inherit',
|
||||||
|
}}
|
||||||
|
allow="clipboard-read; clipboard-write"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
173
src/components/interface/TerminalTab.tsx
Normal file
173
src/components/interface/TerminalTab.tsx
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import { Box, ScrollArea, Text, ActionIcon } from '@mantine/core';
|
||||||
|
import { ChevronDown } from 'lucide-react';
|
||||||
|
|
||||||
|
interface TerminalTabProps {
|
||||||
|
onServerReady?: (serverUrl: string) => void;
|
||||||
|
serverOnly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TerminalTab = ({
|
||||||
|
onServerReady,
|
||||||
|
serverOnly,
|
||||||
|
}: TerminalTabProps) => {
|
||||||
|
const [terminalContent, setTerminalContent] = useState<string>('');
|
||||||
|
const [isUserScrolling, setIsUserScrolling] = useState<boolean>(false);
|
||||||
|
const [shouldAutoScroll, setShouldAutoScroll] = useState<boolean>(true);
|
||||||
|
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||||
|
const viewportRef = useRef<HTMLDivElement>(null);
|
||||||
|
const lastScrollTop = useRef<number>(0);
|
||||||
|
|
||||||
|
const handleScroll = ({ y }: { y: number }) => {
|
||||||
|
if (!viewportRef.current) return;
|
||||||
|
|
||||||
|
const { scrollHeight, clientHeight } = viewportRef.current;
|
||||||
|
const isAtBottomNow = y + clientHeight >= scrollHeight - 10;
|
||||||
|
|
||||||
|
if (y < lastScrollTop.current) {
|
||||||
|
setIsUserScrolling(true);
|
||||||
|
setShouldAutoScroll(false);
|
||||||
|
} else if (isAtBottomNow) {
|
||||||
|
setIsUserScrolling(false);
|
||||||
|
setShouldAutoScroll(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastScrollTop.current = y;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (shouldAutoScroll && !isUserScrolling && viewportRef.current) {
|
||||||
|
const viewport = viewportRef.current;
|
||||||
|
viewport.scrollTop = viewport.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [terminalContent, shouldAutoScroll, isUserScrolling]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const cleanup = window.electronAPI.kobold.onKoboldOutput((data: string) => {
|
||||||
|
setTerminalContent((prev) => {
|
||||||
|
const lines = prev.split('\n');
|
||||||
|
const newData = data.toString();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!serverOnly &&
|
||||||
|
onServerReady &&
|
||||||
|
newData.includes('Please connect to custom endpoint at ')
|
||||||
|
) {
|
||||||
|
const match = newData.match(
|
||||||
|
/Please connect to custom endpoint at (http:\/\/[^\s]+)/
|
||||||
|
);
|
||||||
|
if (match) {
|
||||||
|
const serverUrl = match[1];
|
||||||
|
setTimeout(() => onServerReady(serverUrl), 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newData.includes('\r')) {
|
||||||
|
const parts = newData.split('\r');
|
||||||
|
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
if (i === 0) {
|
||||||
|
if (lines.length > 0) {
|
||||||
|
lines[lines.length - 1] += parts[i];
|
||||||
|
} else {
|
||||||
|
lines.push(parts[i]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (lines.length > 0) {
|
||||||
|
lines[lines.length - 1] = parts[i];
|
||||||
|
} else {
|
||||||
|
lines.push(parts[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines.join('\n');
|
||||||
|
} else {
|
||||||
|
return prev + newData;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return cleanup;
|
||||||
|
}, [onServerReady, serverOnly]);
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
if (viewportRef.current) {
|
||||||
|
const viewport = viewportRef.current;
|
||||||
|
viewport.scrollTop = viewport.scrollHeight;
|
||||||
|
setShouldAutoScroll(true);
|
||||||
|
setIsUserScrolling(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
height: '80vh',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
backgroundColor: 'var(--mantine-color-dark-8)',
|
||||||
|
borderRadius: 'inherit',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScrollArea
|
||||||
|
ref={scrollAreaRef}
|
||||||
|
viewportRef={viewportRef}
|
||||||
|
onScrollPositionChange={handleScroll}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", monospace',
|
||||||
|
fontSize: '14px',
|
||||||
|
lineHeight: 1.4,
|
||||||
|
}}
|
||||||
|
scrollbarSize={8}
|
||||||
|
offsetScrollbars={false}
|
||||||
|
>
|
||||||
|
<Box p="md">
|
||||||
|
{terminalContent.length === 0 ? (
|
||||||
|
<Text c="dimmed" style={{ fontFamily: 'inherit' }}>
|
||||||
|
Starting KoboldCpp...
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Text
|
||||||
|
component="pre"
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
fontSize: 'inherit',
|
||||||
|
lineHeight: 'inherit',
|
||||||
|
color: 'var(--mantine-color-gray-0)',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{terminalContent}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
{isUserScrolling && !shouldAutoScroll && (
|
||||||
|
<ActionIcon
|
||||||
|
variant="filled"
|
||||||
|
color="blue"
|
||||||
|
size="lg"
|
||||||
|
radius="xl"
|
||||||
|
onClick={scrollToBottom}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '20px',
|
||||||
|
right: '20px',
|
||||||
|
zIndex: 10,
|
||||||
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
|
||||||
|
}}
|
||||||
|
aria-label="Scroll to bottom"
|
||||||
|
>
|
||||||
|
<ChevronDown size={20} />
|
||||||
|
</ActionIcon>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
89
src/components/launch/AdvancedTab.tsx
Normal file
89
src/components/launch/AdvancedTab.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
import {
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Group,
|
||||||
|
TextInput,
|
||||||
|
ActionIcon,
|
||||||
|
Tooltip,
|
||||||
|
Switch,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { Info } from 'lucide-react';
|
||||||
|
|
||||||
|
interface AdvancedTabProps {
|
||||||
|
additionalArguments: string;
|
||||||
|
serverOnly: boolean;
|
||||||
|
onAdditionalArgumentsChange: (args: string) => void;
|
||||||
|
onServerOnlyChange: (serverOnly: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AdvancedTab = ({
|
||||||
|
additionalArguments,
|
||||||
|
serverOnly,
|
||||||
|
onAdditionalArgumentsChange,
|
||||||
|
onServerOnlyChange,
|
||||||
|
}: AdvancedTabProps) => (
|
||||||
|
<Stack gap="lg">
|
||||||
|
<div>
|
||||||
|
<Group gap="xs" align="center" mb="xs">
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
Additional arguments
|
||||||
|
</Text>
|
||||||
|
<Tooltip
|
||||||
|
label="Additional command line arguments to pass to the KoboldCPP binary. Leave this empty if you don't know what they are."
|
||||||
|
multiline
|
||||||
|
w={300}
|
||||||
|
withArrow
|
||||||
|
color="dark"
|
||||||
|
styles={{
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: 'var(--mantine-color-dark-6)',
|
||||||
|
color: 'var(--mantine-color-gray-0)',
|
||||||
|
border: '1px solid var(--mantine-color-dark-4)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ActionIcon variant="subtle" size="xs" color="gray">
|
||||||
|
<Info size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Additional command line arguments"
|
||||||
|
value={additionalArguments}
|
||||||
|
onChange={(event) =>
|
||||||
|
onAdditionalArgumentsChange(event.currentTarget.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Group gap="xs" align="center" mb="xs">
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
Server-only mode
|
||||||
|
</Text>
|
||||||
|
<Tooltip
|
||||||
|
label="In server-only mode, the KoboldAI Lite web UI won't be displayed. Use this if you'll be using your own frontend to interact with the LLM."
|
||||||
|
multiline
|
||||||
|
w={300}
|
||||||
|
withArrow
|
||||||
|
color="dark"
|
||||||
|
styles={{
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: 'var(--mantine-color-dark-6)',
|
||||||
|
color: 'var(--mantine-color-gray-0)',
|
||||||
|
border: '1px solid var(--mantine-color-dark-4)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ActionIcon variant="subtle" size="xs" color="gray">
|
||||||
|
<Info size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
<Switch
|
||||||
|
checked={serverOnly}
|
||||||
|
onChange={(event) => onServerOnlyChange(event.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
58
src/components/launch/ConfigurationManager.tsx
Normal file
58
src/components/launch/ConfigurationManager.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { Stack, Text, Group, Button, ActionIcon, Menu } from '@mantine/core';
|
||||||
|
import { RotateCcw, Save, Settings2 } from 'lucide-react';
|
||||||
|
import { ConfigFileSelect } from '@/components/ConfigFileSelect';
|
||||||
|
import type { ConfigFile } from '@/types';
|
||||||
|
|
||||||
|
interface ConfigurationManagerProps {
|
||||||
|
configFiles: ConfigFile[];
|
||||||
|
selectedFile: string | null;
|
||||||
|
onFileSelection: (fileName: string) => void;
|
||||||
|
onRefresh: () => void;
|
||||||
|
onSaveAsNew: () => void;
|
||||||
|
onUpdateCurrent: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConfigurationManager = ({
|
||||||
|
configFiles,
|
||||||
|
selectedFile,
|
||||||
|
onFileSelection,
|
||||||
|
onRefresh,
|
||||||
|
onSaveAsNew,
|
||||||
|
onUpdateCurrent,
|
||||||
|
}: ConfigurationManagerProps) => (
|
||||||
|
<Stack gap="md">
|
||||||
|
<Group justify="space-between" align="center">
|
||||||
|
<Text fw={500}>Configuration File</Text>
|
||||||
|
<Group gap="xs">
|
||||||
|
<Menu>
|
||||||
|
<Menu.Target>
|
||||||
|
<Button variant="light" leftSection={<Save size={16} />}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Menu.Target>
|
||||||
|
<Menu.Dropdown>
|
||||||
|
<Menu.Item leftSection={<Save size={16} />} onClick={onSaveAsNew}>
|
||||||
|
Save as new configuration
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<Settings2 size={16} />}
|
||||||
|
disabled={!selectedFile}
|
||||||
|
onClick={onUpdateCurrent}
|
||||||
|
>
|
||||||
|
Update current configuration
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
<ActionIcon variant="light" onClick={onRefresh} size="lg">
|
||||||
|
<RotateCcw size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<ConfigFileSelect
|
||||||
|
configFiles={configFiles}
|
||||||
|
selectedFile={selectedFile}
|
||||||
|
onFileSelection={onFileSelection}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
197
src/components/launch/GeneralTab.tsx
Normal file
197
src/components/launch/GeneralTab.tsx
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
import {
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Group,
|
||||||
|
TextInput,
|
||||||
|
Button,
|
||||||
|
ActionIcon,
|
||||||
|
Tooltip,
|
||||||
|
Checkbox,
|
||||||
|
Slider,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { File, Info, Search } from 'lucide-react';
|
||||||
|
|
||||||
|
interface GeneralTabProps {
|
||||||
|
modelPath: string;
|
||||||
|
gpuLayers: number;
|
||||||
|
autoGpuLayers: boolean;
|
||||||
|
contextSize: number;
|
||||||
|
onModelPathChange: (path: string) => void;
|
||||||
|
onSelectModelFile: () => void;
|
||||||
|
onGpuLayersChange: (layers: number) => void;
|
||||||
|
onAutoGpuLayersChange: (auto: boolean) => void;
|
||||||
|
onContextSizeChange: (size: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GeneralTab = ({
|
||||||
|
modelPath,
|
||||||
|
gpuLayers,
|
||||||
|
autoGpuLayers,
|
||||||
|
contextSize,
|
||||||
|
onModelPathChange,
|
||||||
|
onSelectModelFile,
|
||||||
|
onGpuLayersChange,
|
||||||
|
onAutoGpuLayersChange,
|
||||||
|
onContextSizeChange,
|
||||||
|
}: GeneralTabProps) => (
|
||||||
|
<Stack gap="lg">
|
||||||
|
<div>
|
||||||
|
<Text size="sm" fw={500} mb="xs">
|
||||||
|
Model File *
|
||||||
|
</Text>
|
||||||
|
<Group gap="xs">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Select a .gguf model file"
|
||||||
|
value={modelPath}
|
||||||
|
onChange={(event) => onModelPathChange(event.currentTarget.value)}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={onSelectModelFile}
|
||||||
|
variant="light"
|
||||||
|
leftSection={<File size={16} />}
|
||||||
|
>
|
||||||
|
Browse
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
window.electronAPI.app.openExternal(
|
||||||
|
'https://huggingface.co/models?pipeline_tag=text-generation&library=gguf&sort=trending'
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
variant="outline"
|
||||||
|
leftSection={<Search size={16} />}
|
||||||
|
>
|
||||||
|
Search HF
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Group justify="space-between" align="center" mb="xs">
|
||||||
|
<Group gap="xs" align="center">
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
GPU Layers
|
||||||
|
</Text>
|
||||||
|
<Tooltip
|
||||||
|
label="The number of layer's to offload to your GPU's VRAM. Ideally the entire LLM should fit inside the VRAM for optimal performance."
|
||||||
|
multiline
|
||||||
|
w={300}
|
||||||
|
withArrow
|
||||||
|
color="dark"
|
||||||
|
styles={{
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: 'var(--mantine-color-dark-6)',
|
||||||
|
color: 'var(--mantine-color-gray-0)',
|
||||||
|
border: '1px solid var(--mantine-color-dark-4)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ActionIcon variant="subtle" size="xs" color="gray">
|
||||||
|
<Info size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
<Group gap="lg" align="center">
|
||||||
|
<Group gap="xs" align="center">
|
||||||
|
<Checkbox
|
||||||
|
label="Auto"
|
||||||
|
checked={autoGpuLayers}
|
||||||
|
onChange={(event) =>
|
||||||
|
onAutoGpuLayersChange(event.currentTarget.checked)
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
label="Automatically try to allocate the GPU layers based on available VRAM."
|
||||||
|
multiline
|
||||||
|
w={300}
|
||||||
|
withArrow
|
||||||
|
color="dark"
|
||||||
|
styles={{
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: 'var(--mantine-color-dark-6)',
|
||||||
|
color: 'var(--mantine-color-gray-0)',
|
||||||
|
border: '1px solid var(--mantine-color-dark-4)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ActionIcon variant="subtle" size="xs" color="gray">
|
||||||
|
<Info size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
<TextInput
|
||||||
|
value={gpuLayers.toString()}
|
||||||
|
onChange={(event) =>
|
||||||
|
onGpuLayersChange(Number(event.target.value) || 0)
|
||||||
|
}
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
size="sm"
|
||||||
|
w={80}
|
||||||
|
disabled={autoGpuLayers}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
<Slider
|
||||||
|
value={gpuLayers}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
onChange={onGpuLayersChange}
|
||||||
|
disabled={autoGpuLayers}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Group justify="space-between" align="center" mb="xs">
|
||||||
|
<Group gap="xs" align="center">
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
Context Size
|
||||||
|
</Text>
|
||||||
|
<Tooltip
|
||||||
|
label="Controls the memory allocated for maximum context size. The larger the context, the larger the required memory."
|
||||||
|
multiline
|
||||||
|
w={300}
|
||||||
|
withArrow
|
||||||
|
color="dark"
|
||||||
|
styles={{
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: 'var(--mantine-color-dark-6)',
|
||||||
|
color: 'var(--mantine-color-gray-0)',
|
||||||
|
border: '1px solid var(--mantine-color-dark-4)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ActionIcon variant="subtle" size="xs" color="gray">
|
||||||
|
<Info size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
<TextInput
|
||||||
|
value={contextSize?.toString() || ''}
|
||||||
|
onChange={(event) =>
|
||||||
|
onContextSizeChange(Number(event.target.value) || 256)
|
||||||
|
}
|
||||||
|
type="number"
|
||||||
|
min={256}
|
||||||
|
max={131072}
|
||||||
|
step={256}
|
||||||
|
size="sm"
|
||||||
|
w={100}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Slider
|
||||||
|
value={contextSize}
|
||||||
|
min={256}
|
||||||
|
max={131072}
|
||||||
|
step={1}
|
||||||
|
onChange={onContextSizeChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
48
src/components/launch/LaunchHeader.tsx
Normal file
48
src/components/launch/LaunchHeader.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
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>
|
||||||
|
);
|
||||||
96
src/components/launch/NetworkTab.tsx
Normal file
96
src/components/launch/NetworkTab.tsx
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
import {
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Group,
|
||||||
|
ActionIcon,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { Info } from 'lucide-react';
|
||||||
|
|
||||||
|
interface NetworkTabProps {
|
||||||
|
port: number;
|
||||||
|
host: string;
|
||||||
|
onPortChange: (port: number) => void;
|
||||||
|
onHostChange: (host: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NetworkTab = ({
|
||||||
|
port,
|
||||||
|
host,
|
||||||
|
onPortChange,
|
||||||
|
onHostChange,
|
||||||
|
}: NetworkTabProps) => (
|
||||||
|
<Stack gap="lg">
|
||||||
|
{/* Port */}
|
||||||
|
<div>
|
||||||
|
<Group gap="xs" align="center" mb="xs">
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
Port
|
||||||
|
</Text>
|
||||||
|
<Tooltip
|
||||||
|
label="The port number on which KoboldCpp will listen for connections. Default is 5001."
|
||||||
|
multiline
|
||||||
|
w={300}
|
||||||
|
withArrow
|
||||||
|
color="dark"
|
||||||
|
styles={{
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: 'var(--mantine-color-dark-6)',
|
||||||
|
color: 'var(--mantine-color-gray-0)',
|
||||||
|
border: '1px solid var(--mantine-color-dark-4)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ActionIcon variant="subtle" size="xs" color="gray">
|
||||||
|
<Info size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
<TextInput
|
||||||
|
placeholder="5001"
|
||||||
|
value={port.toString()}
|
||||||
|
onChange={(event) =>
|
||||||
|
onPortChange(Number(event.currentTarget.value) || 5001)
|
||||||
|
}
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={65535}
|
||||||
|
w={120}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Host */}
|
||||||
|
<div>
|
||||||
|
<Group gap="xs" align="center" mb="xs">
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
Host
|
||||||
|
</Text>
|
||||||
|
<Tooltip
|
||||||
|
label="The hostname or IP address on which KoboldCpp will bind its webserver to."
|
||||||
|
multiline
|
||||||
|
w={300}
|
||||||
|
withArrow
|
||||||
|
color="dark"
|
||||||
|
styles={{
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: 'var(--mantine-color-dark-6)',
|
||||||
|
color: 'var(--mantine-color-gray-0)',
|
||||||
|
border: '1px solid var(--mantine-color-dark-4)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ActionIcon variant="subtle" size="xs" color="gray">
|
||||||
|
<Info size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
<TextInput
|
||||||
|
placeholder="localhost"
|
||||||
|
value={host}
|
||||||
|
onChange={(event) => onHostChange(event.currentTarget.value)}
|
||||||
|
style={{ maxWidth: 200 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
42
src/components/launch/SaveConfigModal.tsx
Normal file
42
src/components/launch/SaveConfigModal.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { Modal, Stack, TextInput, Group, Button } from '@mantine/core';
|
||||||
|
import { Save } from 'lucide-react';
|
||||||
|
|
||||||
|
interface SaveConfigModalProps {
|
||||||
|
opened: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
configName: string;
|
||||||
|
onConfigNameChange: (name: string) => void;
|
||||||
|
onSave: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SaveConfigModal = ({
|
||||||
|
opened,
|
||||||
|
onClose,
|
||||||
|
configName,
|
||||||
|
onConfigNameChange,
|
||||||
|
onSave,
|
||||||
|
}: SaveConfigModalProps) => (
|
||||||
|
<Modal opened={opened} onClose={onClose} title="Save Configuration" size="sm">
|
||||||
|
<Stack gap="md">
|
||||||
|
<TextInput
|
||||||
|
label="Configuration Name"
|
||||||
|
placeholder="Enter a name for this configuration"
|
||||||
|
value={configName}
|
||||||
|
onChange={(event) => onConfigNameChange(event.currentTarget.value)}
|
||||||
|
data-autofocus
|
||||||
|
/>
|
||||||
|
<Group justify="flex-end" gap="sm">
|
||||||
|
<Button variant="outline" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={!configName.trim()}
|
||||||
|
leftSection={<Save size={16} />}
|
||||||
|
onClick={onSave}
|
||||||
|
>
|
||||||
|
Save Configuration
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
|
@ -12,7 +12,7 @@ import { Folder, FolderOpen } from 'lucide-react';
|
||||||
|
|
||||||
export const GeneralTab = () => {
|
export const GeneralTab = () => {
|
||||||
const [installDir, setInstallDir] = useState<string>('');
|
const [installDir, setInstallDir] = useState<string>('');
|
||||||
const [minimizeToTray, setMinimizeToTray] = useState<boolean>(true);
|
const [minimizeToTray, setMinimizeToTray] = useState<boolean | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadCurrentInstallDir();
|
loadCurrentInstallDir();
|
||||||
|
|
@ -33,13 +33,16 @@ export const GeneralTab = () => {
|
||||||
const trayEnabled = (await window.electronAPI.config.get(
|
const trayEnabled = (await window.electronAPI.config.get(
|
||||||
'minimizeToTray'
|
'minimizeToTray'
|
||||||
)) as boolean;
|
)) as boolean;
|
||||||
setMinimizeToTray(trayEnabled !== false);
|
setMinimizeToTray(trayEnabled === true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load tray settings:', error);
|
console.error('Failed to load tray settings:', error);
|
||||||
|
setMinimizeToTray(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTraySettingChange = async (enabled: boolean) => {
|
const handleTraySettingChange = async (enabled: boolean) => {
|
||||||
|
if (minimizeToTray === null) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setMinimizeToTray(enabled);
|
setMinimizeToTray(enabled);
|
||||||
await window.electronAPI.config.set('minimizeToTray', enabled);
|
await window.electronAPI.config.set('minimizeToTray', enabled);
|
||||||
|
|
@ -98,11 +101,12 @@ export const GeneralTab = () => {
|
||||||
Choose what happens when you close or minimize the window
|
Choose what happens when you close or minimize the window
|
||||||
</Text>
|
</Text>
|
||||||
<Switch
|
<Switch
|
||||||
checked={minimizeToTray}
|
checked={minimizeToTray ?? false}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
handleTraySettingChange(event.currentTarget.checked)
|
handleTraySettingChange(event.currentTarget.checked)
|
||||||
}
|
}
|
||||||
label="Minimize to system tray"
|
label="Minimize to system tray"
|
||||||
|
disabled={minimizeToTray === null}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ export const useLaunchConfig = () => {
|
||||||
const [contextSize, setContextSize] = useState<number>(2048);
|
const [contextSize, setContextSize] = useState<number>(2048);
|
||||||
const [modelPath, setModelPath] = useState<string>('');
|
const [modelPath, setModelPath] = useState<string>('');
|
||||||
const [additionalArguments, setAdditionalArguments] = useState<string>('');
|
const [additionalArguments, setAdditionalArguments] = useState<string>('');
|
||||||
|
const [port, setPort] = useState<number>(5001);
|
||||||
|
const [host, setHost] = useState<string>('localhost');
|
||||||
|
|
||||||
const parseAndApplyConfigFile = useCallback(async (configPath: string) => {
|
const parseAndApplyConfigFile = useCallback(async (configPath: string) => {
|
||||||
const configData =
|
const configData =
|
||||||
|
|
@ -28,9 +30,23 @@ export const useLaunchConfig = () => {
|
||||||
if (typeof configData.model_param === 'string') {
|
if (typeof configData.model_param === 'string') {
|
||||||
setModelPath(configData.model_param);
|
setModelPath(configData.model_param);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof configData.port === 'number') {
|
||||||
|
setPort(configData.port);
|
||||||
|
} else {
|
||||||
|
setPort(5001);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof configData.host === 'string') {
|
||||||
|
setHost(configData.host);
|
||||||
|
} else {
|
||||||
|
setHost('localhost');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setGpuLayers(0);
|
setGpuLayers(0);
|
||||||
setContextSize(2048);
|
setContextSize(2048);
|
||||||
|
setPort(5001);
|
||||||
|
setHost('localhost');
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -41,6 +57,8 @@ export const useLaunchConfig = () => {
|
||||||
setModelPath(''); // Model path comes from config file, not saved settings
|
setModelPath(''); // Model path comes from config file, not saved settings
|
||||||
setGpuLayers(0);
|
setGpuLayers(0);
|
||||||
setContextSize(2048);
|
setContextSize(2048);
|
||||||
|
setPort(5001);
|
||||||
|
setHost('localhost');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadConfigFromFile = useCallback(
|
const loadConfigFromFile = useCallback(
|
||||||
|
|
@ -110,6 +128,14 @@ export const useLaunchConfig = () => {
|
||||||
setAutoGpuLayers(checked);
|
setAutoGpuLayers(checked);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handlePortChange = useCallback((value: number) => {
|
||||||
|
setPort(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleHostChange = useCallback((value: string) => {
|
||||||
|
setHost(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
serverOnly,
|
serverOnly,
|
||||||
gpuLayers,
|
gpuLayers,
|
||||||
|
|
@ -117,6 +143,8 @@ export const useLaunchConfig = () => {
|
||||||
contextSize,
|
contextSize,
|
||||||
modelPath,
|
modelPath,
|
||||||
additionalArguments,
|
additionalArguments,
|
||||||
|
port,
|
||||||
|
host,
|
||||||
|
|
||||||
parseAndApplyConfigFile,
|
parseAndApplyConfigFile,
|
||||||
loadSavedSettings,
|
loadSavedSettings,
|
||||||
|
|
@ -128,5 +156,7 @@ export const useLaunchConfig = () => {
|
||||||
handleModelPathChange,
|
handleModelPathChange,
|
||||||
handleSelectModelFile,
|
handleSelectModelFile,
|
||||||
handleAdditionalArgumentsChange,
|
handleAdditionalArgumentsChange,
|
||||||
|
handlePortChange,
|
||||||
|
handleHostChange,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
import { createElement, type ReactNode } from 'react';
|
|
||||||
import { notifications } from '@mantine/notifications';
|
|
||||||
import { Check, X, Info, AlertTriangle } from 'lucide-react';
|
|
||||||
|
|
||||||
export const useNotifications = () => {
|
|
||||||
const success = (title: string, message?: string) => {
|
|
||||||
notifications.show({
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
color: 'green',
|
|
||||||
icon: createElement(Check, { size: 18 }),
|
|
||||||
position: 'top-right',
|
|
||||||
autoClose: 5000,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const error = (title: string, message?: string) => {
|
|
||||||
notifications.show({
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
color: 'red',
|
|
||||||
icon: createElement(X, { size: 18 }),
|
|
||||||
position: 'top-right',
|
|
||||||
autoClose: 7000,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const info = (title: string, message?: string) => {
|
|
||||||
notifications.show({
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
color: 'blue',
|
|
||||||
icon: createElement(Info, { size: 18 }),
|
|
||||||
position: 'top-right',
|
|
||||||
autoClose: 5000,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const warning = (title: string, message?: string) => {
|
|
||||||
notifications.show({
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
color: 'yellow',
|
|
||||||
icon: createElement(AlertTriangle, { size: 18 }),
|
|
||||||
position: 'top-right',
|
|
||||||
autoClose: 6000,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const custom = (options: {
|
|
||||||
title: string;
|
|
||||||
message?: string;
|
|
||||||
color?: string;
|
|
||||||
icon?: ReactNode;
|
|
||||||
autoClose?: number | false;
|
|
||||||
position?:
|
|
||||||
| 'top-left'
|
|
||||||
| 'top-right'
|
|
||||||
| 'top-center'
|
|
||||||
| 'bottom-left'
|
|
||||||
| 'bottom-right'
|
|
||||||
| 'bottom-center';
|
|
||||||
}) => {
|
|
||||||
const { title, message = '', ...rest } = options;
|
|
||||||
notifications.show({
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
position: 'top-right',
|
|
||||||
autoClose: 5000,
|
|
||||||
...rest,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const hide = (id: string) => {
|
|
||||||
notifications.hide(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const clean = () => {
|
|
||||||
notifications.clean();
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
success,
|
|
||||||
error,
|
|
||||||
info,
|
|
||||||
warning,
|
|
||||||
custom,
|
|
||||||
hide,
|
|
||||||
clean,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -21,6 +21,8 @@ class FriendlyKoboldApp {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.configManager = new ConfigManager(this.getConfigPath());
|
this.configManager = new ConfigManager(this.getConfigPath());
|
||||||
|
this.ensureInstallDirectory();
|
||||||
|
|
||||||
this.windowManager = new WindowManager(this.configManager);
|
this.windowManager = new WindowManager(this.configManager);
|
||||||
this.githubService = new GitHubService();
|
this.githubService = new GitHubService();
|
||||||
this.gpuService = new GPUService();
|
this.gpuService = new GPUService();
|
||||||
|
|
@ -35,8 +37,6 @@ class FriendlyKoboldApp {
|
||||||
this.githubService,
|
this.githubService,
|
||||||
this.gpuService
|
this.gpuService
|
||||||
);
|
);
|
||||||
|
|
||||||
this.ensureInstallDirectory();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getConfigPath() {
|
private getConfigPath() {
|
||||||
|
|
@ -71,6 +71,13 @@ class FriendlyKoboldApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize(): Promise<void> {
|
async initialize(): Promise<void> {
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
if (process.env.ELECTRON_OZONE_PLATFORM_HINT === 'wayland') {
|
||||||
|
app.commandLine.appendSwitch('enable-features', 'UseOzonePlatform');
|
||||||
|
app.commandLine.appendSwitch('ozone-platform', 'wayland');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await app.whenReady();
|
await app.whenReady();
|
||||||
|
|
||||||
this.windowManager.setupApplicationMenu();
|
this.windowManager.setupApplicationMenu();
|
||||||
|
|
@ -81,21 +88,17 @@ class FriendlyKoboldApp {
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// On non-macOS platforms, quit the app when all windows are closed
|
|
||||||
app.quit();
|
app.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('before-quit', async (event) => {
|
app.on('before-quit', async (event) => {
|
||||||
// Prevent immediate quit to allow cleanup
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
// Clean up KoboldCpp process
|
|
||||||
await this.koboldManager.cleanup();
|
await this.koboldManager.cleanup();
|
||||||
|
|
||||||
// Clean up window manager
|
|
||||||
this.windowManager.cleanup();
|
this.windowManager.cleanup();
|
||||||
|
|
||||||
// Now actually quit
|
|
||||||
app.exit(0);
|
app.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 { APP_NAME, DIALOG_TITLES, ROCM } from '@/constants/app';
|
import { DIALOG_TITLES, ROCM } from '@/constants/app';
|
||||||
|
|
||||||
interface GitHubAsset {
|
interface GitHubAsset {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -66,9 +66,7 @@ export class KoboldCppManager {
|
||||||
this.configManager = configManager;
|
this.configManager = configManager;
|
||||||
this.githubService = githubService;
|
this.githubService = githubService;
|
||||||
this.windowManager = windowManager;
|
this.windowManager = windowManager;
|
||||||
this.installDir =
|
this.installDir = this.configManager.getInstallDir() || '';
|
||||||
this.configManager.getInstallDir() ||
|
|
||||||
join(process.env.HOME || process.env.USERPROFILE || '.', APP_NAME);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadRelease(
|
async downloadRelease(
|
||||||
|
|
@ -134,18 +132,41 @@ export class KoboldCppManager {
|
||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInstalledVersions(): Promise<InstalledVersion[]> {
|
async getInstalledVersions(
|
||||||
|
includeVersions = true
|
||||||
|
): Promise<InstalledVersion[]> {
|
||||||
try {
|
try {
|
||||||
if (!existsSync(this.installDir)) {
|
if (!existsSync(this.installDir)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = readdirSync(this.installDir);
|
const files = readdirSync(this.installDir);
|
||||||
const koboldFiles = files.filter(
|
const koboldFiles = files.filter((file) => {
|
||||||
(file) =>
|
const filePath = join(this.installDir, file);
|
||||||
statSync(join(this.installDir, file)).isFile() &&
|
try {
|
||||||
file.startsWith('koboldcpp')
|
const stats = statSync(filePath);
|
||||||
);
|
|
||||||
|
if (stats.isFile() && file.startsWith('koboldcpp')) {
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
const isExecutable = (stats.mode & parseInt('111', 8)) !== 0;
|
||||||
|
return isExecutable;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!includeVersions) {
|
||||||
|
return koboldFiles.map((file) => ({
|
||||||
|
version: 'unknown',
|
||||||
|
path: join(this.installDir, file),
|
||||||
|
filename: file,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
const versionPromises = koboldFiles.map(async (file) => {
|
const versionPromises = koboldFiles.map(async (file) => {
|
||||||
const filePath = join(this.installDir, file);
|
const filePath = join(this.installDir, file);
|
||||||
|
|
@ -174,49 +195,6 @@ export class KoboldCppManager {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async isInstalled(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
if (!existsSync(this.installDir)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const files = readdirSync(this.installDir);
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const filePath = join(this.installDir, file);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stats = statSync(filePath);
|
|
||||||
|
|
||||||
// Check if it's a file (not directory) and starts with "koboldcpp"
|
|
||||||
if (stats.isFile() && file.startsWith('koboldcpp')) {
|
|
||||||
// On Unix-like systems, check if file is executable
|
|
||||||
if (process.platform !== 'win32') {
|
|
||||||
// Check if the file has execute permission (owner, group, or other)
|
|
||||||
const isExecutable = (stats.mode & parseInt('111', 8)) !== 0;
|
|
||||||
if (isExecutable) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// On Windows, if it's a file starting with koboldcpp, consider it valid
|
|
||||||
// (Windows doesn't use Unix-style executable permissions)
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Skip files we can't stat (permission issues, etc.)
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Error checking installation:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getConfigFiles(): Promise<
|
async getConfigFiles(): Promise<
|
||||||
Array<{ name: string; path: string; size: number }>
|
Array<{ name: string; path: string; size: number }>
|
||||||
> {
|
> {
|
||||||
|
|
@ -313,12 +291,10 @@ export class KoboldCppManager {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to get current version info:', error);
|
console.warn('Failed to get current version info:', error);
|
||||||
// Clear invalid binary path
|
|
||||||
this.configManager.setCurrentKoboldBinary('');
|
this.configManager.setCurrentKoboldBinary('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no current binary is set or it's invalid, try to find the first available one
|
|
||||||
const versions = await this.getInstalledVersions();
|
const versions = await this.getInstalledVersions();
|
||||||
const firstVersion = versions[0];
|
const firstVersion = versions[0];
|
||||||
|
|
||||||
|
|
@ -400,7 +376,7 @@ export class KoboldCppManager {
|
||||||
try {
|
try {
|
||||||
process.kill('SIGTERM');
|
process.kill('SIGTERM');
|
||||||
} catch {
|
} catch {
|
||||||
// Process might already be dead
|
// ignore
|
||||||
}
|
}
|
||||||
resolve(null);
|
resolve(null);
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
@ -578,7 +554,6 @@ export class KoboldCppManager {
|
||||||
reader.releaseLock();
|
reader.releaseLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make the binary executable on Unix-like systems (Linux/macOS)
|
|
||||||
if (process.platform !== 'win32') {
|
if (process.platform !== 'win32') {
|
||||||
try {
|
try {
|
||||||
chmodSync(filePath, 0o755);
|
chmodSync(filePath, 0o755);
|
||||||
|
|
@ -693,7 +668,7 @@ export class KoboldCppManager {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalArgs = [...args]; // Start with the provided arguments
|
const finalArgs = [...args];
|
||||||
|
|
||||||
if (configFilePath && existsSync(configFilePath)) {
|
if (configFilePath && existsSync(configFilePath)) {
|
||||||
finalArgs.push('--config', configFilePath);
|
finalArgs.push('--config', configFilePath);
|
||||||
|
|
@ -701,36 +676,54 @@ export class KoboldCppManager {
|
||||||
|
|
||||||
const child = spawn(currentVersion.path, finalArgs, {
|
const child = spawn(currentVersion.path, finalArgs, {
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
detached: false,
|
detached: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.koboldProcess = child;
|
this.koboldProcess = child;
|
||||||
|
|
||||||
|
child.unref();
|
||||||
|
|
||||||
const mainWindow = this.windowManager.getMainWindow();
|
const mainWindow = this.windowManager.getMainWindow();
|
||||||
if (mainWindow) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
const commandLine = `$ ${currentVersion.path} ${finalArgs.join(' ')}\n${'─'.repeat(60)}\n`;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('kobold-output', commandLine);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
|
||||||
child.stdout?.on('data', (data) => {
|
child.stdout?.on('data', (data) => {
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
const output = data.toString();
|
const output = data.toString();
|
||||||
mainWindow.webContents.send('kobold-output', output);
|
mainWindow.webContents.send('kobold-output', output);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
child.stderr?.on('data', (data) => {
|
child.stderr?.on('data', (data) => {
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
const output = data.toString();
|
const output = data.toString();
|
||||||
mainWindow.webContents.send('kobold-output', output);
|
mainWindow.webContents.send('kobold-output', output);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on('exit', (code, signal) => {
|
child.on('exit', (code, signal) => {
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
const exitMessage = signal
|
const exitMessage = signal
|
||||||
? `\nProcess terminated with signal ${signal}\n`
|
? `\nProcess terminated with signal ${signal}\n`
|
||||||
: `\nProcess exited with code ${code}\n`;
|
: `\nProcess exited with code ${code}\n`;
|
||||||
mainWindow.webContents.send('kobold-output', exitMessage);
|
mainWindow.webContents.send('kobold-output', exitMessage);
|
||||||
|
}
|
||||||
this.koboldProcess = null;
|
this.koboldProcess = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on('error', (error) => {
|
child.on('error', (error) => {
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.webContents.send(
|
mainWindow.webContents.send(
|
||||||
'kobold-output',
|
'kobold-output',
|
||||||
`\nProcess error: ${error.message}\n`
|
`\nProcess error: ${error.message}\n`
|
||||||
);
|
);
|
||||||
|
}
|
||||||
this.koboldProcess = null;
|
this.koboldProcess = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -744,14 +737,27 @@ export class KoboldCppManager {
|
||||||
stopKoboldCpp(): void {
|
stopKoboldCpp(): void {
|
||||||
if (this.koboldProcess) {
|
if (this.koboldProcess) {
|
||||||
try {
|
try {
|
||||||
// Try graceful termination first
|
// 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');
|
||||||
|
}
|
||||||
|
|
||||||
// Force kill after 5 seconds if still running
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this.koboldProcess && !this.koboldProcess.killed) {
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
this.koboldProcess = null;
|
this.koboldProcess = null;
|
||||||
|
|
@ -762,7 +768,6 @@ export class KoboldCppManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method to handle app termination - ensures process cleanup
|
|
||||||
async cleanup(): Promise<void> {
|
async cleanup(): Promise<void> {
|
||||||
if (this.koboldProcess) {
|
if (this.koboldProcess) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
|
@ -771,26 +776,42 @@ export class KoboldCppManager {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up cleanup timeout
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
this.koboldProcess = null;
|
this.koboldProcess = null;
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Listen for process exit
|
|
||||||
this.koboldProcess.once('exit', cleanup);
|
this.koboldProcess.once('exit', cleanup);
|
||||||
this.koboldProcess.once('error', cleanup);
|
this.koboldProcess.once('error', cleanup);
|
||||||
|
|
||||||
// Try graceful shutdown
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
// Force kill after 3 seconds
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this.koboldProcess && !this.koboldProcess.killed) {
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
cleanup();
|
cleanup();
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error during cleanup:', error);
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,14 @@
|
||||||
import { BrowserWindow, app, Menu, shell, Tray, nativeImage } from 'electron';
|
import {
|
||||||
|
BrowserWindow,
|
||||||
|
app,
|
||||||
|
Menu,
|
||||||
|
shell,
|
||||||
|
Tray,
|
||||||
|
nativeImage,
|
||||||
|
dialog,
|
||||||
|
clipboard,
|
||||||
|
} from 'electron';
|
||||||
|
import * as os from 'os';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { ConfigManager } from './ConfigManager';
|
import { ConfigManager } from './ConfigManager';
|
||||||
|
|
||||||
|
|
@ -16,7 +26,7 @@ export class WindowManager {
|
||||||
this.mainWindow = new BrowserWindow({
|
this.mainWindow = new BrowserWindow({
|
||||||
width: 1000,
|
width: 1000,
|
||||||
height: 600,
|
height: 600,
|
||||||
icon: join(process.cwd(), 'assets', 'icon.png'),
|
icon: join(app.getAppPath(), 'assets', 'icon.png'),
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
|
|
@ -36,10 +46,38 @@ export class WindowManager {
|
||||||
this.mainWindow = null;
|
this.mainWindow = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Allow navigation to localhost URLs for iframe content
|
||||||
|
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' &&
|
||||||
|
!navigationUrl.startsWith('file://')
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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) => {
|
this.mainWindow.on('close', async (event) => {
|
||||||
if (!this.isQuitting) {
|
if (!this.isQuitting) {
|
||||||
const minimizeToTray =
|
const minimizeToTray =
|
||||||
this.configManager.get('minimizeToTray') !== false;
|
this.configManager.get('minimizeToTray') === true;
|
||||||
|
|
||||||
if (minimizeToTray) {
|
if (minimizeToTray) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
@ -53,7 +91,7 @@ export class WindowManager {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.mainWindow.on('minimize', () => {
|
this.mainWindow.on('minimize', () => {
|
||||||
const minimizeToTray = this.configManager.get('minimizeToTray') !== false;
|
const minimizeToTray = this.configManager.get('minimizeToTray') === true;
|
||||||
|
|
||||||
if (minimizeToTray) {
|
if (minimizeToTray) {
|
||||||
this.mainWindow?.hide();
|
this.mainWindow?.hide();
|
||||||
|
|
@ -73,7 +111,7 @@ export class WindowManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private createSystemTray() {
|
private createSystemTray() {
|
||||||
const iconPath = join(process.cwd(), 'assets', 'icon.png');
|
const iconPath = join(app.getAppPath(), 'assets', 'icon.png');
|
||||||
this.tray = new Tray(nativeImage.createFromPath(iconPath));
|
this.tray = new Tray(nativeImage.createFromPath(iconPath));
|
||||||
|
|
||||||
this.tray.setToolTip('Friendly Kobold');
|
this.tray.setToolTip('Friendly Kobold');
|
||||||
|
|
@ -111,10 +149,17 @@ export class WindowManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public cleanup() {
|
public cleanup() {
|
||||||
this.tray?.destroy();
|
if (this.tray) {
|
||||||
|
this.tray.removeAllListeners();
|
||||||
|
this.tray.destroy();
|
||||||
this.tray = null;
|
this.tray = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.mainWindow) {
|
||||||
|
this.mainWindow.removeAllListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private setupContextMenu() {
|
private setupContextMenu() {
|
||||||
if (!this.mainWindow) return;
|
if (!this.mainWindow) return;
|
||||||
|
|
||||||
|
|
@ -217,6 +262,23 @@ export class WindowManager {
|
||||||
{ label: 'Close', accelerator: 'CmdOrCtrl+W', role: 'close' },
|
{ label: 'Close', accelerator: 'CmdOrCtrl+W', role: 'close' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Help',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'About',
|
||||||
|
click: async () => {
|
||||||
|
await this.showAboutDialog();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'KoboldCpp Wiki',
|
||||||
|
click: () => {
|
||||||
|
shell.openExternal('https://github.com/LostRuins/koboldcpp/wiki');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const menu = Menu.buildFromTemplate(
|
const menu = Menu.buildFromTemplate(
|
||||||
|
|
@ -224,4 +286,33 @@ export class WindowManager {
|
||||||
);
|
);
|
||||||
Menu.setApplicationMenu(menu);
|
Menu.setApplicationMenu(menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async showAboutDialog() {
|
||||||
|
const packagePath = join(app.getAppPath(), 'package.json');
|
||||||
|
const packageInfo = require(packagePath);
|
||||||
|
const electronVersion = process.versions.electron;
|
||||||
|
const chromeVersion = process.versions.chrome;
|
||||||
|
const nodeVersion = process.versions.node;
|
||||||
|
const v8Version = process.versions.v8;
|
||||||
|
const osInfo = `${process.platform} ${process.arch} ${os.release()}`;
|
||||||
|
|
||||||
|
const aboutText = `Version: ${packageInfo.version}
|
||||||
|
Electron: ${electronVersion}
|
||||||
|
Chromium: ${chromeVersion}
|
||||||
|
Node.js: ${nodeVersion}
|
||||||
|
V8: ${v8Version}
|
||||||
|
OS: ${osInfo}`;
|
||||||
|
|
||||||
|
const response = await dialog.showMessageBox(this.mainWindow!, {
|
||||||
|
type: 'info',
|
||||||
|
message: 'Friendly Kobold',
|
||||||
|
detail: aboutText,
|
||||||
|
buttons: ['Copy', 'OK'],
|
||||||
|
defaultId: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.response === 0) {
|
||||||
|
clipboard.writeText(aboutText);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,12 +70,10 @@ export class IPCHandlers {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('kobold:getInstalledVersions', () =>
|
ipcMain.handle(
|
||||||
this.koboldManager.getInstalledVersions()
|
'kobold:getInstalledVersions',
|
||||||
);
|
(_, includeVersions?: boolean) =>
|
||||||
|
this.koboldManager.getInstalledVersions(includeVersions)
|
||||||
ipcMain.handle('kobold:isInstalled', () =>
|
|
||||||
this.koboldManager.isInstalled()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle('kobold:getConfigFiles', () =>
|
ipcMain.handle('kobold:getConfigFiles', () =>
|
||||||
|
|
@ -160,15 +158,15 @@ export class IPCHandlers {
|
||||||
const result = await dialog.showMessageBox(mainWindow, {
|
const result = await dialog.showMessageBox(mainWindow, {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
title: 'Confirm Eject',
|
title: 'Confirm Eject',
|
||||||
message: 'Are you sure you want to stop KoboldCpp?',
|
message: 'Are you sure you want to eject KoboldCpp?',
|
||||||
detail:
|
detail:
|
||||||
'This will terminate the running process and return to the launch screen.',
|
'This will terminate the running process and return to the launch screen.',
|
||||||
buttons: ['Cancel', 'Stop KoboldCpp'],
|
buttons: ['Cancel', 'Eject'],
|
||||||
defaultId: 0,
|
defaultId: 0,
|
||||||
cancelId: 0,
|
cancelId: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
return result.response === 1; // Returns true if user clicked "Stop KoboldCpp"
|
return result.response === 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('kobold:parseConfigFile', (_event, filePath) =>
|
ipcMain.handle('kobold:parseConfigFile', (_event, filePath) =>
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ import type {
|
||||||
} from '@/types/electron';
|
} from '@/types/electron';
|
||||||
|
|
||||||
const koboldAPI: KoboldAPI = {
|
const koboldAPI: KoboldAPI = {
|
||||||
isInstalled: () => ipcRenderer.invoke('kobold:isInstalled'),
|
|
||||||
getInstalledVersion: () => ipcRenderer.invoke('kobold:getInstalledVersion'),
|
getInstalledVersion: () => ipcRenderer.invoke('kobold:getInstalledVersion'),
|
||||||
getInstalledVersions: () => ipcRenderer.invoke('kobold:getInstalledVersions'),
|
getInstalledVersions: (includeVersions?: boolean) =>
|
||||||
|
ipcRenderer.invoke('kobold:getInstalledVersions', includeVersions),
|
||||||
getCurrentVersion: () => ipcRenderer.invoke('kobold:getCurrentVersion'),
|
getCurrentVersion: () => ipcRenderer.invoke('kobold:getCurrentVersion'),
|
||||||
setCurrentVersion: (version: string) =>
|
setCurrentVersion: (version: string) =>
|
||||||
ipcRenderer.invoke('kobold:setCurrentVersion', version),
|
ipcRenderer.invoke('kobold:setCurrentVersion', version),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
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 } from '@mantine/core';
|
||||||
import { useNotifications } from '@/hooks/useNotifications';
|
|
||||||
import { DownloadOptionCard } from '@/components/DownloadOptionCard';
|
import { DownloadOptionCard } from '@/components/DownloadOptionCard';
|
||||||
import {
|
import {
|
||||||
getPlatformDisplayName,
|
getPlatformDisplayName,
|
||||||
|
|
@ -20,7 +19,6 @@ interface DownloadScreenProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
const notify = useNotifications();
|
|
||||||
const [latestRelease, setLatestRelease] = useState<GitHubRelease | null>(
|
const [latestRelease, setLatestRelease] = useState<GitHubRelease | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
@ -74,18 +72,16 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
);
|
);
|
||||||
setFilteredAssets(filtered);
|
setFilteredAssets(filtered);
|
||||||
} else {
|
} else {
|
||||||
notify.error(
|
console.error(
|
||||||
'Error',
|
|
||||||
'GitHub API is currently unavailable. Please try again later.'
|
'GitHub API is currently unavailable. Please try again later.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notify.error('Error', 'Failed to load release information');
|
console.error('Failed to load release information:', err);
|
||||||
console.error('Error loading release:', err);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [notify]);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadLatestReleaseAndPlatform();
|
loadLatestReleaseAndPlatform();
|
||||||
|
|
@ -97,7 +93,8 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
return () => {
|
return () => {
|
||||||
window.electronAPI.kobold.removeAllListeners('download-progress');
|
window.electronAPI.kobold.removeAllListeners('download-progress');
|
||||||
};
|
};
|
||||||
}, [loadLatestReleaseAndPlatform]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleDownload = async (type: 'asset' | 'rocm' = 'asset') => {
|
const handleDownload = async (type: 'asset' | 'rocm' = 'asset') => {
|
||||||
if (type === 'asset' && !selectedAsset) return;
|
if (type === 'asset' && !selectedAsset) return;
|
||||||
|
|
@ -115,16 +112,12 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
onDownloadComplete();
|
onDownloadComplete();
|
||||||
} else {
|
} else {
|
||||||
notify.error(
|
console.error(
|
||||||
'Download Failed',
|
'Download Failed',
|
||||||
result.error || `${type === 'rocm' ? 'ROCm' : ''} Download failed`
|
result.error || `${type === 'rocm' ? 'ROCm' : ''} Download failed`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notify.error(
|
|
||||||
'Download Failed',
|
|
||||||
`${type === 'rocm' ? 'ROCm' : ''} Download failed`
|
|
||||||
);
|
|
||||||
console.error(`${type === 'rocm' ? 'ROCm' : ''} Download error:`, err);
|
console.error(`${type === 'rocm' ? 'ROCm' : ''} Download error:`, err);
|
||||||
} finally {
|
} finally {
|
||||||
setDownloading(false);
|
setDownloading(false);
|
||||||
|
|
@ -160,7 +153,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size="sm" py="xl">
|
<Container size="sm">
|
||||||
<Stack gap="xl">
|
<Stack gap="xl">
|
||||||
<Card withBorder radius="md" shadow="sm">
|
<Card withBorder radius="md" shadow="sm">
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
|
|
|
||||||
86
src/screens/InterfaceScreen.tsx
Normal file
86
src/screens/InterfaceScreen.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { Card, Container } from '@mantine/core';
|
||||||
|
import { ChatTab } from '@/components/interface/ChatTab';
|
||||||
|
import { TerminalTab } from '@/components/interface/TerminalTab';
|
||||||
|
|
||||||
|
interface InterfaceScreenProps {
|
||||||
|
activeTab?: string | null;
|
||||||
|
onTabChange?: (tab: string | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InterfaceScreen = ({
|
||||||
|
activeTab,
|
||||||
|
onTabChange,
|
||||||
|
}: InterfaceScreenProps) => {
|
||||||
|
const [serverOnly, setServerOnly] = useState<boolean>(false);
|
||||||
|
const [serverUrl, setServerUrl] = useState<string>('');
|
||||||
|
const [isServerReady, setIsServerReady] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleServerReady = useCallback(
|
||||||
|
(url: string) => {
|
||||||
|
setServerUrl(url);
|
||||||
|
setIsServerReady(true);
|
||||||
|
|
||||||
|
if (!serverOnly && onTabChange) {
|
||||||
|
onTabChange('chat');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[serverOnly, onTabChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadServerOnlySetting = async () => {
|
||||||
|
try {
|
||||||
|
const serverOnlyValue = await window.electronAPI.config.getServerOnly();
|
||||||
|
setServerOnly(serverOnlyValue);
|
||||||
|
|
||||||
|
if (serverOnlyValue && onTabChange) {
|
||||||
|
onTabChange('terminal');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load server-only setting:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadServerOnlySetting();
|
||||||
|
}, [onTabChange]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size="l" style={{ height: '85vh' }}>
|
||||||
|
<Card
|
||||||
|
withBorder
|
||||||
|
radius="md"
|
||||||
|
p="0"
|
||||||
|
style={{ height: 'calc(90vh - 32px)' }}
|
||||||
|
>
|
||||||
|
{serverOnly ? (
|
||||||
|
<TerminalTab
|
||||||
|
onServerReady={handleServerReady}
|
||||||
|
serverOnly={serverOnly}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{ height: '100%' }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '100%',
|
||||||
|
display: activeTab === 'chat' ? 'block' : 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChatTab serverUrl={serverUrl} isServerReady={isServerReady} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: activeTab === 'terminal' ? 'block' : 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TerminalTab
|
||||||
|
onServerReady={handleServerReady}
|
||||||
|
serverOnly={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,23 +1,20 @@
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
Card,
|
Card,
|
||||||
Text,
|
|
||||||
Title,
|
|
||||||
Container,
|
Container,
|
||||||
Stack,
|
Stack,
|
||||||
|
Tabs,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
Group,
|
Group,
|
||||||
ActionIcon,
|
Button,
|
||||||
Switch,
|
|
||||||
Slider,
|
|
||||||
TextInput,
|
|
||||||
NumberInput,
|
|
||||||
Tooltip,
|
|
||||||
Checkbox,
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { RotateCcw, File, Info } from 'lucide-react';
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { ConfigFileSelect } from '@/components/ConfigFileSelect';
|
|
||||||
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
|
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
|
||||||
|
import { ConfigurationManager } from '@/components/launch/ConfigurationManager';
|
||||||
|
import { GeneralTab } from '@/components/launch/GeneralTab';
|
||||||
|
import { AdvancedTab } from '@/components/launch/AdvancedTab';
|
||||||
|
import { NetworkTab } from '@/components/launch/NetworkTab';
|
||||||
|
import { SaveConfigModal } from '@/components/launch/SaveConfigModal';
|
||||||
import type { ConfigFile } from '@/types';
|
import type { ConfigFile } from '@/types';
|
||||||
|
|
||||||
interface LaunchScreenProps {
|
interface LaunchScreenProps {
|
||||||
|
|
@ -28,6 +25,11 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
const [configFiles, setConfigFiles] = useState<ConfigFile[]>([]);
|
const [configFiles, setConfigFiles] = useState<ConfigFile[]>([]);
|
||||||
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
||||||
const [, setInstallDir] = useState<string>('');
|
const [, setInstallDir] = useState<string>('');
|
||||||
|
const [isLaunching, setIsLaunching] = useState(false);
|
||||||
|
const [activeTab, setActiveTab] = useState<string | null>('general');
|
||||||
|
const [saveModalOpened, setSaveModalOpened] = useState(false);
|
||||||
|
const [newConfigName, setNewConfigName] = useState('');
|
||||||
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||||
const {
|
const {
|
||||||
serverOnly,
|
serverOnly,
|
||||||
gpuLayers,
|
gpuLayers,
|
||||||
|
|
@ -35,6 +37,8 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
contextSize,
|
contextSize,
|
||||||
modelPath,
|
modelPath,
|
||||||
additionalArguments,
|
additionalArguments,
|
||||||
|
port,
|
||||||
|
host,
|
||||||
parseAndApplyConfigFile,
|
parseAndApplyConfigFile,
|
||||||
loadSavedSettings,
|
loadSavedSettings,
|
||||||
loadConfigFromFile,
|
loadConfigFromFile,
|
||||||
|
|
@ -45,6 +49,8 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
handleModelPathChange,
|
handleModelPathChange,
|
||||||
handleSelectModelFile,
|
handleSelectModelFile,
|
||||||
handleAdditionalArgumentsChange,
|
handleAdditionalArgumentsChange,
|
||||||
|
handlePortChange,
|
||||||
|
handleHostChange,
|
||||||
} = useLaunchConfig();
|
} = useLaunchConfig();
|
||||||
|
|
||||||
const loadConfigFiles = useCallback(async () => {
|
const loadConfigFiles = useCallback(async () => {
|
||||||
|
|
@ -79,6 +85,50 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
if (selectedConfig) {
|
if (selectedConfig) {
|
||||||
await parseAndApplyConfigFile(selectedConfig.path);
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGpuLayersChangeWithTracking = (layers: number) => {
|
||||||
|
handleGpuLayersChange(layers);
|
||||||
|
setHasUnsavedChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAutoGpuLayersChangeWithTracking = (auto: boolean) => {
|
||||||
|
handleAutoGpuLayersChange(auto);
|
||||||
|
setHasUnsavedChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContextSizeChangeWithTracking = (size: number) => {
|
||||||
|
handleContextSizeChangeWithStep(size);
|
||||||
|
setHasUnsavedChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAdditionalArgumentsChangeWithTracking = (args: string) => {
|
||||||
|
handleAdditionalArgumentsChange(args);
|
||||||
|
setHasUnsavedChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleServerOnlyChangeWithTracking = (serverOnly: boolean) => {
|
||||||
|
handleServerOnlyChange(serverOnly);
|
||||||
|
setHasUnsavedChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePortChangeWithTracking = (port: number) => {
|
||||||
|
handlePortChange(port);
|
||||||
|
setHasUnsavedChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHostChangeWithTracking = (host: string) => {
|
||||||
|
handleHostChange(host);
|
||||||
|
setHasUnsavedChanges(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -96,6 +146,12 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
}, [loadConfigFiles]);
|
}, [loadConfigFiles]);
|
||||||
|
|
||||||
const handleLaunch = async () => {
|
const handleLaunch = async () => {
|
||||||
|
if (isLaunching || !modelPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLaunching(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const selectedConfig = selectedFile
|
const selectedConfig = selectedFile
|
||||||
? configFiles.find((f) => f.name === selectedFile)
|
? configFiles.find((f) => f.name === selectedFile)
|
||||||
|
|
@ -103,9 +159,7 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
|
|
||||||
const args: string[] = [];
|
const args: string[] = [];
|
||||||
|
|
||||||
if (modelPath) {
|
|
||||||
args.push('--model', modelPath);
|
args.push('--model', modelPath);
|
||||||
}
|
|
||||||
|
|
||||||
if (autoGpuLayers) {
|
if (autoGpuLayers) {
|
||||||
args.push('--gpulayers', '-1');
|
args.push('--gpulayers', '-1');
|
||||||
|
|
@ -117,6 +171,14 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
args.push('--contextsize', contextSize.toString());
|
args.push('--contextsize', contextSize.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (port !== 5001) {
|
||||||
|
args.push('--port', port.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host !== 'localhost') {
|
||||||
|
args.push('--host', host);
|
||||||
|
}
|
||||||
|
|
||||||
if (additionalArguments.trim()) {
|
if (additionalArguments.trim()) {
|
||||||
const additionalArgs = additionalArguments.trim().split(/\s+/);
|
const additionalArgs = additionalArguments.trim().split(/\s+/);
|
||||||
args.push(...additionalArgs);
|
args.push(...additionalArgs);
|
||||||
|
|
@ -128,230 +190,136 @@ export const LaunchScreen = ({ onLaunch }: LaunchScreenProps) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
setTimeout(() => {
|
||||||
onLaunch();
|
onLaunch();
|
||||||
|
}, 100);
|
||||||
} else {
|
} else {
|
||||||
console.error('Launch failed:', result.error);
|
console.error('Launch failed:', result.error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error launching KoboldCpp:', error);
|
console.error('Error launching KoboldCpp:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLaunching(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size="sm" py="xl">
|
<Container size="sm">
|
||||||
<Card withBorder radius="md" shadow="sm">
|
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
|
<Card withBorder radius="md" shadow="sm">
|
||||||
|
<Group justify="space-between" align="center">
|
||||||
|
<div>
|
||||||
<Title order={3}>Launch Configuration</Title>
|
<Title order={3}>Launch Configuration</Title>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
<Card withBorder radius="md" w="100%">
|
{selectedFile
|
||||||
<Group justify="space-between" mb="md">
|
? `Using: ${selectedFile}`
|
||||||
<Text fw={500}>Select Configuration</Text>
|
: 'No configuration file selected'}
|
||||||
<ActionIcon variant="light" onClick={loadConfigFiles} size="sm">
|
{hasUnsavedChanges && (
|
||||||
<RotateCcw size={16} />
|
<Text span c="orange">
|
||||||
</ActionIcon>
|
{' '}
|
||||||
|
• Unsaved changes
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
radius="md"
|
||||||
|
disabled={!modelPath || isLaunching}
|
||||||
|
onClick={handleLaunch}
|
||||||
|
loading={isLaunching}
|
||||||
|
size="lg"
|
||||||
|
variant="filled"
|
||||||
|
>
|
||||||
|
{isLaunching
|
||||||
|
? 'Launching...'
|
||||||
|
: modelPath
|
||||||
|
? 'Launch KoboldCpp'
|
||||||
|
: 'Select a model file to launch'}
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<ConfigFileSelect
|
<Card withBorder radius="md">
|
||||||
|
<ConfigurationManager
|
||||||
configFiles={configFiles}
|
configFiles={configFiles}
|
||||||
selectedFile={selectedFile}
|
selectedFile={selectedFile}
|
||||||
onFileSelection={handleFileSelection}
|
onFileSelection={handleFileSelection}
|
||||||
|
onRefresh={loadConfigFiles}
|
||||||
|
onSaveAsNew={() => setSaveModalOpened(true)}
|
||||||
|
onUpdateCurrent={() => {
|
||||||
|
// TODO: Implement update current configuration
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card withBorder radius="md" w="100%">
|
<Card withBorder radius="md">
|
||||||
<Text fw={500} mb="md">
|
<Tabs value={activeTab} onChange={setActiveTab}>
|
||||||
Launch Settings
|
<Tabs.List>
|
||||||
</Text>
|
<Tabs.Tab value="general">General</Tabs.Tab>
|
||||||
|
<Tabs.Tab value="advanced">Advanced</Tabs.Tab>
|
||||||
|
<Tabs.Tab value="network">Network</Tabs.Tab>
|
||||||
|
<Tabs.Tab value="image" disabled>
|
||||||
|
Image Generation
|
||||||
|
</Tabs.Tab>
|
||||||
|
</Tabs.List>
|
||||||
|
|
||||||
<Stack gap="l">
|
<Tabs.Panel value="general" pt="md">
|
||||||
<div>
|
<GeneralTab
|
||||||
<Text size="sm" fw={500} mb="xs">
|
modelPath={modelPath}
|
||||||
Model File
|
gpuLayers={gpuLayers}
|
||||||
</Text>
|
autoGpuLayers={autoGpuLayers}
|
||||||
<Group gap="xs">
|
contextSize={contextSize}
|
||||||
<TextInput
|
onModelPathChange={handleModelPathChangeWithTracking}
|
||||||
placeholder="Select a .gguf model file"
|
onSelectModelFile={handleSelectModelFile}
|
||||||
value={modelPath}
|
onGpuLayersChange={handleGpuLayersChangeWithTracking}
|
||||||
onChange={(event) =>
|
onAutoGpuLayersChange={handleAutoGpuLayersChangeWithTracking}
|
||||||
handleModelPathChange(event.currentTarget.value)
|
onContextSizeChange={handleContextSizeChangeWithTracking}
|
||||||
}
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
/>
|
/>
|
||||||
<Button
|
</Tabs.Panel>
|
||||||
onClick={handleSelectModelFile}
|
|
||||||
variant="light"
|
|
||||||
leftSection={<File size={16} />}
|
|
||||||
>
|
|
||||||
Browse
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<Tabs.Panel value="advanced" pt="md">
|
||||||
<Group justify="space-between" align="center" mb="xs">
|
<AdvancedTab
|
||||||
<Group gap="xs" align="center">
|
additionalArguments={additionalArguments}
|
||||||
<Text size="sm" fw={500}>
|
serverOnly={serverOnly}
|
||||||
GPU Layers
|
onAdditionalArgumentsChange={
|
||||||
</Text>
|
handleAdditionalArgumentsChangeWithTracking
|
||||||
<Tooltip
|
|
||||||
label="The number of layer's to offload to your GPU's VRAM. Ideally the entire LLM should fit inside the VRAM for optimal performance."
|
|
||||||
multiline
|
|
||||||
w={300}
|
|
||||||
withArrow
|
|
||||||
>
|
|
||||||
<ActionIcon variant="subtle" size="xs" color="gray">
|
|
||||||
<Info size={14} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
|
||||||
<Group gap="lg" align="center">
|
|
||||||
<Group gap="xs" align="center">
|
|
||||||
<Checkbox
|
|
||||||
label="Auto"
|
|
||||||
checked={autoGpuLayers}
|
|
||||||
onChange={(event) =>
|
|
||||||
handleAutoGpuLayersChange(event.currentTarget.checked)
|
|
||||||
}
|
}
|
||||||
size="sm"
|
onServerOnlyChange={handleServerOnlyChangeWithTracking}
|
||||||
/>
|
/>
|
||||||
<Tooltip
|
</Tabs.Panel>
|
||||||
label="Automatically try to allocate the GPU layers based on available VRAM."
|
|
||||||
multiline
|
|
||||||
w={300}
|
|
||||||
withArrow
|
|
||||||
>
|
|
||||||
<ActionIcon variant="subtle" size="xs" color="gray">
|
|
||||||
<Info size={14} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
|
||||||
<NumberInput
|
|
||||||
value={gpuLayers}
|
|
||||||
onChange={(value) =>
|
|
||||||
handleGpuLayersChange(Number(value) || 0)
|
|
||||||
}
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
step={1}
|
|
||||||
size="sm"
|
|
||||||
w={80}
|
|
||||||
disabled={autoGpuLayers}
|
|
||||||
hideControls
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
<Slider
|
|
||||||
value={gpuLayers}
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
step={1}
|
|
||||||
onChange={handleGpuLayersChange}
|
|
||||||
disabled={autoGpuLayers}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<Tabs.Panel value="network" pt="md">
|
||||||
<Group justify="space-between" align="center" mb="xs">
|
<NetworkTab
|
||||||
<Group gap="xs" align="center">
|
port={port}
|
||||||
<Text size="sm" fw={500}>
|
host={host}
|
||||||
Context Size
|
onPortChange={handlePortChangeWithTracking}
|
||||||
</Text>
|
onHostChange={handleHostChangeWithTracking}
|
||||||
<Tooltip
|
|
||||||
label="Controls the memory allocated for maximum context size. The larger the context, the larger the required memory."
|
|
||||||
multiline
|
|
||||||
w={300}
|
|
||||||
withArrow
|
|
||||||
>
|
|
||||||
<ActionIcon variant="subtle" size="xs" color="gray">
|
|
||||||
<Info size={14} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
|
||||||
<NumberInput
|
|
||||||
value={contextSize}
|
|
||||||
onChange={(value) =>
|
|
||||||
handleContextSizeChangeWithStep(Number(value) || 256)
|
|
||||||
}
|
|
||||||
min={256}
|
|
||||||
max={131072}
|
|
||||||
step={256}
|
|
||||||
size="sm"
|
|
||||||
w={100}
|
|
||||||
hideControls
|
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Tabs.Panel>
|
||||||
<Slider
|
|
||||||
value={contextSize}
|
|
||||||
min={256}
|
|
||||||
max={131072}
|
|
||||||
step={1}
|
|
||||||
onChange={handleContextSizeChangeWithStep}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<Tabs.Panel value="image" pt="md">
|
||||||
<Group gap="xs" align="center" mb="xs">
|
<Stack gap="lg" align="center" py="xl">
|
||||||
<Text size="sm" fw={500}>
|
<Text c="dimmed" ta="center">
|
||||||
Additional arguments
|
Image generation configuration will be available in a future
|
||||||
|
update.
|
||||||
</Text>
|
</Text>
|
||||||
<Tooltip
|
|
||||||
label="Additional command line arguments to pass to the KoboldCPP binary. Leave this empty if you don't know what they are."
|
|
||||||
multiline
|
|
||||||
w={300}
|
|
||||||
withArrow
|
|
||||||
>
|
|
||||||
<ActionIcon variant="subtle" size="xs" color="gray">
|
|
||||||
<Info size={14} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
|
||||||
<TextInput
|
|
||||||
placeholder="Additional command line arguments"
|
|
||||||
value={additionalArguments}
|
|
||||||
onChange={(event) =>
|
|
||||||
handleAdditionalArgumentsChange(event.currentTarget.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Group gap="xs" align="center" mb="xs">
|
|
||||||
<Text size="sm" fw={500}>
|
|
||||||
Server-only mode
|
|
||||||
</Text>
|
|
||||||
<Tooltip
|
|
||||||
label="In server-only mode, the KoboldAI Lite web UI won't be displayed. Use this if you'll be using your own frontend to interact with the LLM."
|
|
||||||
multiline
|
|
||||||
w={300}
|
|
||||||
withArrow
|
|
||||||
>
|
|
||||||
<ActionIcon variant="subtle" size="xs" color="gray">
|
|
||||||
<Info size={14} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
|
||||||
<Switch
|
|
||||||
checked={serverOnly}
|
|
||||||
onChange={(event) =>
|
|
||||||
handleServerOnlyChange(event.currentTarget.checked)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
|
</Tabs.Panel>
|
||||||
|
</Tabs>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Group gap="md" justify="center">
|
<SaveConfigModal
|
||||||
<Button
|
opened={saveModalOpened}
|
||||||
radius="md"
|
onClose={() => setSaveModalOpened(false)}
|
||||||
disabled={!selectedFile}
|
configName={newConfigName}
|
||||||
onClick={handleLaunch}
|
onConfigNameChange={setNewConfigName}
|
||||||
size="lg"
|
onSave={() => {
|
||||||
>
|
// TODO: Implement save configuration
|
||||||
{selectedFile ? 'Launch' : 'Select a configuration file'}
|
setSaveModalOpened(false);
|
||||||
</Button>
|
setNewConfigName('');
|
||||||
</Group>
|
}}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
||||||
import { useState, useEffect, useRef } from 'react';
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Card,
|
|
||||||
Container,
|
|
||||||
Group,
|
|
||||||
ScrollArea,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
Title,
|
|
||||||
} from '@mantine/core';
|
|
||||||
|
|
||||||
export const TerminalScreen = () => {
|
|
||||||
const [terminalContent, setTerminalContent] = useState<string>('');
|
|
||||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
|
||||||
const viewportRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (viewportRef.current) {
|
|
||||||
viewportRef.current.scrollTop = viewportRef.current.scrollHeight;
|
|
||||||
}
|
|
||||||
}, [terminalContent]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const cleanup = window.electronAPI.kobold.onKoboldOutput((data: string) => {
|
|
||||||
setTerminalContent((prev) => {
|
|
||||||
// Handle carriage returns for progress bars
|
|
||||||
const lines = prev.split('\n');
|
|
||||||
const newData = data.toString();
|
|
||||||
|
|
||||||
// If the new data contains carriage returns, handle line overwriting
|
|
||||||
if (newData.includes('\r')) {
|
|
||||||
const parts = newData.split('\r');
|
|
||||||
for (let i = 0; i < parts.length; i++) {
|
|
||||||
if (i === 0) {
|
|
||||||
// First part gets appended to the last line
|
|
||||||
if (lines.length > 0) {
|
|
||||||
lines[lines.length - 1] += parts[i];
|
|
||||||
} else {
|
|
||||||
lines.push(parts[i]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Subsequent parts overwrite the last line
|
|
||||||
if (lines.length > 0) {
|
|
||||||
lines[lines.length - 1] = parts[i];
|
|
||||||
} else {
|
|
||||||
lines.push(parts[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return lines.join('\n');
|
|
||||||
} else {
|
|
||||||
// No carriage returns, just append
|
|
||||||
return prev + newData;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return cleanup;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container size="lg" style={{ height: '100%', paddingTop: '2rem' }}>
|
|
||||||
<Stack gap="lg" style={{ height: '100%' }}>
|
|
||||||
<Group justify="space-between" align="center">
|
|
||||||
<Title order={3}>KoboldCpp Terminal</Title>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Card
|
|
||||||
withBorder
|
|
||||||
radius="md"
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
backgroundColor: 'var(--mantine-color-dark-8)',
|
|
||||||
minHeight: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ScrollArea
|
|
||||||
ref={scrollAreaRef}
|
|
||||||
viewportRef={viewportRef}
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", monospace',
|
|
||||||
fontSize: '14px',
|
|
||||||
lineHeight: 1.4,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box p="md">
|
|
||||||
{terminalContent.length === 0 ? (
|
|
||||||
<Text c="dimmed" style={{ fontFamily: 'inherit' }}>
|
|
||||||
Starting KoboldCpp...
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<Text
|
|
||||||
component="pre"
|
|
||||||
style={{
|
|
||||||
margin: 0,
|
|
||||||
fontFamily: 'inherit',
|
|
||||||
fontSize: 'inherit',
|
|
||||||
lineHeight: 'inherit',
|
|
||||||
color: 'var(--mantine-color-gray-0)',
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{terminalContent}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</ScrollArea>
|
|
||||||
</Card>
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
5
src/types/electron.d.ts
vendored
5
src/types/electron.d.ts
vendored
|
|
@ -44,9 +44,10 @@ interface ROCmDownload {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KoboldAPI {
|
export interface KoboldAPI {
|
||||||
isInstalled: () => Promise<boolean>;
|
|
||||||
getInstalledVersion: () => Promise<string | undefined>;
|
getInstalledVersion: () => Promise<string | undefined>;
|
||||||
getInstalledVersions: () => Promise<InstalledVersion[]>;
|
getInstalledVersions: (
|
||||||
|
includeVersions?: boolean
|
||||||
|
) => Promise<InstalledVersion[]>;
|
||||||
getCurrentVersion: () => Promise<InstalledVersion | null>;
|
getCurrentVersion: () => Promise<InstalledVersion | null>;
|
||||||
setCurrentVersion: (version: string) => Promise<boolean>;
|
setCurrentVersion: (version: string) => Promise<boolean>;
|
||||||
getVersionFromBinary: (binaryPath: string) => Promise<string | null>;
|
getVersionFromBinary: (binaryPath: string) => Promise<string | null>;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue