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

View file

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

View file

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

View file

@ -10,6 +10,7 @@ import {
Select,
Box,
Anchor,
Badge,
} from '@mantine/core';
import { Folder, FolderOpen, Monitor } from 'lucide-react';
import type { FrontendPreference } from '@/types';
@ -24,6 +25,7 @@ interface FrontendRequirement {
interface FrontendConfig {
value: string;
label: string;
badges: string[];
requirements?: FrontendRequirement[];
requirementCheck?: () => Promise<boolean>;
}
@ -44,10 +46,15 @@ export const GeneralTab = ({
const frontendConfigs: FrontendConfig[] = useMemo(
() => [
{ value: 'koboldcpp', label: 'Built-in Interface' },
{
value: 'koboldcpp',
label: 'Built-in Interface',
badges: ['Text', 'Image'],
},
{
value: 'sillytavern',
label: FRONTENDS.SILLYTAVERN,
badges: ['Text', 'Image'],
requirements: [
{
id: 'nodejs',
@ -60,6 +67,7 @@ export const GeneralTab = ({
{
value: 'openwebui',
label: FRONTENDS.OPENWEBUI,
badges: ['Text'],
requirements: [
{
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 (
<Stack gap="lg" h="100%">
<div>
@ -258,6 +290,7 @@ export const GeneralTab = ({
disabled: !isFrontendAvailable(config.value),
}))}
leftSection={<Monitor style={{ width: rem(16), height: rem(16) }} />}
renderOption={renderSelectOption}
/>
<Box mt="sm">

View file

@ -1,5 +1,6 @@
import { ipcMain, shell, app } from 'electron';
import * as os from 'os';
import type { MantineColorScheme } from '@mantine/core';
import {
launchKoboldCpp,
downloadRelease,
@ -19,6 +20,8 @@ import {
getSelectedConfig,
setSelectedConfig,
getInstallDir,
getColorScheme,
setColorScheme,
} from '@/main/modules/config';
import { logError, getLogsDirectory } from '@/main/modules/logging';
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();
if (mainWindow) {
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 { getConfigDir } from '@/utils/path';
import type { FrontendPreference } from '@/types';
import type { MantineColorScheme } from '@mantine/core';
import { homedir } from 'os';
import { join } from 'path';
import { nativeTheme } from 'electron';
import { PRODUCT_NAME } from '@/constants';
type ConfigValue = string | number | boolean | unknown[] | undefined;
@ -13,6 +15,7 @@ interface AppConfig {
currentKoboldBinary?: string;
selectedConfig?: string;
frontendPreference?: FrontendPreference;
colorScheme?: MantineColorScheme;
[key: string]: ConfigValue;
}
@ -92,3 +95,24 @@ export async function setSelectedConfig(configName: string) {
config.selectedConfig = configName;
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;
const commandLine = `$ ${currentVersion.path} ${finalArgs.join(' ')}`;
const commandLine = `${currentVersion.path} ${finalArgs.join(' ')}`;
sendKoboldOutput(commandLine);

View file

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

View file

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

View file

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