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

View file

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

View file

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

View file

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

View file

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

View file

@ -21,11 +21,14 @@ export const KLITE_CSS_OVERRIDE = `
padding: 0 10px; padding: 0 10px;
} }
#navbarNavDropdown { #navbarNavDropdown {
padding: 0; padding: 0;
} }
#actionmenuitems {
margin-left: 10px;
}
#inputrow > :nth-child(1) { #inputrow > :nth-child(1) {
padding-right: 0 !important; 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 { join } from 'path';
import { release } from 'os'; import { release } from 'os';
import { platform, versions, arch } from 'process'; import { platform, versions, arch } from 'process';
@ -30,8 +30,8 @@ import {
getColorScheme, getColorScheme,
setColorScheme, setColorScheme,
} from '@/main/modules/config'; } from '@/main/modules/config';
import { getConfigDir } from '@/utils/node/path'; import { getConfigDir, openPathHandler, openUrl } from '@/utils/node/path';
import { logError, safeExecute } from '@/utils/node/logging'; import { logError } from '@/utils/node/logging';
import { stopFrontend as stopSillyTavernFrontend } from '@/main/modules/sillytavern'; import { stopFrontend as stopSillyTavernFrontend } from '@/main/modules/sillytavern';
import { stopFrontend as stopOpenWebUIFrontend } from '@/main/modules/openwebui'; import { stopFrontend as stopOpenWebUIFrontend } from '@/main/modules/openwebui';
import { stopFrontend as stopComfyUIFrontend } from '@/main/modules/comfyui'; import { stopFrontend as stopComfyUIFrontend } from '@/main/modules/comfyui';
@ -81,10 +81,9 @@ import type { NotepadState } from '@/types/electron';
export function setupIPCHandlers() { export function setupIPCHandlers() {
const mainWindow = getMainWindow(); const mainWindow = getMainWindow();
ipcMain.handle('kobold:downloadRelease', async (_, asset) => ({ ipcMain.handle('kobold:downloadRelease', async (_, asset) =>
success: true, downloadRelease(asset)
path: await downloadRelease(asset), );
}));
ipcMain.handle('kobold:getInstalledVersions', () => getInstalledVersions()); 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:openPath', (_, path: string) => openPathHandler(path));
ipcMain.handle('app:showLogsFolder', () => { ipcMain.handle('app:showLogsFolder', () =>
const logsDir = join(app.getPath('userData'), 'logs'); openPathHandler(join(app.getPath('userData'), 'logs'))
return openPathHandler(logsDir); );
});
ipcMain.handle('app:viewConfigFile', () => { ipcMain.handle('app:viewConfigFile', () => openPathHandler(getConfigDir()));
const configDir = getConfigDir();
return openPathHandler(configDir);
});
ipcMain.handle('app:minimizeWindow', () => mainWindow.minimize()); ipcMain.handle('app:minimizeWindow', () => mainWindow.minimize());
@ -233,17 +219,7 @@ export function setupIPCHandlers() {
setColorScheme(colorScheme) setColorScheme(colorScheme)
); );
ipcMain.handle( ipcMain.handle('app:openExternal', async (_, url: string) => openUrl(url));
'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',
}
);
mainWindow.webContents.once('did-finish-load', async () => { mainWindow.webContents.once('did-finish-load', async () => {
const savedZoomLevel = await getConfig('zoomLevel'); const savedZoomLevel = await getConfig('zoomLevel');

View file

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

View file

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

View file

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

View file

@ -1,6 +1,9 @@
import { join } from 'path'; import { join } from 'path';
import { homedir } from 'os'; import { homedir } from 'os';
import { platform, resourcesPath } from 'process'; import { platform, resourcesPath } from 'process';
import { shell } from 'electron';
import { safeExecute } from '@/utils/logger';
import { PRODUCT_NAME, CONFIG_FILE_NAME } from '@/constants'; import { PRODUCT_NAME, CONFIG_FILE_NAME } from '@/constants';
import { isDevelopment } from './environment'; import { isDevelopment } from './environment';
import { pathExists } from './fs'; import { pathExists } from './fs';
@ -37,3 +40,21 @@ export async function getLauncherPath(unpackedDir: string) {
return null; 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',
};