code cleanup continues, adding MMAP support, extra polish

This commit is contained in:
Egor 2025-08-21 13:25:16 -07:00
parent d5323d8b3e
commit ded8acb436
22 changed files with 170 additions and 115 deletions

View file

@ -74,9 +74,11 @@ trycloudflare
unquantized
useclblast
usecuda
usemmap
usevulkan
vram
vulkan
vulkaninfo
wayland
websearch
MMAP

View file

@ -1,7 +1,7 @@
{
"name": "friendly-kobold",
"productName": "Friendly Kobold",
"version": "0.4.0",
"version": "0.4.1",
"description": "A modern Electron shell for KoboldCpp",
"main": "out/main/index.js",
"homepage": "./",

View file

@ -6,7 +6,7 @@ import { InterfaceScreen } from '@/components/screens/Interface';
import { WelcomeScreen } from '@/components/screens/Welcome';
import { UpdateAvailableModal } from '@/components/UpdateAvailableModal';
import { SettingsModal } from '@/components/settings/SettingsModal';
import { EjectConfirmDialog } from '@/components/EjectConfirmDialog';
import { EjectConfirmModal } from '@/components/EjectConfirmModal';
import { ScreenTransition } from '@/components/ScreenTransition';
import { AppHeader } from '@/components/AppHeader';
import { useUpdateChecker } from '@/hooks/useUpdateChecker';
@ -20,7 +20,7 @@ export const App = () => {
const [currentScreen, setCurrentScreen] = useState<Screen | null>(null);
const [settingsOpened, setSettingsOpened] = useState(false);
const [hasInitialized, setHasInitialized] = useState(false);
const [showEjectDialog, setShowEjectDialog] = useState(false);
const [showEjectModal, setShowEjectModal] = useState(false);
const [activeInterfaceTab, setActiveInterfaceTab] = useState<string | null>(
'terminal'
);
@ -179,7 +179,7 @@ export const App = () => {
if (skipEjectConfirmation) {
performEject();
} else {
setShowEjectDialog(true);
setShowEjectModal(true);
}
};
@ -309,9 +309,9 @@ export const App = () => {
onClose={() => setSettingsOpened(false)}
currentScreen={currentScreen || undefined}
/>
<EjectConfirmDialog
opened={showEjectDialog}
onClose={() => setShowEjectDialog(false)}
<EjectConfirmModal
opened={showEjectModal}
onClose={() => setShowEjectModal(false)}
onConfirm={handleEjectConfirm}
/>
</AppShell>

View file

@ -14,7 +14,6 @@ import { Settings, ArrowLeft } from 'lucide-react';
import { StyledTooltip } from '@/components/StyledTooltip';
import { soundAssets, playSound, initializeAudio } from '@/utils';
import iconUrl from '/icon.png';
import '@/styles/AppHeader.css';
type Screen = 'welcome' | 'download' | 'launch' | 'interface';

View file

@ -1,17 +1,17 @@
import { useState } from 'react';
import { Modal, Text, Group, Button, Checkbox, Stack } from '@mantine/core';
interface EjectConfirmDialogProps {
interface EjectConfirmModalProps {
opened: boolean;
onClose: () => void;
onConfirm: (skipConfirmation: boolean) => void;
}
export const EjectConfirmDialog = ({
export const EjectConfirmModal = ({
opened,
onClose,
onConfirm,
}: EjectConfirmDialogProps) => {
}: EjectConfirmModalProps) => {
const [skipConfirmation, setSkipConfirmation] = useState(false);
const handleConfirm = () => {

View file

@ -15,7 +15,7 @@ export const ScreenTransition = ({
<Transition
mounted={isActive}
transition="fade"
duration={shouldAnimate ? 150 : 0}
duration={shouldAnimate ? 100 : 0}
timingFunction="ease-out"
>
{(styles) => (

View file

@ -13,6 +13,7 @@ export const AdvancedTab = () => {
failsafe,
lowvram,
quantmatmul,
usemmap,
backend,
handleAdditionalArgumentsChange,
handleNoshiftChange,
@ -21,6 +22,7 @@ export const AdvancedTab = () => {
handleFailsafeChange,
handleLowvramChange,
handleQuantmatmulChange,
handleUsemmapChange,
} = useLaunchConfig();
const [backendSupport, setBackendSupport] = useState<{
noavx2: boolean;
@ -148,6 +150,19 @@ export const AdvancedTab = () => {
/>
</Group>
</div>
<div className={styles.minWidth200}>
<Group gap="xs" align="center">
<Checkbox
checked={usemmap}
onChange={(event) =>
handleUsemmapChange(event.currentTarget.checked)
}
label="MMAP"
/>
<InfoTooltip label="Use MMAP to load models when enabled." />
</Group>
</div>
</Group>
</Stack>
</div>

View file

@ -49,6 +49,7 @@ export const LaunchScreen = ({
failsafe,
lowvram,
quantmatmul,
usemmap,
backend,
gpuDevice,
gpuPlatform,
@ -148,6 +149,7 @@ export const LaunchScreen = ({
flashattention,
noavx2,
failsafe,
usemmap,
usecuda: backend === 'cuda' || backend === 'rocm',
usevulkan: backend === 'vulkan',
useclblast: backend === 'clblast',
@ -248,10 +250,13 @@ export const LaunchScreen = ({
websearch,
noshift,
flashattention,
noavx2,
failsafe,
backend,
lowvram,
gpuDevice,
quantmatmul,
usemmap,
additionalArguments,
sdt5xxl,
sdclipl,

View file

@ -2,5 +2,3 @@ export const DEFAULT_CONTEXT_SIZE = 4096;
export const DEFAULT_MODEL_URL =
'https://huggingface.co/MaziyarPanahi/gemma-3-4b-it-GGUF/resolve/main/gemma-3-4b-it.Q8_0.gguf?download=true';
export const DEFAULT_HOST = '';

View file

@ -26,6 +26,7 @@ export const useLaunchConfig = () => {
failsafe: state.failsafe,
lowvram: state.lowvram,
quantmatmul: state.quantmatmul,
usemmap: state.usemmap,
backend: state.backend,
gpuDevice: state.gpuDevice,
gpuPlatform: state.gpuPlatform,
@ -55,6 +56,7 @@ export const useLaunchConfig = () => {
handleFailsafeChange: state.setFailsafe,
handleLowvramChange: state.setLowvram,
handleQuantmatmulChange: state.setQuantmatmul,
handleUsemmapChange: state.setUsemmap,
handleBackendChange: state.setBackend,
handleGpuDeviceChange: state.setGpuDevice,
handleGpuPlatformChange: state.setGpuPlatform,

View file

@ -21,10 +21,13 @@ interface LaunchArgs {
websearch: boolean;
noshift: boolean;
flashattention: boolean;
noavx2: boolean;
failsafe: boolean;
backend: string;
lowvram: boolean;
gpuDevice: number | string;
quantmatmul: boolean;
usemmap: boolean;
additionalArguments: string;
sdt5xxl: string;
sdclipl: string;
@ -98,6 +101,9 @@ const buildConfigArgs = (launchArgs: LaunchArgs): string[] => {
[launchArgs.websearch, '--websearch'],
[launchArgs.noshift, '--noshift'],
[launchArgs.flashattention, '--flashattention'],
[launchArgs.noavx2, '--noavx2'],
[launchArgs.failsafe, '--failsafe'],
[launchArgs.usemmap, '--usemmap'],
];
flagMappings.forEach(([condition, flag, value]) => {

View file

@ -384,6 +384,7 @@ export class KoboldCppManager {
flashattention?: boolean;
noavx2?: boolean;
failsafe?: boolean;
usemmap?: boolean;
usecuda?: boolean;
usevulkan?: boolean;
useclblast?: [number, number] | boolean;

View file

@ -1,11 +1,4 @@
import {
BrowserWindow,
app,
Menu,
shell,
ipcMain,
nativeImage,
} from 'electron';
import { BrowserWindow, app, Menu, shell, nativeImage } from 'electron';
import * as os from 'os';
import { join } from 'path';
import { GITHUB_API } from '../../constants';
@ -311,8 +304,9 @@ OS: ${osInfo}`;
maximizable: false,
show: false,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
nodeIntegration: false,
contextIsolation: true,
preload: join(__dirname, '../preload/index.js'),
},
});
@ -328,21 +322,6 @@ OS: ${osInfo}`;
aboutWindow.once('ready-to-show', () => {
aboutWindow.show();
});
ipcMain.once('open-github', () => {
shell.openExternal(
`https://github.com/${GITHUB_API.FRIENDLY_KOBOLD_REPO}`
);
});
ipcMain.once('close-about-dialog', () => {
aboutWindow.close();
});
aboutWindow.on('closed', () => {
ipcMain.removeAllListeners('open-github');
ipcMain.removeAllListeners('close-about-dialog');
});
}
private getTemplatePath(filename: string): string {

View file

@ -131,8 +131,6 @@
</div>
<script>
const { ipcRenderer } = require('electron');
function setVersionInfo(info) {
document.getElementById('version-info').innerHTML = info.replace(
/\n/g,
@ -141,11 +139,16 @@
}
function openGitHub() {
ipcRenderer.send('open-github');
// Send IPC message directly since this is a simple modal
if (window.electronAPI && window.electronAPI.app) {
window.electronAPI.app.openExternal(
'https://github.com/lone-cloud/friendly-kobold'
);
}
}
function closeDialog() {
ipcRenderer.send('close-about-dialog');
window.close();
}
// Handle keyboard shortcuts

View file

@ -2,14 +2,10 @@ import { contextBridge, ipcRenderer, type IpcRendererEvent } from 'electron';
import type { KoboldAPI, AppAPI, ConfigAPI, LogsAPI } from '@/types/electron';
const koboldAPI: KoboldAPI = {
getInstalledVersion: () => ipcRenderer.invoke('kobold:getInstalledVersion'),
getInstalledVersions: () => ipcRenderer.invoke('kobold:getInstalledVersions'),
getCurrentVersion: () => ipcRenderer.invoke('kobold:getCurrentVersion'),
getCurrentBinaryInfo: () => ipcRenderer.invoke('kobold:getCurrentBinaryInfo'),
setCurrentVersion: (version: string) =>
ipcRenderer.invoke('kobold:setCurrentVersion', version),
getVersionFromBinary: (binaryPath: string) =>
ipcRenderer.invoke('kobold:getVersionFromBinary', binaryPath),
getLatestReleaseWithStatus: () =>
ipcRenderer.invoke('kobold:getLatestReleaseWithStatus'),
getROCmDownload: () => ipcRenderer.invoke('kobold:getROCmDownload'),
@ -18,11 +14,7 @@ const koboldAPI: KoboldAPI = {
getPlatform: () => ipcRenderer.invoke('kobold:getPlatform'),
detectGPU: () => ipcRenderer.invoke('kobold:detectGPU'),
detectCPU: () => ipcRenderer.invoke('kobold:detectCPU'),
detectGPUCapabilities: () =>
ipcRenderer.invoke('kobold:detectGPUCapabilities'),
detectROCm: () => ipcRenderer.invoke('kobold:detectROCm'),
detectAllCapabilities: () =>
ipcRenderer.invoke('kobold:detectAllCapabilities'),
detectBackendSupport: (binaryPath: string) =>
ipcRenderer.invoke('kobold:detectBackendSupport', binaryPath),
getAvailableBackends: () => ipcRenderer.invoke('kobold:getAvailableBackends'),
@ -33,7 +25,6 @@ const koboldAPI: KoboldAPI = {
ipcRenderer.invoke('kobold:downloadRelease', asset),
launchKoboldCpp: (args?: string[], configFilePath?: string) =>
ipcRenderer.invoke('kobold:launchKoboldCpp', args, configFilePath),
checkForUpdates: () => ipcRenderer.invoke('kobold:checkForUpdates'),
getConfigFiles: () => ipcRenderer.invoke('kobold:getConfigFiles'),
saveConfigFile: (
configName: string,
@ -52,6 +43,7 @@ const koboldAPI: KoboldAPI = {
flashattention?: boolean;
noavx2?: boolean;
failsafe?: boolean;
usemmap?: boolean;
usecuda?: boolean;
usevulkan?: boolean;
useclblast?: boolean;
@ -107,7 +99,6 @@ const koboldAPI: KoboldAPI = {
};
const appAPI: AppAPI = {
getVersion: () => ipcRenderer.invoke('app:getVersion'),
openExternal: (url) => ipcRenderer.invoke('app:openExternal', url),
};

View file

@ -1,7 +1,7 @@
import { create } from 'zustand';
import type { ConfigFile } from '@/types';
import type { ImageModelPreset } from '@/utils/imageModelPresets';
import { DEFAULT_CONTEXT_SIZE, DEFAULT_HOST } from '@/constants';
import { DEFAULT_CONTEXT_SIZE } from '@/constants';
interface LaunchConfigState {
gpuLayers: number;
@ -22,6 +22,7 @@ interface LaunchConfigState {
failsafe: boolean;
lowvram: boolean;
quantmatmul: boolean;
usemmap: boolean;
backend: string;
gpuDevice: number;
gpuPlatform: number;
@ -51,6 +52,7 @@ interface LaunchConfigState {
setFailsafe: (failsafe: boolean) => void;
setLowvram: (lowvram: boolean) => void;
setQuantmatmul: (quantmatmul: boolean) => void;
setUsemmap: (usemmap: boolean) => void;
setBackend: (backend: string) => void;
setGpuDevice: (device: number) => void;
setGpuPlatform: (platform: number) => void;
@ -86,7 +88,7 @@ export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
modelPath: '',
additionalArguments: '',
port: undefined,
host: DEFAULT_HOST,
host: '',
multiuser: false,
multiplayer: false,
remotetunnel: false,
@ -98,6 +100,7 @@ export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
failsafe: false,
lowvram: false,
quantmatmul: true,
usemmap: true,
backend: '',
gpuDevice: 0,
gpuPlatform: 0,
@ -127,6 +130,7 @@ export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
setFailsafe: (failsafe) => set({ failsafe }),
setLowvram: (lowvram) => set({ lowvram }),
setQuantmatmul: (quantmatmul) => set({ quantmatmul }),
setUsemmap: (usemmap) => set({ usemmap }),
setBackend: (backend) => set({ backend }),
setGpuDevice: (device) => set({ gpuDevice: device }),
setGpuPlatform: (platform) => set({ gpuPlatform: platform }),
@ -171,7 +175,7 @@ export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
if (typeof configData.host === 'string') {
updates.host = configData.host;
} else {
updates.host = DEFAULT_HOST;
updates.host = '';
}
if (typeof configData.multiuser === 'number') {
@ -236,6 +240,12 @@ export const useLaunchConfigStore = create<LaunchConfigState>((set, get) => ({
updates.quantmatmul = configData.quantmatmul;
}
if (typeof configData.usemmap === 'boolean') {
updates.usemmap = configData.usemmap;
} else {
updates.usemmap = true;
}
if (configData.usecuda === true) {
const gpuInfo = await window.electronAPI.kobold.detectGPU();
updates.backend = gpuInfo.hasNVIDIA ? 'cuda' : 'rocm';

View file

@ -1,45 +0,0 @@
@keyframes elephantShake {
0%,
100% {
transform: scale(1.3) rotate(5deg) translateX(0px);
}
10% {
transform: scale(1.4) rotate(-3deg) translateX(-2px);
}
20% {
transform: scale(1.5) rotate(8deg) translateX(2px);
}
30% {
transform: scale(1.4) rotate(-5deg) translateX(-1px);
}
40% {
transform: scale(1.6) rotate(10deg) translateX(3px);
}
50% {
transform: scale(1.3) rotate(-8deg) translateX(-2px);
}
60% {
transform: scale(1.5) rotate(6deg) translateX(1px);
}
70% {
transform: scale(1.2) rotate(-4deg) translateX(-1px);
}
80% {
transform: scale(1.4) rotate(3deg) translateX(2px);
}
90% {
transform: scale(1.1) rotate(-2deg) translateX(-1px);
}
}
@keyframes mouseSqueak {
0% {
transform: scale(1) rotate(0deg);
}
50% {
transform: scale(1.15) rotate(2deg);
}
100% {
transform: scale(1) rotate(0deg);
}
}

View file

@ -1 +1,97 @@
@import '@mantine/core/styles.css';
/* Global smooth scrolling */
* {
scroll-behavior: smooth;
}
html {
scroll-behavior: smooth;
}
/* Custom scrollbars */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: rgba(134, 142, 150, 0.5);
border-radius: 4px;
transition: all 0.2s ease;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(134, 142, 150, 0.8);
}
::-webkit-scrollbar-thumb:active {
background: rgba(73, 80, 87, 0.9);
}
/* Dark mode scrollbar support */
@media (prefers-color-scheme: dark) {
::-webkit-scrollbar-thumb {
background: rgba(173, 181, 189, 0.5);
}
::-webkit-scrollbar-thumb:hover {
background: rgba(173, 181, 189, 0.8);
}
::-webkit-scrollbar-thumb:active {
background: rgba(206, 212, 218, 0.9);
}
}
/* AppHeader animations */
@keyframes elephantShake {
0%,
100% {
transform: scale(1.3) rotate(5deg) translateX(0px);
}
10% {
transform: scale(1.4) rotate(-3deg) translateX(-2px);
}
20% {
transform: scale(1.5) rotate(8deg) translateX(2px);
}
30% {
transform: scale(1.4) rotate(-5deg) translateX(-1px);
}
40% {
transform: scale(1.6) rotate(10deg) translateX(3px);
}
50% {
transform: scale(1.3) rotate(-8deg) translateX(-2px);
}
60% {
transform: scale(1.5) rotate(6deg) translateX(1px);
}
70% {
transform: scale(1.2) rotate(-4deg) translateX(-1px);
}
80% {
transform: scale(1.4) rotate(3deg) translateX(2px);
}
90% {
transform: scale(1.1) rotate(-2deg) translateX(-1px);
}
}
@keyframes mouseSqueak {
0% {
transform: scale(1) rotate(0deg);
}
50% {
transform: scale(1.15) rotate(2deg);
}
100% {
transform: scale(1) rotate(0deg);
}
}

View file

@ -57,22 +57,17 @@ export interface DownloadItem {
}
export interface KoboldAPI {
getInstalledVersion: () => Promise<string | undefined>;
getInstalledVersions: () => Promise<InstalledVersion[]>;
getCurrentVersion: () => Promise<InstalledVersion | null>;
getCurrentBinaryInfo: () => Promise<{
path: string;
filename: string;
} | null>;
setCurrentVersion: (version: string) => Promise<boolean>;
getVersionFromBinary: (binaryPath: string) => Promise<string | null>;
getLatestRelease: () => Promise<DownloadItem[]>;
getPlatform: () => Promise<PlatformInfo>;
detectGPU: () => Promise<BasicGPUInfo>;
detectCPU: () => Promise<CPUCapabilities>;
detectGPUCapabilities: () => Promise<GPUCapabilities>;
detectROCm: () => Promise<{ supported: boolean; devices: string[] }>;
detectAllCapabilities: () => Promise<HardwareInfo>;
detectBackendSupport: (binaryPath: string) => Promise<{
rocm: boolean;
vulkan: boolean;
@ -95,7 +90,6 @@ export interface KoboldAPI {
error?: string;
}>;
getROCmDownload: () => Promise<DownloadItem | null>;
checkForUpdates: () => Promise<UpdateInfo | null>;
getLatestReleaseWithStatus: () => Promise<ReleaseWithStatus | null>;
launchKoboldCpp: (
args?: string[],
@ -121,6 +115,7 @@ export interface KoboldAPI {
flashattention?: boolean;
noavx2?: boolean;
failsafe?: boolean;
usemmap?: boolean;
usecuda?: boolean;
usevulkan?: boolean;
useclblast?: boolean;
@ -152,7 +147,6 @@ export interface KoboldAPI {
}
export interface AppAPI {
getVersion: () => Promise<string>;
openExternal: (url: string) => Promise<void>;
}

View file

@ -1,4 +1,3 @@
import { formatFileSizeInMB } from '@/utils/fileSize';
import { KOBOLDAI_URLS } from '@/constants';
export const formatDownloadSize = (size: number, url?: string): string => {
@ -35,3 +34,9 @@ export const compareVersions = (versionA: string, versionB: string): number => {
return 0;
};
const formatFileSizeInMB = (bytes: number) => {
if (bytes === 0) return '0 MB';
const mb = bytes / (1024 * 1024);
return parseFloat(mb.toFixed(1)) + ' MB';
};

View file

@ -1,5 +0,0 @@
export const formatFileSizeInMB = (bytes: number) => {
if (bytes === 0) return '0 MB';
const mb = bytes / (1024 * 1024);
return parseFloat(mb.toFixed(1)) + ' MB';
};

View file

@ -1,7 +1,6 @@
export * from './assets';
export * from './clblast';
export * from './downloadUtils';
export * from './fileSize';
export * from './hardware';
export * from './imageModelPresets';
export * from './platform';