better old window state recreation, more distrinct select option highlights in light mode, better updated version cleanup

This commit is contained in:
Egor 2025-09-24 23:14:46 -07:00
parent 03b62d3499
commit 7cef3ce90d
20 changed files with 133 additions and 181 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "gerbil", "name": "gerbil",
"productName": "Gerbil", "productName": "Gerbil",
"version": "1.5.2", "version": "1.5.3",
"description": "Run Large Language Models locally", "description": "Run Large Language Models locally",
"main": "out/main/index.js", "main": "out/main/index.js",
"homepage": "./", "homepage": "./",
@ -15,7 +15,7 @@
"build": "electron-vite build", "build": "electron-vite build",
"package": "electron-vite build && electron-builder --publish=never", "package": "electron-vite build && electron-builder --publish=never",
"analyze": "cross-env ANALYZE=true electron-vite build && yarn dlx open-cli dist/stats.html", "analyze": "cross-env ANALYZE=true electron-vite build && yarn dlx open-cli dist/stats.html",
"format": "prettier --write . --ignore-path .gitignore", "format": "prettier --write . --ignore-path .gitignore | grep -v '(unchanged)' || true",
"lint": "eslint .", "lint": "eslint .",
"lint:fix": "eslint . --fix", "lint:fix": "eslint . --fix",
"compile": "tsc --noEmit", "compile": "tsc --noEmit",
@ -68,8 +68,8 @@
"@codemirror/theme-one-dark": "^6.1.3", "@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.38.3", "@codemirror/view": "^6.38.3",
"@fontsource/inter": "^5.2.8", "@fontsource/inter": "^5.2.8",
"@mantine/core": "^8.3.1", "@mantine/core": "8.3.1",
"@mantine/hooks": "^8.3.1", "@mantine/hooks": "8.3.1",
"@types/yauzl": "^2.10.3", "@types/yauzl": "^2.10.3",
"@uiw/react-codemirror": "^4.25.2", "@uiw/react-codemirror": "^4.25.2",
"electron-updater": "6.6.2", "electron-updater": "6.6.2",

View file

