mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 19:54:44 -07:00
fighting rocm on windows, allow deleting kcpp binaries from versions tab
This commit is contained in:
parent
e531c8319f
commit
715220c6cd
14 changed files with 286 additions and 216 deletions
|
|
@ -9,7 +9,7 @@ import {
|
||||||
Progress,
|
Progress,
|
||||||
rem,
|
rem,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Download } from 'lucide-react';
|
import { Download, Trash2 } from 'lucide-react';
|
||||||
import { MouseEvent } from 'react';
|
import { MouseEvent } from 'react';
|
||||||
import { pretifyBinName, isWindowsROCmBuild } from '@/utils/assets';
|
import { pretifyBinName, isWindowsROCmBuild } from '@/utils/assets';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
|
@ -19,26 +19,28 @@ interface DownloadCardProps {
|
||||||
version: VersionInfo;
|
version: VersionInfo;
|
||||||
size: string;
|
size: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
isDownloading?: boolean;
|
isLoading?: boolean;
|
||||||
downloadProgress?: number;
|
downloadProgress?: number;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onDownload: (e: MouseEvent<HTMLButtonElement>) => void;
|
onDownload: (e: MouseEvent<HTMLButtonElement>) => void;
|
||||||
onMakeCurrent?: () => void;
|
onMakeCurrent?: () => void;
|
||||||
onUpdate?: (e: MouseEvent<HTMLButtonElement>) => void;
|
onUpdate?: (e: MouseEvent<HTMLButtonElement>) => void;
|
||||||
onRedownload?: (e: MouseEvent<HTMLButtonElement>) => void;
|
onRedownload?: (e: MouseEvent<HTMLButtonElement>) => void;
|
||||||
|
onDelete?: (e: MouseEvent<HTMLButtonElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DownloadCard = ({
|
export const DownloadCard = ({
|
||||||
version: versionInfo,
|
version: versionInfo,
|
||||||
size,
|
size,
|
||||||
description,
|
description,
|
||||||
isDownloading = false,
|
isLoading = false,
|
||||||
downloadProgress = 0,
|
downloadProgress = 0,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
onDownload,
|
onDownload,
|
||||||
onMakeCurrent,
|
onMakeCurrent,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onRedownload,
|
onRedownload,
|
||||||
|
onDelete,
|
||||||
}: DownloadCardProps) => {
|
}: DownloadCardProps) => {
|
||||||
const { resolvedColorScheme: colorScheme } = usePreferencesStore();
|
const { resolvedColorScheme: colorScheme } = usePreferencesStore();
|
||||||
const hasVersionMismatch = Boolean(
|
const hasVersionMismatch = Boolean(
|
||||||
|
|
@ -46,6 +48,8 @@ export const DownloadCard = ({
|
||||||
versionInfo.actualVersion &&
|
versionInfo.actualVersion &&
|
||||||
versionInfo.version !== versionInfo.actualVersion
|
versionInfo.version !== versionInfo.actualVersion
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
const renderActionButtons = () => {
|
const renderActionButtons = () => {
|
||||||
const buttons = [];
|
const buttons = [];
|
||||||
|
|
||||||
|
|
@ -56,17 +60,17 @@ export const DownloadCard = ({
|
||||||
variant="filled"
|
variant="filled"
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={onDownload}
|
onClick={onDownload}
|
||||||
loading={isDownloading}
|
loading={isLoading}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
leftSection={
|
leftSection={
|
||||||
isDownloading ? (
|
isLoading ? (
|
||||||
<Loader size="1rem" />
|
<Loader size="1rem" />
|
||||||
) : (
|
) : (
|
||||||
<Download style={{ width: rem(14), height: rem(14) }} />
|
<Download style={{ width: rem(14), height: rem(14) }} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isDownloading ? 'Downloading...' : 'Download'}
|
{isLoading ? 'Downloading...' : 'Download'}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -92,20 +96,18 @@ export const DownloadCard = ({
|
||||||
variant="filled"
|
variant="filled"
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={onUpdate}
|
onClick={onUpdate}
|
||||||
loading={isDownloading}
|
loading={isLoading}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
color="orange"
|
color="orange"
|
||||||
leftSection={
|
leftSection={
|
||||||
isDownloading ? (
|
isLoading ? (
|
||||||
<Loader size="1rem" />
|
<Loader size="1rem" />
|
||||||
) : (
|
) : (
|
||||||
<Download style={{ width: rem(14), height: rem(14) }} />
|
<Download style={{ width: rem(14), height: rem(14) }} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isDownloading
|
{isLoading ? 'Updating...' : `Update to ${versionInfo.newerVersion}`}
|
||||||
? 'Updating...'
|
|
||||||
: `Update to ${versionInfo.newerVersion}`}
|
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -117,18 +119,41 @@ export const DownloadCard = ({
|
||||||
variant="filled"
|
variant="filled"
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={onRedownload}
|
onClick={onRedownload}
|
||||||
loading={isDownloading}
|
loading={isLoading}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
color="red"
|
color="red"
|
||||||
leftSection={
|
leftSection={
|
||||||
isDownloading ? (
|
isLoading ? (
|
||||||
<Loader size="1rem" />
|
<Loader size="1rem" />
|
||||||
) : (
|
) : (
|
||||||
<Download style={{ width: rem(14), height: rem(14) }} />
|
<Download style={{ width: rem(14), height: rem(14) }} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isDownloading ? 'Re-downloading...' : 'Re-download'}
|
{isLoading ? 'Re-downloading...' : 'Re-download'}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onDelete && versionInfo.isInstalled && !versionInfo.isCurrent) {
|
||||||
|
buttons.push(
|
||||||
|
<Button
|
||||||
|
key="delete"
|
||||||
|
variant="light"
|
||||||
|
size="xs"
|
||||||
|
onClick={onDelete}
|
||||||
|
loading={isLoading}
|
||||||
|
disabled={disabled}
|
||||||
|
color="red"
|
||||||
|
leftSection={
|
||||||
|
isLoading ? (
|
||||||
|
<Loader size="1rem" />
|
||||||
|
) : (
|
||||||
|
<Trash2 style={{ width: rem(14), height: rem(14) }} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isLoading ? 'Deleting...' : 'Delete'}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -201,7 +226,7 @@ export const DownloadCard = ({
|
||||||
{renderActionButtons()}
|
{renderActionButtons()}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{isDownloading && downloadProgress !== undefined && (
|
{isLoading && downloadProgress !== undefined && (
|
||||||
<Stack gap="xs" mt="sm">
|
<Stack gap="xs" mt="sm">
|
||||||
<Progress
|
<Progress
|
||||||
value={Math.min(downloadProgress, 100)}
|
value={Math.min(downloadProgress, 100)}
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
||||||
download.url
|
download.url
|
||||||
)}
|
)}
|
||||||
description={getAssetDescription(download.name)}
|
description={getAssetDescription(download.name)}
|
||||||
isDownloading={isDownloading}
|
isLoading={isDownloading}
|
||||||
downloadProgress={
|
downloadProgress={
|
||||||
isDownloading
|
isDownloading
|
||||||
? downloadProgress[download.name] || 0
|
? downloadProgress[download.name] || 0
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,10 @@ export const BackendSelector = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadBackends = async () => {
|
const loadBackends = async () => {
|
||||||
setIsLoadingBackends(true);
|
setIsLoadingBackends(true);
|
||||||
|
|
||||||
const backends =
|
const backends =
|
||||||
await window.electronAPI.kobold.getAvailableBackends(true);
|
await window.electronAPI.kobold.getAvailableBackends(true);
|
||||||
|
|
||||||
setAvailableBackends(backends || []);
|
setAvailableBackends(backends || []);
|
||||||
setIsLoadingBackends(false);
|
setIsLoadingBackends(false);
|
||||||
hasInitialized.current = true;
|
hasInitialized.current = true;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import {
|
||||||
createSoftwareItems,
|
createSoftwareItems,
|
||||||
createDriverItems,
|
createDriverItems,
|
||||||
createHardwareItems,
|
createHardwareItems,
|
||||||
type HardwareInfo,
|
|
||||||
} from '@/utils/systemInfo';
|
} from '@/utils/systemInfo';
|
||||||
import {
|
import {
|
||||||
Text,
|
Text,
|
||||||
|
|
@ -18,9 +17,10 @@ import {
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Github, FolderOpen, FileText } from 'lucide-react';
|
import { Github, FolderOpen, FileText } from 'lucide-react';
|
||||||
import { useLogoClickSounds } from '@/hooks/useLogoClickSounds';
|
import { useLogoClickSounds } from '@/hooks/useLogoClickSounds';
|
||||||
import type { SystemVersionInfo } from '@/types/electron';
|
|
||||||
import { PRODUCT_NAME, GITHUB_API } from '@/constants';
|
import { PRODUCT_NAME, GITHUB_API } from '@/constants';
|
||||||
import { InfoCard } from '@/components/InfoCard';
|
import { InfoCard } from '@/components/InfoCard';
|
||||||
|
import type { HardwareInfo } from '@/types/hardware';
|
||||||
|
import type { SystemVersionInfo } from '@/types/electron';
|
||||||
|
|
||||||
import icon from '/icon.png';
|
import icon from '/icon.png';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,17 @@ export const VersionsTab = () => {
|
||||||
await loadInstalledVersions();
|
await loadInstalledVersions();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (version: VersionInfo) => {
|
||||||
|
if (!version.installedPath || version.isCurrent) return;
|
||||||
|
|
||||||
|
const result = await window.electronAPI.kobold.deleteRelease(
|
||||||
|
version.installedPath
|
||||||
|
);
|
||||||
|
if (result.success) {
|
||||||
|
await loadInstalledVersions();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const makeCurrent = (version: VersionInfo) => {
|
const makeCurrent = (version: VersionInfo) => {
|
||||||
if (!version.installedPath) return;
|
if (!version.installedPath) return;
|
||||||
|
|
||||||
|
|
@ -270,7 +281,7 @@ export const VersionsTab = () => {
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
description={getAssetDescription(version.name)}
|
description={getAssetDescription(version.name)}
|
||||||
isDownloading={isDownloading}
|
isLoading={isDownloading}
|
||||||
downloadProgress={downloadProgress[version.name]}
|
downloadProgress={downloadProgress[version.name]}
|
||||||
disabled={downloading !== null}
|
disabled={downloading !== null}
|
||||||
onDownload={(e) => {
|
onDownload={(e) => {
|
||||||
|
|
@ -285,6 +296,10 @@ export const VersionsTab = () => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleRedownload(version);
|
handleRedownload(version);
|
||||||
}}
|
}}
|
||||||
|
onDelete={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDelete(version);
|
||||||
|
}}
|
||||||
onMakeCurrent={() => makeCurrent(version)}
|
onMakeCurrent={() => makeCurrent(version)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
getInstalledVersions,
|
getInstalledVersions,
|
||||||
getCurrentVersion,
|
getCurrentVersion,
|
||||||
setCurrentVersion,
|
setCurrentVersion,
|
||||||
|
deleteRelease,
|
||||||
} from '@/main/modules/koboldcpp/version';
|
} from '@/main/modules/koboldcpp/version';
|
||||||
import {
|
import {
|
||||||
getConfigFiles,
|
getConfigFiles,
|
||||||
|
|
@ -138,6 +139,10 @@ export function setupIPCHandlers() {
|
||||||
launchKoboldCppWithCustomFrontends(args)
|
launchKoboldCppWithCustomFrontends(args)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ipcMain.handle('kobold:deleteRelease', (_, binaryPath) =>
|
||||||
|
deleteRelease(binaryPath)
|
||||||
|
);
|
||||||
|
|
||||||
ipcMain.handle('kobold:stopKoboldCpp', () => {
|
ipcMain.handle('kobold:stopKoboldCpp', () => {
|
||||||
stopKoboldCpp();
|
stopKoboldCpp();
|
||||||
stopSillyTavernFrontend();
|
stopSillyTavernFrontend();
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ async function detectVulkan() {
|
||||||
const isIntegrated = gpu.isIntegrated;
|
const isIntegrated = gpu.isIntegrated;
|
||||||
|
|
||||||
devices.push({
|
devices.push({
|
||||||
name: isIntegrated ? gpu.deviceName : formatDeviceName(gpu.deviceName),
|
name: isIntegrated ? gpu.name : formatDeviceName(gpu.name),
|
||||||
isIntegrated,
|
isIntegrated,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -192,155 +192,30 @@ async function detectCUDA() {
|
||||||
export async function detectROCm() {
|
export async function detectROCm() {
|
||||||
try {
|
try {
|
||||||
const rocminfoCommand = platform === 'win32' ? 'hipInfo' : 'rocminfo';
|
const rocminfoCommand = platform === 'win32' ? 'hipInfo' : 'rocminfo';
|
||||||
const { stdout } = await execa(rocminfoCommand, [], COMMON_EXEC_OPTIONS);
|
const [rocminfoResult, vulkanInfo, hipccVersion] = await Promise.all([
|
||||||
|
execa(rocminfoCommand, [], COMMON_EXEC_OPTIONS),
|
||||||
if (stdout.trim()) {
|
getVulkanInfo(),
|
||||||
const devices: GPUDevice[] = [];
|
execa('hipcc', ['--version'], COMMON_EXEC_OPTIONS),
|
||||||
|
]);
|
||||||
if (platform === 'win32') {
|
const { stdout } = rocminfoResult;
|
||||||
const lines = stdout.split('\n');
|
const { stdout: hipccOutput } = hipccVersion;
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const line = lines[i];
|
|
||||||
|
|
||||||
if (line.includes('Marketing Name:')) {
|
|
||||||
const name = line.split('Marketing Name:')[1]?.trim();
|
|
||||||
if (name && !name.toLowerCase().includes('cpu')) {
|
|
||||||
let deviceType = '';
|
|
||||||
|
|
||||||
const searchRangeLines = 20;
|
|
||||||
const searchStartIndex = Math.max(0, i - searchRangeLines);
|
|
||||||
const searchEndIndex = Math.min(
|
|
||||||
lines.length,
|
|
||||||
i + searchRangeLines
|
|
||||||
);
|
|
||||||
|
|
||||||
for (
|
|
||||||
let searchIndex = searchStartIndex;
|
|
||||||
searchIndex < searchEndIndex;
|
|
||||||
searchIndex++
|
|
||||||
) {
|
|
||||||
if (lines[searchIndex].includes('Device Type:')) {
|
|
||||||
deviceType =
|
|
||||||
lines[searchIndex].split('Device Type:')[1]?.trim() || '';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deviceType !== 'CPU') {
|
|
||||||
let isIntegrated = true;
|
|
||||||
try {
|
|
||||||
const vulkanInfo = await getVulkanInfo();
|
|
||||||
const matchingGPU = vulkanInfo.allGPUs.find(
|
|
||||||
(gpu) =>
|
|
||||||
gpu.deviceName.includes(name) ||
|
|
||||||
name.includes(gpu.deviceName)
|
|
||||||
);
|
|
||||||
isIntegrated = matchingGPU ? matchingGPU.isIntegrated : false;
|
|
||||||
} catch {
|
|
||||||
isIntegrated = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
devices.push({
|
|
||||||
name: isIntegrated ? name : formatDeviceName(name),
|
|
||||||
isIntegrated,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const lines = stdout.split('\n');
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const line = lines[i];
|
|
||||||
|
|
||||||
if (line.includes('Marketing Name:')) {
|
|
||||||
const name = line.split('Marketing Name:')[1]?.trim();
|
|
||||||
if (name) {
|
|
||||||
let deviceType = '';
|
|
||||||
|
|
||||||
const searchRangeLines = 20;
|
|
||||||
const searchStartIndex = Math.max(0, i - searchRangeLines);
|
|
||||||
const searchEndIndex = Math.min(
|
|
||||||
lines.length,
|
|
||||||
i + searchRangeLines
|
|
||||||
);
|
|
||||||
|
|
||||||
for (
|
|
||||||
let searchIndex = searchStartIndex;
|
|
||||||
searchIndex < searchEndIndex;
|
|
||||||
searchIndex++
|
|
||||||
) {
|
|
||||||
if (lines[searchIndex].includes('Device Type:')) {
|
|
||||||
deviceType =
|
|
||||||
lines[searchIndex].split('Device Type:')[1]?.trim() || '';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deviceType !== 'CPU') {
|
|
||||||
// Check if integrated by cross-referencing with vulkan GPU list
|
|
||||||
let isIntegrated = true;
|
|
||||||
try {
|
|
||||||
const vulkanInfo = await getVulkanInfo();
|
|
||||||
const matchingGPU = vulkanInfo.allGPUs.find(
|
|
||||||
(gpu) =>
|
|
||||||
gpu.deviceName.includes(name) ||
|
|
||||||
name.includes(gpu.deviceName)
|
|
||||||
);
|
|
||||||
isIntegrated = matchingGPU ? matchingGPU.isIntegrated : false;
|
|
||||||
} catch {
|
|
||||||
isIntegrated = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
devices.push({
|
|
||||||
name: isIntegrated ? name : formatDeviceName(name),
|
|
||||||
isIntegrated,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let version: string | undefined;
|
let version: string | undefined;
|
||||||
|
let driverVersion: string | undefined;
|
||||||
|
|
||||||
if (platform === 'linux' || platform === 'darwin') {
|
|
||||||
try {
|
try {
|
||||||
const { stdout: amdSmiOutput } = await execa(
|
|
||||||
'amd-smi',
|
|
||||||
COMMON_EXEC_OPTIONS
|
|
||||||
);
|
|
||||||
|
|
||||||
if (amdSmiOutput.trim()) {
|
|
||||||
const match =
|
|
||||||
amdSmiOutput.match(/ROCm version:\s*(\d+\.\d+\.\d+)/i) ||
|
|
||||||
amdSmiOutput.match(/version\s*(\d+\.\d+\.\d+)/i) ||
|
|
||||||
amdSmiOutput.match(/(\d+\.\d+\.\d+)/);
|
|
||||||
if (match) {
|
|
||||||
version = match[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
const { stdout: hipccOutput } = await execa(
|
|
||||||
'hipcc',
|
|
||||||
['--version'],
|
|
||||||
COMMON_EXEC_OPTIONS
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hipccOutput.trim()) {
|
if (hipccOutput.trim()) {
|
||||||
const hipVersionMatch = hipccOutput.match(
|
const hipVersionMatch = hipccOutput.match(
|
||||||
/HIP version:\s*(\d+\.\d+(?:\.\d+)?)/i
|
/HIP version:\s*(\d+\.\d+(?:\.\d+)?)/i
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hipVersionMatch) {
|
if (hipVersionMatch) {
|
||||||
version = hipVersionMatch[1];
|
version = hipVersionMatch[1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
|
||||||
|
|
||||||
let driverVersion: string | undefined;
|
if (stdout.trim()) {
|
||||||
|
const devices = parseRocmOutput(stdout, vulkanInfo);
|
||||||
|
|
||||||
if (platform === 'win32') {
|
if (platform === 'win32') {
|
||||||
try {
|
try {
|
||||||
|
|
@ -357,10 +232,8 @@ export async function detectROCm() {
|
||||||
driverVersion = driverOutput.trim();
|
driverVersion = driverOutput.trim();
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
} else if (platform === 'linux') {
|
} else {
|
||||||
try {
|
try {
|
||||||
const vulkanInfo = await getVulkanInfo();
|
|
||||||
|
|
||||||
for (const gpu of vulkanInfo.allGPUs) {
|
for (const gpu of vulkanInfo.allGPUs) {
|
||||||
if (gpu.driverInfo && !gpu.isIntegrated) {
|
if (gpu.driverInfo && !gpu.isIntegrated) {
|
||||||
driverVersion = gpu.driverInfo;
|
driverVersion = gpu.driverInfo;
|
||||||
|
|
@ -467,6 +340,119 @@ function findComputeUnitsInClInfo(lines: string[], startIndex: number) {
|
||||||
|
|
||||||
const isDiscreteGPU = (computeUnits: number) => computeUnits > 12;
|
const isDiscreteGPU = (computeUnits: number) => computeUnits > 12;
|
||||||
|
|
||||||
|
function parseRocmOutput(output: string, vulkanInfo: { allGPUs: GPUDevice[] }) {
|
||||||
|
const devices: GPUDevice[] = [];
|
||||||
|
const lines = output.split('\n');
|
||||||
|
let currentDevice: Partial<GPUDevice> | null = null;
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const trimmedLine = line.trim();
|
||||||
|
|
||||||
|
// Handle hipInfo format
|
||||||
|
if (handleHipInfoLine(trimmedLine, currentDevice, devices)) {
|
||||||
|
currentDevice = trimmedLine.startsWith('device#') ? {} : currentDevice;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle rocminfo format
|
||||||
|
if (line.includes('Marketing Name:')) {
|
||||||
|
const device = parseRocmInfoDevice(line, lines, i, vulkanInfo);
|
||||||
|
if (device) {
|
||||||
|
devices.push(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the last device for hipInfo format
|
||||||
|
if (currentDevice?.name) {
|
||||||
|
devices.push(
|
||||||
|
createDevice(currentDevice.name, currentDevice.isIntegrated || false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return devices;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleHipInfoLine(
|
||||||
|
trimmedLine: string,
|
||||||
|
currentDevice: Partial<GPUDevice> | null,
|
||||||
|
devices: GPUDevice[]
|
||||||
|
): boolean {
|
||||||
|
if (trimmedLine.startsWith('device#')) {
|
||||||
|
if (currentDevice?.name) {
|
||||||
|
devices.push(
|
||||||
|
createDevice(currentDevice.name, currentDevice.isIntegrated || false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentDevice) {
|
||||||
|
if (trimmedLine.startsWith('Name:')) {
|
||||||
|
currentDevice.name = trimmedLine.split('Name:')[1]?.trim();
|
||||||
|
} else if (trimmedLine.startsWith('isIntegrated:')) {
|
||||||
|
const value = trimmedLine.split('isIntegrated:')[1]?.trim();
|
||||||
|
currentDevice.isIntegrated = value === '1';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRocmInfoDevice(
|
||||||
|
line: string,
|
||||||
|
lines: string[],
|
||||||
|
index: number,
|
||||||
|
vulkanInfo: { allGPUs: GPUDevice[] }
|
||||||
|
) {
|
||||||
|
const name = line.split('Marketing Name:')[1]?.trim();
|
||||||
|
if (!name) return null;
|
||||||
|
|
||||||
|
const deviceType = findDeviceType(lines, index);
|
||||||
|
if (deviceType === 'CPU') return null;
|
||||||
|
|
||||||
|
const isIntegrated = determineIfIntegrated(name, vulkanInfo);
|
||||||
|
return createDevice(name, isIntegrated);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createDevice = (name: string, isIntegrated: boolean) => ({
|
||||||
|
name: isIntegrated ? name : formatDeviceName(name),
|
||||||
|
isIntegrated,
|
||||||
|
});
|
||||||
|
|
||||||
|
function findDeviceType(lines: string[], startIndex: number) {
|
||||||
|
const searchRangeLines = 20;
|
||||||
|
const searchStartIndex = Math.max(0, startIndex - searchRangeLines);
|
||||||
|
const searchEndIndex = Math.min(lines.length, startIndex + searchRangeLines);
|
||||||
|
|
||||||
|
for (
|
||||||
|
let searchIndex = searchStartIndex;
|
||||||
|
searchIndex < searchEndIndex;
|
||||||
|
searchIndex++
|
||||||
|
) {
|
||||||
|
if (lines[searchIndex].includes('Device Type:')) {
|
||||||
|
return lines[searchIndex].split('Device Type:')[1]?.trim() || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function determineIfIntegrated(
|
||||||
|
name: string,
|
||||||
|
vulkanInfo: { allGPUs: GPUDevice[] }
|
||||||
|
): boolean {
|
||||||
|
try {
|
||||||
|
const matchingGPU = vulkanInfo.allGPUs.find(
|
||||||
|
(gpu) => gpu.name.includes(name) || name.includes(gpu.name)
|
||||||
|
);
|
||||||
|
return matchingGPU ? matchingGPU.isIntegrated : false;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function detectCLBlast() {
|
async function detectCLBlast() {
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execa('clinfo', [], COMMON_EXEC_OPTIONS);
|
const { stdout } = await execa('clinfo', [], COMMON_EXEC_OPTIONS);
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { pathExists } from '@/utils/node/fs';
|
||||||
import { stripAssetExtensions } from '@/utils/version';
|
import { stripAssetExtensions } from '@/utils/version';
|
||||||
import { getLauncherPath } from '@/utils/node/path';
|
import { getLauncherPath } from '@/utils/node/path';
|
||||||
import type { DownloadReleaseOptions, GitHubAsset } from '@/types/electron';
|
import type { DownloadReleaseOptions, GitHubAsset } from '@/types/electron';
|
||||||
|
import { clearVersionCache } from './version';
|
||||||
|
|
||||||
async function removeDirectoryWithRetry(
|
async function removeDirectoryWithRetry(
|
||||||
dirPath: string,
|
dirPath: string,
|
||||||
|
|
@ -174,6 +175,8 @@ export async function downloadRelease(
|
||||||
unpackedDirPath
|
unpackedDirPath
|
||||||
);
|
);
|
||||||
|
|
||||||
|
clearVersionCache(launcherPath);
|
||||||
|
|
||||||
if (options.oldVersionPath && options.isUpdate) {
|
if (options.oldVersionPath && options.isUpdate) {
|
||||||
const oldInstallDir = join(options.oldVersionPath, '..');
|
const oldInstallDir = join(options.oldVersionPath, '..');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,19 @@ import { logError } from '@/utils/node/logging';
|
||||||
import { getLauncherPath } from '@/utils/node/path';
|
import { getLauncherPath } from '@/utils/node/path';
|
||||||
import type { InstalledVersion } from '@/types/electron';
|
import type { InstalledVersion } from '@/types/electron';
|
||||||
|
|
||||||
|
const versionCache = new Map<
|
||||||
|
string,
|
||||||
|
{ version: string; actualVersion?: string } | null
|
||||||
|
>();
|
||||||
|
|
||||||
|
export function clearVersionCache(path?: string) {
|
||||||
|
if (path) {
|
||||||
|
versionCache.delete(path);
|
||||||
|
} else {
|
||||||
|
versionCache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getInstalledVersions() {
|
export async function getInstalledVersions() {
|
||||||
try {
|
try {
|
||||||
const installDir = getInstallDir();
|
const installDir = getInstallDir();
|
||||||
|
|
@ -129,12 +142,48 @@ export async function setCurrentVersion(binaryPath: string) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteRelease(binaryPath: string) {
|
||||||
|
try {
|
||||||
|
if (!(await pathExists(binaryPath))) {
|
||||||
|
return { success: false, error: 'Release not found' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentBinaryPath = getCurrentKoboldBinary();
|
||||||
|
if (currentBinaryPath === binaryPath) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Cannot delete the currently active release',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseDir = binaryPath.split(/[/\\]/).slice(0, -1).join('/');
|
||||||
|
|
||||||
|
if (await pathExists(releaseDir)) {
|
||||||
|
const { rm } = await import('fs/promises');
|
||||||
|
await rm(releaseDir, { recursive: true, force: true });
|
||||||
|
|
||||||
|
clearVersionCache(binaryPath);
|
||||||
|
sendToRenderer('versions-updated');
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, error: 'Release directory not found' };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: (error as Error).message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getVersionFromBinary(launcherPath: string) {
|
export async function getVersionFromBinary(launcherPath: string) {
|
||||||
try {
|
try {
|
||||||
if (!(await pathExists(launcherPath))) {
|
if (!(await pathExists(launcherPath))) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (versionCache.has(launcherPath)) {
|
||||||
|
return versionCache.get(launcherPath);
|
||||||
|
}
|
||||||
|
|
||||||
let folderVersion: string | null = null;
|
let folderVersion: string | null = null;
|
||||||
let actualVersion: string | null = null;
|
let actualVersion: string | null = null;
|
||||||
|
|
||||||
|
|
@ -155,37 +204,31 @@ export async function getVersionFromBinary(launcherPath: string) {
|
||||||
});
|
});
|
||||||
|
|
||||||
const allOutput = (result.stdout + result.stderr).trim();
|
const allOutput = (result.stdout + result.stderr).trim();
|
||||||
|
const lines = allOutput.split('\n').filter((line) => line.trim());
|
||||||
|
|
||||||
if (/^\d+\.\d+/.test(allOutput)) {
|
if (lines.length > 0) {
|
||||||
const versionParts = allOutput.split(/\s+/)[0];
|
const lastLine = lines[lines.length - 1].trim();
|
||||||
if (versionParts && /^\d+\.\d+/.test(versionParts)) {
|
const versionMatch = lastLine.match(
|
||||||
actualVersion = versionParts;
|
/^(\d+\.\d+(?:\.\d+)?(?:\.[a-zA-Z0-9]+)*(?:-[a-zA-Z0-9]+)*)$/
|
||||||
}
|
);
|
||||||
}
|
if (versionMatch) {
|
||||||
|
actualVersion = versionMatch[1];
|
||||||
if (!actualVersion) {
|
|
||||||
const lines = allOutput.split('\n');
|
|
||||||
for (const line of lines) {
|
|
||||||
const trimmedLine = line.trim();
|
|
||||||
if (/^\d+\.\d+/.test(trimmedLine)) {
|
|
||||||
const versionPart = trimmedLine.split(/\s+/)[0];
|
|
||||||
if (versionPart) {
|
|
||||||
actualVersion = versionPart;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
return {
|
const result = {
|
||||||
version: folderVersion || actualVersion || 'unknown',
|
version: folderVersion || actualVersion || 'unknown',
|
||||||
actualVersion:
|
actualVersion:
|
||||||
folderVersion && actualVersion && folderVersion !== actualVersion
|
folderVersion && actualVersion && folderVersion !== actualVersion
|
||||||
? actualVersion
|
? actualVersion
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
versionCache.set(launcherPath, result);
|
||||||
|
return result;
|
||||||
} catch {
|
} catch {
|
||||||
|
versionCache.set(launcherPath, null);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ const koboldAPI: KoboldAPI = {
|
||||||
ipcRenderer.invoke('kobold:selectInstallDirectory'),
|
ipcRenderer.invoke('kobold:selectInstallDirectory'),
|
||||||
downloadRelease: (asset, options) =>
|
downloadRelease: (asset, options) =>
|
||||||
ipcRenderer.invoke('kobold:downloadRelease', asset, options),
|
ipcRenderer.invoke('kobold:downloadRelease', asset, options),
|
||||||
|
deleteRelease: (binaryPath) =>
|
||||||
|
ipcRenderer.invoke('kobold:deleteRelease', binaryPath),
|
||||||
launchKoboldCpp: (args) => ipcRenderer.invoke('kobold:launchKoboldCpp', args),
|
launchKoboldCpp: (args) => ipcRenderer.invoke('kobold:launchKoboldCpp', args),
|
||||||
getConfigFiles: () => ipcRenderer.invoke('kobold:getConfigFiles'),
|
getConfigFiles: () => ipcRenderer.invoke('kobold:getConfigFiles'),
|
||||||
saveConfigFile: (configName, configData) =>
|
saveConfigFile: (configName, configData) =>
|
||||||
|
|
|
||||||
3
src/types/electron.d.ts
vendored
3
src/types/electron.d.ts
vendored
|
|
@ -124,6 +124,9 @@ export interface KoboldAPI {
|
||||||
asset: GitHubAsset,
|
asset: GitHubAsset,
|
||||||
options: DownloadReleaseOptions
|
options: DownloadReleaseOptions
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
deleteRelease: (
|
||||||
|
binaryPath: string
|
||||||
|
) => Promise<{ success: boolean; error?: string }>;
|
||||||
launchKoboldCpp: (
|
launchKoboldCpp: (
|
||||||
args?: string[]
|
args?: string[]
|
||||||
) => Promise<{ success: boolean; pid?: number; error?: string }>;
|
) => Promise<{ success: boolean; pid?: number; error?: string }>;
|
||||||
|
|
|
||||||
10
src/types/hardware.d.ts
vendored
10
src/types/hardware.d.ts
vendored
|
|
@ -13,8 +13,8 @@ export interface SystemMemoryInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GPUDevice {
|
export interface GPUDevice {
|
||||||
readonly name: string;
|
name: string;
|
||||||
readonly isIntegrated: boolean;
|
isIntegrated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GPUCapabilities {
|
export interface GPUCapabilities {
|
||||||
|
|
@ -52,7 +52,7 @@ export interface HardwareDetectionResult {
|
||||||
export interface HardwareInfo {
|
export interface HardwareInfo {
|
||||||
cpu: CPUCapabilities;
|
cpu: CPUCapabilities;
|
||||||
gpu: BasicGPUInfo;
|
gpu: BasicGPUInfo;
|
||||||
gpuCapabilities?: GPUCapabilities;
|
gpuCapabilities: GPUCapabilities;
|
||||||
gpuMemory?: GPUMemoryInfo[];
|
gpuMemory: GPUMemoryInfo[];
|
||||||
systemMemory?: SystemMemoryInfo;
|
systemMemory: SystemMemoryInfo;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { formatDeviceName } from '@/utils/format';
|
||||||
|
|
||||||
let vulkanInfoCache: {
|
let vulkanInfoCache: {
|
||||||
allGPUs: {
|
allGPUs: {
|
||||||
deviceName: string;
|
name: string;
|
||||||
driverInfo?: string;
|
driverInfo?: string;
|
||||||
apiVersion?: string;
|
apiVersion?: string;
|
||||||
hasAMD: boolean;
|
hasAMD: boolean;
|
||||||
|
|
@ -27,7 +27,7 @@ export async function getVulkanInfo() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const allGPUs: {
|
const allGPUs: {
|
||||||
deviceName: string;
|
name: string;
|
||||||
driverInfo?: string;
|
driverInfo?: string;
|
||||||
apiVersion?: string;
|
apiVersion?: string;
|
||||||
hasAMD: boolean;
|
hasAMD: boolean;
|
||||||
|
|
@ -62,7 +62,7 @@ export async function getVulkanInfo() {
|
||||||
'PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU'
|
'PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU'
|
||||||
);
|
);
|
||||||
currentGPU = {
|
currentGPU = {
|
||||||
deviceName: '',
|
name: '',
|
||||||
hasAMD: false,
|
hasAMD: false,
|
||||||
hasNVIDIA: false,
|
hasNVIDIA: false,
|
||||||
isIntegrated,
|
isIntegrated,
|
||||||
|
|
@ -77,7 +77,7 @@ export async function getVulkanInfo() {
|
||||||
if (parts.length >= 2) {
|
if (parts.length >= 2) {
|
||||||
const name = parts[1]?.trim();
|
const name = parts[1]?.trim();
|
||||||
if (name) {
|
if (name) {
|
||||||
currentGPU.deviceName = name;
|
currentGPU.name = name;
|
||||||
currentGPU.hasAMD =
|
currentGPU.hasAMD =
|
||||||
name.toLowerCase().includes('amd') ||
|
name.toLowerCase().includes('amd') ||
|
||||||
name.toLowerCase().includes('radeon');
|
name.toLowerCase().includes('radeon');
|
||||||
|
|
@ -107,7 +107,7 @@ export async function getVulkanInfo() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (foundGPU && currentGPU && line.includes('GPU')) {
|
} else if (foundGPU && currentGPU && line.includes('GPU')) {
|
||||||
if (currentGPU.deviceName) {
|
if (currentGPU.name) {
|
||||||
allGPUs.push(currentGPU);
|
allGPUs.push(currentGPU);
|
||||||
}
|
}
|
||||||
foundGPU = false;
|
foundGPU = false;
|
||||||
|
|
@ -115,7 +115,7 @@ export async function getVulkanInfo() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (foundGPU && currentGPU && currentGPU.deviceName) {
|
if (foundGPU && currentGPU && currentGPU.name) {
|
||||||
allGPUs.push(currentGPU);
|
allGPUs.push(currentGPU);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -143,7 +143,7 @@ export async function detectGPUViaVulkan() {
|
||||||
const gpuInfo: string[] = [];
|
const gpuInfo: string[] = [];
|
||||||
|
|
||||||
for (const gpu of vulkanInfo.allGPUs.filter((g) => !g.isIntegrated)) {
|
for (const gpu of vulkanInfo.allGPUs.filter((g) => !g.isIntegrated)) {
|
||||||
gpuInfo.push(formatDeviceName(gpu.deviceName));
|
gpuInfo.push(formatDeviceName(gpu.name));
|
||||||
|
|
||||||
if (gpu.hasAMD) {
|
if (gpu.hasAMD) {
|
||||||
hasAMD = true;
|
hasAMD = true;
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,7 @@
|
||||||
import type { SystemVersionInfo } from '@/types/electron';
|
import type { SystemVersionInfo } from '@/types/electron';
|
||||||
import type {
|
|
||||||
CPUCapabilities,
|
|
||||||
GPUCapabilities,
|
|
||||||
BasicGPUInfo,
|
|
||||||
GPUMemoryInfo,
|
|
||||||
SystemMemoryInfo,
|
|
||||||
} from '@/types/hardware';
|
|
||||||
import { PRODUCT_NAME } from '@/constants';
|
import { PRODUCT_NAME } from '@/constants';
|
||||||
import type { InfoItem } from '@/components/InfoCard';
|
import type { InfoItem } from '@/components/InfoCard';
|
||||||
|
import type { HardwareInfo } from '@/types/hardware';
|
||||||
export interface HardwareInfo {
|
|
||||||
cpu: CPUCapabilities;
|
|
||||||
gpu: BasicGPUInfo;
|
|
||||||
gpuCapabilities: GPUCapabilities;
|
|
||||||
gpuMemory: GPUMemoryInfo[];
|
|
||||||
systemMemory: SystemMemoryInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createSoftwareItems = (versionInfo: SystemVersionInfo) => [
|
export const createSoftwareItems = (versionInfo: SystemVersionInfo) => [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue