better terminal transition, main window bg color based on theme preference, center the launched window on open

This commit is contained in:
lone-cloud 2025-09-09 11:23:58 -07:00
parent de0718b8ee
commit a3a74af342
10 changed files with 159 additions and 51 deletions

View file

@ -40,10 +40,6 @@ export const App = () => {
downloadProgress, downloadProgress,
} = useKoboldVersions(); } = useKoboldVersions();
const setCurrentScreenWithTransition = (screen: Screen) => {
setCurrentScreen(screen);
};
useEffect(() => { useEffect(() => {
const checkInstallation = async () => { const checkInstallation = async () => {
await safeExecute(async () => { await safeExecute(async () => {
@ -58,11 +54,11 @@ export const App = () => {
setFrontendPreference(preference || 'koboldcpp'); setFrontendPreference(preference || 'koboldcpp');
if (!hasSeenWelcome) { if (!hasSeenWelcome) {
setCurrentScreenWithTransition('welcome'); setCurrentScreen('welcome');
} else if (currentVersion) { } else if (currentVersion) {
setCurrentScreenWithTransition('launch'); setCurrentScreen('launch');
} else { } else {
setCurrentScreenWithTransition('download'); setCurrentScreen('download');
} }
if (currentVersion) { if (currentVersion) {
@ -99,17 +95,17 @@ export const App = () => {
}, 'Error refreshing versions after download:'); }, 'Error refreshing versions after download:');
setTimeout(() => { setTimeout(() => {
setCurrentScreenWithTransition('launch'); setCurrentScreen('launch');
}, 100); }, 100);
}; };
const handleLaunch = () => { const handleLaunch = () => {
setActiveInterfaceTab('terminal'); setActiveInterfaceTab('terminal');
setCurrentScreenWithTransition('interface'); setCurrentScreen('interface');
}; };
const handleBackToLaunch = () => { const handleBackToLaunch = () => {
setCurrentScreenWithTransition('launch'); setCurrentScreen('launch');
}; };
const handleEject = async () => { const handleEject = async () => {
@ -141,9 +137,9 @@ export const App = () => {
const versions = await window.electronAPI.kobold.getInstalledVersions(); const versions = await window.electronAPI.kobold.getInstalledVersions();
if (versions.length > 0) { if (versions.length > 0) {
setCurrentScreenWithTransition('launch'); setCurrentScreen('launch');
} else { } else {
setCurrentScreenWithTransition('download'); setCurrentScreen('download');
} }
}; };

View file

@ -8,7 +8,6 @@ import {
import { import {
Box, Box,
ScrollArea, ScrollArea,
Text,
ActionIcon, ActionIcon,
useComputedColorScheme, useComputedColorScheme,
} from '@mantine/core'; } from '@mantine/core';
@ -36,12 +35,21 @@ export const TerminalTab = forwardRef<TerminalTabRef, TerminalTabProps>(
const [terminalContent, setTerminalContent] = useState<string>(''); const [terminalContent, setTerminalContent] = useState<string>('');
const [isUserScrolling, setIsUserScrolling] = useState<boolean>(false); const [isUserScrolling, setIsUserScrolling] = useState<boolean>(false);
const [shouldAutoScroll, setShouldAutoScroll] = useState<boolean>(true); const [shouldAutoScroll, setShouldAutoScroll] = useState<boolean>(true);
const [isVisible, setIsVisible] = useState<boolean>(false);
const scrollAreaRef = useRef<HTMLDivElement>(null); const scrollAreaRef = useRef<HTMLDivElement>(null);
const viewportRef = useRef<HTMLDivElement>(null); const viewportRef = useRef<HTMLDivElement>(null);
const lastScrollTop = useRef<number>(0); const lastScrollTop = useRef<number>(0);
const isDark = computedColorScheme === 'dark'; const isDark = computedColorScheme === 'dark';
useEffect(() => {
const timer = setTimeout(() => {
setIsVisible(true);
}, 150);
return () => clearTimeout(timer);
}, []);
const handleScroll = ({ y }: { y: number }) => { const handleScroll = ({ y }: { y: number }) => {
if (!viewportRef.current) return; if (!viewportRef.current) return;
@ -143,29 +151,25 @@ export const TerminalTab = forwardRef<TerminalTabRef, TerminalTabProps>(
offsetScrollbars={false} offsetScrollbars={false}
> >
<Box p="md"> <Box p="md">
{terminalContent.length === 0 ? ( <div
<Text c="dimmed" style={{ fontFamily: 'inherit' }}> style={{
Starting... margin: 0,
</Text> fontFamily:
) : ( 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
<div fontSize: '0.875em',
style={{ lineHeight: 1.4,
margin: 0, color: isDark
fontFamily: ? 'var(--mantine-color-gray-0)'
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', : 'var(--mantine-color-dark-filled)',
fontSize: '0.875em', whiteSpace: 'pre-wrap',
lineHeight: 1.4, wordBreak: 'break-word',
color: isDark opacity: isVisible ? 1 : 0,
? 'var(--mantine-color-gray-0)' transition: 'opacity 0.2s ease-in-out',
: 'var(--mantine-color-dark-filled)', }}
whiteSpace: 'pre-wrap', dangerouslySetInnerHTML={{
wordBreak: 'break-word', __html: processTerminalContent(terminalContent),
}} }}
dangerouslySetInnerHTML={{ />
__html: processTerminalContent(terminalContent),
}}
/>
)}
</Box> </Box>
</ScrollArea> </ScrollArea>

View file

@ -8,6 +8,7 @@ import {
useComputedColorScheme, useComputedColorScheme,
Slider, Slider,
TextInput, TextInput,
type MantineColorScheme,
} from '@mantine/core'; } from '@mantine/core';
import { Sun, Moon, Monitor } from 'lucide-react'; import { Sun, Moon, Monitor } from 'lucide-react';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
@ -32,19 +33,40 @@ export const AppearanceTab = () => {
); );
useEffect(() => { useEffect(() => {
const loadZoomLevel = async () => { const loadSettings = async () => {
const currentZoom = await safeExecute( const [currentZoom, savedColorScheme] = await Promise.all([
() => window.electronAPI.app.getZoomLevel(), safeExecute(
'Failed to load zoom level:' () => window.electronAPI.app.getZoomLevel(),
); 'Failed to load zoom level:'
),
safeExecute(
() => window.electronAPI.app.getColorScheme(),
'Failed to load color scheme:'
),
]);
if (typeof currentZoom === 'number') { if (typeof currentZoom === 'number') {
setZoomLevel(currentZoom); setZoomLevel(currentZoom);
setZoomPercentage(zoomLevelToPercentage(currentZoom).toString()); setZoomPercentage(zoomLevelToPercentage(currentZoom).toString());
} }
if (savedColorScheme && savedColorScheme !== colorScheme) {
setColorScheme(savedColorScheme as MantineColorScheme);
}
}; };
void loadZoomLevel(); void loadSettings();
}, []); }, [colorScheme, setColorScheme]);
const handleColorSchemeChange = async (value: string) => {
const newColorScheme = value as MantineColorScheme;
setColorScheme(newColorScheme);
await safeExecute(
() => window.electronAPI.app.setColorScheme(newColorScheme),
'Failed to save color scheme:'
);
};
const handleZoomChange = async (newZoomLevel: number) => { const handleZoomChange = async (newZoomLevel: number) => {
setZoomLevel(newZoomLevel); setZoomLevel(newZoomLevel);
@ -83,9 +105,7 @@ export const AppearanceTab = () => {
<SegmentedControl <SegmentedControl
fullWidth fullWidth
value={colorScheme} value={colorScheme}
onChange={(value) => onChange={handleColorSchemeChange}
setColorScheme(value as 'light' | 'dark' | 'auto')
}
styles={(theme) => ({ styles={(theme) => ({
indicator: { indicator: {
backgroundColor: isDark backgroundColor: isDark

View file

@ -10,6 +10,7 @@ import {
Select, Select,
Box, Box,
Anchor, Anchor,
Badge,
} from '@mantine/core'; } from '@mantine/core';
import { Folder, FolderOpen, Monitor } from 'lucide-react'; import { Folder, FolderOpen, Monitor } from 'lucide-react';
import type { FrontendPreference } from '@/types'; import type { FrontendPreference } from '@/types';
@ -24,6 +25,7 @@ interface FrontendRequirement {
interface FrontendConfig { interface FrontendConfig {
value: string; value: string;
label: string; label: string;
badges: string[];
requirements?: FrontendRequirement[]; requirements?: FrontendRequirement[];
requirementCheck?: () => Promise<boolean>; requirementCheck?: () => Promise<boolean>;
} }
@ -44,10 +46,15 @@ export const GeneralTab = ({
const frontendConfigs: FrontendConfig[] = useMemo( const frontendConfigs: FrontendConfig[] = useMemo(
() => [ () => [
{ value: 'koboldcpp', label: 'Built-in Interface' }, {
value: 'koboldcpp',
label: 'Built-in Interface',
badges: ['Text', 'Image'],
},
{ {
value: 'sillytavern', value: 'sillytavern',
label: FRONTENDS.SILLYTAVERN, label: FRONTENDS.SILLYTAVERN,
badges: ['Text', 'Image'],
requirements: [ requirements: [
{ {
id: 'nodejs', id: 'nodejs',
@ -60,6 +67,7 @@ export const GeneralTab = ({
{ {
value: 'openwebui', value: 'openwebui',
label: FRONTENDS.OPENWEBUI, label: FRONTENDS.OPENWEBUI,
badges: ['Text'],
requirements: [ requirements: [
{ {
id: 'uv', id: 'uv',
@ -186,6 +194,30 @@ export const GeneralTab = ({
} }
}; };
const renderSelectOption = ({
option,
}: {
option: { value: string; label: string; disabled?: boolean };
}) => {
const config = frontendConfigs.find((c) => c.value === option.value);
if (!config) return <Text>{option.label}</Text>;
return (
<Group justify="space-between" wrap="nowrap">
<Text size="sm" truncate c={option.disabled ? 'dimmed' : undefined}>
{config.label}
</Text>
<Group gap={4}>
{config.badges.map((badge) => (
<Badge key={badge} size="sm" variant="light" color="blue">
{badge}
</Badge>
))}
</Group>
</Group>
);
};
return ( return (
<Stack gap="lg" h="100%"> <Stack gap="lg" h="100%">
<div> <div>
@ -258,6 +290,7 @@ export const GeneralTab = ({
disabled: !isFrontendAvailable(config.value), disabled: !isFrontendAvailable(config.value),
}))} }))}
leftSection={<Monitor style={{ width: rem(16), height: rem(16) }} />} leftSection={<Monitor style={{ width: rem(16), height: rem(16) }} />}
renderOption={renderSelectOption}
/> />
<Box mt="sm"> <Box mt="sm">

View file

@ -1,5 +1,6 @@
import { ipcMain, shell, app } from 'electron'; import { ipcMain, shell, app } from 'electron';
import * as os from 'os'; import * as os from 'os';
import type { MantineColorScheme } from '@mantine/core';
import { import {
launchKoboldCpp, launchKoboldCpp,
downloadRelease, downloadRelease,
@ -19,6 +20,8 @@ import {
getSelectedConfig, getSelectedConfig,
setSelectedConfig, setSelectedConfig,
getInstallDir, getInstallDir,
getColorScheme,
setColorScheme,
} from '@/main/modules/config'; } from '@/main/modules/config';
import { logError, getLogsDirectory } from '@/main/modules/logging'; import { logError, getLogsDirectory } from '@/main/modules/logging';
import { getSillyTavernManager } from '@/main/modules/sillytavern'; import { getSillyTavernManager } from '@/main/modules/sillytavern';
@ -198,6 +201,15 @@ export function setupIPCHandlers() {
} }
}); });
ipcMain.handle('app:getColorScheme', () => getColorScheme());
ipcMain.handle(
'app:setColorScheme',
async (_, colorScheme: MantineColorScheme) => {
await setColorScheme(colorScheme);
}
);
const mainWindow = getMainWindow(); const mainWindow = getMainWindow();
if (mainWindow) { if (mainWindow) {
mainWindow.webContents.once('did-finish-load', async () => { mainWindow.webContents.once('did-finish-load', async () => {

View file

@ -2,8 +2,10 @@ import { logError } from '@/main/modules/logging';
import { readJsonFile, writeJsonFile } from '@/utils/fs'; import { readJsonFile, writeJsonFile } from '@/utils/fs';
import { getConfigDir } from '@/utils/path'; import { getConfigDir } from '@/utils/path';
import type { FrontendPreference } from '@/types'; import type { FrontendPreference } from '@/types';
import type { MantineColorScheme } from '@mantine/core';
import { homedir } from 'os'; import { homedir } from 'os';
import { join } from 'path'; import { join } from 'path';
import { nativeTheme } from 'electron';
import { PRODUCT_NAME } from '@/constants'; import { PRODUCT_NAME } from '@/constants';
type ConfigValue = string | number | boolean | unknown[] | undefined; type ConfigValue = string | number | boolean | unknown[] | undefined;
@ -13,6 +15,7 @@ interface AppConfig {
currentKoboldBinary?: string; currentKoboldBinary?: string;
selectedConfig?: string; selectedConfig?: string;
frontendPreference?: FrontendPreference; frontendPreference?: FrontendPreference;
colorScheme?: MantineColorScheme;
[key: string]: ConfigValue; [key: string]: ConfigValue;
} }
@ -92,3 +95,24 @@ export async function setSelectedConfig(configName: string) {
config.selectedConfig = configName; config.selectedConfig = configName;
await saveConfig(); await saveConfig();
} }
export function getColorScheme() {
return config.colorScheme || 'auto';
}
export async function setColorScheme(colorScheme: MantineColorScheme) {
config.colorScheme = colorScheme;
await saveConfig();
}
export function getBackgroundColor(): string {
const colorScheme = getColorScheme();
if (colorScheme === 'light') {
return '#ffffff';
} else if (colorScheme === 'dark') {
return '#1a1b1e';
} else {
return nativeTheme.shouldUseDarkColors ? '#1a1b1e' : '#ffffff';
}
}

View file

@ -642,7 +642,7 @@ export async function launchKoboldCpp(
koboldProcess = child; koboldProcess = child;
const commandLine = `$ ${currentVersion.path} ${finalArgs.join(' ')}`; const commandLine = `${currentVersion.path} ${finalArgs.join(' ')}`;
sendKoboldOutput(commandLine); sendKoboldOutput(commandLine);

View file

@ -4,21 +4,29 @@ import { stripVTControlCharacters } from 'util';
import { PRODUCT_NAME } from '../../constants'; import { PRODUCT_NAME } from '../../constants';
import type { IPCChannel, IPCChannelPayloads } from '@/types/ipc'; import type { IPCChannel, IPCChannelPayloads } from '@/types/ipc';
import { isDevelopment } from '@/utils/environment'; import { isDevelopment } from '@/utils/environment';
import { getBackgroundColor } from './config';
let mainWindow: BrowserWindow | null = null; let mainWindow: BrowserWindow | null = null;
export function createMainWindow() { export function createMainWindow() {
const { size } = screen.getPrimaryDisplay(); const { size } = screen.getPrimaryDisplay();
const windowHeight = Math.floor(size.height * 0.86); const windowHeight = Math.floor(size.height * 0.86);
const windowWidth = 800;
const icon = isDevelopment
? join(__dirname, '../../src/assets/icon.png')
: join(__dirname, '../../assets/icon.png');
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
width: 1000, width: windowWidth,
height: windowHeight, height: windowHeight,
x: Math.floor((size.width - windowWidth) / 2),
y: Math.floor((size.height - windowHeight) / 2),
frame: false, frame: false,
title: PRODUCT_NAME, title: PRODUCT_NAME,
show: false, show: false,
backgroundColor: '#ffffff', backgroundColor: getBackgroundColor(),
icon: join(__dirname, '../../src/assets/icon.png'), icon,
webPreferences: { webPreferences: {
nodeIntegration: false, nodeIntegration: false,
contextIsolation: true, contextIsolation: true,
@ -60,6 +68,11 @@ export function createMainWindow() {
mainWindow.webContents.setWindowOpenHandler(() => ({ mainWindow.webContents.setWindowOpenHandler(() => ({
action: 'allow', action: 'allow',
overrideBrowserWindowOptions: {
icon,
title: PRODUCT_NAME,
backgroundColor: getBackgroundColor(),
},
})); }));
mainWindow.on('close', () => { mainWindow.on('close', () => {

View file

@ -84,6 +84,9 @@ const appAPI: AppAPI = {
closeWindow: () => ipcRenderer.invoke('app:closeWindow'), closeWindow: () => ipcRenderer.invoke('app:closeWindow'),
getZoomLevel: () => ipcRenderer.invoke('app:getZoomLevel'), getZoomLevel: () => ipcRenderer.invoke('app:getZoomLevel'),
setZoomLevel: (level) => ipcRenderer.invoke('app:setZoomLevel', level), setZoomLevel: (level) => ipcRenderer.invoke('app:setZoomLevel', level),
getColorScheme: () => ipcRenderer.invoke('app:getColorScheme'),
setColorScheme: (colorScheme) =>
ipcRenderer.invoke('app:setColorScheme', colorScheme),
}; };
const configAPI: ConfigAPI = { const configAPI: ConfigAPI = {

View file

@ -5,6 +5,7 @@ import type {
GPUMemoryInfo, GPUMemoryInfo,
} from '@/types/hardware'; } from '@/types/hardware';
import type { BackendOption, BackendSupport } from '@/types'; import type { BackendOption, BackendSupport } from '@/types';
import type { MantineColorScheme } from '@mantine/core';
export interface GitHubAsset { export interface GitHubAsset {
name: string; name: string;
@ -150,6 +151,8 @@ export interface AppAPI {
closeWindow: () => void; closeWindow: () => void;
getZoomLevel: () => Promise<number>; getZoomLevel: () => Promise<number>;
setZoomLevel: (level: number) => Promise<void>; setZoomLevel: (level: number) => Promise<void>;
getColorScheme: () => Promise<MantineColorScheme>;
setColorScheme: (colorScheme: MantineColorScheme) => Promise<void>;
} }
export interface ConfigAPI { export interface ConfigAPI {