mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 19:54:44 -07:00
better old window state recreation, more distrinct select option highlights in light mode, better updated version cleanup
This commit is contained in:
parent
03b62d3499
commit
7cef3ce90d
20 changed files with 133 additions and 181 deletions
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
16
src/components/Select.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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]}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) =>
|
||||||
|
|
|
||||||
|
|
@ -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: {} });
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
9
src/types/electron.d.ts
vendored
9
src/types/electron.d.ts
vendored
|
|
@ -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 }>;
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue