new BackendCrashModal component to notify the user if kcpp crashes at runtime, replace the kcpp tunnel implementation with our own custom one and present its copyable URL in the statusbar

This commit is contained in:
Egor 2025-12-02 18:52:47 -08:00
parent f825cd0d2f
commit c111dfbddc
13 changed files with 447 additions and 86 deletions

View file

@ -12,7 +12,7 @@
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"[typescript]": { "[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "prettier.prettier-vscode"
}, },
"[typescriptreact]": { "[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"

View file

@ -44,8 +44,8 @@
"@types/react": "^19.2.7", "@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@types/yauzl": "^2.10.3", "@types/yauzl": "^2.10.3",
"@typescript-eslint/eslint-plugin": "^8.48.0", "@typescript-eslint/eslint-plugin": "^8.48.1",
"@typescript-eslint/parser": "^8.48.0", "@typescript-eslint/parser": "^8.48.1",
"@vitejs/plugin-react": "^5.1.1", "@vitejs/plugin-react": "^5.1.1",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
"electron": "^38.7.2", "electron": "^38.7.2",
@ -74,6 +74,7 @@
"@mantine/core": "^8.3.9", "@mantine/core": "^8.3.9",
"@mantine/hooks": "^8.3.9", "@mantine/hooks": "^8.3.9",
"@uiw/react-codemirror": "^4.25.3", "@uiw/react-codemirror": "^4.25.3",
"cloudflared": "^0.7.1",
"electron-updater": "^6.6.2", "electron-updater": "^6.6.2",
"execa": "^9.6.1", "execa": "^9.6.1",
"lucide-react": "^0.555.0", "lucide-react": "^0.555.0",

View file

@ -0,0 +1,66 @@
import { Text, Group, Button, Stack } from '@mantine/core';
import { Modal } from '@/components/Modal';
import type { KoboldCrashInfo } from '@/types/ipc';
interface BackendCrashModalProps {
opened: boolean;
onClose: () => void;
crashInfo: KoboldCrashInfo | null;
}
const getCrashDescription = (crashInfo: KoboldCrashInfo) => {
if (crashInfo.errorMessage) {
return crashInfo.errorMessage;
}
if (crashInfo.signal) {
const signalDescriptions: Record<string, string> = {
SIGKILL: 'The process was forcefully terminated',
SIGSEGV: 'Memory access violation (segmentation fault)',
SIGABRT: 'The process aborted unexpectedly',
SIGBUS: 'Bus error (invalid memory access)',
SIGFPE: 'Floating-point exception',
SIGILL: 'Illegal instruction',
SIGTERM: 'The process was terminated',
SIGSTOP: 'The process was stopped',
SIGHUP: 'The process lost its controlling terminal',
};
return (
signalDescriptions[crashInfo.signal] ||
`Terminated by signal ${crashInfo.signal}`
);
}
if (crashInfo.exitCode !== null) {
return `Process exited with error code ${crashInfo.exitCode}`;
}
return 'The process terminated unexpectedly';
};
export const BackendCrashModal = ({
opened,
onClose,
crashInfo,
}: BackendCrashModalProps) => {
if (!crashInfo) return null;
const description = getCrashDescription(crashInfo);
return (
<Modal opened={opened} onClose={onClose} title="Backend Crashed">
<Stack gap="md">
<Text size="sm">{description}</Text>
<Text size="sm" c="dimmed">
Check the terminal output for more details about the crash.
</Text>
<Group justify="flex-end" gap="sm">
<Button onClick={onClose}>Dismiss</Button>
</Group>
</Stack>
</Modal>
);
};

View file

@ -1,8 +1,16 @@
import { useEffect, useState } from 'react'; import { useEffect, useState, useMemo } from 'react';
import { Group, AppShell, ActionIcon, Tooltip } from '@mantine/core'; import {
import { NotepadText } from 'lucide-react'; Group,
AppShell,
ActionIcon,
Tooltip,
CopyButton,
} from '@mantine/core';
import { NotepadText, Globe, Check } from 'lucide-react';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { useNotepadStore } from '@/stores/notepad'; import { useNotepadStore } from '@/stores/notepad';
import { useLaunchConfigStore } from '@/stores/launchConfig';
import { getTunnelInterfaceUrl } from '@/utils/interface';
import type { import type {
CpuMetrics, CpuMetrics,
MemoryMetrics, MemoryMetrics,
@ -20,9 +28,27 @@ export const StatusBar = ({ maxDataPoints = 60 }: StatusBarProps) => {
null null
); );
const [gpuMetrics, setGpuMetrics] = useState<GpuMetrics | null>(null); const [gpuMetrics, setGpuMetrics] = useState<GpuMetrics | null>(null);
const { resolvedColorScheme: colorScheme, systemMonitoringEnabled } = const [tunnelBaseUrl, setTunnelBaseUrl] = useState<string | null>(null);
usePreferencesStore(); const {
resolvedColorScheme: colorScheme,
systemMonitoringEnabled,
frontendPreference,
imageGenerationFrontendPreference,
} = usePreferencesStore();
const { isVisible, setVisible } = useNotepadStore(); const { isVisible, setVisible } = useNotepadStore();
const { isImageGenerationMode } = useLaunchConfigStore();
const tunnelUrl = useMemo(() => {
if (!tunnelBaseUrl) return null;
if (frontendPreference === 'sillytavern' || frontendPreference === 'openwebui') {
return tunnelBaseUrl;
}
return getTunnelInterfaceUrl(tunnelBaseUrl, {
frontendPreference,
imageGenerationFrontendPreference,
isImageGenerationMode,
});
}, [tunnelBaseUrl, frontendPreference, imageGenerationFrontendPreference, isImageGenerationMode]);
useEffect(() => { useEffect(() => {
if (!systemMonitoringEnabled) { if (!systemMonitoringEnabled) {
@ -63,6 +89,11 @@ export const StatusBar = ({ maxDataPoints = 60 }: StatusBarProps) => {
}; };
}, [maxDataPoints, systemMonitoringEnabled]); }, [maxDataPoints, systemMonitoringEnabled]);
useEffect(() => {
const cleanup = window.electronAPI.kobold.onTunnelUrlChanged(setTunnelBaseUrl);
return cleanup;
}, []);
const displayCpuMetrics = systemMonitoringEnabled ? cpuMetrics : null; const displayCpuMetrics = systemMonitoringEnabled ? cpuMetrics : null;
const displayMemoryMetrics = systemMonitoringEnabled ? memoryMetrics : null; const displayMemoryMetrics = systemMonitoringEnabled ? memoryMetrics : null;
const displayGpuMetrics = systemMonitoringEnabled ? gpuMetrics : null; const displayGpuMetrics = systemMonitoringEnabled ? gpuMetrics : null;
@ -90,6 +121,25 @@ export const StatusBar = ({ maxDataPoints = 60 }: StatusBarProps) => {
<NotepadText size="1.25rem" /> <NotepadText size="1.25rem" />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
{tunnelUrl && (
<CopyButton value={tunnelUrl}>
{({ copied, copy }) => (
<Tooltip
label={copied ? 'Copied!' : 'Copy Tunnel URL'}
position="top"
>
<ActionIcon
variant="subtle"
size="sm"
color={copied ? 'teal' : undefined}
onClick={copy}
>
{copied ? <Check size="1.25rem" /> : <Globe size="1.25rem" />}
</ActionIcon>
</Tooltip>
)}
</CopyButton>
)}
</Group> </Group>
<Group gap="xs"> <Group gap="xs">

View file

@ -9,6 +9,7 @@ import {
} from '@mantine/core'; } from '@mantine/core';
import { UpdateAvailableModal } from '@/components/App/UpdateAvailableModal'; import { UpdateAvailableModal } from '@/components/App/UpdateAvailableModal';
import { EjectConfirmModal } from '@/components/App/EjectConfirmModal'; import { EjectConfirmModal } from '@/components/App/EjectConfirmModal';
import { BackendCrashModal } from '@/components/App/BackendCrashModal';
import { TitleBar } from '@/components/App/TitleBar'; import { TitleBar } from '@/components/App/TitleBar';
import { StatusBar } from '@/components/App/StatusBar'; import { StatusBar } from '@/components/App/StatusBar';
import { ErrorBoundary } from '@/components/App/ErrorBoundary'; import { ErrorBoundary } from '@/components/App/ErrorBoundary';
@ -21,6 +22,7 @@ import { useLaunchConfigStore } from '@/stores/launchConfig';
import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants'; import { STATUSBAR_HEIGHT, TITLEBAR_HEIGHT } from '@/constants';
import type { DownloadItem } from '@/types/electron'; import type { DownloadItem } from '@/types/electron';
import type { InterfaceTab, Screen } from '@/types'; import type { InterfaceTab, Screen } from '@/types';
import type { KoboldCrashInfo } from '@/types/ipc';
export const App = () => { export const App = () => {
const [currentScreen, setCurrentScreen] = useState<Screen | null>(null); const [currentScreen, setCurrentScreen] = useState<Screen | null>(null);
@ -28,6 +30,7 @@ export const App = () => {
const [activeInterfaceTab, setActiveInterfaceTab] = const [activeInterfaceTab, setActiveInterfaceTab] =
useState<InterfaceTab>('terminal'); useState<InterfaceTab>('terminal');
const [ejectConfirmModalOpen, setEjectConfirmModalOpen] = useState(false); const [ejectConfirmModalOpen, setEjectConfirmModalOpen] = useState(false);
const [crashInfo, setCrashInfo] = useState<KoboldCrashInfo | null>(null);
const isInterfaceScreen = currentScreen === 'interface'; const isInterfaceScreen = currentScreen === 'interface';
const { resolvedColorScheme: appColorScheme, systemMonitoringEnabled } = const { resolvedColorScheme: appColorScheme, systemMonitoringEnabled } =
@ -73,6 +76,18 @@ export const App = () => {
}; };
}, []); }, []);
useEffect(() => {
const crashCleanup = window.electronAPI.kobold.onKoboldCrashed(
(crashData) => {
setCrashInfo(crashData);
}
);
return () => {
crashCleanup();
};
}, []);
const { const {
updateInfo: binaryUpdateInfo, updateInfo: binaryUpdateInfo,
showUpdateModal, showUpdateModal,
@ -245,6 +260,12 @@ export const App = () => {
onConfirm={handleEjectConfirm} onConfirm={handleEjectConfirm}
/> />
<BackendCrashModal
opened={crashInfo !== null}
onClose={() => setCrashInfo(null)}
crashInfo={crashInfo}
/>
<NotepadContainer /> <NotepadContainer />
</AppShell> </AppShell>
); );

View file

@ -3,8 +3,9 @@ import { platform } from 'process';
import { terminateProcess } from '@/utils/node/process'; import { terminateProcess } from '@/utils/node/process';
import { logError, safeExecute } from '@/utils/node/logging'; import { logError, safeExecute } from '@/utils/node/logging';
import { sendKoboldOutput } from '@/main/modules/window'; import { sendKoboldOutput, sendToRenderer } from '@/main/modules/window';
import { SERVER_READY_SIGNALS } from '@/constants'; import { SERVER_READY_SIGNALS } from '@/constants';
import type { KoboldCrashInfo } from '@/types/ipc';
import { pathExists } from '@/utils/node/fs'; import { pathExists } from '@/utils/node/fs';
import { parseKoboldConfig } from '@/utils/node/kobold'; import { parseKoboldConfig } from '@/utils/node/kobold';
import { getCurrentBackend } from '../backend'; import { getCurrentBackend } from '../backend';
@ -17,6 +18,7 @@ import { startFrontend as startSillyTavernFrontend } from '@/main/modules/sillyt
import { startFrontend as startOpenWebUIFrontend } from '@/main/modules/openwebui'; import { startFrontend as startOpenWebUIFrontend } from '@/main/modules/openwebui';
import { patchKliteEmbd, patchKcppSduiEmbd, filterSpam } from './patches'; import { patchKliteEmbd, patchKcppSduiEmbd, filterSpam } from './patches';
import { startProxy, stopProxy } from '../proxy'; import { startProxy, stopProxy } from '../proxy';
import { startTunnel, stopTunnel } from '../tunnel';
import { resolveModelPath, abortActiveDownloads } from '../model-download'; import { resolveModelPath, abortActiveDownloads } from '../model-download';
import type { import type {
FrontendPreference, FrontendPreference,
@ -25,6 +27,8 @@ import type {
} from '@/types'; } from '@/types';
let koboldProcess: ChildProcess | null = null; let koboldProcess: ChildProcess | null = null;
let isIntentionalStop = false;
let hasProcessStartedSuccessfully = false;
const preLaunchProcesses = new Set<ChildProcess>(); const preLaunchProcesses = new Set<ChildProcess>();
function spawnPreLaunchCommands(commands: string[]) { function spawnPreLaunchCommands(commands: string[]) {
@ -149,6 +153,9 @@ export async function launchKoboldCpp(
await stopKoboldCpp(); await stopKoboldCpp();
} }
isIntentionalStop = false;
hasProcessStartedSuccessfully = false;
if (preLaunchCommands.length > 0) { if (preLaunchCommands.length > 0) {
spawnPreLaunchCommands(preLaunchCommands); spawnPreLaunchCommands(preLaunchCommands);
} }
@ -172,7 +179,14 @@ export async function launchKoboldCpp(
const binaryDir = currentBackend.path.split(/[/\\]/).slice(0, -1).join('/'); const binaryDir = currentBackend.path.split(/[/\\]/).slice(0, -1).join('/');
const { isImageMode, isTextMode, debugmode } = parseKoboldConfig(args); const {
isImageMode,
isTextMode,
debugmode,
remotetunnel,
host: koboldHost,
port: koboldPort,
} = parseKoboldConfig(args);
if (frontendPreference === 'koboldcpp') { if (frontendPreference === 'koboldcpp') {
if (isImageMode) { if (isImageMode) {
@ -186,12 +200,12 @@ export async function launchKoboldCpp(
} }
const resolvedArgs = await resolveModelPaths(args); const resolvedArgs = await resolveModelPaths(args);
const finalArgs = [...resolvedArgs]; const finalArgs = resolvedArgs.filter((arg) => arg !== '--remotetunnel');
const { host: koboldHost, port: koboldPort } = parseKoboldConfig(args);
await startProxy(koboldHost, koboldPort); await startProxy(koboldHost, koboldPort);
const child = spawn(currentBackend.path, finalArgs, { const child = spawn(currentBackend.path, finalArgs, {
cwd: binaryDir,
stdio: ['pipe', 'pipe', 'pipe'], stdio: ['pipe', 'pipe', 'pipe'],
detached: false, detached: false,
}); });
@ -202,6 +216,10 @@ export async function launchKoboldCpp(
sendKoboldOutput(commandLine); sendKoboldOutput(commandLine);
if (remotetunnel) {
startTunnel();
}
let readyResolve: let readyResolve:
| ((value: { success: boolean; pid?: number; error?: string }) => void) | ((value: { success: boolean; pid?: number; error?: string }) => void)
| null = null; | null = null;
@ -217,6 +235,10 @@ export async function launchKoboldCpp(
readyReject = reject; readyReject = reject;
}); });
const handleServerReady = () => {
readyResolve?.({ success: true, pid: child.pid });
};
child.stdout?.on('data', (data) => { child.stdout?.on('data', (data) => {
const output = data.toString(); const output = data.toString();
const filtered = debugmode ? output : filterSpam(output); const filtered = debugmode ? output : filterSpam(output);
@ -226,7 +248,8 @@ export async function launchKoboldCpp(
if (!isReady && output.includes(SERVER_READY_SIGNALS.KOBOLDCPP)) { if (!isReady && output.includes(SERVER_READY_SIGNALS.KOBOLDCPP)) {
isReady = true; isReady = true;
readyResolve?.({ success: true, pid: child.pid }); hasProcessStartedSuccessfully = true;
handleServerReady();
} }
}); });
@ -239,20 +262,35 @@ export async function launchKoboldCpp(
if (!isReady && output.includes(SERVER_READY_SIGNALS.KOBOLDCPP)) { if (!isReady && output.includes(SERVER_READY_SIGNALS.KOBOLDCPP)) {
isReady = true; isReady = true;
readyResolve?.({ success: true, pid: child.pid }); hasProcessStartedSuccessfully = true;
handleServerReady();
} }
}); });
child.on('exit', (code, signal) => { child.on('exit', (code, signal) => {
const isCrash = signal !== null || (code !== null && code !== 0);
const displayMessage = signal const displayMessage = signal
? `\n[INFO] Process terminated with signal ${signal}` ? `\nProcess terminated with signal ${signal}`
: code === 0 : code === 0
? `\n[INFO] Process exited successfully` ? `\nProcess exited successfully`
: code && (code > 1 || code < 0) : code && (code > 1 || code < 0)
? `\n[ERROR] Process exited with code ${code}` ? `\nProcess exited with code ${code}`
: `\n[INFO] Process exited with code ${code}`; : `\nProcess exited with code ${code}`;
sendKoboldOutput(displayMessage); sendKoboldOutput(displayMessage);
const wasIntentionalStop = isIntentionalStop;
const hadStartedSuccessfully = hasProcessStartedSuccessfully;
koboldProcess = null; koboldProcess = null;
isIntentionalStop = false;
hasProcessStartedSuccessfully = false;
if (isCrash && hadStartedSuccessfully && !wasIntentionalStop) {
const crashInfo: KoboldCrashInfo = {
exitCode: code,
signal,
};
sendToRenderer('kobold-crashed', crashInfo);
}
if (!isReady) { if (!isReady) {
readyReject?.( readyReject?.(
@ -266,9 +304,18 @@ export async function launchKoboldCpp(
child.on('error', (error) => { child.on('error', (error) => {
logError(`Process error: ${error.message}`, error); logError(`Process error: ${error.message}`, error);
sendKoboldOutput(`\n[ERROR] Process error: ${error.message}\n`); sendKoboldOutput(`\nProcess error: ${error.message}\n`);
koboldProcess = null; koboldProcess = null;
if (isReady) {
const crashInfo: KoboldCrashInfo = {
exitCode: null,
signal: null,
errorMessage: error.message,
};
sendToRenderer('kobold-crashed', crashInfo);
}
if (!isReady) { if (!isReady) {
readyReject?.(error); readyReject?.(error);
} }
@ -285,7 +332,9 @@ export async function launchKoboldCpp(
export async function stopKoboldCpp() { export async function stopKoboldCpp() {
abortActiveDownloads(); abortActiveDownloads();
stopProxy(); stopProxy();
stopTunnel();
stopPreLaunchProcesses(); stopPreLaunchProcesses();
isIntentionalStop = true;
return terminateProcess(koboldProcess); return terminateProcess(koboldProcess);
} }

View file

@ -0,0 +1,97 @@
import fs from 'fs';
import { Tunnel, bin, install } from 'cloudflared';
import { logError } from '@/utils/node/logging';
import { sendKoboldOutput, sendToRenderer } from '../window';
import { PROXY } from '@/constants/proxy';
let activeTunnel: Tunnel | null = null;
let tunnelUrl: string | null = null;
export const startTunnel = async () => {
if (activeTunnel) {
return tunnelUrl;
}
try {
sendKoboldOutput('Starting Cloudflare tunnel...');
if (!fs.existsSync(bin)) {
sendKoboldOutput('Installing cloudflared binary...');
await install(bin);
sendKoboldOutput('cloudflared binary installed');
}
const tunnel = Tunnel.quick(`http://${PROXY.HOST}:${PROXY.PORT}`, {
'--no-autoupdate': true,
});
activeTunnel = tunnel;
const url = await new Promise<string>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Tunnel connection timed out'));
}, 30000);
tunnel.once('url', (url) => {
clearTimeout(timeout);
resolve(url);
});
tunnel.once('error', (error) => {
clearTimeout(timeout);
reject(error);
});
});
tunnelUrl = url;
sendKoboldOutput(`Tunnel ready at ${tunnelUrl}`);
sendToRenderer('tunnel-url-changed', tunnelUrl);
tunnel.on('error', (error) => {
logError(`Tunnel error: ${error.message}`, error);
sendKoboldOutput(`[TUNNEL ERROR] ${error.message}`);
});
tunnel.on('exit', (code, signal) => {
sendKoboldOutput(
`Tunnel process exited (code: ${code}, signal: ${signal})`
);
activeTunnel = null;
tunnelUrl = null;
sendToRenderer('tunnel-url-changed', null);
});
return tunnelUrl;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logError(`Failed to start tunnel: ${errorMessage}`, error as Error);
sendKoboldOutput(`[TUNNEL ERROR] ${errorMessage}`);
activeTunnel = null;
return null;
}
};
export const stopTunnel = () => {
if (!activeTunnel) {
return;
}
try {
sendKoboldOutput('Stopping Cloudflare tunnel...');
activeTunnel.stop();
activeTunnel = null;
tunnelUrl = null;
sendToRenderer('tunnel-url-changed', null);
sendKoboldOutput('Tunnel stopped');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logError(`Failed to stop tunnel: ${errorMessage}`, error as Error);
activeTunnel = null;
tunnelUrl = null;
}
};
export const getTunnelUrl = () => tunnelUrl;
export const isTunnelActive = () => activeTunnel !== null;

View file

@ -14,6 +14,7 @@ import type {
MemoryMetrics, MemoryMetrics,
GpuMetrics, GpuMetrics,
} from '@/main/modules/monitoring'; } from '@/main/modules/monitoring';
import type { KoboldCrashInfo } from '@/types/ipc';
const koboldAPI: KoboldAPI = { const koboldAPI: KoboldAPI = {
getInstalledBackends: () => ipcRenderer.invoke('kobold:getInstalledBackends'), getInstalledBackends: () => ipcRenderer.invoke('kobold:getInstalledBackends'),
@ -105,6 +106,23 @@ const koboldAPI: KoboldAPI = {
ipcRenderer.removeListener('kobold-output', handler); ipcRenderer.removeListener('kobold-output', handler);
}; };
}, },
onKoboldCrashed: (callback) => {
const handler = (_: IpcRendererEvent, crashInfo: KoboldCrashInfo) =>
callback(crashInfo);
ipcRenderer.on('kobold-crashed', handler);
return () => {
ipcRenderer.removeListener('kobold-crashed', handler);
};
},
onTunnelUrlChanged: (callback) => {
const handler = (_: IpcRendererEvent, url: string | null) => callback(url);
ipcRenderer.on('tunnel-url-changed', handler);
return () => {
ipcRenderer.removeListener('tunnel-url-changed', handler);
};
},
}; };
const appAPI: AppAPI = { const appAPI: AppAPI = {

View file

@ -18,6 +18,7 @@ import type {
MemoryMetrics, MemoryMetrics,
GpuMetrics, GpuMetrics,
} from '@/main/modules/monitoring'; } from '@/main/modules/monitoring';
import type { KoboldCrashInfo } from '@/types/ipc';
export interface GitHubAsset { export interface GitHubAsset {
name: string; name: string;
@ -176,6 +177,10 @@ export interface KoboldAPI {
onInstallDirChanged: (callback: (newPath: string) => void) => () => void; onInstallDirChanged: (callback: (newPath: string) => void) => () => void;
onVersionsUpdated: (callback: () => void) => () => void; onVersionsUpdated: (callback: () => void) => () => void;
onKoboldOutput: (callback: (data: string) => void) => () => void; onKoboldOutput: (callback: (data: string) => void) => () => void;
onKoboldCrashed: (
callback: (crashInfo: KoboldCrashInfo) => void
) => () => void;
onTunnelUrlChanged: (callback: (url: string | null) => void) => () => void;
} }
export interface SystemVersionInfo { export interface SystemVersionInfo {

10
src/types/ipc.d.ts vendored
View file

@ -3,15 +3,25 @@ export type IPCChannel =
| 'install-dir-changed' | 'install-dir-changed'
| 'versions-updated' | 'versions-updated'
| 'kobold-output' | 'kobold-output'
| 'kobold-crashed'
| 'tunnel-url-changed'
| 'window-maximized' | 'window-maximized'
| 'window-unmaximized' | 'window-unmaximized'
| 'line-numbers-changed'; | 'line-numbers-changed';
export interface KoboldCrashInfo {
exitCode: number | null;
signal: string | null;
errorMessage?: string;
}
export interface IPCChannelPayloads { export interface IPCChannelPayloads {
'download-progress': [progress: number]; 'download-progress': [progress: number];
'install-dir-changed': [newPath: string]; 'install-dir-changed': [newPath: string];
'versions-updated': []; 'versions-updated': [];
'kobold-output': [message: string]; 'kobold-output': [message: string];
'kobold-crashed': [crashInfo: KoboldCrashInfo];
'tunnel-url-changed': [url: string | null];
'window-maximized': []; 'window-maximized': [];
'window-unmaximized': []; 'window-unmaximized': [];
'line-numbers-changed': [showLineNumbers: boolean]; 'line-numbers-changed': [showLineNumbers: boolean];

View file

@ -175,3 +175,34 @@ export function getServerInterfaceInfo({
: FRONTENDS.KOBOLDAI_LITE, : FRONTENDS.KOBOLDAI_LITE,
}; };
} }
export function getTunnelInterfaceUrl(
tunnelBaseUrl: string,
params: Omit<ServerInterfaceParams, 'frontendPreference'> & {
frontendPreference: Exclude<
FrontendPreference,
'sillytavern' | 'openwebui'
>;
}
) {
const {
frontendPreference,
imageGenerationFrontendPreference,
isImageGenerationMode,
} = params;
if (
isImageGenerationMode &&
imageGenerationFrontendPreference === 'builtin'
) {
return `${tunnelBaseUrl}/sdui`;
}
if (frontendPreference === 'llamacpp') {
return isImageGenerationMode
? `${tunnelBaseUrl}/sdui`
: `${tunnelBaseUrl}/lcpp`;
}
return isImageGenerationMode ? `${tunnelBaseUrl}/sdui` : tunnelBaseUrl;
}

View file

@ -4,6 +4,7 @@ export function parseKoboldConfig(args: string[]) {
let hasSdModel = false; let hasSdModel = false;
let hasTextModel = false; let hasTextModel = false;
let debugmode = false; let debugmode = false;
let remotetunnel = false;
for (let i = 0; i < args.length; i++) { for (let i = 0; i < args.length; i++) {
if ( if (
@ -22,11 +23,13 @@ export function parseKoboldConfig(args: string[]) {
hasTextModel = true; hasTextModel = true;
} else if (args[i] === '--debugmode') { } else if (args[i] === '--debugmode') {
debugmode = true; debugmode = true;
} else if (args[i] === '--remotetunnel') {
remotetunnel = true;
} }
} }
const isImageMode = hasSdModel; const isImageMode = hasSdModel;
const isTextMode = hasTextModel; const isTextMode = hasTextModel;
return { host, port, isImageMode, isTextMode, debugmode }; return { host, port, isImageMode, isTextMode, debugmode, remotetunnel };
} }

142
yarn.lock
View file

@ -1452,106 +1452,106 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/eslint-plugin@npm:^8.48.0": "@typescript-eslint/eslint-plugin@npm:^8.48.1":
version: 8.48.0 version: 8.48.1
resolution: "@typescript-eslint/eslint-plugin@npm:8.48.0" resolution: "@typescript-eslint/eslint-plugin@npm:8.48.1"
dependencies: dependencies:
"@eslint-community/regexpp": "npm:^4.10.0" "@eslint-community/regexpp": "npm:^4.10.0"
"@typescript-eslint/scope-manager": "npm:8.48.0" "@typescript-eslint/scope-manager": "npm:8.48.1"
"@typescript-eslint/type-utils": "npm:8.48.0" "@typescript-eslint/type-utils": "npm:8.48.1"
"@typescript-eslint/utils": "npm:8.48.0" "@typescript-eslint/utils": "npm:8.48.1"
"@typescript-eslint/visitor-keys": "npm:8.48.0" "@typescript-eslint/visitor-keys": "npm:8.48.1"
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.48.0 "@typescript-eslint/parser": ^8.48.1
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/5f4f9ac3ace3f615bac428859026b70fb7fa236666cfe8856fed3add7e4ba73c7113264c2df7a9d68247b679dfcc21b0414488bda7b9b3de1c209b1807ed7842 checksum: 10c0/aeb4692ac27ded73dce5ddba08d46f15d617651f629cdfc5e874dd4ac767eac0523807f1f4e51f6f80675efff78e5937690f1c58740b8cb92b44b87d757a6a1a
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/parser@npm:^8.48.0": "@typescript-eslint/parser@npm:^8.48.1":
version: 8.48.0 version: 8.48.1
resolution: "@typescript-eslint/parser@npm:8.48.0" resolution: "@typescript-eslint/parser@npm:8.48.1"
dependencies: dependencies:
"@typescript-eslint/scope-manager": "npm:8.48.0" "@typescript-eslint/scope-manager": "npm:8.48.1"
"@typescript-eslint/types": "npm:8.48.0" "@typescript-eslint/types": "npm:8.48.1"
"@typescript-eslint/typescript-estree": "npm:8.48.0" "@typescript-eslint/typescript-estree": "npm:8.48.1"
"@typescript-eslint/visitor-keys": "npm:8.48.0" "@typescript-eslint/visitor-keys": "npm:8.48.1"
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/180753e1dc55cd5174a236b738d3b0dd6dd6c131797cd417b3b3b8fac344168f3d21bd49eae6c0a075be29ed69b7bc74d97cadd917f1f4d4c113c29e76c1f9cd checksum: 10c0/54ec22c82cc631f56131bfed9747f8cadf52ab123463a406c5221f258f9533431c4a33ebe21ef178840d50235e69bb370d36aa2fd6a066e7223b38bfa41a1788
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/project-service@npm:8.48.0": "@typescript-eslint/project-service@npm:8.48.1":
version: 8.48.0 version: 8.48.1
resolution: "@typescript-eslint/project-service@npm:8.48.0" resolution: "@typescript-eslint/project-service@npm:8.48.1"
dependencies: dependencies:
"@typescript-eslint/tsconfig-utils": "npm:^8.48.0" "@typescript-eslint/tsconfig-utils": "npm:^8.48.1"
"@typescript-eslint/types": "npm:^8.48.0" "@typescript-eslint/types": "npm:^8.48.1"
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/6e1d08312fe55a91ba37eb19131af91ad7834bafd15d1cddb83a1e35e5134382e10dc0b14531036ba1c075ce4cba627123625ed6f2e209fb3355f3dda25da0a1 checksum: 10c0/0aeeea5e65d0f837bd9a47265f144f14ca72969d259ee929e63e06526b21f4e990e70c7bafdb2ceb3783373df7d9f5bae32c328a4c6403606f01339bc984b3f5
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/scope-manager@npm:8.48.0": "@typescript-eslint/scope-manager@npm:8.48.1":
version: 8.48.0 version: 8.48.1
resolution: "@typescript-eslint/scope-manager@npm:8.48.0" resolution: "@typescript-eslint/scope-manager@npm:8.48.1"
dependencies: dependencies:
"@typescript-eslint/types": "npm:8.48.0" "@typescript-eslint/types": "npm:8.48.1"
"@typescript-eslint/visitor-keys": "npm:8.48.0" "@typescript-eslint/visitor-keys": "npm:8.48.1"
checksum: 10c0/0766e365901a8af9d9e41fa70464254aacf8b4d167734d88b6cdaa0235e86bfdffc57a3e39a20e105929b8df499d252090f64f81f86770f74626ca809afe54b6 checksum: 10c0/16514823784cb598817b87d3d2b4fb618ab8b2378b3401a4c1160a5c914e51e7a925c3c1e7be73e0250e38390f0be70fecb3e0e0bdde7b243d74444933b95d3e
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/tsconfig-utils@npm:8.48.0, @typescript-eslint/tsconfig-utils@npm:^8.48.0": "@typescript-eslint/tsconfig-utils@npm:8.48.1, @typescript-eslint/tsconfig-utils@npm:^8.48.1":
version: 8.48.0 version: 8.48.1
resolution: "@typescript-eslint/tsconfig-utils@npm:8.48.0" resolution: "@typescript-eslint/tsconfig-utils@npm:8.48.1"
peerDependencies: peerDependencies:
typescript: ">=4.8.4 <6.0.0" typescript: ">=4.8.4 <6.0.0"
checksum: 10c0/52e9ce8ffbaf32f3c6f4b8fa8af6e3901c430411e137a0baf650fcefdd8edf3dcc4569eba726a28424471d4d1d96b815aa4cf7b63aa7b67380efd6a8dd354222 checksum: 10c0/0d540f7ab3018ed1bab8f008c0d30229e0ea12806fdbf1c756572b5cf536a1f2a6c59ca2544c09bcd5b89dcfcf79e5f6be3d765e725492b9c7e4cd64fcecffc6
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/type-utils@npm:8.48.0": "@typescript-eslint/type-utils@npm:8.48.1":
version: 8.48.0 version: 8.48.1
resolution: "@typescript-eslint/type-utils@npm:8.48.0" resolution: "@typescript-eslint/type-utils@npm:8.48.1"
dependencies: dependencies:
"@typescript-eslint/types": "npm:8.48.0" "@typescript-eslint/types": "npm:8.48.1"
"@typescript-eslint/typescript-estree": "npm:8.48.0" "@typescript-eslint/typescript-estree": "npm:8.48.1"
"@typescript-eslint/utils": "npm:8.48.0" "@typescript-eslint/utils": "npm:8.48.1"
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/72ab5c7d183b844e4870bfa5dfeb68e2e7ce5f3e1b33c06d5a8e70f0d0a012c9152ad15071d41ba3788266109804a9f4cdb85d664b11df8948bc930e29e0c244 checksum: 10c0/c98a71f7d374be249ecc7c9f20b0a867a73ad4f64e646a6bf9f2c1a5d74f0dc7bd59e9c94a0842068caa366af39ae0c550ede6d653b5c9418a0a587510bbb6d5
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/types@npm:8.48.0, @typescript-eslint/types@npm:^8.48.0": "@typescript-eslint/types@npm:8.48.1, @typescript-eslint/types@npm:^8.48.1":
version: 8.48.0 version: 8.48.1
resolution: "@typescript-eslint/types@npm:8.48.0" resolution: "@typescript-eslint/types@npm:8.48.1"
checksum: 10c0/865a8f4ae4a50aa8976f3d7e0f874f1a1c80227ec53ded68644d41011c729a489bb59f70683b29237ab945716ea0258e1d47387163379eab3edaaf5e5cc3b757 checksum: 10c0/366b8140f4c69319f1796b66b33c0c6e16eb6cbe543b9517003104e12ed143b620c1433ccf60d781a629d9433bd509a363c0c9d21fd438c17bb8840733af6caa
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/typescript-estree@npm:8.48.0": "@typescript-eslint/typescript-estree@npm:8.48.1":
version: 8.48.0 version: 8.48.1
resolution: "@typescript-eslint/typescript-estree@npm:8.48.0" resolution: "@typescript-eslint/typescript-estree@npm:8.48.1"
dependencies: dependencies:
"@typescript-eslint/project-service": "npm:8.48.0" "@typescript-eslint/project-service": "npm:8.48.1"
"@typescript-eslint/tsconfig-utils": "npm:8.48.0" "@typescript-eslint/tsconfig-utils": "npm:8.48.1"
"@typescript-eslint/types": "npm:8.48.0" "@typescript-eslint/types": "npm:8.48.1"
"@typescript-eslint/visitor-keys": "npm:8.48.0" "@typescript-eslint/visitor-keys": "npm:8.48.1"
debug: "npm:^4.3.4" debug: "npm:^4.3.4"
minimatch: "npm:^9.0.4" minimatch: "npm:^9.0.4"
semver: "npm:^7.6.0" semver: "npm:^7.6.0"
@ -1559,32 +1559,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/f17dd35f7b82654fae9fe83c2eb650572464dbce0170d55b3ef94b99e9aae010f2cbadd436089c8e59eef97d41719ace3a2deb4ac3cdfac26d43b36f34df5590 checksum: 10c0/72c0802f74222160f6a13ebbd32b0d504142a2427678c87ea78fc32672c65fd522377d43b31a97c944cbd0aefc36b320bf02f04e47c44f2797d6ccd0a8aa30ec
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/utils@npm:8.48.0": "@typescript-eslint/utils@npm:8.48.1":
version: 8.48.0 version: 8.48.1
resolution: "@typescript-eslint/utils@npm:8.48.0" resolution: "@typescript-eslint/utils@npm:8.48.1"
dependencies: dependencies:
"@eslint-community/eslint-utils": "npm:^4.7.0" "@eslint-community/eslint-utils": "npm:^4.7.0"
"@typescript-eslint/scope-manager": "npm:8.48.0" "@typescript-eslint/scope-manager": "npm:8.48.1"
"@typescript-eslint/types": "npm:8.48.0" "@typescript-eslint/types": "npm:8.48.1"
"@typescript-eslint/typescript-estree": "npm:8.48.0" "@typescript-eslint/typescript-estree": "npm:8.48.1"
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/56334312d1dc114a5c8b05dac4da191c40a416a5705fa76797ebdc9f6a96d35727fd0993cf8776f5c4411837e5fc2151bfa61d3eecc98b24f5a821a63a4d56f3 checksum: 10c0/1775ac217b578f52d6c1e85258098f8ef764d04830c6ce11043b434860da80f1a5f7cc1b9f2e0a63de161e83b8d876f7ae8362d7644d5d8e636e60ad5eeff4e2
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/visitor-keys@npm:8.48.0": "@typescript-eslint/visitor-keys@npm:8.48.1":
version: 8.48.0 version: 8.48.1
resolution: "@typescript-eslint/visitor-keys@npm:8.48.0" resolution: "@typescript-eslint/visitor-keys@npm:8.48.1"
dependencies: dependencies:
"@typescript-eslint/types": "npm:8.48.0" "@typescript-eslint/types": "npm:8.48.1"
eslint-visitor-keys: "npm:^4.2.1" eslint-visitor-keys: "npm:^4.2.1"
checksum: 10c0/20ae9ec255a786de40cdba281b63f634a642dcc34d2a79c5ffc160109f7f6227c28ae2c64be32cbc53dc68dc398c3da715bfcce90422b5024f15f7124a3c1704 checksum: 10c0/ecf4078ce63c296dd340672b516f42bf452534c75af7e7d6c1a3f32b143ff184cb3a4071d7429a9f870371ff9091a790acce28b85ce3c450bfc60554c79d43ca
languageName: node languageName: node
linkType: hard linkType: hard
@ -2304,6 +2304,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"cloudflared@npm:^0.7.1":
version: 0.7.1
resolution: "cloudflared@npm:0.7.1"
bin:
cloudflared: lib/cloudflared.js
checksum: 10c0/e5cb2a44f514c19f80078eed019cf772d545ff58e528359b5d31a96de0885e959085fee37b21d3c99053d93b5a4abeb8c23b5cb66127257ad4dc92ef01912626
languageName: node
linkType: hard
"clsx@npm:^2.1.1": "clsx@npm:^2.1.1":
version: 2.1.1 version: 2.1.1
resolution: "clsx@npm:2.1.1" resolution: "clsx@npm:2.1.1"
@ -3715,10 +3724,11 @@ __metadata:
"@types/react": "npm:^19.2.7" "@types/react": "npm:^19.2.7"
"@types/react-dom": "npm:^19.2.3" "@types/react-dom": "npm:^19.2.3"
"@types/yauzl": "npm:^2.10.3" "@types/yauzl": "npm:^2.10.3"
"@typescript-eslint/eslint-plugin": "npm:^8.48.0" "@typescript-eslint/eslint-plugin": "npm:^8.48.1"
"@typescript-eslint/parser": "npm:^8.48.0" "@typescript-eslint/parser": "npm:^8.48.1"
"@uiw/react-codemirror": "npm:^4.25.3" "@uiw/react-codemirror": "npm:^4.25.3"
"@vitejs/plugin-react": "npm:^5.1.1" "@vitejs/plugin-react": "npm:^5.1.1"
cloudflared: "npm:^0.7.1"
cross-env: "npm:^10.1.0" cross-env: "npm:^10.1.0"
electron: "npm:^38.7.2" electron: "npm:^38.7.2"
electron-builder: "npm:^26.0.12" electron-builder: "npm:^26.0.12"