clean up previously installed kcpp versions after an update, bring back a needed monkey patch, small refactors

This commit is contained in:
Egor 2025-09-24 14:33:44 -07:00
parent 4fd87a619a
commit 03b62d3499
11 changed files with 101 additions and 126 deletions

View file

@ -13,7 +13,8 @@ import { useState } from 'react';
import type { DownloadItem } from '@/types/electron';
import type { BinaryUpdateInfo } from '@/hooks/useUpdateChecker';
import { useKoboldVersionsStore } from '@/stores/koboldVersions';
import { getDisplayNameFromPath } from '@/utils/version';
import { pretifyBinName } from '@/utils/assets';
import { formatDownloadSize } from '@/utils/format';
import { GITHUB_API } from '@/constants';
import { safeExecute } from '@/utils/logger';
import { Modal } from '@/components/Modal';
@ -60,7 +61,7 @@ export const UpdateAvailableModal = ({
opened={opened}
onClose={onClose}
size="sm"
title="An update is available"
title="An Update is Available"
closeOnEscape={!isDownloading && !isUpdating}
>
<Stack gap="md">
@ -92,26 +93,28 @@ export const UpdateAvailableModal = ({
{currentVersion && (
<Text size="xs" c="dimmed">
Binary: {getDisplayNameFromPath(currentVersion)}
Binary Type: {pretifyBinName(currentVersion.filename)}
</Text>
)}
<Group gap="xs" align="center" mt="xs">
{availableUpdate?.size && (
<Text size="xs" c="dimmed">
View release notes:
Update Size:{' '}
{formatDownloadSize(availableUpdate.size, availableUpdate.url)}
</Text>
<Anchor
size="xs"
href={`${GITHUB_API.GITHUB_BASE_URL}/${GITHUB_API.KOBOLDCPP_REPO}/releases/tag/v${availableUpdate?.version}`}
target="_blank"
rel="noopener noreferrer"
>
<Group gap={4} align="center">
<span>v{availableUpdate?.version}</span>
<ExternalLink size={12} />
</Group>
</Anchor>
</Group>
)}
<Anchor
size="xs"
href={`${GITHUB_API.GITHUB_BASE_URL}/${GITHUB_API.KOBOLDCPP_REPO}/releases/tag/v${availableUpdate?.version}`}
target="_blank"
rel="noopener noreferrer"
>
<Group gap={4} align="center">
<span>View Release Notes</span>
<ExternalLink size={12} />
</Group>
</Anchor>
</Stack>
</Card>

View file

@ -90,15 +90,13 @@ export const App = () => {
};
const handleBinaryUpdate = async (download: DownloadItem) => {
const success = await handleDownload({
await handleDownload({
item: download,
isUpdate: true,
wasCurrentBinary: true,
});
if (success) {
closeModal();
}
closeModal();
};
const handleEject = async () => {

View file

@ -31,21 +31,17 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
async (download: DownloadItem) => {
setDownloadingAsset(download.name);
const success = await handleDownloadFromStore({
await handleDownloadFromStore({
item: download,
isUpdate: false,
wasCurrentBinary: false,
});
if (success) {
onDownloadComplete();
onDownloadComplete();
setTimeout(() => {
setDownloadingAsset(null);
}, 200);
}
setDownloadingAsset(null);
setTimeout(() => {
setDownloadingAsset(null);
}, 200);
},
[handleDownloadFromStore, onDownloadComplete]
);

View file

@ -158,7 +158,7 @@ export const AboutTab = () => {
</Card>
<Card withBorder radius="md" p="xs" style={{ position: 'relative' }}>
<Tooltip label="Copy Version Info">
<Tooltip label="Copy Info">
<ActionIcon
variant="subtle"
size="sm"

View file

@ -171,30 +171,26 @@ export const VersionsTab = () => {
const download = availableDownloads.find((d) => d.name === version.name);
if (!download) return;
const success = await handleDownloadFromStore({
await handleDownloadFromStore({
item: download,
isUpdate: false,
wasCurrentBinary: false,
});
if (success) {
await loadInstalledVersions();
}
await loadInstalledVersions();
};
const handleUpdate = async (version: VersionInfo) => {
const download = availableDownloads.find((d) => d.name === version.name);
if (!download) return;
const success = await handleDownloadFromStore({
await handleDownloadFromStore({
item: download,
isUpdate: true,
wasCurrentBinary: version.isCurrent,
});
if (success) {
await loadInstalledVersions();
}
await loadInstalledVersions();
};
const makeCurrent = async (version: VersionInfo) => {

View file

@ -21,11 +21,14 @@ export const KLITE_CSS_OVERRIDE = `
padding: 0 10px;
}
#navbarNavDropdown {
padding: 0;
}
#actionmenuitems {
margin-left: 10px;
}
#inputrow > :nth-child(1) {
padding-right: 0 !important;
}

View file

@ -1,4 +1,4 @@
import { ipcMain, shell, app } from 'electron';
import { ipcMain, app } from 'electron';
import { join } from 'path';
import { release } from 'os';
import { platform, versions, arch } from 'process';
@ -30,8 +30,8 @@ import {
getColorScheme,
setColorScheme,
} from '@/main/modules/config';
import { getConfigDir } from '@/utils/node/path';
import { logError, safeExecute } from '@/utils/node/logging';
import { getConfigDir, openPathHandler, openUrl } from '@/utils/node/path';
import { logError } from '@/utils/node/logging';
import { stopFrontend as stopSillyTavernFrontend } from '@/main/modules/sillytavern';
import { stopFrontend as stopOpenWebUIFrontend } from '@/main/modules/openwebui';
import { stopFrontend as stopComfyUIFrontend } from '@/main/modules/comfyui';
@ -81,10 +81,9 @@ import type { NotepadState } from '@/types/electron';
export function setupIPCHandlers() {
const mainWindow = getMainWindow();
ipcMain.handle('kobold:downloadRelease', async (_, asset) => ({
success: true,
path: await downloadRelease(asset),
}));
ipcMain.handle('kobold:downloadRelease', async (_, asset) =>
downloadRelease(asset)
);
ipcMain.handle('kobold:getInstalledVersions', () => getInstalledVersions());
@ -183,26 +182,13 @@ export function setupIPCHandlers() {
};
});
const openPathHandler = async (path: string) =>
(await safeExecute(async () => {
await shell.openPath(path);
return { success: true };
}, 'Failed to open path')) || {
success: false,
error: 'Failed to open path',
};
ipcMain.handle('app:openPath', (_, path: string) => openPathHandler(path));
ipcMain.handle('app:showLogsFolder', () => {
const logsDir = join(app.getPath('userData'), 'logs');
return openPathHandler(logsDir);
});
ipcMain.handle('app:showLogsFolder', () =>
openPathHandler(join(app.getPath('userData'), 'logs'))
);
ipcMain.handle('app:viewConfigFile', () => {
const configDir = getConfigDir();
return openPathHandler(configDir);
});
ipcMain.handle('app:viewConfigFile', () => openPathHandler(getConfigDir()));
ipcMain.handle('app:minimizeWindow', () => mainWindow.minimize());
@ -233,17 +219,7 @@ export function setupIPCHandlers() {
setColorScheme(colorScheme)
);
ipcMain.handle(
'app:openExternal',
async (_, url: string) =>
(await safeExecute(async () => {
await shell.openExternal(url);
return { success: true };
}, 'Failed to open external URL')) || {
success: false,
error: 'Failed to open external URL',
}
);
ipcMain.handle('app:openExternal', async (_, url: string) => openUrl(url));
mainWindow.webContents.once('did-finish-load', async () => {
const savedZoomLevel = await getConfig('zoomLevel');

View file

@ -2,8 +2,8 @@ import { createWriteStream } from 'fs';
import { join } from 'path';
import { platform } from 'process';
import { rm, unlink, rename, mkdir, chmod } from 'fs/promises';
import { execa } from 'execa';
import {
getInstallDir,
getCurrentKoboldBinary,
@ -37,27 +37,6 @@ async function removeDirectoryWithRetry(
}
}
async function handleExistingDirectory(
unpackedDirPath: string,
isUpdate: boolean,
wasCurrentBinary: boolean
) {
if (await pathExists(unpackedDirPath)) {
if (isUpdate || wasCurrentBinary) {
try {
await removeDirectoryWithRetry(unpackedDirPath);
} catch (error) {
logError('Failed to remove existing directory:', error as Error);
throw new Error('Failed to remove existing installation');
}
} else {
throw new Error(
'Installation directory already exists. Please uninstall the existing version first.'
);
}
}
}
async function downloadFile(asset: GitHubAsset, tempPackedFilePath: string) {
const writer = createWriteStream(tempPackedFilePath);
const mainWindow = getMainWindow();
@ -177,12 +156,16 @@ export async function downloadRelease(asset: GitHubAsset) {
: baseFilename;
const unpackedDirPath = join(getInstallDir(), folderName);
let currentBinaryPath: string | null = null;
if (asset.isUpdate && asset.wasCurrentBinary) {
currentBinaryPath = getCurrentKoboldBinary() || null;
}
try {
await handleExistingDirectory(
unpackedDirPath,
Boolean(asset.isUpdate),
Boolean(asset.wasCurrentBinary)
);
if (await pathExists(unpackedDirPath)) {
await removeDirectoryWithRetry(unpackedDirPath);
}
await downloadFile(asset, tempPackedFilePath);
await mkdir(unpackedDirPath, { recursive: true });
@ -197,8 +180,15 @@ export async function downloadRelease(asset: GitHubAsset) {
await setCurrentKoboldBinary(launcherPath);
}
if (currentBinaryPath && asset.isUpdate && asset.wasCurrentBinary) {
const oldInstallDir = join(currentBinaryPath, '..');
if (oldInstallDir !== unpackedDirPath) {
await removeDirectoryWithRetry(oldInstallDir);
}
}
sendToRenderer('versions-updated');
return launcherPath;
} catch (error) {
logError('Failed to download or unpack binary:', error as Error);
throw new Error('Failed to download or unpack binary');

View file

@ -56,7 +56,7 @@ interface KoboldVersionsState {
downloadProgress: Record<string, number>;
initialize: () => Promise<void>;
handleDownload: (params: HandleDownloadParams) => Promise<boolean>;
handleDownload: (params: HandleDownloadParams) => Promise<void>;
getLatestReleaseWithDownloadStatus: () => Promise<ReleaseWithStatus | null>;
}
@ -100,7 +100,7 @@ export const useKoboldVersionsStore = create<KoboldVersionsState>(
const { downloading } = get();
if (downloading) {
return false;
return;
}
set({ downloading: item.name, downloadProgress: { [item.name]: 0 } });
@ -111,25 +111,19 @@ export const useKoboldVersionsStore = create<KoboldVersionsState>(
}
);
try {
const asset: GitHubAsset = {
name: item.name,
browser_download_url: item.url,
size: item.size,
version: item.version,
isUpdate,
wasCurrentBinary,
};
const asset: GitHubAsset = {
name: item.name,
browser_download_url: item.url,
size: item.size,
version: item.version,
isUpdate,
wasCurrentBinary,
};
const result = await window.electronAPI.kobold.downloadRelease(asset);
return result.success;
} catch (err) {
logError('Download failed:', err as Error);
return false;
} finally {
progressCleanup();
set({ downloading: null, downloadProgress: {} });
}
await window.electronAPI.kobold.downloadRelease(asset);
progressCleanup();
set({ downloading: null, downloadProgress: {} });
},
getLatestReleaseWithDownloadStatus: async () =>
safeExecute(async () => {

View file

@ -113,9 +113,7 @@ export interface KoboldAPI {
getAvailableBackends: (includeDisabled?: boolean) => Promise<BackendOption[]>;
getCurrentInstallDir: () => Promise<string>;
selectInstallDirectory: () => Promise<string | null>;
downloadRelease: (
asset: GitHubAsset
) => Promise<{ success: boolean; path?: string; error?: string }>;
downloadRelease: (asset: GitHubAsset) => Promise<void>;
launchKoboldCpp: (
args?: string[]
) => Promise<{ success: boolean; pid?: number; error?: string }>;

View file

@ -1,6 +1,9 @@
import { join } from 'path';
import { homedir } from 'os';
import { platform, resourcesPath } from 'process';
import { shell } from 'electron';
import { safeExecute } from '@/utils/logger';
import { PRODUCT_NAME, CONFIG_FILE_NAME } from '@/constants';
import { isDevelopment } from './environment';
import { pathExists } from './fs';
@ -37,3 +40,21 @@ export async function getLauncherPath(unpackedDir: string) {
return null;
}
export const openPathHandler = async (path: string) =>
(await safeExecute(async () => {
await shell.openPath(path);
return { success: true };
}, 'Failed to open path')) || {
success: false,
error: 'Failed to open path',
};
export const openUrl = (url: string) =>
safeExecute(async () => {
await shell.openExternal(url);
return { success: true };
}, 'Failed to open external URL') || {
success: false,
error: 'Failed to open external URL',
};