@ -3,7 +3,6 @@ import {
ActionIcon, ActionIcon,
Box, Box,
Image, Image,
Select,
AppShell, AppShell,
Tooltip, Tooltip,
} from '@mantine/core'; } from '@mantine/core';
@ -18,6 +17,7 @@ import { UpdateButton } from '@/components/App/UpdateButton';
import icon from '/icon.png'; import icon from '/icon.png';
import { PRODUCT_NAME, TITLEBAR_HEIGHT } from '@/constants'; import { PRODUCT_NAME, TITLEBAR_HEIGHT } from '@/constants';
import type { InterfaceTab, Screen, SelectOption } from '@/types'; import type { InterfaceTab, Screen, SelectOption } from '@/types';
import { Select } from '@/components/Select';
interface TitleBarProps { interface TitleBarProps {
currentScreen: Screen; currentScreen: Screen;
@ -135,9 +135,7 @@ export const TitleBar = ({
onDropdownClose={() => setIsSelectOpen(false)} onDropdownClose={() => setIsSelectOpen(false)}
data={interfaceOptions} data={interfaceOptions}
renderOption={renderOption} renderOption={renderOption}
allowDeselect={false}
variant="unstyled" variant="unstyled"
size="sm"
style={{ textAlign: 'center', minWidth: '7.5rem' }} style={{ textAlign: 'center', minWidth: '7.5rem' }}
styles={{ styles={{
input: { input: {

View file

@ -91,9 +91,9 @@ export const UpdateAvailableModal = ({
</div> </div>
</Group> </Group>
{currentVersion && ( {availableUpdate?.name && (
<Text size="xs" c="dimmed"> <Text size="xs" c="dimmed">
Binary Type: {pretifyBinName(currentVersion.filename)} Binary Type: {pretifyBinName(availableUpdate?.name)}
</Text> </Text>
)} )}

View file

@ -90,10 +90,13 @@ export const App = () => {
}; };
const handleBinaryUpdate = async (download: DownloadItem) => { const handleBinaryUpdate = async (download: DownloadItem) => {
const currentVersion = await window.electronAPI.kobold.getCurrentVersion();
await handleDownload({ await handleDownload({
item: download, item: download,
isUpdate: true, isUpdate: true,
wasCurrentBinary: true, wasCurrentBinary: true,
oldVersionPath: currentVersion?.path,
}); });
closeModal(); closeModal();

16
src/components/Select.tsx Normal file
View file

@ -0,0 +1,16 @@
import { Select as MantineSelect } from '@mantine/core';
import type { SelectProps } from '@mantine/core';
export const Select = ({
allowDeselect = false,
clearable = false,
size = 'sm',
...props
}: SelectProps) => (
<MantineSelect
allowDeselect={allowDeselect}
clearable={clearable}
size={size}
{...props}
/>
);

View file

@ -1,8 +1,8 @@
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import { Select } from '@mantine/core';
import type { ComboboxItem } from '@mantine/core'; import type { ComboboxItem } from '@mantine/core';
import { LabelWithTooltip } from '@/components/LabelWithTooltip'; import { LabelWithTooltip } from '@/components/LabelWithTooltip';
import type { SelectOption } from '@/types'; import type { SelectOption } from '@/types';
import { Select } from '@/components/Select';
interface SelectWithTooltipProps { interface SelectWithTooltipProps {
label: string; label: string;
@ -36,7 +36,6 @@ export const SelectWithTooltip = ({
data={data} data={data}
disabled={disabled} disabled={disabled}
clearable={clearable} clearable={clearable}
allowDeselect={false}
/> />
</div> </div>
); );

View file

@ -1,9 +1,10 @@
import { Stack, Text, Group, Button, Select, Tooltip } from '@mantine/core'; import { Stack, Text, Group, Button, Tooltip } from '@mantine/core';
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { Save, File, Plus, Check, Trash2 } from 'lucide-react'; import { Save, File, Plus, Check, Trash2 } from 'lucide-react';
import type { ConfigFile } from '@/types'; import type { ConfigFile } from '@/types';
import { CreateConfigModal } from './CreateConfigModal'; import { CreateConfigModal } from './CreateConfigModal';
import { DeleteConfigModal } from './DeleteConfigModal'; import { DeleteConfigModal } from './DeleteConfigModal';
import { Select } from '@/components/Select';
interface ConfigFileManagerProps { interface ConfigFileManagerProps {
configFiles: ConfigFile[]; configFiles: ConfigFile[];
@ -108,8 +109,6 @@ export const ConfigFileManager = ({
data={selectData} data={selectData}
leftSection={<File size={16} />} leftSection={<File size={16} />}
searchable searchable
clearable={false}
allowDeselect={false}
/> />
</div> </div>
<Button <Button

View file

@ -1,10 +1,11 @@
import { Text, Group, Select, Checkbox, TextInput } from '@mantine/core'; import { Text, Group, Checkbox, TextInput } from '@mantine/core';
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
import { BackendSelectItem } from '@/components/screens/Launch/GeneralTab/BackendSelectItem'; import { BackendSelectItem } from '@/components/screens/Launch/GeneralTab/BackendSelectItem';
import { GpuDeviceSelector } from '@/components/screens/Launch/GeneralTab/GpuDeviceSelector'; import { GpuDeviceSelector } from '@/components/screens/Launch/GeneralTab/GpuDeviceSelector';
import { useLaunchConfig } from '@/hooks/useLaunchConfig'; import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import type { BackendOption } from '@/types'; import type { BackendOption } from '@/types';
import { Select } from '@/components/Select';
export const BackendSelector = () => { export const BackendSelector = () => {
const { const {
@ -89,11 +90,11 @@ export const BackendSelector = () => {
disabled: b.disabled, disabled: b.disabled,
}))} }))}
disabled={isLoadingBackends || availableBackends.length === 0} disabled={isLoadingBackends || availableBackends.length === 0}
allowDeselect={false}
renderOption={({ option }) => { renderOption={({ option }) => {
const backendData = availableBackends.find( const backendData = availableBackends.find(
(b) => b.value === option.value (b) => b.value === option.value
); );
return ( return (
<BackendSelectItem <BackendSelectItem
label={backendData?.label || option.label.split(' (')[0]} label={backendData?.label || option.label.split(' (')[0]}

View file

@ -1,6 +1,7 @@
import { Text, Group, Select, TextInput } from '@mantine/core'; import { Text, Group, TextInput } from '@mantine/core';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
import { useLaunchConfig } from '@/hooks/useLaunchConfig'; import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import { Select } from '@/components/Select';
import type { BackendOption } from '@/types'; import type { BackendOption } from '@/types';
interface GpuDeviceSelectorProps { interface GpuDeviceSelectorProps {
@ -68,7 +69,6 @@ export const GpuDeviceSelector = ({
} }
}} }}
data={deviceOptions} data={deviceOptions}
allowDeselect={false}
/> />
</div> </div>

View file

@ -6,7 +6,6 @@ import {
TextInput, TextInput,
Button, Button,
rem, rem,
Select,
Box, Box,
Anchor, Anchor,
} from '@mantine/core'; } from '@mantine/core';
@ -14,6 +13,7 @@ import { Folder, FolderOpen, Monitor, ExternalLink } from 'lucide-react';
import type { FrontendPreference } from '@/types'; import type { FrontendPreference } from '@/types';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { FRONTENDS } from '@/constants'; import { FRONTENDS } from '@/constants';
import { Select } from '@/components/Select';
interface FrontendRequirement { interface FrontendRequirement {
id: string; id: string;
@ -267,7 +267,6 @@ export const GeneralTab = ({
disabled: !isFrontendAvailable(config.value), disabled: !isFrontendAvailable(config.value),
}))} }))}
leftSection={<Monitor style={{ width: rem(16), height: rem(16) }} />} leftSection={<Monitor style={{ width: rem(16), height: rem(16) }} />}
allowDeselect={false}
/> />
<Box mt="sm"> <Box mt="sm">

View file

@ -188,6 +188,7 @@ export const VersionsTab = () => {
item: download, item: download,
isUpdate: true, isUpdate: true,
wasCurrentBinary: version.isCurrent, wasCurrentBinary: version.isCurrent,
oldVersionPath: version.installedPath,
}); });
await loadInstalledVersions(); await loadInstalledVersions();

View file

@ -2,7 +2,6 @@ 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';
import type { MantineColorScheme } from '@mantine/core';
import { import {
stopKoboldCpp, stopKoboldCpp,
launchKoboldCppWithCustomFrontends, launchKoboldCppWithCustomFrontends,
@ -76,13 +75,12 @@ import {
canAutoUpdate, canAutoUpdate,
} from '@/main/modules/autoUpdater'; } from '@/main/modules/autoUpdater';
import { getAppVersion } from '@/utils/node/fs'; import { getAppVersion } from '@/utils/node/fs';
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, options) =>
downloadRelease(asset) downloadRelease(asset, options)
); );
ipcMain.handle('kobold:getInstalledVersions', () => getInstalledVersions()); ipcMain.handle('kobold:getInstalledVersions', () => getInstalledVersions());
@ -182,7 +180,7 @@ export function setupIPCHandlers() {
}; };
}); });
ipcMain.handle('app:openPath', (_, path: string) => openPathHandler(path)); ipcMain.handle('app:openPath', (_, path) => openPathHandler(path));
ipcMain.handle('app:showLogsFolder', () => ipcMain.handle('app:showLogsFolder', () =>
openPathHandler(join(app.getPath('userData'), 'logs')) openPathHandler(join(app.getPath('userData'), 'logs'))
@ -208,18 +206,18 @@ export function setupIPCHandlers() {
mainWindow.webContents.getZoomLevel() mainWindow.webContents.getZoomLevel()
); );
ipcMain.handle('app:setZoomLevel', (_, level: number) => { ipcMain.handle('app:setZoomLevel', (_, level) => {
mainWindow.webContents.setZoomLevel(level); mainWindow.webContents.setZoomLevel(level);
setConfig('zoomLevel', level); setConfig('zoomLevel', level);
}); });
ipcMain.handle('app:getColorScheme', () => getColorScheme()); ipcMain.handle('app:getColorScheme', () => getColorScheme());
ipcMain.handle('app:setColorScheme', (_, colorScheme: MantineColorScheme) => ipcMain.handle('app:setColorScheme', (_, colorScheme) =>
setColorScheme(colorScheme) setColorScheme(colorScheme)
); );
ipcMain.handle('app:openExternal', async (_, url: string) => openUrl(url)); ipcMain.handle('app:openExternal', async (_, url) => openUrl(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');
@ -230,9 +228,7 @@ export function setupIPCHandlers() {
ipcMain.handle('app:openPerformanceManager', () => openPerformanceManager()); ipcMain.handle('app:openPerformanceManager', () => openPerformanceManager());
ipcMain.on('logs:logError', (_, message: string, error?: Error) => ipcMain.on('logs:logError', (_, message, error?) => logError(message, error));
logError(message, error)
);
ipcMain.handle('dependencies:isNpxAvailable', () => isNpxAvailable()); ipcMain.handle('dependencies:isNpxAvailable', () => isNpxAvailable());
@ -257,30 +253,21 @@ export function setupIPCHandlers() {
return aurPackageVersion !== null; return aurPackageVersion !== null;
}); });
ipcMain.handle( ipcMain.handle('notepad:saveTabContent', (_, title, content) =>
'notepad:saveTabContent', saveTabContent(title, content)
(_, title: string, content: string) => saveTabContent(title, content)
); );
ipcMain.handle('notepad:loadTabContent', (_, title: string) => ipcMain.handle('notepad:loadTabContent', (_, title) => loadTabContent(title));
loadTabContent(title)
);
ipcMain.handle('notepad:renameTab', (_, oldTitle: string, newTitle: string) => ipcMain.handle('notepad:renameTab', (_, oldTitle, newTitle) =>
renameTab(oldTitle, newTitle) renameTab(oldTitle, newTitle)
); );
ipcMain.handle('notepad:saveState', (_, state: NotepadState) => ipcMain.handle('notepad:saveState', (_, state) => saveNotepadState(state));
saveNotepadState(state)
);
ipcMain.handle('notepad:loadState', () => loadNotepadState()); ipcMain.handle('notepad:loadState', () => loadNotepadState());
ipcMain.handle('notepad:deleteTab', (_, title: string) => ipcMain.handle('notepad:deleteTab', (_, title) => deleteTabFile(title));
deleteTabFile(title)
);
ipcMain.handle('notepad:createNewTab', (_, title?: string) => ipcMain.handle('notepad:createNewTab', (_, title) => createNewTab(title));
createNewTab(title)
);
} }

View file

@ -10,11 +10,11 @@ import type { FrontendPreference } from '@/types';
import type { MantineColorScheme } from '@mantine/core'; import type { MantineColorScheme } from '@mantine/core';
import type { SavedNotepadState } from '@/types/electron'; import type { SavedNotepadState } from '@/types/electron';
interface WindowBounds { export interface WindowBounds {
x: number; x?: number;
y: number; y?: number;
width: number; width?: number;
height: number; height?: number;
isMaximized: boolean; isMaximized: boolean;
} }

View file

@ -14,7 +14,7 @@ import { getMainWindow, sendToRenderer } from '../window';
import { pathExists } from '@/utils/node/fs'; 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 { GitHubAsset } from '@/types/electron'; import type { DownloadReleaseOptions, GitHubAsset } from '@/types/electron';
async function removeDirectoryWithRetry( async function removeDirectoryWithRetry(
dirPath: string, dirPath: string,
@ -132,6 +132,7 @@ async function setupLauncher(
async function unpackKoboldCpp(packedPath: string, unpackDir: string) { async function unpackKoboldCpp(packedPath: string, unpackDir: string) {
try { try {
await mkdir(unpackDir, { recursive: true });
await execa(packedPath, ['--unpack', unpackDir], { await execa(packedPath, ['--unpack', unpackDir], {
timeout: 60000, timeout: 60000,
stdio: ['ignore', 'pipe', 'pipe'], stdio: ['ignore', 'pipe', 'pipe'],
@ -148,7 +149,10 @@ async function unpackKoboldCpp(packedPath: string, unpackDir: string) {
} }
} }
export async function downloadRelease(asset: GitHubAsset) { export async function downloadRelease(
asset: GitHubAsset,
options: DownloadReleaseOptions
) {
const tempPackedFilePath = join(getInstallDir(), `${asset.name}.packed`); const tempPackedFilePath = join(getInstallDir(), `${asset.name}.packed`);
const baseFilename = stripAssetExtensions(asset.name); const baseFilename = stripAssetExtensions(asset.name);
const folderName = asset.version const folderName = asset.version
@ -156,11 +160,6 @@ 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 {
if (await pathExists(unpackedDirPath)) { if (await pathExists(unpackedDirPath)) {
await removeDirectoryWithRetry(unpackedDirPath); await removeDirectoryWithRetry(unpackedDirPath);
@ -168,26 +167,28 @@ export async function downloadRelease(asset: GitHubAsset) {
await downloadFile(asset, tempPackedFilePath); await downloadFile(asset, tempPackedFilePath);
await mkdir(unpackedDirPath, { recursive: true });
await unpackKoboldCpp(tempPackedFilePath, unpackedDirPath); await unpackKoboldCpp(tempPackedFilePath, unpackedDirPath);
const launcherPath = await setupLauncher( const launcherPath = await setupLauncher(
tempPackedFilePath, tempPackedFilePath,
unpackedDirPath unpackedDirPath
); );
const currentBinary = getCurrentKoboldBinary(); if (options.oldVersionPath && options.isUpdate) {
if (!currentBinary || (asset.isUpdate && asset.wasCurrentBinary)) { const oldInstallDir = join(options.oldVersionPath, '..');
await setCurrentKoboldBinary(launcherPath);
}
if (currentBinaryPath && asset.isUpdate && asset.wasCurrentBinary) {
const oldInstallDir = join(currentBinaryPath, '..');
if (oldInstallDir !== unpackedDirPath) { if (oldInstallDir !== unpackedDirPath) {
await removeDirectoryWithRetry(oldInstallDir); await removeDirectoryWithRetry(oldInstallDir);
} }
} }
if (
!getCurrentKoboldBinary() ||
(options.isUpdate && options.wasCurrentBinary)
) {
await setCurrentKoboldBinary(launcherPath);
}
sendToRenderer('versions-updated'); sendToRenderer('versions-updated');
} catch (error) { } catch (error) {
logError('Failed to download or unpack binary:', error as Error); logError('Failed to download or unpack binary:', error as Error);

View file

@ -2,10 +2,15 @@ import { BrowserWindow, app, shell, screen, Menu, clipboard } from 'electron';
import type { BrowserWindowConstructorOptions } from 'electron'; import type { BrowserWindowConstructorOptions } from 'electron';
import { join } from 'path'; import { join } from 'path';
import { stripVTControlCharacters } from 'util'; import { stripVTControlCharacters } from 'util';
import { PRODUCT_NAME } from '../../constants'; import { PRODUCT_NAME } from '@/constants';
import type { IPCChannel, IPCChannelPayloads } from '@/types/ipc'; import type { IPCChannel, IPCChannelPayloads } from '@/types/ipc';
import { isDevelopment } from '@/utils/node/environment'; import { isDevelopment } from '@/utils/node/environment';
import { getBackgroundColor, getWindowBounds, setWindowBounds } from './config'; import {
getBackgroundColor,
getWindowBounds,
setWindowBounds,
WindowBounds,
} from './config';
let mainWindow: BrowserWindow | null = null; let mainWindow: BrowserWindow | null = null;
@ -42,18 +47,7 @@ export function createMainWindow() {
}, },
} as BrowserWindowConstructorOptions; } as BrowserWindowConstructorOptions;
if (savedBounds) { if (savedBounds?.x !== undefined && savedBounds?.y !== undefined) {
const isPseudoMaximized =
(savedBounds.width >= size.width * 0.95 ||
savedBounds.height >= size.height * 0.95) &&
!savedBounds.isMaximized;
if (isPseudoMaximized) {
windowOptions.width = defaultWidth;
windowOptions.height = defaultHeight;
windowOptions.x = Math.floor((size.width - defaultWidth) / 2);
windowOptions.y = Math.floor((size.height - defaultHeight) / 2);
} else {
const minVisibleSize = 100; const minVisibleSize = 100;
if ( if (
savedBounds.x >= -minVisibleSize && savedBounds.x >= -minVisibleSize &&
@ -71,7 +65,6 @@ export function createMainWindow() {
(size.height - (savedBounds.height || defaultHeight)) / 2 (size.height - (savedBounds.height || defaultHeight)) / 2
); );
} }
}
} else { } else {
windowOptions.x = Math.floor((size.width - defaultWidth) / 2); windowOptions.x = Math.floor((size.width - defaultWidth) / 2);
windowOptions.y = Math.floor((size.height - defaultHeight) / 2); windowOptions.y = Math.floor((size.height - defaultHeight) / 2);
@ -83,103 +76,38 @@ export function createMainWindow() {
mainWindow.maximize(); mainWindow.maximize();
} }
let restoreBounds: {
x: number;
y: number;
width: number;
height: number;
} | null = savedBounds
? {
x: savedBounds.x || 0,
y: savedBounds.y || 0,
width: savedBounds.width,
height: savedBounds.height,
}
: null;
const saveBounds = () => { const saveBounds = () => {
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
const isMaximized = mainWindow.isMaximized(); const isMaximized = mainWindow.isMaximized();
const currentBounds = mainWindow.getBounds(); const currentBounds = mainWindow.getBounds();
let bounds: WindowBounds = { isMaximized };
if (isMaximized) { if (!isMaximized) {
if (restoreBounds) { bounds = {
setWindowBounds({
x: restoreBounds.x,
y: restoreBounds.y,
width: restoreBounds.width,
height: restoreBounds.height,
isMaximized: true,
});
}
} else {
restoreBounds = currentBounds;
setWindowBounds({
x: currentBounds.x, x: currentBounds.x,
y: currentBounds.y, y: currentBounds.y,
width: currentBounds.width, width: currentBounds.width,
height: currentBounds.height, height: currentBounds.height,
isMaximized: false, isMaximized,
}); };
} }
setWindowBounds(bounds);
} }
}; };
let lastSavedBounds: string | null = null;
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
const debouncedSave = () => {
if (saveTimeout) {
clearTimeout(saveTimeout);
}
saveTimeout = setTimeout(() => {
saveBounds();
saveTimeout = null;
}, 1000);
};
const checkBounds = () => {
if (mainWindow && !mainWindow.isDestroyed()) {
const currentBounds = mainWindow.getBounds();
const isMaximized = mainWindow.isMaximized();
const boundsString = JSON.stringify({ ...currentBounds, isMaximized });
if (boundsString !== lastSavedBounds) {
lastSavedBounds = boundsString;
debouncedSave();
}
}
};
const boundsInterval = setInterval(checkBounds, 500);
mainWindow.on('moved', () => {
debouncedSave();
});
mainWindow.on('resized', () => {
debouncedSave();
});
mainWindow.on('maximize', () => { mainWindow.on('maximize', () => {
saveBounds();
sendToRenderer('window-maximized'); sendToRenderer('window-maximized');
}); });
mainWindow.on('unmaximize', () => { mainWindow.on('unmaximize', () => {
saveBounds();
sendToRenderer('window-unmaximized'); sendToRenderer('window-unmaximized');
}); });
mainWindow.on('closed', () => { mainWindow.on('closed', () => {
if (saveTimeout) {
clearTimeout(saveTimeout);
}
clearInterval(boundsInterval);
mainWindow = null; mainWindow = null;
}); });
mainWindow.once('ready-to-show', () => { mainWindow.once('ready-to-show', () => mainWindow?.show());
mainWindow?.show();
});
if (isDevelopment) { if (isDevelopment) {
mainWindow.loadURL('http://localhost:5173'); mainWindow.loadURL('http://localhost:5173');
@ -218,6 +146,7 @@ export function createMainWindow() {
})); }));
mainWindow.on('close', () => { mainWindow.on('close', () => {
saveBounds();
app.quit(); app.quit();
}); });

View file

@ -33,8 +33,8 @@ const koboldAPI: KoboldAPI = {
getCurrentInstallDir: () => ipcRenderer.invoke('kobold:getCurrentInstallDir'), getCurrentInstallDir: () => ipcRenderer.invoke('kobold:getCurrentInstallDir'),
selectInstallDirectory: () => selectInstallDirectory: () =>
ipcRenderer.invoke('kobold:selectInstallDirectory'), ipcRenderer.invoke('kobold:selectInstallDirectory'),
downloadRelease: (asset) => downloadRelease: (asset, options) =>
ipcRenderer.invoke('kobold:downloadRelease', asset), ipcRenderer.invoke('kobold:downloadRelease', asset, options),
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) =>

View file

@ -16,6 +16,7 @@ interface HandleDownloadParams {
item: DownloadItem; item: DownloadItem;
isUpdate?: boolean; isUpdate?: boolean;
wasCurrentBinary?: boolean; wasCurrentBinary?: boolean;
oldVersionPath?: string;
} }
const transformReleaseToDownloadItems = ( const transformReleaseToDownloadItems = (
@ -96,7 +97,12 @@ export const useKoboldVersionsStore = create<KoboldVersionsState>(
}, },
handleDownload: async (params: HandleDownloadParams) => { handleDownload: async (params: HandleDownloadParams) => {
const { item, isUpdate = false, wasCurrentBinary = false } = params; const {
item,
isUpdate = false,
wasCurrentBinary = false,
oldVersionPath,
} = params;
const { downloading } = get(); const { downloading } = get();
if (downloading) { if (downloading) {
@ -116,11 +122,13 @@ export const useKoboldVersionsStore = create<KoboldVersionsState>(
browser_download_url: item.url, browser_download_url: item.url,
size: item.size, size: item.size,
version: item.version, version: item.version,
isUpdate,
wasCurrentBinary,
}; };
await window.electronAPI.kobold.downloadRelease(asset); await window.electronAPI.kobold.downloadRelease(asset, {
isUpdate,
wasCurrentBinary,
oldVersionPath,
});
progressCleanup(); progressCleanup();
set({ downloading: null, downloadProgress: {} }); set({ downloading: null, downloadProgress: {} });

View file

@ -16,6 +16,10 @@
border-right: 1px solid rgba(255, 255, 255, 0.15); border-right: 1px solid rgba(255, 255, 255, 0.15);
} }
[data-mantine-color-scheme='light'] .mantine-Select-option:hover {
background-color: #e9ecef !important;
}
/* Custom scrollbars */ /* Custom scrollbars */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 0.5rem; width: 0.5rem;

View file

@ -17,8 +17,12 @@ export interface GitHubAsset {
browser_download_url: string; browser_download_url: string;
size: number; size: number;
version?: string; version?: string;
}
export interface DownloadReleaseOptions {
isUpdate?: boolean; isUpdate?: boolean;
wasCurrentBinary?: boolean; wasCurrentBinary?: boolean;
oldVersionPath?: string;
} }
export interface GitHubRelease { export interface GitHubRelease {
@ -113,7 +117,10 @@ 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: (asset: GitHubAsset) => Promise<void>; downloadRelease: (
asset: GitHubAsset,
options: DownloadReleaseOptions
) => Promise<void>;
launchKoboldCpp: ( launchKoboldCpp: (
args?: string[] args?: string[]
) => Promise<{ success: boolean; pid?: number; error?: string }>; ) => Promise<{ success: boolean; pid?: number; error?: string }>;

View file

@ -989,7 +989,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@mantine/core@npm:^8.3.1": "@mantine/core@npm:8.3.1":
version: 8.3.1 version: 8.3.1
resolution: "@mantine/core@npm:8.3.1" resolution: "@mantine/core@npm:8.3.1"
dependencies: dependencies:
@ -1007,7 +1007,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@mantine/hooks@npm:^8.3.1": "@mantine/hooks@npm:8.3.1":
version: 8.3.1 version: 8.3.1
resolution: "@mantine/hooks@npm:8.3.1" resolution: "@mantine/hooks@npm:8.3.1"
peerDependencies: peerDependencies:
@ -3863,8 +3863,8 @@ __metadata:
"@codemirror/view": "npm:^6.38.3" "@codemirror/view": "npm:^6.38.3"
"@eslint/js": "npm:^9.36.0" "@eslint/js": "npm:^9.36.0"
"@fontsource/inter": "npm:^5.2.8" "@fontsource/inter": "npm:^5.2.8"
"@mantine/core": "npm:^8.3.1" "@mantine/core": "npm:8.3.1"
"@mantine/hooks": "npm:^8.3.1" "@mantine/hooks": "npm:8.3.1"
"@types/node": "npm:^24.5.2" "@types/node": "npm:^24.5.2"
"@types/react": "npm:^19.1.13" "@types/react": "npm:^19.1.13"
"@types/react-dom": "npm:^19.1.9" "@types/react-dom": "npm:^19.1.9"