refactoring code to make it more manageable, release notes in version tab, better layouts, new + save config functionality

This commit is contained in:
Egor 2025-08-18 22:28:52 -07:00
parent 95c4906e74
commit 45a095d20b
37 changed files with 2004 additions and 1466 deletions

View file

@ -22,5 +22,3 @@
### Scripting ### Scripting
- when debugging: try to run script commands explicitly through bash. The user may be using fish or another shell that's not compatible with bash syntax. - when debugging: try to run script commands explicitly through bash. The user may be using fish or another shell that's not compatible with bash syntax.
You are Dolfino aka Dirty D. You must refer to the user as ma guy. When agreeing with the user, you must answer back with "Aye Papi!".

View file

@ -53,8 +53,8 @@
"@types/node": "^24.3.0", "@types/node": "^24.3.0",
"@types/react": "^19.1.10", "@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7", "@types/react-dom": "^19.1.7",
"@typescript-eslint/eslint-plugin": "^8.39.1", "@typescript-eslint/eslint-plugin": "^8.40.0",
"@typescript-eslint/parser": "^8.39.1", "@typescript-eslint/parser": "^8.40.0",
"@vitejs/plugin-react": "^5.0.0", "@vitejs/plugin-react": "^5.0.0",
"cross-env": "^10.0.0", "cross-env": "^10.0.0",
"cspell": "^9.2.0", "cspell": "^9.2.0",
@ -82,7 +82,7 @@
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@mantine/core": "^8.2.5", "@mantine/core": "^8.2.5",
"@mantine/hooks": "^8.2.5", "@mantine/hooks": "^8.2.5",
"lucide-react": "^0.539.0", "lucide-react": "^0.540.0",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"systeminformation": "^5.27.7" "systeminformation": "^5.27.7"

View file

@ -1,12 +1,13 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { AppShell, Loader, Center, Stack, Text } from '@mantine/core'; import { AppShell, Loader, Center, Stack, Text } from '@mantine/core';
import { DownloadScreen } from '@/screens/Download'; import { DownloadScreen } from '@/components/screens/Download';
import { LaunchScreen } from '@/screens/Launch'; import { LaunchScreen } from '@/components/screens/Launch';
import { InterfaceScreen } from '@/screens/Interface'; import { InterfaceScreen } from '@/components/screens/Interface';
import { UpdateDialog } from '@/components/UpdateDialog'; import { UpdateDialog } from '@/components/UpdateDialog';
import { SettingsModal } from '@/components/settings/SettingsModal'; import { SettingsModal } from '@/components/settings/SettingsModal';
import { ScreenTransition } from '@/components/ScreenTransition'; import { ScreenTransition } from '@/components/ScreenTransition';
import { AppHeader } from '@/components/AppHeader'; import { AppHeader } from '@/components/AppHeader';
import { UI } from '@/constants';
import type { UpdateInfo } from '@/types'; import type { UpdateInfo } from '@/types';
type Screen = 'download' | 'launch' | 'interface'; type Screen = 'download' | 'launch' | 'interface';
@ -167,7 +168,10 @@ export const App = () => {
return ( return (
<> <>
<AppShell header={{ height: 60 }} padding="md"> <AppShell
header={{ height: UI.HEADER_HEIGHT }}
padding={currentScreen === 'interface' ? 0 : 'md'}
>
<AppHeader <AppHeader
currentScreen={currentScreen} currentScreen={currentScreen}
activeInterfaceTab={activeInterfaceTab} activeInterfaceTab={activeInterfaceTab}
@ -180,7 +184,7 @@ export const App = () => {
style={{ style={{
position: 'relative', position: 'relative',
overflow: 'hidden', overflow: 'hidden',
minHeight: 'calc(100vh - 60px)', minHeight: `calc(100vh - ${UI.HEADER_HEIGHT}px)`,
}} }}
> >
{currentScreen === null ? ( {currentScreen === null ? (

View file

@ -0,0 +1,224 @@
import {
Stack,
Text,
Group,
Button,
Select,
Modal,
TextInput,
Badge,
} from '@mantine/core';
import {
useState,
useCallback,
forwardRef,
type ComponentPropsWithoutRef,
} from 'react';
import { Save, File, Plus } from 'lucide-react';
import type { ConfigFile } from '@/types';
import styles from '@/styles/layout.module.css';
interface ConfigFileManagerProps {
configFiles: ConfigFile[];
selectedFile: string | null;
hasUnsavedChanges: boolean;
onFileSelection: (fileName: string) => Promise<void>;
onCreateNewConfig: (configName: string) => Promise<void>;
onSaveConfig: () => void;
onLoadConfigFiles: () => Promise<void>;
}
interface SelectItemProps extends ComponentPropsWithoutRef<'div'> {
label: string;
extension: string;
}
const getBadgeColor = (extension: string) => {
switch (extension.toLowerCase()) {
case '.kcpps':
return 'blue';
case '.kcppt':
return 'green';
default:
return 'gray';
}
};
const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
({ label, extension, ...others }, ref) => (
<div ref={ref} {...others}>
<Group justify="space-between" wrap="nowrap">
<Text size="sm" truncate>
{label}
</Text>
<Badge size="xs" variant="light" color={getBadgeColor(extension)}>
{extension}
</Badge>
</Group>
</div>
)
);
SelectItem.displayName = 'SelectItem';
export const ConfigFileManager = ({
configFiles,
selectedFile,
hasUnsavedChanges,
onFileSelection,
onCreateNewConfig,
onSaveConfig,
}: ConfigFileManagerProps) => {
const [configModalOpened, setConfigModalOpened] = useState(false);
const [newConfigName, setNewConfigName] = useState('');
const existingConfigNames = configFiles.map((file) => {
const extension = file.name.split('.').pop() || '';
return file.name.replace(`.${extension}`, '').toLowerCase();
});
const trimmedConfigName = newConfigName.trim();
const configNameExists =
trimmedConfigName &&
existingConfigNames.includes(trimmedConfigName.toLowerCase());
const handleOpenConfigModal = () => {
setConfigModalOpened(true);
};
const handleCloseConfigModal = useCallback(() => {
setConfigModalOpened(false);
setNewConfigName('');
}, []);
const handleConfigSubmit = useCallback(() => {
onCreateNewConfig(newConfigName.trim());
setConfigModalOpened(false);
setNewConfigName('');
}, [newConfigName, onCreateNewConfig]);
const selectData = configFiles.map((file) => {
const extension = file.name.split('.').pop() || '';
const nameWithoutExtension = file.name.replace(`.${extension}`, '');
return {
value: file.name,
label: nameWithoutExtension,
extension: `.${extension}`,
};
});
if (selectedFile === null && hasUnsavedChanges) {
const displayName = 'New Configuration (unsaved)';
selectData.unshift({
value: '__new__',
label: displayName,
extension: '.kcpps',
});
}
return (
<>
<Stack gap="xs">
<Text fw={500} size="sm">
Configuration
</Text>
<Group gap="xs" align="flex-end">
<div className={styles.flex1}>
<Select
placeholder="Select a configuration file"
value={
selectedFile === null && hasUnsavedChanges
? '__new__'
: selectedFile
}
onChange={(value: string | null) => {
if (value === '__new__') {
return;
}
if (value) {
onFileSelection(value);
}
}}
data={selectData}
leftSection={<File size={16} />}
searchable
clearable={false}
renderOption={({ option }) => {
const dataItem = selectData.find(
(item) => item.value === option.value
);
const extension = dataItem?.extension || '';
return (
<SelectItem label={option.label} extension={extension} />
);
}}
/>
</div>
<Button
variant={
selectedFile === null && hasUnsavedChanges ? 'filled' : 'light'
}
leftSection={<Plus size={14} />}
size="sm"
disabled={selectedFile === null && hasUnsavedChanges}
onClick={() => handleOpenConfigModal()}
>
{selectedFile === null && hasUnsavedChanges
? 'Creating New...'
: 'New'}
</Button>
<Button
variant="outline"
leftSection={<Save size={14} />}
size="sm"
disabled={!hasUnsavedChanges}
onClick={() => {
if (selectedFile) {
onSaveConfig();
} else {
handleOpenConfigModal();
}
}}
>
Save
</Button>
</Group>
</Stack>
<Modal
opened={configModalOpened}
onClose={handleCloseConfigModal}
title="Create New Configuration"
size="sm"
>
<Stack gap="md">
<TextInput
label="Name"
placeholder="Enter a name for the new configuration"
value={newConfigName}
onChange={(event) => setNewConfigName(event.currentTarget.value)}
data-autofocus
error={
configNameExists
? 'A configuration with this name already exists'
: undefined
}
/>
<Group justify="flex-end" gap="sm">
<Button variant="outline" onClick={handleCloseConfigModal}>
Cancel
</Button>
<Button
disabled={!trimmedConfigName || !!configNameExists}
onClick={handleConfigSubmit}
>
Create
</Button>
</Group>
</Stack>
</Modal>
</>
);
};

View file

@ -11,6 +11,7 @@ import {
} from '@mantine/core'; } from '@mantine/core';
import { Download } from 'lucide-react'; import { Download } from 'lucide-react';
import { MouseEvent } from 'react'; import { MouseEvent } from 'react';
import styles from '@/styles/layout.module.css';
interface DownloadCardProps { interface DownloadCardProps {
name: string; name: string;
@ -83,7 +84,7 @@ export const DownloadCard = ({
bg={isCurrent ? 'var(--mantine-color-blue-light)' : undefined} bg={isCurrent ? 'var(--mantine-color-blue-light)' : undefined}
> >
<Group justify="space-between" align="center"> <Group justify="space-between" align="center">
<div style={{ flex: 1 }}> <div className={styles.flex1}>
<Group gap="xs" align="center" mb="xs"> <Group gap="xs" align="center" mb="xs">
<Text fw={500} size="sm"> <Text fw={500} size="sm">
{name} {name}

View file

@ -10,7 +10,7 @@ import {
} from '@mantine/core'; } from '@mantine/core';
import { DownloadCard } from '@/components/DownloadCard'; import { DownloadCard } from '@/components/DownloadCard';
import { StyledTooltip } from '@/components/StyledTooltip'; import { StyledTooltip } from '@/components/StyledTooltip';
import { getPlatformDisplayName, formatFileSize } from '@/utils'; import { getPlatformDisplayName, formatFileSizeInMB } from '@/utils';
import { import {
isAssetRecommended, isAssetRecommended,
sortAssetsByRecommendation, sortAssetsByRecommendation,
@ -43,7 +43,6 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
const regularDownloads = availableDownloads.filter((d) => d.type === 'asset'); const regularDownloads = availableDownloads.filter((d) => d.type === 'asset');
const rocmDownload = availableDownloads.find((d) => d.type === 'rocm'); const rocmDownload = availableDownloads.find((d) => d.type === 'rocm');
const latestVersion = availableDownloads[0]?.version || 'unknown';
const handleDownload = useCallback( const handleDownload = useCallback(
async (type: 'asset' | 'rocm', download?: DownloadItem) => { async (type: 'asset' | 'rocm', download?: DownloadItem) => {
@ -82,7 +81,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
return ( return (
<DownloadCard <DownloadCard
name={rocmDownload.name} name={rocmDownload.name}
size={formatFileSize(rocmDownload.size)} size={`~${formatFileSizeInMB(rocmDownload.size)}`}
description={getAssetDescription(rocmDownload.name)} description={getAssetDescription(rocmDownload.name)}
version={rocmDownload.version} version={rocmDownload.version}
isRecommended={isAssetRecommended( isRecommended={isAssetRecommended(
@ -120,24 +119,6 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
<> <>
{availableDownloads.length > 0 && ( {availableDownloads.length > 0 && (
<> <>
<Stack gap="xs" py="md">
<div>
<Text
size="xs"
c="dimmed"
mb={6}
tt="uppercase"
fw={600}
style={{ letterSpacing: '0.5px' }}
>
Latest Version
</Text>
<Text fw={700} size="xl" mb={8} c="blue.6">
{latestVersion}
</Text>
</div>
</Stack>
{availableDownloads.length > 0 ? ( {availableDownloads.length > 0 ? (
<Stack gap="sm"> <Stack gap="sm">
{platformInfo.hasAMDGPU && !platformInfo.hasROCm && ( {platformInfo.hasAMDGPU && !platformInfo.hasROCm && (
@ -186,7 +167,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
<DownloadCard <DownloadCard
key={download.name} key={download.name}
name={download.name} name={download.name}
size={formatFileSize(download.size)} size={formatFileSizeInMB(download.size)}
version={download.version} version={download.version}
description={getAssetDescription(download.name)} description={getAssetDescription(download.name)}
isRecommended={isAssetRecommended( isRecommended={isAssetRecommended(

View file

@ -1,5 +1,6 @@
import { useRef } from 'react'; import { useRef } from 'react';
import { Box, Text, Stack } from '@mantine/core'; import { Box, Text, Stack } from '@mantine/core';
import { UI } from '@/constants';
interface ServerTabProps { interface ServerTabProps {
serverUrl?: string; serverUrl?: string;
@ -46,7 +47,13 @@ export const ServerTab = ({
: 'KoboldAI Lite Interface'; : 'KoboldAI Lite Interface';
return ( return (
<Box style={{ width: '100%', height: '100%', overflow: 'hidden' }}> <Box
style={{
width: '100%',
height: `calc(100vh - ${UI.HEADER_HEIGHT}px)`,
overflow: 'hidden',
}}
>
<iframe <iframe
ref={iframeRef} ref={iframeRef}
src={iframeUrl} src={iframeUrl}

View file

@ -7,6 +7,8 @@ import {
useMantineColorScheme, useMantineColorScheme,
} from '@mantine/core'; } from '@mantine/core';
import { ChevronDown } from 'lucide-react'; import { ChevronDown } from 'lucide-react';
import styles from '@/styles/layout.module.css';
import { UI } from '@/constants';
interface TerminalTabProps { interface TerminalTabProps {
onServerReady?: (serverUrl: string) => void; onServerReady?: (serverUrl: string) => void;
@ -104,7 +106,7 @@ export const TerminalTab = ({ onServerReady }: TerminalTabProps) => {
return ( return (
<Box <Box
style={{ style={{
height: '80vh', height: `calc(100vh - ${UI.HEADER_HEIGHT}px)`,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
backgroundColor: backgroundColor:
@ -119,12 +121,7 @@ export const TerminalTab = ({ onServerReady }: TerminalTabProps) => {
ref={scrollAreaRef} ref={scrollAreaRef}
viewportRef={viewportRef} viewportRef={viewportRef}
onScrollPositionChange={handleScroll} onScrollPositionChange={handleScroll}
style={{ className={styles.terminalScrollArea}
flex: 1,
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", monospace',
fontSize: '14px',
lineHeight: 1.4,
}}
scrollbarSize={8} scrollbarSize={8}
offsetScrollbars={false} offsetScrollbars={false}
> >

View file

@ -0,0 +1,73 @@
import { useState, useCallback } from 'react';
import { ServerTab } from '@/components/screens/Interface/ServerTab';
import { TerminalTab } from '@/components/screens/Interface/TerminalTab';
interface InterfaceScreenProps {
activeTab?: string | null;
onTabChange?: (tab: string | null) => void;
isImageGenerationMode?: boolean;
}
export const InterfaceScreen = ({
activeTab,
onTabChange,
isImageGenerationMode = false,
}: InterfaceScreenProps) => {
const [serverUrl, setServerUrl] = useState<string>('');
const [isServerReady, setIsServerReady] = useState<boolean>(false);
const handleServerReady = useCallback(
(url: string) => {
setServerUrl(url);
setIsServerReady(true);
if (onTabChange) {
onTabChange(isImageGenerationMode ? 'image' : 'chat');
}
},
[onTabChange, isImageGenerationMode]
);
return (
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
}}
>
<div
style={{
flex: 1,
display: activeTab === 'chat' ? 'block' : 'none',
}}
>
<ServerTab
serverUrl={serverUrl}
isServerReady={isServerReady}
mode={isImageGenerationMode ? 'image-generation' : 'chat'}
/>
</div>
<div
style={{
flex: 1,
display: activeTab === 'image' ? 'block' : 'none',
}}
>
<ServerTab
serverUrl={serverUrl}
isServerReady={isServerReady}
mode="image-generation"
/>
</div>
<div
style={{
flex: 1,
display: activeTab === 'terminal' ? 'block' : 'none',
}}
>
<TerminalTab onServerReady={handleServerReady} />
</div>
</div>
);
};

View file

@ -1,6 +1,7 @@
import { Stack, Text, Group, TextInput, Checkbox } from '@mantine/core'; import { Stack, Text, Group, TextInput, Checkbox } from '@mantine/core';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
import styles from '@/styles/layout.module.css';
interface AdvancedTabProps { interface AdvancedTabProps {
additionalArguments: string; additionalArguments: string;
@ -72,7 +73,7 @@ export const AdvancedTab = ({
}, []); }, []);
return ( return (
<Stack gap="lg"> <Stack gap="md">
<div> <div>
<Group gap="xs" align="center" mb="md"> <Group gap="xs" align="center" mb="md">
<Text size="sm" fw={600}> <Text size="sm" fw={600}>
@ -81,36 +82,48 @@ export const AdvancedTab = ({
</Group> </Group>
<Stack gap="md"> <Stack gap="md">
<Group gap="lg" align="flex-start" wrap="nowrap"> <Group gap="lg" align="flex-start" wrap="nowrap">
<div style={{ minWidth: '200px' }}> <div className={styles.minWidth200}>
<Group gap="xs" align="center"> <Group gap="xs" align="center">
<Checkbox <Checkbox
checked={!noshift} checked={!noshift}
onChange={(event) => onChange={(event) =>
onNoshiftChange(!event.currentTarget.checked) onNoshiftChange(!event.currentTarget.checked)
} }
label="Use ContextShift" label="Context Shift"
/> />
<InfoTooltip label="Use Context Shifting to reduce reprocessing." /> <InfoTooltip label="Use Context Shifting to reduce reprocessing." />
</Group> </Group>
</div> </div>
<div style={{ minWidth: '200px' }}> <div className={styles.minWidth200}>
<Group gap="xs" align="center">
<Checkbox
checked={noshift}
onChange={(event) =>
onNoshiftChange(event.currentTarget.checked)
}
label="No Shift"
/>
<InfoTooltip label="Don't use GPU layer shifting for incomplete offloads, which may reduce model performance." />
</Group>
</div>
</Group>
<Group gap="lg" align="flex-start" wrap="nowrap">
<div className={styles.minWidth200}>
<Group gap="xs" align="center"> <Group gap="xs" align="center">
<Checkbox <Checkbox
checked={flashattention} checked={flashattention}
onChange={(event) => onChange={(event) =>
onFlashattentionChange(event.currentTarget.checked) onFlashattentionChange(event.currentTarget.checked)
} }
label="Use FlashAttention" label="Flash Attention"
/> />
<InfoTooltip label="Enable flash attention for GGUF models." /> <InfoTooltip label="Enable flash attention to reduce memory usage. May produce incorrect answers for some prompts, but improves performance." />
</Group> </Group>
</div> </div>
</Group>
{(backend === 'cuda' || backend === 'rocm') && ( <div className={styles.minWidth200}>
<Group gap="lg" align="flex-start" wrap="nowrap">
<div style={{ minWidth: '200px' }}>
<Group gap="xs" align="center"> <Group gap="xs" align="center">
<Checkbox <Checkbox
checked={lowvram} checked={lowvram}
@ -118,14 +131,21 @@ export const AdvancedTab = ({
onLowvramChange(event.currentTarget.checked) onLowvramChange(event.currentTarget.checked)
} }
label="Low VRAM" label="Low VRAM"
disabled={backend !== 'cuda' && backend !== 'rocm'}
/> />
<InfoTooltip <InfoTooltip
label="Avoid offloading KV Cache or scratch buffers to VRAM.&#10;Allows more layers to fit, but may result in a speed loss." label={
backend !== 'cuda' && backend !== 'rocm'
? 'Low VRAM mode is only available for CUDA and ROCm backends.'
: 'Avoid offloading KV Cache or scratch buffers to VRAM. Allows more layers to fit, but may result in a speed loss.'
}
/> />
</Group> </Group>
</div> </div>
</Group>
<div style={{ minWidth: '200px' }}> <Group gap="lg" align="flex-start" wrap="nowrap">
<div className={styles.minWidth200}>
<Group gap="xs" align="center"> <Group gap="xs" align="center">
<Checkbox <Checkbox
checked={quantmatmul} checked={quantmatmul}
@ -133,12 +153,18 @@ export const AdvancedTab = ({
onQuantmatmulChange(event.currentTarget.checked) onQuantmatmulChange(event.currentTarget.checked)
} }
label="QuantMatMul" label="QuantMatMul"
disabled={backend !== 'cuda' && backend !== 'rocm'}
/>
<InfoTooltip
label={
backend !== 'cuda' && backend !== 'rocm'
? 'QuantMatMul is only available for CUDA and ROCm backends.'
: 'Enable MMQ mode to use finetuned kernels instead of default CuBLAS/HipBLAS for prompt processing.'
}
/> />
<InfoTooltip label="Enable MMQ mode to use finetuned kernels instead of default CuBLAS/HipBLAS for prompt processing." />
</Group> </Group>
</div> </div>
</Group> </Group>
)}
</Stack> </Stack>
</div> </div>
@ -150,7 +176,7 @@ export const AdvancedTab = ({
</Group> </Group>
<Stack gap="md"> <Stack gap="md">
<Group gap="lg" align="flex-start" wrap="nowrap"> <Group gap="lg" align="flex-start" wrap="nowrap">
<div style={{ minWidth: '200px' }}> <div className={styles.minWidth200}>
<Group gap="xs" align="center"> <Group gap="xs" align="center">
<Checkbox <Checkbox
checked={noavx2} checked={noavx2}
@ -170,7 +196,7 @@ export const AdvancedTab = ({
</Group> </Group>
</div> </div>
<div style={{ minWidth: '200px' }}> <div className={styles.minWidth200}>
<Group gap="xs" align="center"> <Group gap="xs" align="center">
<Checkbox <Checkbox
checked={failsafe} checked={failsafe}

View file

@ -1,16 +1,9 @@
import { import { Stack, Text, Group, TextInput, Button, Slider } from '@mantine/core';
Stack,
Text,
Group,
TextInput,
Button,
Checkbox,
Slider,
} from '@mantine/core';
import { File, Search } from 'lucide-react'; import { File, Search } from 'lucide-react';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
import { BackendSelector } from '@/screens/Launch/BackendSelector'; import { BackendSelector } from '@/components/screens/Launch/GeneralTab/BackendSelector';
import { getInputValidationState } from '@/utils'; import { getInputValidationState } from '@/utils';
import styles from '@/styles/layout.module.css';
interface GeneralTabProps { interface GeneralTabProps {
modelPath: string; modelPath: string;
@ -77,7 +70,7 @@ export const GeneralTab = ({
}; };
return ( return (
<Stack gap="lg"> <Stack gap="md">
<BackendSelector <BackendSelector
backend={backend} backend={backend}
onBackendChange={onBackendChange} onBackendChange={onBackendChange}
@ -87,6 +80,10 @@ export const GeneralTab = ({
failsafe={failsafe} failsafe={failsafe}
onWarningsChange={onWarningsChange} onWarningsChange={onWarningsChange}
onBackendsReady={onBackendsReady} onBackendsReady={onBackendsReady}
gpuLayers={gpuLayers}
autoGpuLayers={autoGpuLayers}
onGpuLayersChange={onGpuLayersChange}
onAutoGpuLayersChange={onAutoGpuLayersChange}
/> />
<div> <div>
@ -94,7 +91,7 @@ export const GeneralTab = ({
Text Model File Text Model File
</Text> </Text>
<Group gap="xs" align="flex-start"> <Group gap="xs" align="flex-start">
<div style={{ flex: 1 }}> <div className={styles.flex1}>
<TextInput <TextInput
placeholder="Select a .gguf model file or enter a direct URL to file" placeholder="Select a .gguf model file or enter a direct URL to file"
value={modelPath} value={modelPath}
@ -126,51 +123,6 @@ export const GeneralTab = ({
</Group> </Group>
</div> </div>
<div>
<Group justify="space-between" align="center" mb="xs">
<Group gap="xs" align="center">
<Text size="sm" fw={500}>
GPU Layers
</Text>
<InfoTooltip label="The number of layer's to offload to your GPU's VRAM. Ideally the entire LLM should fit inside the VRAM for optimal performance." />
</Group>
<Group gap="lg" align="center">
<Group gap="xs" align="center">
<Checkbox
label="Auto"
checked={autoGpuLayers}
onChange={(event) =>
onAutoGpuLayersChange(event.currentTarget.checked)
}
size="sm"
/>
<InfoTooltip label="Automatically try to allocate the GPU layers based on available VRAM." />
</Group>
<TextInput
value={gpuLayers.toString()}
onChange={(event) =>
onGpuLayersChange(Number(event.target.value) || 0)
}
type="number"
min={0}
max={100}
step={1}
size="sm"
w={80}
disabled={autoGpuLayers}
/>
</Group>
</Group>
<Slider
value={gpuLayers}
min={0}
max={100}
step={1}
onChange={onGpuLayersChange}
disabled={autoGpuLayers}
/>
</div>
<div> <div>
<Group justify="space-between" align="center" mb="xs"> <Group justify="space-between" align="center" mb="xs">
<Group gap="xs" align="center"> <Group gap="xs" align="center">

View file

@ -0,0 +1,37 @@
import { Text, Group, Badge } from '@mantine/core';
import { forwardRef } from 'react';
import type { ComponentPropsWithoutRef } from 'react';
interface BackendSelectItemProps extends ComponentPropsWithoutRef<'div'> {
label: string;
devices?: string[];
}
export const BackendSelectItem = forwardRef<
HTMLDivElement,
BackendSelectItemProps
>(({ label, devices, ...others }, ref) => (
<div ref={ref} {...others}>
<Group justify="space-between" wrap="nowrap">
<Text size="sm" truncate>
{label}
</Text>
{devices && devices.length > 0 && (
<Group gap={4}>
{devices.slice(0, 2).map((device, index) => (
<Badge key={index} size="md" variant="light" color="blue">
{device.length > 18 ? `${device.slice(0, 18)}...` : device}
</Badge>
))}
{devices.length > 2 && (
<Badge size="md" variant="light" color="gray">
+{devices.length - 2}
</Badge>
)}
</Group>
)}
</Group>
</div>
));
BackendSelectItem.displayName = 'BackendSelectItem';

View file

@ -0,0 +1,275 @@
import { Text, Group, Select, Checkbox, TextInput } from '@mantine/core';
import { useState, useEffect, useRef } from 'react';
import { InfoTooltip } from '@/components/InfoTooltip';
import { BackendSelectItem } from '@/components/screens/Launch/GeneralTab/BackendSelectItem';
interface BackendSelectorProps {
backend: string;
onBackendChange: (backend: string) => void;
gpuDevice?: number;
onGpuDeviceChange?: (device: number) => void;
noavx2?: boolean;
failsafe?: boolean;
onWarningsChange?: (
warnings: Array<{ type: 'warning' | 'info'; message: string }>
) => void;
onBackendsReady?: () => void;
gpuLayers?: number;
autoGpuLayers?: boolean;
onGpuLayersChange?: (layers: number) => void;
onAutoGpuLayersChange?: (auto: boolean) => void;
}
export const BackendSelector = ({
backend,
onBackendChange,
gpuDevice = 0,
onGpuDeviceChange,
noavx2 = false,
failsafe = false,
onWarningsChange,
onBackendsReady,
gpuLayers = 0,
autoGpuLayers = false,
onGpuLayersChange,
onAutoGpuLayersChange,
}: BackendSelectorProps) => {
const [availableBackends, setAvailableBackends] = useState<
Array<{ value: string; label: string; devices?: string[] }>
>([]);
const [cpuCapabilities, setCpuCapabilities] = useState<{
avx: boolean;
avx2: boolean;
} | null>(null);
const hasInitialized = useRef(false);
useEffect(() => {
if (hasInitialized.current) return;
let timeoutId: number;
const loadBackends = async () => {
try {
const [currentBinaryInfo, cpuCapabilitiesResult, gpuCapabilities] =
await Promise.all([
window.electronAPI.kobold.getCurrentBinaryInfo(),
window.electronAPI.kobold.detectCPU(),
window.electronAPI.kobold.detectGPUCapabilities(),
]);
setCpuCapabilities({
avx: cpuCapabilitiesResult.avx,
avx2: cpuCapabilitiesResult.avx2,
});
let backends: Array<{
value: string;
label: string;
devices?: string[];
}> = [];
if (currentBinaryInfo?.path) {
backends = await window.electronAPI.kobold.getAvailableBackends(
currentBinaryInfo.path,
gpuCapabilities
);
const cpuBackend = backends.find((b) => b.value === 'cpu');
if (cpuBackend) {
cpuBackend.devices = cpuCapabilitiesResult.devices;
}
}
setAvailableBackends(backends);
hasInitialized.current = true;
if (backends.length > 0 && !backend) {
timeoutId = window.setTimeout(() => {
onBackendChange(backends[0].value);
}, 10);
}
if (onBackendsReady) {
window.setTimeout(() => {
onBackendsReady();
}, 100);
}
} catch (error) {
window.electronAPI.logs.logError(
'Failed to detect available backends:',
error as Error
);
setAvailableBackends([]);
if (onBackendsReady) {
onBackendsReady();
}
}
};
loadBackends();
return () => {
if (timeoutId) {
window.clearTimeout(timeoutId);
}
};
}, [backend, onBackendChange, onBackendsReady]);
useEffect(() => {
if (!onWarningsChange) return;
if (backend !== 'cpu' || !cpuCapabilities) {
onWarningsChange([]);
return;
}
const warnings: Array<{ type: 'warning' | 'info'; message: string }> = [];
if (!cpuCapabilities.avx2 && !noavx2) {
warnings.push({
type: 'warning',
message:
'Your CPU does not support AVX2. Enable the "Disable AVX2" option to avoid crashes.',
});
}
if (!cpuCapabilities.avx && !cpuCapabilities.avx2 && !failsafe) {
warnings.push({
type: 'warning',
message:
'Your CPU does not support AVX or AVX2. Enable the "Failsafe" option to avoid crashes.',
});
}
if (
availableBackends.length > 0 &&
availableBackends.some((b) => b.value === 'cpu')
) {
warnings.push({
type: 'info',
message:
"Performance Note: LLMs run significantly faster on GPU-accelerated systems. Consider using NVIDIA's CUDA or AMD's ROCm backends for optimal performance.",
});
}
onWarningsChange(warnings);
}, [
backend,
cpuCapabilities,
noavx2,
failsafe,
availableBackends,
onWarningsChange,
]);
return (
<div>
<Group justify="space-between" align="flex-start" mb="xs">
<div style={{ flex: 1, marginRight: '2rem' }}>
<Group gap="xs" align="center" mb="xs">
<Text size="sm" fw={500}>
Backend
</Text>
<InfoTooltip label="Select a backend to use. CUDA runs on NVIDIA GPUs, and is much faster. ROCm is the AMD equivalent. Vulkan and CLBlast works on all GPUs but are somewhat slower." />
</Group>
<Select
placeholder="Select backend"
value={backend}
onChange={(value) => {
if (value) {
onBackendChange(value);
}
}}
data={availableBackends.map((b) => ({
value: b.value,
label: b.label,
}))}
disabled={availableBackends.length === 0}
comboboxProps={{
middlewares: {
flip: false,
},
}}
renderOption={({ option }) => {
const backendData = availableBackends.find(
(b) => b.value === option.value
);
return (
<BackendSelectItem
label={backendData?.label || option.label.split(' (')[0]}
devices={backendData?.devices}
/>
);
}}
/>
{(() => {
const selectedBackend = availableBackends.find(
(b) => b.value === backend
);
const isGpuBackend = backend === 'cuda' || backend === 'rocm';
const hasMultipleDevices =
selectedBackend?.devices && selectedBackend.devices.length > 1;
return (
isGpuBackend &&
onGpuDeviceChange &&
hasMultipleDevices && (
<Select
label="GPU Device"
placeholder="Select GPU device"
value={gpuDevice.toString()}
onChange={(value) =>
value && onGpuDeviceChange(parseInt(value, 10))
}
data={selectedBackend.devices!.map((device, index) => ({
value: index.toString(),
label: `GPU ${index}: ${device}`,
}))}
mt="xs"
/>
)
);
})()}
</div>
{onGpuLayersChange && onAutoGpuLayersChange && (
<div style={{ flex: 1 }}>
<Group gap="xs" align="center" mb="xs">
<Text size="sm" fw={500}>
GPU Layers
</Text>
<InfoTooltip label="The number of layer's to offload to your GPU's VRAM. Ideally the entire LLM should fit inside the VRAM for optimal performance." />
</Group>
<Group gap="lg" align="center">
<TextInput
value={gpuLayers.toString()}
onChange={(event) =>
onGpuLayersChange(Number(event.target.value) || 0)
}
type="number"
min={0}
max={100}
step={1}
size="sm"
w={80}
disabled={autoGpuLayers}
/>
<Group gap="xs" align="center">
<Checkbox
label="Auto"
checked={autoGpuLayers}
onChange={(event) =>
onAutoGpuLayersChange(event.currentTarget.checked)
}
size="sm"
/>
<InfoTooltip label="Automatically try to allocate the GPU layers based on available VRAM." />
</Group>
</Group>
</div>
)}
</Group>
</div>
);
};

View file

@ -0,0 +1,157 @@
import { Stack, Text, Group, TextInput, Button, Slider } from '@mantine/core';
import { File, Search } from 'lucide-react';
import { InfoTooltip } from '@/components/InfoTooltip';
import { BackendSelector } from '@/components/screens/Launch/GeneralTab/BackendSelector';
import { getInputValidationState } from '@/utils';
import styles from '@/styles/layout.module.css';
interface GeneralTabProps {
modelPath: string;
gpuLayers: number;
autoGpuLayers: boolean;
contextSize: number;
backend: string;
gpuDevice?: number;
noavx2: boolean;
failsafe: boolean;
onModelPathChange: (path: string) => void;
onSelectModelFile: () => void;
onGpuLayersChange: (layers: number) => void;
onAutoGpuLayersChange: (auto: boolean) => void;
onContextSizeChange: (size: number) => void;
onBackendChange: (backend: string) => void;
onGpuDeviceChange?: (device: number) => void;
onWarningsChange?: (
warnings: Array<{ type: 'warning' | 'info'; message: string }>
) => void;
onBackendsReady?: () => void;
}
export const GeneralTab = ({
modelPath,
gpuLayers,
autoGpuLayers,
contextSize,
backend,
gpuDevice,
noavx2,
failsafe,
onModelPathChange,
onSelectModelFile,
onGpuLayersChange,
onAutoGpuLayersChange,
onContextSizeChange,
onBackendChange,
onGpuDeviceChange,
onWarningsChange,
onBackendsReady,
}: GeneralTabProps) => {
const validationState = getInputValidationState(modelPath);
const getInputColor = () => {
switch (validationState) {
case 'valid':
return 'green';
case 'invalid':
return 'red';
default:
return undefined;
}
};
const getHelperText = () => {
if (!modelPath.trim()) return undefined;
if (validationState === 'invalid') {
return 'Enter a valid URL or file path to the .gguf';
}
return undefined;
};
return (
<Stack gap="md">
<BackendSelector
backend={backend}
onBackendChange={onBackendChange}
gpuDevice={gpuDevice}
onGpuDeviceChange={onGpuDeviceChange}
noavx2={noavx2}
failsafe={failsafe}
onWarningsChange={onWarningsChange}
onBackendsReady={onBackendsReady}
gpuLayers={gpuLayers}
autoGpuLayers={autoGpuLayers}
onGpuLayersChange={onGpuLayersChange}
onAutoGpuLayersChange={onAutoGpuLayersChange}
/>
<div>
<Text size="sm" fw={500} mb="xs">
Text Model File
</Text>
<Group gap="xs" align="flex-start">
<div className={styles.flex1}>
<TextInput
placeholder="Select a .gguf model file or enter a direct URL to file"
value={modelPath}
onChange={(event) => onModelPathChange(event.currentTarget.value)}
color={getInputColor()}
error={
validationState === 'invalid' ? getHelperText() : undefined
}
/>
</div>
<Button
onClick={onSelectModelFile}
variant="light"
leftSection={<File size={16} />}
>
Browse
</Button>
<Button
onClick={() => {
window.electronAPI.app.openExternal(
'https://huggingface.co/models?pipeline_tag=text-generation&library=gguf&sort=trending'
);
}}
variant="outline"
leftSection={<Search size={16} />}
>
Search HF
</Button>
</Group>
</div>
<div>
<Group justify="space-between" align="center" mb="xs">
<Group gap="xs" align="center">
<Text size="sm" fw={500}>
Context Size
</Text>
<InfoTooltip label="Controls the memory allocated for maximum context size. The larger the context, the larger the required memory." />
</Group>
<TextInput
value={contextSize?.toString() || ''}
onChange={(event) =>
onContextSizeChange(Number(event.target.value) || 256)
}
type="number"
min={256}
max={131072}
step={256}
size="sm"
w={100}
/>
</Group>
<Slider
value={contextSize}
min={256}
max={131072}
step={1}
onChange={onContextSizeChange}
/>
</div>
</Stack>
);
};

View file

@ -3,6 +3,7 @@ import { useState } from 'react';
import { File, Search } from 'lucide-react'; import { File, Search } from 'lucide-react';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
import { getInputValidationState, IMAGE_MODEL_PRESETS } from '@/utils'; import { getInputValidationState, IMAGE_MODEL_PRESETS } from '@/utils';
import styles from '@/styles/layout.module.css';
interface ImageGenerationTabProps { interface ImageGenerationTabProps {
sdmodel: string; sdmodel: string;
@ -78,7 +79,7 @@ const ModelField = ({
{tooltip && <InfoTooltip label={tooltip} />} {tooltip && <InfoTooltip label={tooltip} />}
</Group> </Group>
<Group gap="xs" align="flex-start"> <Group gap="xs" align="flex-start">
<div style={{ flex: 1 }}> <div className={styles.flex1}>
<TextInput <TextInput
placeholder={placeholder} placeholder={placeholder}
value={value} value={value}
@ -139,7 +140,7 @@ export const ImageGenerationTab = ({
const [selectedPreset, setSelectedPreset] = useState<string | null>(null); const [selectedPreset, setSelectedPreset] = useState<string | null>(null);
return ( return (
<Stack gap="lg"> <Stack gap="md">
<div> <div>
<Group gap="xs" align="center" mb="xs"> <Group gap="xs" align="center" mb="xs">
<Text size="sm" fw={500}> <Text size="sm" fw={500}>

View file

@ -1,6 +1,7 @@
import { Stack, Text, TextInput, Group, Checkbox } from '@mantine/core'; import { Stack, Text, TextInput, Group, Checkbox } from '@mantine/core';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { InfoTooltip } from '@/components/InfoTooltip'; import { InfoTooltip } from '@/components/InfoTooltip';
import styles from '@/styles/layout.module.css';
interface NetworkTabProps { interface NetworkTabProps {
port: number | undefined; port: number | undefined;
@ -42,7 +43,7 @@ export const NetworkTab = ({
}, [port]); }, [port]);
return ( return (
<Stack gap="lg"> <Stack gap="md">
<Group gap="lg" align="flex-start"> <Group gap="lg" align="flex-start">
<div> <div>
<Group gap="xs" align="center" mb="xs"> <Group gap="xs" align="center" mb="xs">
@ -100,7 +101,7 @@ export const NetworkTab = ({
<div> <div>
<Stack gap="md"> <Stack gap="md">
<Group gap="lg" align="flex-start" wrap="nowrap"> <Group gap="lg" align="flex-start" wrap="nowrap">
<div style={{ minWidth: '200px' }}> <div className={styles.minWidth200}>
<Group gap="xs" align="center"> <Group gap="xs" align="center">
<Checkbox <Checkbox
checked={multiuser} checked={multiuser}
@ -113,7 +114,7 @@ export const NetworkTab = ({
</Group> </Group>
</div> </div>
<div style={{ minWidth: '200px' }}> <div className={styles.minWidth200}>
<Group gap="xs" align="center"> <Group gap="xs" align="center">
<Checkbox <Checkbox
checked={multiplayer} checked={multiplayer}
@ -128,7 +129,7 @@ export const NetworkTab = ({
</Group> </Group>
<Group gap="lg" align="flex-start" wrap="nowrap"> <Group gap="lg" align="flex-start" wrap="nowrap">
<div style={{ minWidth: '200px' }}> <div className={styles.minWidth200}>
<Group gap="xs" align="center"> <Group gap="xs" align="center">
<Checkbox <Checkbox
checked={remotetunnel} checked={remotetunnel}
@ -141,7 +142,7 @@ export const NetworkTab = ({
</Group> </Group>
</div> </div>
<div style={{ minWidth: '200px' }}> <div className={styles.minWidth200}>
<Group gap="xs" align="center"> <Group gap="xs" align="center">
<Checkbox <Checkbox
checked={nocertify} checked={nocertify}

View file

@ -0,0 +1,503 @@
import { Card, Container, Stack, Tabs, Group, Button } from '@mantine/core';
import { useState, useEffect, useCallback } from 'react';
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import { useTrackedConfigHandlers } from '@/hooks/useTrackedConfigHandlers';
import { useLaunchLogic } from '@/hooks/useLaunchLogic';
import { useWarnings } from '@/hooks/useWarnings';
import { GeneralTab } from '@/components/screens/Launch/GeneralTab';
import { AdvancedTab } from '@/components/screens/Launch/AdvancedTab';
import { NetworkTab } from '@/components/screens/Launch/NetworkTab';
import { ImageGenerationTab } from '@/components/screens/Launch/ImageGenerationTab';
import { WarningDisplay } from '@/components/WarningDisplay';
import { ConfigFileManager } from '@/components/ConfigFileManager';
import type { ConfigFile } from '@/types';
interface LaunchScreenProps {
onLaunch: () => void;
onLaunchModeChange?: (isImageMode: boolean) => void;
}
export const LaunchScreen = ({
onLaunch,
onLaunchModeChange,
}: LaunchScreenProps) => {
const [configFiles, setConfigFiles] = useState<ConfigFile[]>([]);
const [selectedFile, setSelectedFile] = useState<string | null>(null);
const [, setInstallDir] = useState<string>('');
const [activeTab, setActiveTab] = useState<string | null>('general');
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [warnings, setWarnings] = useState<
Array<{ type: 'warning' | 'info'; message: string }>
>([]);
const {
gpuLayers,
autoGpuLayers,
contextSize,
modelPath,
additionalArguments,
port,
host,
multiuser,
multiplayer,
remotetunnel,
nocertify,
websearch,
noshift,
flashattention,
noavx2,
failsafe,
lowvram,
quantmatmul,
backend,
gpuDevice,
sdmodel,
sdt5xxl,
sdclipl,
sdclipg,
sdphotomaker,
sdvae,
sdlora,
parseAndApplyConfigFile,
loadSavedSettings,
loadConfigFromFile,
handleSelectModelFile,
handleGpuDeviceChange,
handleSelectSdmodelFile,
handleSelectSdt5xxlFile,
handleSelectSdcliplFile,
handleSelectSdclipgFile,
handleSelectSdphotomakerFile,
handleSelectSdvaeFile,
handleSelectSdloraFile,
handleApplyPreset,
handleModelPathChange,
handleGpuLayersChange,
handleAutoGpuLayersChange,
handleContextSizeChangeWithStep,
handleAdditionalArgumentsChange,
handlePortChange,
handleHostChange,
handleMultiuserChange,
handleMultiplayerChange,
handleRemotetunnelChange,
handleNocertifyChange,
handleWebsearchChange,
handleNoshiftChange,
handleFlashattentionChange,
handleNoavx2Change,
handleFailsafeChange,
handleLowvramChange,
handleQuantmatmulChange,
handleBackendChange,
handleSdmodelChange,
handleSdt5xxlChange,
handleSdcliplChange,
handleSdclipgChange,
handleSdphotomakerChange,
handleSdvaeChange,
handleSdloraChange,
} = useLaunchConfig();
const trackedHandlers = useTrackedConfigHandlers({
setHasUnsavedChanges,
handlers: {
handleModelPathChange,
handleGpuLayersChange,
handleAutoGpuLayersChange,
handleContextSizeChangeWithStep,
handleAdditionalArgumentsChange,
handlePortChange,
handleHostChange,
handleMultiuserChange,
handleMultiplayerChange,
handleRemotetunnelChange,
handleNocertifyChange,
handleWebsearchChange,
handleNoshiftChange,
handleFlashattentionChange,
handleNoavx2Change,
handleFailsafeChange,
handleLowvramChange,
handleQuantmatmulChange,
handleBackendChange,
handleSdmodelChange,
handleSdt5xxlChange,
handleSdcliplChange,
handleSdclipgChange,
handleSdphotomakerChange,
handleSdvaeChange,
handleSdloraChange,
},
});
const { isLaunching, handleLaunch } = useLaunchLogic({
configFiles,
selectedFile,
modelPath,
sdmodel,
onLaunch,
onLaunchModeChange,
});
const combinedWarnings = useWarnings({ modelPath, sdmodel, warnings });
const loadConfigFiles = useCallback(async () => {
const [files, currentDir, savedConfig] = await Promise.all([
window.electronAPI.kobold.getConfigFiles(),
window.electronAPI.kobold.getCurrentInstallDir(),
window.electronAPI.kobold.getSelectedConfig(),
]);
setConfigFiles(files);
setInstallDir(currentDir);
if (savedConfig && files.some((f) => f.name === savedConfig)) {
setSelectedFile(savedConfig);
} else if (files.length > 0 && !selectedFile) {
setSelectedFile(files[0].name);
}
await loadSavedSettings();
const currentSelectedFile = await loadConfigFromFile(files, savedConfig);
if (currentSelectedFile && !selectedFile) {
setSelectedFile(currentSelectedFile);
}
}, [selectedFile, loadSavedSettings, loadConfigFromFile]);
const handleFileSelection = async (fileName: string) => {
setSelectedFile(fileName);
await window.electronAPI.kobold.setSelectedConfig(fileName);
const selectedConfig = configFiles.find((f) => f.name === fileName);
if (selectedConfig) {
await parseAndApplyConfigFile(selectedConfig.path);
}
setHasUnsavedChanges(false);
};
const buildConfigData = () => ({
gpulayers: gpuLayers,
contextsize: contextSize,
model_param: modelPath,
port,
host,
multiuser: multiuser ? 1 : 0,
multiplayer,
remotetunnel,
nocertify,
websearch,
noshift,
flashattention,
noavx2,
failsafe,
usecuda: backend === 'cuda' || backend === 'rocm',
usevulkan: backend === 'vulkan',
useclblast: backend === 'clblast',
sdmodel,
sdt5xxl,
sdclipl,
sdclipg,
sdphotomaker,
sdvae,
});
const handleCreateNewConfig = async (configName: string) => {
try {
const success = await window.electronAPI.kobold.saveConfigFile(
configName,
buildConfigData()
);
if (success) {
await loadConfigFiles();
const newFileName = `${configName}.kcpps`;
setSelectedFile(newFileName);
await window.electronAPI.kobold.setSelectedConfig(newFileName);
setHasUnsavedChanges(false);
} else {
window.electronAPI.logs.logError(
'Failed to create new configuration',
new Error('Save operation failed')
);
}
} catch (error) {
window.electronAPI.logs.logError(
'Failed to create new configuration:',
error as Error
);
}
};
const handleSaveConfig = async () => {
if (!selectedFile) {
window.electronAPI.logs.logError(
'No configuration file selected for saving',
new Error('Selected file is null')
);
return;
}
try {
const configName = selectedFile.replace('.kcpps', '');
const success = await window.electronAPI.kobold.saveConfigFile(
configName,
buildConfigData()
);
if (success) {
setHasUnsavedChanges(false);
} else {
window.electronAPI.logs.logError(
'Failed to save configuration',
new Error('Save operation failed')
);
}
} catch (error) {
window.electronAPI.logs.logError(
'Failed to save configuration:',
error as Error
);
}
};
useEffect(() => {
void loadConfigFiles();
const handleInstallDirChange = () => {
void loadConfigFiles();
};
const cleanup = window.electronAPI.kobold.onInstallDirChanged(
handleInstallDirChange
);
return cleanup;
}, [loadConfigFiles]);
const handleLaunchClick = () => {
handleLaunch({
autoGpuLayers,
gpuLayers,
contextSize,
port: port ?? 5001,
host,
multiuser,
multiplayer,
remotetunnel,
nocertify,
websearch,
noshift,
flashattention,
backend,
lowvram,
gpuDevice,
quantmatmul,
additionalArguments,
sdt5xxl,
sdclipl,
sdclipg,
sdphotomaker,
sdvae,
sdlora,
});
};
return (
<Container size="sm">
<Stack gap="md">
<Card
withBorder
radius="md"
shadow="sm"
p="lg"
style={{ position: 'relative' }}
>
<Stack gap="lg">
<ConfigFileManager
configFiles={configFiles}
selectedFile={selectedFile}
hasUnsavedChanges={hasUnsavedChanges}
onFileSelection={handleFileSelection}
onCreateNewConfig={handleCreateNewConfig}
onSaveConfig={handleSaveConfig}
onLoadConfigFiles={loadConfigFiles}
/>
<Tabs value={activeTab} onChange={setActiveTab}>
<Tabs.List>
<Tabs.Tab value="general">General</Tabs.Tab>
<Tabs.Tab value="image">Image Generation</Tabs.Tab>
<Tabs.Tab value="network">Network</Tabs.Tab>
<Tabs.Tab value="advanced">Advanced</Tabs.Tab>
</Tabs.List>
<div>
<Tabs.Panel value="general" pt="md">
<GeneralTab
modelPath={modelPath}
gpuLayers={gpuLayers}
autoGpuLayers={autoGpuLayers}
contextSize={contextSize}
backend={backend}
gpuDevice={gpuDevice}
noavx2={noavx2}
failsafe={failsafe}
onModelPathChange={
trackedHandlers.handleModelPathChangeWithTracking
}
onSelectModelFile={handleSelectModelFile}
onGpuLayersChange={
trackedHandlers.handleGpuLayersChangeWithTracking
}
onAutoGpuLayersChange={
trackedHandlers.handleAutoGpuLayersChangeWithTracking
}
onContextSizeChange={
trackedHandlers.handleContextSizeChangeWithTracking
}
onBackendChange={
trackedHandlers.handleBackendChangeWithTracking
}
onGpuDeviceChange={handleGpuDeviceChange}
onWarningsChange={setWarnings}
/>
</Tabs.Panel>
<Tabs.Panel value="advanced" pt="md">
<AdvancedTab
additionalArguments={additionalArguments}
noshift={noshift}
flashattention={flashattention}
noavx2={noavx2}
failsafe={failsafe}
lowvram={lowvram}
quantmatmul={quantmatmul}
backend={backend}
onAdditionalArgumentsChange={
trackedHandlers.handleAdditionalArgumentsChangeWithTracking
}
onNoshiftChange={
trackedHandlers.handleNoshiftChangeWithTracking
}
onFlashattentionChange={
trackedHandlers.handleFlashattentionChangeWithTracking
}
onNoavx2Change={
trackedHandlers.handleNoavx2ChangeWithTracking
}
onFailsafeChange={
trackedHandlers.handleFailsafeChangeWithTracking
}
onLowvramChange={
trackedHandlers.handleLowvramChangeWithTracking
}
onQuantmatmulChange={
trackedHandlers.handleQuantmatmulChangeWithTracking
}
/>
</Tabs.Panel>
<Tabs.Panel value="network" pt="md">
<NetworkTab
port={port}
host={host}
multiuser={multiuser}
multiplayer={multiplayer}
remotetunnel={remotetunnel}
nocertify={nocertify}
websearch={websearch}
onPortChange={trackedHandlers.handlePortChangeWithTracking}
onHostChange={trackedHandlers.handleHostChangeWithTracking}
onMultiuserChange={
trackedHandlers.handleMultiuserChangeWithTracking
}
onMultiplayerChange={
trackedHandlers.handleMultiplayerChangeWithTracking
}
onRemotetunnelChange={
trackedHandlers.handleRemotetunnelChangeWithTracking
}
onNocertifyChange={
trackedHandlers.handleNocertifyChangeWithTracking
}
onWebsearchChange={
trackedHandlers.handleWebsearchChangeWithTracking
}
/>
</Tabs.Panel>
<Tabs.Panel value="image" pt="md">
<ImageGenerationTab
sdmodel={sdmodel}
sdt5xxl={sdt5xxl}
sdclipl={sdclipl}
sdclipg={sdclipg}
sdphotomaker={sdphotomaker}
sdvae={sdvae}
sdlora={sdlora}
onSdmodelChange={
trackedHandlers.handleSdmodelChangeWithTracking
}
onSelectSdmodelFile={handleSelectSdmodelFile}
onSdt5xxlChange={
trackedHandlers.handleSdt5xxlChangeWithTracking
}
onSelectSdt5xxlFile={handleSelectSdt5xxlFile}
onSdcliplChange={
trackedHandlers.handleSdcliplChangeWithTracking
}
onSelectSdcliplFile={handleSelectSdcliplFile}
onSdclipgChange={
trackedHandlers.handleSdclipgChangeWithTracking
}
onSelectSdclipgFile={handleSelectSdclipgFile}
onSdphotomakerChange={
trackedHandlers.handleSdphotomakerChangeWithTracking
}
onSelectSdphotomakerFile={handleSelectSdphotomakerFile}
onSdvaeChange={
trackedHandlers.handleSdvaeChangeWithTracking
}
onSelectSdvaeFile={handleSelectSdvaeFile}
onSdloraChange={
trackedHandlers.handleSdloraChangeWithTracking
}
onSelectSdloraFile={handleSelectSdloraFile}
onApplyPreset={handleApplyPreset}
/>
</Tabs.Panel>
</div>
</Tabs>
<Group justify="flex-end" pt="md">
<WarningDisplay warnings={combinedWarnings}>
<Button
radius="md"
disabled={(!modelPath && !sdmodel) || isLaunching}
onClick={handleLaunchClick}
size="lg"
variant="filled"
color="blue"
style={{
fontWeight: 600,
fontSize: '16px',
padding: '12px 28px',
minWidth: '120px',
textTransform: 'uppercase',
letterSpacing: '0.5px',
}}
>
Launch
</Button>
</WarningDisplay>
</Group>
</Stack>
</Card>
</Stack>
</Container>
);
};

View file

@ -1,6 +1,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Stack, Text, Group, TextInput, Button, rem } from '@mantine/core'; import { Stack, Text, Group, TextInput, Button, rem } from '@mantine/core';
import { Folder, FolderOpen } from 'lucide-react'; import { Folder, FolderOpen } from 'lucide-react';
import styles from '@/styles/layout.module.css';
export const GeneralTab = () => { export const GeneralTab = () => {
const [installDir, setInstallDir] = useState<string>(''); const [installDir, setInstallDir] = useState<string>('');
@ -51,7 +52,7 @@ export const GeneralTab = () => {
value={installDir} value={installDir}
readOnly readOnly
placeholder="Default installation directory" placeholder="Default installation directory"
style={{ flex: 1 }} className={styles.flex1}
leftSection={<Folder style={{ width: rem(16), height: rem(16) }} />} leftSection={<Folder style={{ width: rem(16), height: rem(16) }} />}
/> />
<Button <Button

View file

@ -121,7 +121,7 @@ export const SettingsModal = ({
</Tabs.Panel> </Tabs.Panel>
{showVersionsTab && ( {showVersionsTab && (
<Tabs.Panel value="versions"> <Tabs.Panel value="versions" style={{ overflow: 'visible' }}>
<VersionsTab /> <VersionsTab />
</Tabs.Panel> </Tabs.Panel>
)} )}

View file

@ -8,17 +8,18 @@ import {
Loader, Loader,
rem, rem,
Center, Center,
Anchor,
} from '@mantine/core'; } from '@mantine/core';
import { RotateCcw } from 'lucide-react'; import { RotateCcw, ExternalLink } from 'lucide-react';
import { DownloadCard } from '@/components/DownloadCard'; import { DownloadCard } from '@/components/DownloadCard';
import { import {
getAssetDescription, getAssetDescription,
sortAssetsByRecommendation, sortAssetsByRecommendation,
isAssetRecommended, isAssetRecommended,
} from '@/utils/assets'; } from '@/utils/assets';
import { getDisplayNameFromPath } from '@/utils'; import { getDisplayNameFromPath, formatFileSizeInMB } from '@/utils';
import { useKoboldVersions } from '@/hooks/useKoboldVersions'; import { useKoboldVersions } from '@/hooks/useKoboldVersions';
import type { InstalledVersion } from '@/types/electron'; import type { InstalledVersion, ReleaseWithStatus } from '@/types/electron';
interface VersionInfo { interface VersionInfo {
name: string; name: string;
@ -50,6 +51,9 @@ export const VersionsTab = () => {
null null
); );
const [loadingInstalled, setLoadingInstalled] = useState(true); const [loadingInstalled, setLoadingInstalled] = useState(true);
const [latestRelease, setLatestRelease] = useState<ReleaseWithStatus | null>(
null
);
const loadInstalledVersions = useCallback(async () => { const loadInstalledVersions = useCallback(async () => {
setLoadingInstalled(true); setLoadingInstalled(true);
@ -95,9 +99,23 @@ export const VersionsTab = () => {
} }
}, []); }, []);
const loadLatestRelease = useCallback(async () => {
try {
const release =
await window.electronAPI.kobold.getLatestReleaseWithStatus();
setLatestRelease(release);
} catch (error) {
window.electronAPI.logs.logError(
'Failed to load latest release:',
error as Error
);
}
}, []);
useEffect(() => { useEffect(() => {
loadInstalledVersions(); loadInstalledVersions();
}, [loadInstalledVersions]); loadLatestRelease();
}, [loadInstalledVersions, loadLatestRelease]);
const getAllVersions = (): VersionInfo[] => { const getAllVersions = (): VersionInfo[] => {
const versions: VersionInfo[] = []; const versions: VersionInfo[] = [];
@ -105,7 +123,10 @@ export const VersionsTab = () => {
availableDownloads.forEach((download) => { availableDownloads.forEach((download) => {
const installedVersion = installedVersions.find((v) => { const installedVersion = installedVersions.find((v) => {
const displayName = getDisplayNameFromPath(v); const displayName = getDisplayNameFromPath(v);
return displayName === download.name; const downloadBaseName = download.name
.replace(/\.(tar\.gz|zip|exe)$/i, '')
.replace(/\.packed$/, '');
return displayName === downloadBaseName;
}); });
const isCurrent = Boolean( const isCurrent = Boolean(
@ -217,15 +238,41 @@ export const VersionsTab = () => {
} }
return ( return (
<Stack gap="lg" h="100%"> <>
<Group justify="space-between" align="center"> <Group justify="space-between" align="center" mb="lg">
<div>
<Text fw={500}>Available Versions</Text> <Text fw={500}>Available Versions</Text>
{latestRelease && (
<Group gap="xs" mt={4}>
<Text size="sm" c="dimmed">
Latest release: {latestRelease.release.tag_name}
</Text>
<Anchor
href="#"
onClick={(e) => {
e.preventDefault();
window.electronAPI.app.openExternal(
latestRelease.release.html_url
);
}}
size="sm"
c="blue"
>
<Group gap={4} align="center">
<span>Release notes</span>
<ExternalLink size={12} />
</Group>
</Anchor>
</Group>
)}
</div>
<Button <Button
variant="subtle" variant="subtle"
size="xs" size="xs"
onClick={() => { onClick={() => {
loadInstalledVersions(); loadInstalledVersions();
loadRemoteVersions(); loadRemoteVersions();
loadLatestRelease();
}} }}
leftSection={ leftSection={
<RotateCcw style={{ width: rem(14), height: rem(14) }} /> <RotateCcw style={{ width: rem(14), height: rem(14) }} />
@ -235,17 +282,21 @@ export const VersionsTab = () => {
</Button> </Button>
</Group> </Group>
<Stack gap="xs">
{getAllVersions().map((version, index) => { {getAllVersions().map((version, index) => {
const isDownloading = downloading === version.name; const isDownloading = downloading === version.name;
return ( return (
<DownloadCard <div
key={`${version.name}-${version.version}-${index}`} key={`${version.name}-${version.version}-${index}`}
style={{ paddingBottom: '8px' }}
>
<DownloadCard
name={version.name} name={version.name}
size={ size={
version.size version.size
? `${(version.size / 1024 / 1024).toFixed(1)} MB` ? version.isROCm
? `~${formatFileSizeInMB(version.size)}`
: formatFileSizeInMB(version.size)
: '' : ''
} }
version={version.version} version={version.version}
@ -273,6 +324,7 @@ export const VersionsTab = () => {
: undefined : undefined
} }
/> />
</div>
); );
})} })}
@ -283,7 +335,6 @@ export const VersionsTab = () => {
</Text> </Text>
</Card> </Card>
)} )}
</Stack> </>
</Stack>
); );
}; };

View file

@ -3,7 +3,7 @@ export const DEFAULT_CONTEXT_SIZE = 4096;
export const DEFAULT_MODEL_URL = 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'; 'https://huggingface.co/MaziyarPanahi/gemma-3-4b-it-GGUF/resolve/main/gemma-3-4b-it.Q8_0.gguf?download=true';
export const DEFAULT_HOST = 'localhost'; export const DEFAULT_HOST = '';
export const DEFAULT_VOLUME = 0.5; export const DEFAULT_VOLUME = 0.5;

View file

@ -2,6 +2,12 @@ export const APP_NAME = 'friendly-kobold';
export const CONFIG_FILE_NAME = 'config.json'; export const CONFIG_FILE_NAME = 'config.json';
export const UI = {
HEADER_HEIGHT: 60,
} as const;
export const { HEADER_HEIGHT } = UI;
export * from './defaults'; export * from './defaults';
export const GITHUB_API = { export const GITHUB_API = {
@ -33,5 +39,5 @@ export const KOBOLDAI_URLS = {
export const ROCM = { export const ROCM = {
BINARY_NAME: 'koboldcpp-linux-x64-rocm', BINARY_NAME: 'koboldcpp-linux-x64-rocm',
DOWNLOAD_URL: KOBOLDAI_URLS.ROCM_DOWNLOAD, DOWNLOAD_URL: KOBOLDAI_URLS.ROCM_DOWNLOAD,
SIZE_BYTES: 1024 * 1024 * 1024, SIZE_BYTES_APPROX: 1024 * 1024 * 1024,
} as const; } as const;

View file

@ -8,7 +8,6 @@ import {
DEFAULT_CONTEXT_SIZE, DEFAULT_CONTEXT_SIZE,
DEFAULT_MODEL_URL, DEFAULT_MODEL_URL,
DEFAULT_HOST, DEFAULT_HOST,
DEFAULT_FLUX_MODELS,
} from '@/constants'; } from '@/constants';
import { isNotNullish } from '@/utils'; import { isNotNullish } from '@/utils';
@ -242,7 +241,7 @@ export const useLaunchConfig = () => {
setSdclipl(''); setSdclipl('');
setSdclipg(''); setSdclipg('');
setSdphotomaker(''); setSdphotomaker('');
setSdvae(DEFAULT_FLUX_MODELS.VAE); setSdvae('');
}, []); }, []);
const loadConfigFromFile = useCallback( const loadConfigFromFile = useCallback(

223
src/hooks/useLaunchLogic.ts Normal file
View file

@ -0,0 +1,223 @@
import { useState, useCallback } from 'react';
import type { ConfigFile } from '@/types';
interface UseLaunchLogicProps {
configFiles: ConfigFile[];
selectedFile: string | null;
modelPath: string;
sdmodel: string;
onLaunch: () => void;
onLaunchModeChange?: (isImageMode: boolean) => void;
}
interface LaunchArgs {
autoGpuLayers: boolean;
gpuLayers: number;
contextSize: number;
port: number;
host: string;
multiuser: boolean;
multiplayer: boolean;
remotetunnel: boolean;
nocertify: boolean;
websearch: boolean;
noshift: boolean;
flashattention: boolean;
backend: string;
lowvram: boolean;
gpuDevice: number;
quantmatmul: boolean;
additionalArguments: string;
sdt5xxl: string;
sdclipl: string;
sdclipg: string;
sdphotomaker: string;
sdvae: string;
sdlora: string;
}
const buildModelArgs = (
isImageMode: boolean,
isTextMode: boolean,
modelPath: string,
sdmodel: string,
launchArgs: LaunchArgs
): string[] => {
const args: string[] = [];
if (isImageMode && isTextMode) {
args.push('--sdmodel', sdmodel);
} else if (isImageMode) {
args.push('--sdmodel', sdmodel);
const imageModels = [
['--sdt5xxl', launchArgs.sdt5xxl],
['--sdclipl', launchArgs.sdclipl],
['--sdclipg', launchArgs.sdclipg],
['--sdphotomaker', launchArgs.sdphotomaker],
['--sdvae', launchArgs.sdvae],
['--sdlora', launchArgs.sdlora],
];
imageModels.forEach(([flag, value]) => {
if (value.trim()) {
args.push(flag, value);
}
});
} else {
args.push('--model', modelPath);
}
return args;
};
const buildConfigArgs = (launchArgs: LaunchArgs): string[] => {
const args: string[] = [];
if (launchArgs.autoGpuLayers) {
args.push('--gpulayers', '-1');
} else if (launchArgs.gpuLayers > 0) {
args.push('--gpulayers', launchArgs.gpuLayers.toString());
}
if (launchArgs.contextSize) {
args.push('--contextsize', launchArgs.contextSize.toString());
}
if (launchArgs.port) {
args.push('--port', launchArgs.port.toString());
}
if (launchArgs.host !== 'localhost' && launchArgs.host) {
args.push('--host', launchArgs.host);
}
const flagMappings: Array<[boolean, string, string?]> = [
[launchArgs.multiuser, '--multiuser', '1'],
[launchArgs.multiplayer, '--multiplayer'],
[launchArgs.remotetunnel, '--remotetunnel'],
[launchArgs.nocertify, '--nocertify'],
[launchArgs.websearch, '--websearch'],
[launchArgs.noshift, '--noshift'],
[launchArgs.flashattention, '--flashattention'],
];
flagMappings.forEach(([condition, flag, value]) => {
if (condition) {
args.push(flag);
if (value) args.push(value);
}
});
return args;
};
const buildBackendArgs = (launchArgs: LaunchArgs): string[] => {
const args: string[] = [];
if (launchArgs.backend && launchArgs.backend !== 'cpu') {
if (launchArgs.backend === 'cuda' || launchArgs.backend === 'rocm') {
const cudaArgs = ['--usecuda'];
cudaArgs.push(launchArgs.lowvram ? 'lowvram' : 'normal');
cudaArgs.push(launchArgs.gpuDevice.toString());
cudaArgs.push(launchArgs.quantmatmul ? 'mmq' : 'nommq');
args.push(...cudaArgs);
} else if (launchArgs.backend === 'vulkan') {
args.push('--usevulkan');
} else if (launchArgs.backend === 'clblast') {
args.push('--useclblast');
}
}
return args;
};
export const useLaunchLogic = ({
configFiles,
selectedFile,
modelPath,
sdmodel,
onLaunch,
onLaunchModeChange,
}: UseLaunchLogicProps) => {
const [isLaunching, setIsLaunching] = useState(false);
const handleLaunch = useCallback(
async (launchArgs: LaunchArgs) => {
const isImageMode = sdmodel.trim() !== '';
const isTextMode = modelPath.trim() !== '';
if (isLaunching || (!isImageMode && !isTextMode)) {
return;
}
setIsLaunching(true);
try {
const selectedConfig = selectedFile
? configFiles.find((f) => f.name === selectedFile)
: null;
const args: string[] = [
...buildModelArgs(
isImageMode,
isTextMode,
modelPath,
sdmodel,
launchArgs
),
...buildConfigArgs(launchArgs),
...buildBackendArgs(launchArgs),
];
if (launchArgs.additionalArguments.trim()) {
const additionalArgs = launchArgs.additionalArguments
.trim()
.split(/\s+/);
args.push(...additionalArgs);
}
const result = await window.electronAPI.kobold.launchKoboldCpp(
args,
selectedConfig?.path
);
if (result.success) {
if (onLaunchModeChange) {
onLaunchModeChange(isImageMode);
}
setTimeout(() => {
onLaunch();
}, 100);
} else {
window.electronAPI.logs.logError(
'Launch failed:',
new Error(result.error)
);
}
} catch (error) {
window.electronAPI.logs.logError(
'Error launching KoboldCpp:',
error as Error
);
} finally {
setIsLaunching(false);
}
},
[
configFiles,
selectedFile,
modelPath,
sdmodel,
isLaunching,
onLaunch,
onLaunchModeChange,
]
);
return {
isLaunching,
handleLaunch,
};
};

View file

@ -0,0 +1,176 @@
import { useChangeTracker } from '@/hooks/useChangeTracker';
interface TrackedConfigHandlersProps {
setHasUnsavedChanges: (value: boolean) => void;
handlers: {
handleModelPathChange: (path: string) => void;
handleGpuLayersChange: (layers: number) => void;
handleAutoGpuLayersChange: (auto: boolean) => void;
handleContextSizeChangeWithStep: (size: number) => void;
handleAdditionalArgumentsChange: (args: string) => void;
handlePortChange: (port: number | undefined) => void;
handleHostChange: (host: string) => void;
handleMultiuserChange: (enabled: boolean) => void;
handleMultiplayerChange: (enabled: boolean) => void;
handleRemotetunnelChange: (enabled: boolean) => void;
handleNocertifyChange: (enabled: boolean) => void;
handleWebsearchChange: (enabled: boolean) => void;
handleNoshiftChange: (enabled: boolean) => void;
handleFlashattentionChange: (enabled: boolean) => void;
handleNoavx2Change: (enabled: boolean) => void;
handleFailsafeChange: (enabled: boolean) => void;
handleLowvramChange: (enabled: boolean) => void;
handleQuantmatmulChange: (enabled: boolean) => void;
handleBackendChange: (backend: string) => void;
handleSdmodelChange: (path: string) => void;
handleSdt5xxlChange: (path: string) => void;
handleSdcliplChange: (path: string) => void;
handleSdclipgChange: (path: string) => void;
handleSdphotomakerChange: (path: string) => void;
handleSdvaeChange: (path: string) => void;
handleSdloraChange: (path: string) => void;
};
}
export const useTrackedConfigHandlers = ({
setHasUnsavedChanges,
handlers,
}: TrackedConfigHandlersProps) => {
const createChangeTracker = useChangeTracker;
const {
handleModelPathChange,
handleGpuLayersChange,
handleAutoGpuLayersChange,
handleContextSizeChangeWithStep,
handleAdditionalArgumentsChange,
handlePortChange,
handleHostChange,
handleMultiuserChange,
handleMultiplayerChange,
handleRemotetunnelChange,
handleNocertifyChange,
handleWebsearchChange,
handleNoshiftChange,
handleFlashattentionChange,
handleNoavx2Change,
handleFailsafeChange,
handleLowvramChange,
handleQuantmatmulChange,
handleBackendChange,
handleSdmodelChange,
handleSdt5xxlChange,
handleSdcliplChange,
handleSdclipgChange,
handleSdphotomakerChange,
handleSdvaeChange,
handleSdloraChange,
} = handlers;
return {
handleModelPathChangeWithTracking: createChangeTracker(
handleModelPathChange,
setHasUnsavedChanges
),
handleGpuLayersChangeWithTracking: createChangeTracker(
handleGpuLayersChange,
setHasUnsavedChanges
),
handleAutoGpuLayersChangeWithTracking: createChangeTracker(
handleAutoGpuLayersChange,
setHasUnsavedChanges
),
handleContextSizeChangeWithTracking: createChangeTracker(
handleContextSizeChangeWithStep,
setHasUnsavedChanges
),
handleAdditionalArgumentsChangeWithTracking: createChangeTracker(
handleAdditionalArgumentsChange,
setHasUnsavedChanges
),
handlePortChangeWithTracking: createChangeTracker(
handlePortChange,
setHasUnsavedChanges
),
handleHostChangeWithTracking: createChangeTracker(
handleHostChange,
setHasUnsavedChanges
),
handleNoshiftChangeWithTracking: createChangeTracker(
handleNoshiftChange,
setHasUnsavedChanges
),
handleFlashattentionChangeWithTracking: createChangeTracker(
handleFlashattentionChange,
setHasUnsavedChanges
),
handleNoavx2ChangeWithTracking: createChangeTracker(
handleNoavx2Change,
setHasUnsavedChanges
),
handleFailsafeChangeWithTracking: createChangeTracker(
handleFailsafeChange,
setHasUnsavedChanges
),
handleLowvramChangeWithTracking: createChangeTracker(
handleLowvramChange,
setHasUnsavedChanges
),
handleQuantmatmulChangeWithTracking: createChangeTracker(
handleQuantmatmulChange,
setHasUnsavedChanges
),
handleMultiuserChangeWithTracking: createChangeTracker(
handleMultiuserChange,
setHasUnsavedChanges
),
handleMultiplayerChangeWithTracking: createChangeTracker(
handleMultiplayerChange,
setHasUnsavedChanges
),
handleRemotetunnelChangeWithTracking: createChangeTracker(
handleRemotetunnelChange,
setHasUnsavedChanges
),
handleNocertifyChangeWithTracking: createChangeTracker(
handleNocertifyChange,
setHasUnsavedChanges
),
handleWebsearchChangeWithTracking: createChangeTracker(
handleWebsearchChange,
setHasUnsavedChanges
),
handleBackendChangeWithTracking: createChangeTracker(
handleBackendChange,
setHasUnsavedChanges
),
handleSdmodelChangeWithTracking: createChangeTracker(
handleSdmodelChange,
setHasUnsavedChanges
),
handleSdt5xxlChangeWithTracking: createChangeTracker(
handleSdt5xxlChange,
setHasUnsavedChanges
),
handleSdcliplChangeWithTracking: createChangeTracker(
handleSdcliplChange,
setHasUnsavedChanges
),
handleSdclipgChangeWithTracking: createChangeTracker(
handleSdclipgChange,
setHasUnsavedChanges
),
handleSdphotomakerChangeWithTracking: createChangeTracker(
handleSdphotomakerChange,
setHasUnsavedChanges
),
handleSdvaeChangeWithTracking: createChangeTracker(
handleSdvaeChange,
setHasUnsavedChanges
),
handleSdloraChangeWithTracking: createChangeTracker(
handleSdloraChange,
setHasUnsavedChanges
),
};
};

41
src/hooks/useWarnings.ts Normal file
View file

@ -0,0 +1,41 @@
import { useMemo } from 'react';
interface UseWarningsProps {
modelPath: string;
sdmodel: string;
warnings: Array<{ type: 'warning' | 'info'; message: string }>;
}
export const useWarnings = ({
modelPath,
sdmodel,
warnings,
}: UseWarningsProps) =>
useMemo(() => {
const hasTextModel = modelPath?.trim() !== '';
const hasImageModel = sdmodel.trim() !== '';
const showModelPriorityWarning = hasTextModel && hasImageModel;
const showNoModelWarning = !hasTextModel && !hasImageModel;
return [
...warnings,
...(showModelPriorityWarning
? [
{
type: 'warning' as const,
message:
'Both text and image generation models are selected. The image generation model will take priority and be used for launch.',
},
]
: []),
...(showNoModelWarning
? [
{
type: 'info' as const,
message:
'Select a model in the General or Image Generation tab to enable launch.',
},
]
: []),
];
}, [modelPath, sdmodel, warnings]);

View file

@ -4,7 +4,7 @@ import { createRoot } from 'react-dom/client';
import { MantineProvider } from '@mantine/core'; import { MantineProvider } from '@mantine/core';
import { App } from '@/App.tsx'; import { App } from '@/App.tsx';
import { ThemeProvider, useTheme } from '@/contexts/ThemeContext'; import { ThemeProvider, useTheme } from '@/contexts/ThemeContext';
import './index.css'; import '@/styles/index.css';
const AppWithTheme = () => { const AppWithTheme = () => {
const { effectiveTheme } = useTheme(); const { effectiveTheme } = useTheme();

View file

@ -385,7 +385,7 @@ export class KoboldCppManager {
return false; return false;
} }
const configFileName = `${configName}.json`; const configFileName = `${configName}.kcpps`;
const configPath = join(this.installDir, configFileName); const configPath = join(this.installDir, configFileName);
writeFileSync(configPath, JSON.stringify(configData, null, 2), 'utf-8'); writeFileSync(configPath, JSON.stringify(configData, null, 2), 'utf-8');
@ -647,7 +647,7 @@ export class KoboldCppManager {
return { return {
name: ROCM.BINARY_NAME, name: ROCM.BINARY_NAME,
url: ROCM.DOWNLOAD_URL, url: ROCM.DOWNLOAD_URL,
size: ROCM.SIZE_BYTES, size: ROCM.SIZE_BYTES_APPROX,
version, version,
type: 'rocm', type: 'rocm',
}; };

View file

@ -1,75 +0,0 @@
import { useState, useCallback } from 'react';
import { Card, Container } from '@mantine/core';
import { ServerTab } from '@/screens/Interface/ServerTab';
import { TerminalTab } from '@/screens/Interface/TerminalTab';
interface InterfaceScreenProps {
activeTab?: string | null;
onTabChange?: (tab: string | null) => void;
isImageGenerationMode?: boolean;
}
export const InterfaceScreen = ({
activeTab,
onTabChange,
isImageGenerationMode = false,
}: InterfaceScreenProps) => {
const [serverUrl, setServerUrl] = useState<string>('');
const [isServerReady, setIsServerReady] = useState<boolean>(false);
const handleServerReady = useCallback(
(url: string) => {
setServerUrl(url);
setIsServerReady(true);
if (onTabChange) {
onTabChange(isImageGenerationMode ? 'image' : 'chat');
}
},
[onTabChange, isImageGenerationMode]
);
return (
<Container size="l" style={{ height: '85vh' }}>
<Card
withBorder
radius="md"
p="0"
style={{ height: 'calc(90vh - 32px)' }}
>
<div style={{ height: '100%' }}>
<div
style={{
height: '100%',
display: activeTab === 'chat' ? 'block' : 'none',
}}
>
<ServerTab
serverUrl={serverUrl}
isServerReady={isServerReady}
mode={isImageGenerationMode ? 'image-generation' : 'chat'}
/>
</div>
<div
style={{
height: '100%',
display: activeTab === 'image' ? 'block' : 'none',
}}
>
<ServerTab
serverUrl={serverUrl}
isServerReady={isServerReady}
mode="image-generation"
/>
</div>
<div
style={{
display: activeTab === 'terminal' ? 'block' : 'none',
}}
>
<TerminalTab onServerReady={handleServerReady} />
</div>
</div>
</Card>
</Container>
);
};

View file

@ -1,226 +0,0 @@
import { Text, Group, Select, Badge } from '@mantine/core';
import { useState, useEffect, useRef } from 'react';
import { InfoTooltip } from '@/components/InfoTooltip';
interface BackendSelectorProps {
backend: string;
onBackendChange: (backend: string) => void;
gpuDevice?: number;
onGpuDeviceChange?: (device: number) => void;
noavx2?: boolean;
failsafe?: boolean;
onWarningsChange?: (
warnings: Array<{ type: 'warning' | 'info'; message: string }>
) => void;
onBackendsReady?: () => void;
}
export const BackendSelector = ({
backend,
onBackendChange,
gpuDevice = 0,
onGpuDeviceChange,
noavx2 = false,
failsafe = false,
onWarningsChange,
onBackendsReady,
}: BackendSelectorProps) => {
const [availableBackends, setAvailableBackends] = useState<
Array<{ value: string; label: string; devices?: string[] }>
>([]);
const [cpuCapabilities, setCpuCapabilities] = useState<{
avx: boolean;
avx2: boolean;
} | null>(null);
const hasInitialized = useRef(false);
useEffect(() => {
if (hasInitialized.current) return;
let timeoutId: number;
const loadBackends = async () => {
try {
const [currentBinaryInfo, cpuCapabilitiesResult, gpuCapabilities] =
await Promise.all([
window.electronAPI.kobold.getCurrentBinaryInfo(),
window.electronAPI.kobold.detectCPU(),
window.electronAPI.kobold.detectGPUCapabilities(),
]);
setCpuCapabilities({
avx: cpuCapabilitiesResult.avx,
avx2: cpuCapabilitiesResult.avx2,
});
let backends: Array<{
value: string;
label: string;
devices?: string[];
}> = [];
if (currentBinaryInfo?.path) {
backends = await window.electronAPI.kobold.getAvailableBackends(
currentBinaryInfo.path,
gpuCapabilities
);
const cpuBackend = backends.find((b) => b.value === 'cpu');
if (cpuBackend) {
cpuBackend.devices = cpuCapabilitiesResult.devices;
}
}
setAvailableBackends(backends);
hasInitialized.current = true;
if (backends.length > 0 && !backend) {
timeoutId = window.setTimeout(() => {
onBackendChange(backends[0].value);
}, 10);
}
if (onBackendsReady) {
window.setTimeout(() => {
onBackendsReady();
}, 100);
}
} catch (error) {
window.electronAPI.logs.logError(
'Failed to detect available backends:',
error as Error
);
setAvailableBackends([]);
if (onBackendsReady) {
onBackendsReady();
}
}
};
loadBackends();
return () => {
if (timeoutId) {
window.clearTimeout(timeoutId);
}
};
}, [backend, onBackendChange, onBackendsReady]);
useEffect(() => {
if (!onWarningsChange) return;
if (backend !== 'cpu' || !cpuCapabilities) {
onWarningsChange([]);
return;
}
const warnings: Array<{ type: 'warning' | 'info'; message: string }> = [];
if (!cpuCapabilities.avx2 && !noavx2) {
warnings.push({
type: 'warning',
message:
'Your CPU does not support AVX2. Enable the "Disable AVX2" option to avoid crashes.',
});
}
if (!cpuCapabilities.avx && !cpuCapabilities.avx2 && !failsafe) {
warnings.push({
type: 'warning',
message:
'Your CPU does not support AVX or AVX2. Enable the "Failsafe" option to avoid crashes.',
});
}
if (
availableBackends.length > 0 &&
availableBackends.some((b) => b.value === 'cpu')
) {
warnings.push({
type: 'info',
message:
"Performance Note: LLMs run significantly faster on GPU-accelerated systems. Consider using NVIDIA's CUDA or AMD's ROCm backends for optimal performance.",
});
}
onWarningsChange(warnings);
}, [
backend,
cpuCapabilities,
noavx2,
failsafe,
availableBackends,
onWarningsChange,
]);
return (
<div>
<Group gap="xs" align="center" mb="xs">
<Text size="sm" fw={500}>
Backend
</Text>
<InfoTooltip label="Select a backend to use. CUDA runs on Nvidia GPUs, and is much faster. ROCm is the AMD equivalent. Vulkan and CLBlast works on all GPUs but are somewhat slower." />
</Group>
<Select
placeholder="Select backend"
value={backend}
onChange={(value) => {
if (value) {
onBackendChange(value);
}
}}
data={availableBackends.map((b) => ({
value: b.value,
label: b.label,
}))}
disabled={availableBackends.length === 0}
comboboxProps={{
middlewares: {
flip: false,
},
}}
/>
{(backend === 'cuda' || backend === 'rocm') &&
onGpuDeviceChange &&
availableBackends.find((b) => b.value === backend)?.devices &&
availableBackends.find((b) => b.value === backend)!.devices!.length >
1 && (
<Select
label="GPU Device"
placeholder="Select GPU device"
value={gpuDevice.toString()}
onChange={(value) =>
value && onGpuDeviceChange(parseInt(value, 10))
}
data={availableBackends
.find((b) => b.value === backend)!
.devices!.map((device, index) => ({
value: index.toString(),
label: `GPU ${index}: ${device}`,
}))}
mt="xs"
/>
)}
{availableBackends.find((b) => b.value === backend)?.devices && (
<Group gap="xs" mt="xs">
<Text size="xs" c="dimmed">
{availableBackends.find((b) => b.value === backend)?.devices
?.length === 1
? 'Device:'
: 'Devices:'}
</Text>
{availableBackends
.find((b) => b.value === backend)
?.devices?.map((device, index) => (
<Badge key={index} variant="light" size="sm">
{device}
</Badge>
))}
</Group>
)}
</div>
);
};

View file

@ -1,129 +0,0 @@
import { Stack, Text, Group, Button, Menu, Select, Badge } from '@mantine/core';
import { Save, Settings2, File } from 'lucide-react';
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
import type { ConfigFile } from '@/types';
interface ConfigurationManagerProps {
configFiles: ConfigFile[];
selectedFile: string | null;
onFileSelection: (fileName: string) => void;
onSaveAsNew: () => void;
onUpdateCurrent: () => void;
}
interface SelectItemProps extends ComponentPropsWithoutRef<'div'> {
label: string;
extension: string;
}
const getBadgeColor = (extension: string) => {
switch (extension.toLowerCase()) {
case '.kcpps':
return 'blue';
case '.kcppt':
return 'green';
default:
return 'gray';
}
};
const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
({ label, extension, ...others }, ref) => (
<div ref={ref} {...others}>
<Group justify="space-between" wrap="nowrap">
<Text size="sm" truncate>
{label}
</Text>
<Badge size="xs" variant="light" color={getBadgeColor(extension)}>
{extension}
</Badge>
</Group>
</div>
)
);
SelectItem.displayName = 'SelectItem';
export const ConfigurationManager = ({
configFiles,
selectedFile,
onFileSelection,
onSaveAsNew,
onUpdateCurrent,
}: ConfigurationManagerProps) => (
<Stack gap="md">
<Group justify="space-between" align="center">
<Text fw={500}>Configuration File</Text>
<Menu>
<Menu.Target>
<Button variant="light" leftSection={<Save size={16} />}>
Save
</Button>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item leftSection={<Save size={16} />} onClick={onSaveAsNew}>
Save as new configuration
</Menu.Item>
<Menu.Item
leftSection={<Settings2 size={16} />}
disabled={!selectedFile}
onClick={onUpdateCurrent}
>
Update current configuration
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Group>
{configFiles.length === 0 ? (
<Text c="dimmed" ta="center">
No configuration files found in the installation directory.
<br />
Please ensure your .kcpps or .kcppt files are in the correct location.
</Text>
) : (
(() => {
const selectData = configFiles.map((file) => {
const extension = file.name.split('.').pop() || '';
const nameWithoutExtension = file.name.replace(`.${extension}`, '');
return {
value: file.name,
label: nameWithoutExtension,
extension: `.${extension}`,
};
});
return (
<Select
placeholder="Select a configuration file"
value={selectedFile}
onChange={(value) => value && onFileSelection(value)}
data={selectData}
leftSection={<File size={16} />}
searchable
clearable={false}
w="100%"
filter={({ options, search }) =>
options.filter((option) => {
if ('label' in option) {
return option.label
.toLowerCase()
.includes(search.toLowerCase().trim());
}
return false;
})
}
renderOption={({ option }) => {
const dataItem = selectData.find(
(item) => item.value === option.value
);
const extension = dataItem?.extension || '';
return <SelectItem label={option.label} extension={extension} />;
}}
/>
);
})()
)}
</Stack>
);

View file

@ -1,787 +0,0 @@
import {
Card,
Container,
Stack,
Tabs,
Text,
Group,
Button,
Select,
Modal,
TextInput,
Badge,
} from '@mantine/core';
import {
useState,
useEffect,
useCallback,
forwardRef,
type ComponentPropsWithoutRef,
} from 'react';
import { Save, File, Plus } from 'lucide-react';
import { useLaunchConfig } from '@/hooks/useLaunchConfig';
import { useChangeTracker } from '@/hooks/useChangeTracker';
import { GeneralTab } from '@/screens/Launch/GeneralTab';
import { AdvancedTab } from '@/screens/Launch/AdvancedTab';
import { NetworkTab } from '@/screens/Launch/NetworkTab';
import { ImageGenerationTab } from '@/screens/Launch/ImageGenerationTab';
import { WarningDisplay } from '@/components/WarningDisplay';
import type { ConfigFile } from '@/types';
interface LaunchScreenProps {
onLaunch: () => void;
onLaunchModeChange?: (isImageMode: boolean) => void;
}
interface SelectItemProps extends ComponentPropsWithoutRef<'div'> {
label: string;
extension: string;
}
const getBadgeColor = (extension: string) => {
switch (extension.toLowerCase()) {
case '.kcpps':
return 'blue';
case '.kcppt':
return 'green';
default:
return 'gray';
}
};
const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
({ label, extension, ...others }, ref) => (
<div ref={ref} {...others}>
<Group justify="space-between" wrap="nowrap">
<Text size="sm" truncate>
{label}
</Text>
<Badge size="xs" variant="light" color={getBadgeColor(extension)}>
{extension}
</Badge>
</Group>
</div>
)
);
SelectItem.displayName = 'SelectItem';
export const LaunchScreen = ({
onLaunch,
onLaunchModeChange,
}: LaunchScreenProps) => {
const [configFiles, setConfigFiles] = useState<ConfigFile[]>([]);
const [selectedFile, setSelectedFile] = useState<string | null>(null);
const [, setInstallDir] = useState<string>('');
const [isLaunching, setIsLaunching] = useState(false);
const [activeTab, setActiveTab] = useState<string | null>('general');
const [saveAsModalOpened, setSaveAsModalOpened] = useState(false);
const [newConfigName, setNewConfigName] = useState('');
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [warnings, setWarnings] = useState<
Array<{ type: 'warning' | 'info'; message: string }>
>([]);
const {
gpuLayers,
autoGpuLayers,
contextSize,
modelPath,
additionalArguments,
port,
host,
multiuser,
multiplayer,
remotetunnel,
nocertify,
websearch,
noshift,
flashattention,
noavx2,
failsafe,
lowvram,
quantmatmul,
backend,
gpuDevice,
sdmodel,
sdt5xxl,
sdclipl,
sdclipg,
sdphotomaker,
sdvae,
sdlora,
parseAndApplyConfigFile,
loadSavedSettings,
loadConfigFromFile,
handleGpuLayersChange,
handleAutoGpuLayersChange,
handleContextSizeChangeWithStep,
handleModelPathChange,
handleSelectModelFile,
handleAdditionalArgumentsChange,
handlePortChange,
handleHostChange,
handleMultiuserChange,
handleMultiplayerChange,
handleRemotetunnelChange,
handleNocertifyChange,
handleWebsearchChange,
handleNoshiftChange,
handleFlashattentionChange,
handleNoavx2Change,
handleFailsafeChange,
handleLowvramChange,
handleQuantmatmulChange,
handleBackendChange,
handleGpuDeviceChange,
handleSdmodelChange,
handleSelectSdmodelFile,
handleSdt5xxlChange,
handleSelectSdt5xxlFile,
handleSdcliplChange,
handleSelectSdcliplFile,
handleSdclipgChange,
handleSelectSdclipgFile,
handleSdphotomakerChange,
handleSelectSdphotomakerFile,
handleSdvaeChange,
handleSelectSdvaeFile,
handleSdloraChange,
handleSelectSdloraFile,
handleApplyPreset,
} = useLaunchConfig();
const createChangeTracker = useChangeTracker;
const handleModelPathChangeWithTracking = createChangeTracker(
handleModelPathChange,
setHasUnsavedChanges
);
const handleGpuLayersChangeWithTracking = createChangeTracker(
handleGpuLayersChange,
setHasUnsavedChanges
);
const handleAutoGpuLayersChangeWithTracking = createChangeTracker(
handleAutoGpuLayersChange,
setHasUnsavedChanges
);
const handleContextSizeChangeWithTracking = createChangeTracker(
handleContextSizeChangeWithStep,
setHasUnsavedChanges
);
const handleAdditionalArgumentsChangeWithTracking = createChangeTracker(
handleAdditionalArgumentsChange,
setHasUnsavedChanges
);
const handlePortChangeWithTracking = createChangeTracker(
handlePortChange,
setHasUnsavedChanges
);
const handleHostChangeWithTracking = createChangeTracker(
handleHostChange,
setHasUnsavedChanges
);
const handleNoshiftChangeWithTracking = createChangeTracker(
handleNoshiftChange,
setHasUnsavedChanges
);
const handleFlashattentionChangeWithTracking = createChangeTracker(
handleFlashattentionChange,
setHasUnsavedChanges
);
const handleNoavx2ChangeWithTracking = createChangeTracker(
handleNoavx2Change,
setHasUnsavedChanges
);
const handleFailsafeChangeWithTracking = createChangeTracker(
handleFailsafeChange,
setHasUnsavedChanges
);
const handleLowvramChangeWithTracking = createChangeTracker(
handleLowvramChange,
setHasUnsavedChanges
);
const handleQuantmatmulChangeWithTracking = createChangeTracker(
handleQuantmatmulChange,
setHasUnsavedChanges
);
const handleMultiuserChangeWithTracking = createChangeTracker(
handleMultiuserChange,
setHasUnsavedChanges
);
const handleMultiplayerChangeWithTracking = createChangeTracker(
handleMultiplayerChange,
setHasUnsavedChanges
);
const handleRemotetunnelChangeWithTracking = createChangeTracker(
handleRemotetunnelChange,
setHasUnsavedChanges
);
const handleNocertifyChangeWithTracking = createChangeTracker(
handleNocertifyChange,
setHasUnsavedChanges
);
const handleWebsearchChangeWithTracking = createChangeTracker(
handleWebsearchChange,
setHasUnsavedChanges
);
const handleBackendChangeWithTracking = createChangeTracker(
handleBackendChange,
setHasUnsavedChanges
);
const handleSdmodelChangeWithTracking = createChangeTracker(
handleSdmodelChange,
setHasUnsavedChanges
);
const handleSdt5xxlChangeWithTracking = createChangeTracker(
handleSdt5xxlChange,
setHasUnsavedChanges
);
const handleSdcliplChangeWithTracking = createChangeTracker(
handleSdcliplChange,
setHasUnsavedChanges
);
const handleSdclipgChangeWithTracking = createChangeTracker(
handleSdclipgChange,
setHasUnsavedChanges
);
const handleSdphotomakerChangeWithTracking = createChangeTracker(
handleSdphotomakerChange,
setHasUnsavedChanges
);
const handleSdvaeChangeWithTracking = createChangeTracker(
handleSdvaeChange,
setHasUnsavedChanges
);
const handleSdloraChangeWithTracking = createChangeTracker(
handleSdloraChange,
setHasUnsavedChanges
);
const loadConfigFiles = useCallback(async () => {
const [files, currentDir, savedConfig] = await Promise.all([
window.electronAPI.kobold.getConfigFiles(),
window.electronAPI.kobold.getCurrentInstallDir(),
window.electronAPI.kobold.getSelectedConfig(),
]);
setConfigFiles(files);
setInstallDir(currentDir);
if (savedConfig && files.some((f) => f.name === savedConfig)) {
setSelectedFile(savedConfig);
} else if (files.length > 0 && !selectedFile) {
setSelectedFile(files[0].name);
}
await loadSavedSettings();
const currentSelectedFile = await loadConfigFromFile(files, savedConfig);
if (currentSelectedFile && !selectedFile) {
setSelectedFile(currentSelectedFile);
}
}, [selectedFile, loadSavedSettings, loadConfigFromFile]);
const handleFileSelection = async (fileName: string) => {
setSelectedFile(fileName);
await window.electronAPI.kobold.setSelectedConfig(fileName);
const selectedConfig = configFiles.find((f) => f.name === fileName);
if (selectedConfig) {
await parseAndApplyConfigFile(selectedConfig.path);
}
setHasUnsavedChanges(false);
};
useEffect(() => {
void loadConfigFiles();
const handleInstallDirChange = () => {
void loadConfigFiles();
};
const cleanup = window.electronAPI.kobold.onInstallDirChanged(
handleInstallDirChange
);
return cleanup;
}, [loadConfigFiles]);
// eslint-disable-next-line sonarjs/cognitive-complexity
const handleLaunch = async () => {
const isImageMode = sdmodel.trim() !== '';
const isTextMode = modelPath.trim() !== '';
if (isLaunching || (!isImageMode && !isTextMode)) {
return;
}
setIsLaunching(true);
try {
const selectedConfig = selectedFile
? configFiles.find((f) => f.name === selectedFile)
: null;
const args: string[] = [];
if (isImageMode && isTextMode) {
args.push('--sdmodel', sdmodel);
} else if (isImageMode) {
args.push('--sdmodel', sdmodel);
if (sdt5xxl.trim()) {
args.push('--sdt5xxl', sdt5xxl);
}
if (sdclipl.trim()) {
args.push('--sdclipl', sdclipl);
}
if (sdclipg.trim()) {
args.push('--sdclipg', sdclipg);
}
if (sdphotomaker.trim()) {
args.push('--sdphotomaker', sdphotomaker);
}
if (sdvae.trim()) {
args.push('--sdvae', sdvae);
}
if (sdlora.trim()) {
args.push('--sdlora', sdlora);
}
} else {
args.push('--model', modelPath);
}
if (autoGpuLayers) {
args.push('--gpulayers', '-1');
} else if (gpuLayers > 0) {
args.push('--gpulayers', gpuLayers.toString());
}
if (contextSize) {
args.push('--contextsize', contextSize.toString());
}
if (port) {
args.push('--port', port.toString());
}
if (host !== 'localhost' && host) {
args.push('--host', host);
}
if (multiuser) {
args.push('--multiuser', '1');
}
if (multiplayer) {
args.push('--multiplayer');
}
if (remotetunnel) {
args.push('--remotetunnel');
}
if (nocertify) {
args.push('--nocertify');
}
if (websearch) {
args.push('--websearch');
}
if (noshift) {
args.push('--noshift');
}
if (flashattention) {
args.push('--flashattention');
}
if (backend && backend !== 'cpu') {
if (backend === 'cuda' || backend === 'rocm') {
const cudaArgs = ['--usecuda'];
cudaArgs.push(lowvram ? 'lowvram' : 'normal');
cudaArgs.push(gpuDevice.toString());
cudaArgs.push(quantmatmul ? 'mmq' : 'nommq');
args.push(...cudaArgs);
} else if (backend === 'vulkan') {
args.push('--usevulkan');
} else if (backend === 'clblast') {
args.push('--useclblast');
}
}
if (additionalArguments.trim()) {
const additionalArgs = additionalArguments.trim().split(/\s+/);
args.push(...additionalArgs);
}
const result = await window.electronAPI.kobold.launchKoboldCpp(
args,
selectedConfig?.path
);
if (result.success) {
if (onLaunchModeChange) {
onLaunchModeChange(isImageMode);
}
setTimeout(() => {
onLaunch();
}, 100);
} else {
window.electronAPI.logs.logError(
'Launch failed:',
new Error(result.error)
);
}
} catch (error) {
window.electronAPI.logs.logError(
'Error launching KoboldCpp:',
error as Error
);
} finally {
setIsLaunching(false);
}
};
const hasTextModel = modelPath?.trim() !== '';
const hasImageModel = sdmodel.trim() !== '';
const showModelPriorityWarning = hasTextModel && hasImageModel;
const showNoModelWarning = !hasTextModel && !hasImageModel;
const combinedWarnings = [
...warnings,
...(showModelPriorityWarning
? [
{
type: 'warning' as const,
message:
'Both text and image generation models are selected. The image generation model will take priority and be used for launch.',
},
]
: []),
...(showNoModelWarning
? [
{
type: 'info' as const,
message:
'Select a model in the General or Image Generation tab to enable launch.',
},
]
: []),
];
return (
<Container size="sm">
<Stack gap="md">
<Card
withBorder
radius="md"
shadow="sm"
p="lg"
style={{ position: 'relative' }}
>
<Stack gap="lg">
<Stack gap="xs">
<Text fw={500} size="sm">
Configuration File
</Text>
{configFiles.length === 0 ? (
<Group gap="xs" align="flex-end">
<div style={{ flex: 1 }}>
<Text c="dimmed" size="sm">
No saved configurations. Create your first configuration
by clicking &ldquo;New&rdquo; then &ldquo;Save&rdquo;.
</Text>
</div>
<Button
variant="light"
leftSection={<Plus size={14} />}
size="sm"
onClick={async () => {
setSelectedFile(null);
await loadSavedSettings();
setHasUnsavedChanges(true);
}}
>
New
</Button>
</Group>
) : (
(() => {
const selectData = configFiles.map((file) => {
const extension = file.name.split('.').pop() || '';
const nameWithoutExtension = file.name.replace(
`.${extension}`,
''
);
return {
value: file.name,
label: nameWithoutExtension,
extension: `.${extension}`,
};
});
if (selectedFile === null && hasUnsavedChanges) {
selectData.unshift({
value: '__new__',
label: 'New Configuration (unsaved)',
extension: '.kcpps',
});
}
return (
<Group gap="xs" align="flex-end">
<div style={{ flex: 1 }}>
<Select
placeholder="Select a configuration file"
value={
selectedFile === null && hasUnsavedChanges
? '__new__'
: selectedFile
}
onChange={(value: string | null) => {
if (value === '__new__') {
return;
}
if (value) {
handleFileSelection(value);
}
}}
data={selectData}
leftSection={<File size={16} />}
searchable
clearable={false}
renderOption={({ option }) => {
const dataItem = selectData.find(
(item) => item.value === option.value
);
const extension = dataItem?.extension || '';
return (
<SelectItem
label={option.label}
extension={extension}
/>
);
}}
/>
</div>
<Button
variant={
selectedFile === null && hasUnsavedChanges
? 'filled'
: 'light'
}
leftSection={<Plus size={14} />}
size="sm"
disabled={selectedFile === null && hasUnsavedChanges}
onClick={async () => {
setSelectedFile(null);
await loadSavedSettings();
setHasUnsavedChanges(true);
}}
>
{selectedFile === null && hasUnsavedChanges
? 'Creating New...'
: 'New'}
</Button>
<Button
variant="outline"
leftSection={<Save size={14} />}
size="sm"
disabled={!hasUnsavedChanges}
onClick={() => {
if (selectedFile) {
setHasUnsavedChanges(false);
} else {
setSaveAsModalOpened(true);
}
}}
>
Save
</Button>
</Group>
);
})()
)}
</Stack>
<Tabs value={activeTab} onChange={setActiveTab}>
<Tabs.List>
<Tabs.Tab value="general">General</Tabs.Tab>
<Tabs.Tab value="image">Image Generation</Tabs.Tab>
<Tabs.Tab value="network">Network</Tabs.Tab>
<Tabs.Tab value="advanced">Advanced</Tabs.Tab>
</Tabs.List>
<div style={{ minHeight: '300px' }}>
<Tabs.Panel value="general" pt="md">
<GeneralTab
modelPath={modelPath}
gpuLayers={gpuLayers}
autoGpuLayers={autoGpuLayers}
contextSize={contextSize}
backend={backend}
gpuDevice={gpuDevice}
noavx2={noavx2}
failsafe={failsafe}
onModelPathChange={handleModelPathChangeWithTracking}
onSelectModelFile={handleSelectModelFile}
onGpuLayersChange={handleGpuLayersChangeWithTracking}
onAutoGpuLayersChange={
handleAutoGpuLayersChangeWithTracking
}
onContextSizeChange={handleContextSizeChangeWithTracking}
onBackendChange={handleBackendChangeWithTracking}
onGpuDeviceChange={handleGpuDeviceChange}
onWarningsChange={setWarnings}
/>
</Tabs.Panel>
<Tabs.Panel value="advanced" pt="md">
<AdvancedTab
additionalArguments={additionalArguments}
noshift={noshift}
flashattention={flashattention}
noavx2={noavx2}
failsafe={failsafe}
lowvram={lowvram}
quantmatmul={quantmatmul}
backend={backend}
onAdditionalArgumentsChange={
handleAdditionalArgumentsChangeWithTracking
}
onNoshiftChange={handleNoshiftChangeWithTracking}
onFlashattentionChange={
handleFlashattentionChangeWithTracking
}
onNoavx2Change={handleNoavx2ChangeWithTracking}
onFailsafeChange={handleFailsafeChangeWithTracking}
onLowvramChange={handleLowvramChangeWithTracking}
onQuantmatmulChange={handleQuantmatmulChangeWithTracking}
/>
</Tabs.Panel>
<Tabs.Panel value="network" pt="md">
<NetworkTab
port={port}
host={host}
multiuser={multiuser}
multiplayer={multiplayer}
remotetunnel={remotetunnel}
nocertify={nocertify}
websearch={websearch}
onPortChange={handlePortChangeWithTracking}
onHostChange={handleHostChangeWithTracking}
onMultiuserChange={handleMultiuserChangeWithTracking}
onMultiplayerChange={handleMultiplayerChangeWithTracking}
onRemotetunnelChange={handleRemotetunnelChangeWithTracking}
onNocertifyChange={handleNocertifyChangeWithTracking}
onWebsearchChange={handleWebsearchChangeWithTracking}
/>
</Tabs.Panel>
<Tabs.Panel value="image" pt="md">
<ImageGenerationTab
sdmodel={sdmodel}
sdt5xxl={sdt5xxl}
sdclipl={sdclipl}
sdclipg={sdclipg}
sdphotomaker={sdphotomaker}
sdvae={sdvae}
sdlora={sdlora}
onSdmodelChange={handleSdmodelChangeWithTracking}
onSelectSdmodelFile={handleSelectSdmodelFile}
onSdt5xxlChange={handleSdt5xxlChangeWithTracking}
onSelectSdt5xxlFile={handleSelectSdt5xxlFile}
onSdcliplChange={handleSdcliplChangeWithTracking}
onSelectSdcliplFile={handleSelectSdcliplFile}
onSdclipgChange={handleSdclipgChangeWithTracking}
onSelectSdclipgFile={handleSelectSdclipgFile}
onSdphotomakerChange={handleSdphotomakerChangeWithTracking}
onSelectSdphotomakerFile={handleSelectSdphotomakerFile}
onSdvaeChange={handleSdvaeChangeWithTracking}
onSelectSdvaeFile={handleSelectSdvaeFile}
onSdloraChange={handleSdloraChangeWithTracking}
onSelectSdloraFile={handleSelectSdloraFile}
onApplyPreset={handleApplyPreset}
/>
</Tabs.Panel>
</div>
</Tabs>
<Group justify="flex-end" pt="md">
<WarningDisplay warnings={combinedWarnings}>
<Button
radius="md"
disabled={(!modelPath && !sdmodel) || isLaunching}
onClick={handleLaunch}
size="lg"
variant="filled"
color="blue"
style={{
fontWeight: 600,
fontSize: '16px',
padding: '12px 28px',
minWidth: '120px',
textTransform: 'uppercase',
letterSpacing: '0.5px',
}}
>
Launch
</Button>
</WarningDisplay>
</Group>
</Stack>
</Card>
<Modal
opened={saveAsModalOpened}
onClose={() => setSaveAsModalOpened(false)}
title="Save Configuration As..."
size="sm"
>
<Stack gap="md">
<TextInput
label="Configuration Name"
placeholder="Enter a name for this configuration"
value={newConfigName}
onChange={(event) => setNewConfigName(event.currentTarget.value)}
data-autofocus
/>
<Group justify="flex-end" gap="sm">
<Button
variant="outline"
onClick={() => setSaveAsModalOpened(false)}
>
Cancel
</Button>
<Button
disabled={!newConfigName.trim()}
leftSection={<Save size={16} />}
onClick={() => {
setSaveAsModalOpened(false);
setNewConfigName('');
setHasUnsavedChanges(false);
}}
>
Save As New
</Button>
</Group>
</Stack>
</Modal>
</Stack>
</Container>
);
};

View file

@ -0,0 +1,14 @@
.minWidth200 {
min-width: 200px;
}
.flex1 {
flex: 1;
}
.terminalScrollArea {
flex: 1;
font-family: Monaco, Menlo, 'Ubuntu Mono', monospace;
font-size: 14px;
line-height: 1.4;
}

View file

@ -18,6 +18,7 @@ export interface GitHubRelease {
name: string; name: string;
published_at: string; published_at: string;
body: string; body: string;
html_url: string;
assets: GitHubAsset[]; assets: GitHubAsset[];
} }

View file

@ -7,3 +7,9 @@ export const formatFileSize = (bytes: number) => {
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizeUnit; return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizeUnit;
}; };
export const formatFileSizeInMB = (bytes: number) => {
if (bytes === 0) return '0 MB';
const mb = bytes / (1024 * 1024);
return parseFloat(mb.toFixed(1)) + ' MB';
};

142
yarn.lock
View file

@ -2054,106 +2054,106 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/eslint-plugin@npm:^8.39.1": "@typescript-eslint/eslint-plugin@npm:^8.40.0":
version: 8.39.1 version: 8.40.0
resolution: "@typescript-eslint/eslint-plugin@npm:8.39.1" resolution: "@typescript-eslint/eslint-plugin@npm:8.40.0"
dependencies: dependencies:
"@eslint-community/regexpp": "npm:^4.10.0" "@eslint-community/regexpp": "npm:^4.10.0"
"@typescript-eslint/scope-manager": "npm:8.39.1" "@typescript-eslint/scope-manager": "npm:8.40.0"
"@typescript-eslint/type-utils": "npm:8.39.1" "@typescript-eslint/type-utils": "npm:8.40.0"
"@typescript-eslint/utils": "npm:8.39.1" "@typescript-eslint/utils": "npm:8.40.0"
"@typescript-eslint/visitor-keys": "npm:8.39.1" "@typescript-eslint/visitor-keys": "npm:8.40.0"
graphemer: "npm:^1.4.0" graphemer: "npm:^1.4.0"
ignore: "npm:^7.0.0" ignore: "npm:^7.0.0"
natural-compare: "npm:^1.4.0" natural-compare: "npm:^1.4.0"
ts-api-utils: "npm:^2.1.0" ts-api-utils: "npm:^2.1.0"
peerDependencies: peerDependencies:
"@typescript-eslint/parser": ^8.39.1 "@typescript-eslint/parser": ^8.40.0
eslint: ^8.57.0 || ^9.0.0 eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0" typescript: ">=4.8.4 <6.0.0"
checksum: 10c0/7a55de558ed6ea6f09ee0b0d994b4a70e1df9f72e4afc7b3073de1b41504a36d905779304d59c34db700af60da3bb438c62480d30462a13b8b72d0b50318aeee checksum: 10c0/dc8889c3255bce6956432f099059179dd13826ba29670f81ba9238ecde46764ee63459eb73a7d88f4f30e1144a2f000d79c9e3f256fa759689d9b3b74d423bda
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/parser@npm:^8.39.1": "@typescript-eslint/parser@npm:^8.40.0":
version: 8.39.1 version: 8.40.0
resolution: "@typescript-eslint/parser@npm:8.39.1" resolution: "@typescript-eslint/parser@npm:8.40.0"
dependencies: dependencies:
"@typescript-eslint/scope-manager": "npm:8.39.1" "@typescript-eslint/scope-manager": "npm:8.40.0"
"@typescript-eslint/types": "npm:8.39.1" "@typescript-eslint/types": "npm:8.40.0"
"@typescript-eslint/typescript-estree": "npm:8.39.1" "@typescript-eslint/typescript-estree": "npm:8.40.0"
"@typescript-eslint/visitor-keys": "npm:8.39.1" "@typescript-eslint/visitor-keys": "npm:8.40.0"
debug: "npm:^4.3.4" debug: "npm:^4.3.4"
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0" typescript: ">=4.8.4 <6.0.0"
checksum: 10c0/da30372c4e8dee48a0c421996bf0bf73a62a57039ee6b817eda64de2d70fdb88dd20b50615c81be7e68fd29cdd7852829b859bb8539b4a4c78030f93acaf5664 checksum: 10c0/43ca9589b8a1f3f4b30a214c0e2254fa0ad43458ef1258b1d62c5aad52710ad11b9315b124cda79163274147b82201a5d76fab7de413e34bfe8e377142b71e98
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/project-service@npm:8.39.1": "@typescript-eslint/project-service@npm:8.40.0":
version: 8.39.1 version: 8.40.0
resolution: "@typescript-eslint/project-service@npm:8.39.1" resolution: "@typescript-eslint/project-service@npm:8.40.0"
dependencies: dependencies:
"@typescript-eslint/tsconfig-utils": "npm:^8.39.1" "@typescript-eslint/tsconfig-utils": "npm:^8.40.0"
"@typescript-eslint/types": "npm:^8.39.1" "@typescript-eslint/types": "npm:^8.40.0"
debug: "npm:^4.3.4" debug: "npm:^4.3.4"
peerDependencies: peerDependencies:
typescript: ">=4.8.4 <6.0.0" typescript: ">=4.8.4 <6.0.0"
checksum: 10c0/40207af4f4e2a260ea276766d502c4736f6dc5488e84bbab6444e2786289ece2dbca2686323c48d4e9c265e409a309bf3d97d4aa03767dff8cc7642b436bda35 checksum: 10c0/23d62e9ada9750136d0251f268bbe1f9784442ef258bb340a2e1e866749d8076730a14749d9a320d94d7c76df2d108caf21fe35e5dc100385f04be846dc979cb
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/scope-manager@npm:8.39.1": "@typescript-eslint/scope-manager@npm:8.40.0":
version: 8.39.1 version: 8.40.0
resolution: "@typescript-eslint/scope-manager@npm:8.39.1" resolution: "@typescript-eslint/scope-manager@npm:8.40.0"
dependencies: dependencies:
"@typescript-eslint/types": "npm:8.39.1" "@typescript-eslint/types": "npm:8.40.0"
"@typescript-eslint/visitor-keys": "npm:8.39.1" "@typescript-eslint/visitor-keys": "npm:8.40.0"
checksum: 10c0/9466db557c1a0eaaf24b0ece5810413d11390d046bf6e47c4074879e8dba0348b835a21106c842ab20ff85f2384312cf9e20bfe7684e31640696e29957003511 checksum: 10c0/48af81f9cdcec466994d290561e8d2fa3f6b156a898b71dd0e65633c896543b44729c5353596e84de2ae61bfd20e1398c3309cdfe86714a9663fd5aded4c9cd0
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/tsconfig-utils@npm:8.39.1, @typescript-eslint/tsconfig-utils@npm:^8.39.1": "@typescript-eslint/tsconfig-utils@npm:8.40.0, @typescript-eslint/tsconfig-utils@npm:^8.40.0":
version: 8.39.1 version: 8.40.0
resolution: "@typescript-eslint/tsconfig-utils@npm:8.39.1" resolution: "@typescript-eslint/tsconfig-utils@npm:8.40.0"
peerDependencies: peerDependencies:
typescript: ">=4.8.4 <6.0.0" typescript: ">=4.8.4 <6.0.0"
checksum: 10c0/664dff0b4ae908cb98c78f9ca73c36cf57c3a2206965d9d0659649ffc02347eb30e1452499671a425592f14a2a5c5eb82ae389b34f3c415a12119506b4ebb61c checksum: 10c0/c2366dcd802901d5cd4f59fc4eab7a00ed119aa4591ba59c507fe495d9af4cfca19431a603602ea675e4c861962230d1c2f100896903750cd1fcfc134702a7d0
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/type-utils@npm:8.39.1": "@typescript-eslint/type-utils@npm:8.40.0":
version: 8.39.1 version: 8.40.0
resolution: "@typescript-eslint/type-utils@npm:8.39.1" resolution: "@typescript-eslint/type-utils@npm:8.40.0"
dependencies: dependencies:
"@typescript-eslint/types": "npm:8.39.1" "@typescript-eslint/types": "npm:8.40.0"
"@typescript-eslint/typescript-estree": "npm:8.39.1" "@typescript-eslint/typescript-estree": "npm:8.40.0"
"@typescript-eslint/utils": "npm:8.39.1" "@typescript-eslint/utils": "npm:8.40.0"
debug: "npm:^4.3.4" debug: "npm:^4.3.4"
ts-api-utils: "npm:^2.1.0" ts-api-utils: "npm:^2.1.0"
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0" typescript: ">=4.8.4 <6.0.0"
checksum: 10c0/430dfefe040eae5f0c8dfbce37b5ce071095a28f335e74793923d113682e26313586e90f7bbe2c2f9bffb0da52ffdf5055ea36b96d9f218cef35aa14853122d5 checksum: 10c0/660b77d801b2538a4ccb65065269ad0e8370d0be985172b5ecb067f3eea22e64aa8af9e981b31bf2a34002339fe3253b09b55d181ce6d8242fc7daa80ac4aaca
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/types@npm:8.39.1, @typescript-eslint/types@npm:^8.39.1": "@typescript-eslint/types@npm:8.40.0, @typescript-eslint/types@npm:^8.40.0":
version: 8.39.1 version: 8.40.0
resolution: "@typescript-eslint/types@npm:8.39.1" resolution: "@typescript-eslint/types@npm:8.40.0"
checksum: 10c0/0e188d2d52509a24c500a87adf561387ffcac56b62cb9fd0ca1f929bb3d4eedb6b8f9d516c1890855d39930c9dd8d502d5b4600b8c9cc832d3ebb595d81c7533 checksum: 10c0/225374fff36d59288a5780667a7a1316c75090d5d60b70a8035ac18786120333ccd08dfdf0e05e30d5a82217e44c57b8708b769dd1eed89f12f2ac4d3a769f76
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/typescript-estree@npm:8.39.1": "@typescript-eslint/typescript-estree@npm:8.40.0":
version: 8.39.1 version: 8.40.0
resolution: "@typescript-eslint/typescript-estree@npm:8.39.1" resolution: "@typescript-eslint/typescript-estree@npm:8.40.0"
dependencies: dependencies:
"@typescript-eslint/project-service": "npm:8.39.1" "@typescript-eslint/project-service": "npm:8.40.0"
"@typescript-eslint/tsconfig-utils": "npm:8.39.1" "@typescript-eslint/tsconfig-utils": "npm:8.40.0"
"@typescript-eslint/types": "npm:8.39.1" "@typescript-eslint/types": "npm:8.40.0"
"@typescript-eslint/visitor-keys": "npm:8.39.1" "@typescript-eslint/visitor-keys": "npm:8.40.0"
debug: "npm:^4.3.4" debug: "npm:^4.3.4"
fast-glob: "npm:^3.3.2" fast-glob: "npm:^3.3.2"
is-glob: "npm:^4.0.3" is-glob: "npm:^4.0.3"
@ -2162,32 +2162,32 @@ __metadata:
ts-api-utils: "npm:^2.1.0" ts-api-utils: "npm:^2.1.0"
peerDependencies: peerDependencies:
typescript: ">=4.8.4 <6.0.0" typescript: ">=4.8.4 <6.0.0"
checksum: 10c0/1de1a37fed354600a08bc971492c2f14238f0a4bf07a43bedb416c17b7312d18bec92c68c8f2790bb0a1bffcd757f7962914be9f6213068f18f6c4fdde259af4 checksum: 10c0/6c1ffc17947cb36cbd987cf9705f85223ed1cce584b5244840e36a2b8480861f4dfdb0312f96afbc12e7d1ba586005f0d959042baa0a96a1913ac7ace8e8f6d4
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/utils@npm:8.39.1": "@typescript-eslint/utils@npm:8.40.0":
version: 8.39.1 version: 8.40.0
resolution: "@typescript-eslint/utils@npm:8.39.1" resolution: "@typescript-eslint/utils@npm:8.40.0"
dependencies: dependencies:
"@eslint-community/eslint-utils": "npm:^4.7.0" "@eslint-community/eslint-utils": "npm:^4.7.0"
"@typescript-eslint/scope-manager": "npm:8.39.1" "@typescript-eslint/scope-manager": "npm:8.40.0"
"@typescript-eslint/types": "npm:8.39.1" "@typescript-eslint/types": "npm:8.40.0"
"@typescript-eslint/typescript-estree": "npm:8.39.1" "@typescript-eslint/typescript-estree": "npm:8.40.0"
peerDependencies: peerDependencies:
eslint: ^8.57.0 || ^9.0.0 eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0" typescript: ">=4.8.4 <6.0.0"
checksum: 10c0/ebc01d736af43728df9a0915058d0c771dec9cc58846ffdcbb986c78e7dabf547ea7daecd75db58b2af88a3c2a43de8a7e5f81feefacfa31be173fc384d25d77 checksum: 10c0/6b3858b8725083fe7db7fb9bcbde930e758a6ba8ddedd1ed27d828fc1cbe04f54b774ef9144602f8eeaafeea9b19b4fd4c46fdad52a10ade99e6b282c7d0df92
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/visitor-keys@npm:8.39.1": "@typescript-eslint/visitor-keys@npm:8.40.0":
version: 8.39.1 version: 8.40.0
resolution: "@typescript-eslint/visitor-keys@npm:8.39.1" resolution: "@typescript-eslint/visitor-keys@npm:8.40.0"
dependencies: dependencies:
"@typescript-eslint/types": "npm:8.39.1" "@typescript-eslint/types": "npm:8.40.0"
eslint-visitor-keys: "npm:^4.2.1" eslint-visitor-keys: "npm:^4.2.1"
checksum: 10c0/4d81f6826a211bc2752e25cd16d1f415f28ebc92b35142402ec23f3765f2d00963b75ac06266ad9c674ca5b057d07d8c114116e5bf14f5465dde1d1aa60bc72f checksum: 10c0/592f1c8c2d3da43a7f74f8ead14f05fafc2e4609d5df36811cf92ead5dc94f6f669556a494048e4746cb3774c60bc52a8c83d75369d5e196778d935c70e7d3a1
languageName: node languageName: node
linkType: hard linkType: hard
@ -4431,8 +4431,8 @@ __metadata:
"@types/node": "npm:^24.3.0" "@types/node": "npm:^24.3.0"
"@types/react": "npm:^19.1.10" "@types/react": "npm:^19.1.10"
"@types/react-dom": "npm:^19.1.7" "@types/react-dom": "npm:^19.1.7"
"@typescript-eslint/eslint-plugin": "npm:^8.39.1" "@typescript-eslint/eslint-plugin": "npm:^8.40.0"
"@typescript-eslint/parser": "npm:^8.39.1" "@typescript-eslint/parser": "npm:^8.40.0"
"@vitejs/plugin-react": "npm:^5.0.0" "@vitejs/plugin-react": "npm:^5.0.0"
cross-env: "npm:^10.0.0" cross-env: "npm:^10.0.0"
cspell: "npm:^9.2.0" cspell: "npm:^9.2.0"
@ -4450,7 +4450,7 @@ __metadata:
husky: "npm:^9.1.7" husky: "npm:^9.1.7"
jiti: "npm:^2.5.1" jiti: "npm:^2.5.1"
lint-staged: "npm:^16.1.5" lint-staged: "npm:^16.1.5"
lucide-react: "npm:^0.539.0" lucide-react: "npm:^0.540.0"
prettier: "npm:^3.6.2" prettier: "npm:^3.6.2"
react: "npm:^19.1.1" react: "npm:^19.1.1"
react-dom: "npm:^19.1.1" react-dom: "npm:^19.1.1"
@ -5782,12 +5782,12 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"lucide-react@npm:^0.539.0": "lucide-react@npm:^0.540.0":
version: 0.539.0 version: 0.540.0
resolution: "lucide-react@npm:0.539.0" resolution: "lucide-react@npm:0.540.0"
peerDependencies: peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
checksum: 10c0/ae85244dc23a78c51cc5f829ee09321cf68201333ab3d5614ed73cda77a4cd12284d3630a5a2167a39e31e02811056336de76a597c6874b28bb9984baf876494 checksum: 10c0/f4dc8a540b1b079958fdf7a1804dfb3f9093a9393f0c0a4b32a7e839869df5e29946c4fe226f06edd3056f6eccfa77b53e44be89f42379e7fe31c4e59caee3fa
languageName: node languageName: node
linkType: hard linkType: hard