open all links in electron windows, minor refactors

This commit is contained in:
Egor 2025-09-07 00:17:16 -07:00
parent 25971a6127
commit fe2dfc02c4
18 changed files with 52 additions and 160 deletions

View file

@ -3,7 +3,6 @@ import globals from 'globals';
import tseslint from '@typescript-eslint/eslint-plugin'; import tseslint from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser'; import tsParser from '@typescript-eslint/parser';
import reactHooks from 'eslint-plugin-react-hooks'; import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import react from 'eslint-plugin-react'; import react from 'eslint-plugin-react';
import importPlugin from 'eslint-plugin-import'; import importPlugin from 'eslint-plugin-import';
import sonarjs from 'eslint-plugin-sonarjs'; import sonarjs from 'eslint-plugin-sonarjs';
@ -50,7 +49,6 @@ const config = [
plugins: { plugins: {
'@typescript-eslint': tseslint, '@typescript-eslint': tseslint,
'react-hooks': reactHooks, 'react-hooks': reactHooks,
'react-refresh': reactRefresh,
react: react, react: react,
import: importPlugin, import: importPlugin,
sonarjs: sonarjs, sonarjs: sonarjs,
@ -80,10 +78,6 @@ const config = [
'no-unused-vars': 'off', 'no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-explicit-any': 'warn',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'react/function-component-definition': [ 'react/function-component-definition': [
'error', 'error',
{ {

View file

@ -1,7 +1,7 @@
{ {
"name": "gerbil", "name": "gerbil",
"productName": "Gerbil", "productName": "Gerbil",
"version": "1.0.4", "version": "1.0.5",
"description": "Run Large Language Models locally", "description": "Run Large Language Models locally",
"main": "out/main/index.js", "main": "out/main/index.js",
"homepage": "./", "homepage": "./",
@ -54,7 +54,6 @@
"eslint-plugin-no-comments": "^1.1.10", "eslint-plugin-no-comments": "^1.1.10",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"eslint-plugin-sonarjs": "^3.0.5", "eslint-plugin-sonarjs": "^3.0.5",
"globals": "^16.3.0", "globals": "^16.3.0",
"jiti": "^2.5.1", "jiti": "^2.5.1",

View file

@ -57,9 +57,10 @@ export const ModelFileField = ({
</Button> </Button>
{showSearchHF && ( {showSearchHF && (
<Button <Button
onClick={() => { component="a"
window.electronAPI.app.openExternal(searchUrl); href={searchUrl}
}} target="_blank"
rel="noopener noreferrer"
variant="outline" variant="outline"
leftSection={<Search size={16} />} leftSection={<Search size={16} />}
> >

View file

@ -43,7 +43,7 @@ export const TitleBar = ({
const computedColorScheme = useComputedColorScheme('light', { const computedColorScheme = useComputedColorScheme('light', {
getInitialValueInEffect: false, getInitialValueInEffect: false,
}); });
const { hasUpdate, openReleasePage } = useAppUpdateChecker(); const { hasUpdate, releaseUrl } = useAppUpdateChecker();
const { isImageGenerationMode } = useLaunchConfigStore(); const { isImageGenerationMode } = useLaunchConfigStore();
const [logoClickCount, setLogoClickCount] = useState(0); const [logoClickCount, setLogoClickCount] = useState(0);
const [isElephantMode, setIsElephantMode] = useState(false); const [isElephantMode, setIsElephantMode] = useState(false);
@ -214,12 +214,15 @@ export const TitleBar = ({
</Box> </Box>
<Group gap="0" style={{ WebkitAppRegion: 'no-drag' }}> <Group gap="0" style={{ WebkitAppRegion: 'no-drag' }}>
{hasUpdate && ( {hasUpdate && releaseUrl && (
<ActionIcon <ActionIcon
component="a"
href={releaseUrl}
target="_blank"
rel="noopener noreferrer"
variant="subtle" variant="subtle"
color="orange" color="orange"
size={TITLEBAR_HEIGHT} size={TITLEBAR_HEIGHT}
onClick={openReleasePage}
aria-label="New release available" aria-label="New release available"
tabIndex={-1} tabIndex={-1}
style={{ style={{
@ -240,6 +243,7 @@ export const TitleBar = ({
style={{ style={{
borderRadius: '0.25rem', borderRadius: '0.25rem',
margin: 0, margin: 0,
outline: 'none',
}} }}
> >
<Settings size="1.25rem" /> <Settings size="1.25rem" />

View file

@ -101,11 +101,6 @@ export const UpdateAvailableModal = ({
href={`https://github.com/${GITHUB_API.KOBOLDCPP_REPO}/releases/tag/v${availableUpdate?.version}`} href={`https://github.com/${GITHUB_API.KOBOLDCPP_REPO}/releases/tag/v${availableUpdate?.version}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
onClick={() =>
window.electronAPI.app.openExternal(
`https://github.com/${GITHUB_API.KOBOLDCPP_REPO}/releases/tag/v${availableUpdate?.version}`
)
}
> >
<Group gap={4} align="center"> <Group gap={4} align="center">
<span>v{availableUpdate?.version}</span> <span>v{availableUpdate?.version}</span>

View file

@ -3,7 +3,7 @@ import { Card, Text, Title, Loader, Stack, Container } from '@mantine/core';
import { DownloadCard } from '@/components/DownloadCard'; import { DownloadCard } from '@/components/DownloadCard';
import { getPlatformDisplayName } from '@/utils/platform'; import { getPlatformDisplayName } from '@/utils/platform';
import { formatDownloadSize } from '@/utils/download'; import { formatDownloadSize } from '@/utils/download';
import { getAssetDescription, sortDownloadsByType } from '@/utils/assets'; import { getAssetDescription } from '@/utils/assets';
import { useKoboldVersions } from '@/hooks/useKoboldVersions'; import { useKoboldVersions } from '@/hooks/useKoboldVersions';
import { safeExecute } from '@/utils/logger'; import { safeExecute } from '@/utils/logger';
import type { DownloadItem } from '@/types/electron'; import type { DownloadItem } from '@/types/electron';
@ -28,8 +28,6 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
const loading = loadingPlatform || loadingRemote; const loading = loadingPlatform || loadingRemote;
const sortedDownloads = sortDownloadsByType(availableDownloads);
const handleDownload = useCallback( const handleDownload = useCallback(
async (download: DownloadItem) => { async (download: DownloadItem) => {
setDownloadingAsset(download.name); setDownloadingAsset(download.name);
@ -80,7 +78,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
<> <>
{availableDownloads.length > 0 ? ( {availableDownloads.length > 0 ? (
<Stack gap="sm"> <Stack gap="sm">
{sortedDownloads.map((download) => { {availableDownloads.map((download) => {
const isDownloading = const isDownloading =
Boolean(downloading) && Boolean(downloading) &&
downloadingAsset === download.name; downloadingAsset === download.name;

View file

@ -100,12 +100,7 @@ export const AboutTab = () => {
<Anchor <Anchor
href="https://github.com/lone-cloud/gerbil" href="https://github.com/lone-cloud/gerbil"
target="_blank" target="_blank"
onClick={(e) => { rel="noopener noreferrer"
e.preventDefault();
window.electronAPI.app.openExternal(
'https://github.com/lone-cloud/gerbil'
);
}}
style={{ textDecoration: 'none' }} style={{ textDecoration: 'none' }}
> >
<Group gap="xs" align="center"> <Group gap="xs" align="center">

View file

@ -227,11 +227,9 @@ export const GeneralTab = () => {
{getUnmetRequirements().map((req, index) => ( {getUnmetRequirements().map((req, index) => (
<span key={req.id}> <span key={req.id}>
<Anchor <Anchor
href="#" href={req.url}
onClick={(e) => { target="_blank"
e.preventDefault(); rel="noopener noreferrer"
window.electronAPI.app.openExternal(req.url);
}}
c="red" c="red"
td="underline" td="underline"
> >
@ -266,11 +264,9 @@ export const GeneralTab = () => {
{unmetReqs.map((req, index) => ( {unmetReqs.map((req, index) => (
<span key={req.id}> <span key={req.id}>
<Anchor <Anchor
href="#" href={req.url}
onClick={(e) => { target="_blank"
e.preventDefault(); rel="noopener noreferrer"
window.electronAPI.app.openExternal(req.url);
}}
c="orange" c="orange"
td="underline" td="underline"
> >

View file

@ -81,9 +81,6 @@ export const SettingsModal = ({
position: 'relative', position: 'relative',
}, },
}} }}
transitionProps={{
duration: 200,
}}
> >
<Tabs <Tabs
value={activeTab} value={activeTab}

View file

@ -3,16 +3,14 @@ import {
Stack, Stack,
Text, Text,
Group, Group,
Button,
Card, Card,
Loader, Loader,
rem,
Center, Center,
Anchor, Anchor,
} from '@mantine/core'; } from '@mantine/core';
import { RotateCcw, ExternalLink } from 'lucide-react'; import { ExternalLink } from 'lucide-react';
import { DownloadCard } from '@/components/DownloadCard'; import { DownloadCard } from '@/components/DownloadCard';
import { getAssetDescription, sortDownloadsByType } from '@/utils/assets'; import { getAssetDescription } from '@/utils/assets';
import { import {
getDisplayNameFromPath, getDisplayNameFromPath,
stripAssetExtensions, stripAssetExtensions,
@ -43,7 +41,6 @@ export const VersionsTab = () => {
loadingRemote, loadingRemote,
downloading, downloading,
downloadProgress, downloadProgress,
loadRemoteVersions,
handleDownload: sharedHandleDownload, handleDownload: sharedHandleDownload,
getLatestReleaseWithDownloadStatus, getLatestReleaseWithDownloadStatus,
} = useKoboldVersions(); } = useKoboldVersions();
@ -95,9 +92,7 @@ export const VersionsTab = () => {
const versions: VersionInfo[] = []; const versions: VersionInfo[] = [];
const processedInstalled = new Set<string>(); const processedInstalled = new Set<string>();
const sortedDownloads = sortDownloadsByType(availableDownloads); availableDownloads.forEach((download) => {
sortedDownloads.forEach((download) => {
const downloadBaseName = stripAssetExtensions(download.name); const downloadBaseName = stripAssetExtensions(download.name);
const installedVersion = installedVersions.find((v) => { const installedVersion = installedVersions.find((v) => {
@ -230,9 +225,7 @@ export const VersionsTab = () => {
}, 'Failed to set current version:'); }, 'Failed to set current version:');
}; };
const isLoading = loadingInstalled || loadingPlatform || loadingRemote; if (loadingInstalled || loadingPlatform || loadingRemote) {
if (isLoading) {
return ( return (
<Center h="100%"> <Center h="100%">
<Stack align="center" gap="md"> <Stack align="center" gap="md">
@ -252,20 +245,15 @@ export const VersionsTab = () => {
return ( return (
<> <>
<Group justify="space-between" align="center" mb="sm"> <Group justify="space-between" align="center" mb="sm">
<div>
{latestRelease && ( {latestRelease && (
<Group gap="xs"> <Group gap="xs">
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
Latest release: {latestRelease.release.tag_name} Latest release: {latestRelease.release.tag_name}
</Text> </Text>
<Anchor <Anchor
href="#" href={latestRelease.release.html_url}
onClick={(e) => { target="_blank"
e.preventDefault(); rel="noopener noreferrer"
window.electronAPI.app.openExternal(
latestRelease.release.html_url
);
}}
size="sm" size="sm"
c="blue" c="blue"
> >
@ -276,21 +264,6 @@ export const VersionsTab = () => {
</Anchor> </Anchor>
</Group> </Group>
)} )}
</div>
<Button
variant="subtle"
size="xs"
onClick={() => {
loadInstalledVersions();
loadRemoteVersions();
loadLatestRelease();
}}
leftSection={
<RotateCcw style={{ width: rem(14), height: rem(14) }} />
}
>
Refresh
</Button>
</Group> </Group>
{allVersions.map((version, index) => { {allVersions.map((version, index) => {

View file

@ -140,7 +140,6 @@ export const KLITE_AUTOSCROLL_PATCHES = `
const currentHeight = el.scrollHeight; const currentHeight = el.scrollHeight;
const lastHeight = lastScrollHeights[id] || currentHeight; const lastHeight = lastScrollHeights[id] || currentHeight;
// Calculate dynamic threshold based on recent growth
const heightGrowth = Math.max(0, currentHeight - lastHeight); const heightGrowth = Math.max(0, currentHeight - lastHeight);
const dynamicThreshold = Math.min(Math.max(heightGrowth * 1.2, 30), 200); const dynamicThreshold = Math.min(Math.max(heightGrowth * 1.2, 30), 200);

View file

@ -57,12 +57,6 @@ export const useAppUpdateChecker = () => {
} }
}, []); }, []);
const openReleasePage = useCallback(() => {
if (updateInfo?.releaseUrl) {
window.electronAPI.app.openExternal(updateInfo.releaseUrl);
}
}, [updateInfo]);
useEffect(() => { useEffect(() => {
checkForAppUpdates(); checkForAppUpdates();
}, [checkForAppUpdates]); }, [checkForAppUpdates]);
@ -72,7 +66,7 @@ export const useAppUpdateChecker = () => {
isChecking, isChecking,
lastChecked, lastChecked,
checkForAppUpdates, checkForAppUpdates,
openReleasePage, releaseUrl: updateInfo?.releaseUrl,
hasUpdate: updateInfo?.hasUpdate || false, hasUpdate: updateInfo?.hasUpdate || false,
}; };
}; };

View file

@ -10,6 +10,7 @@ import type {
GitHubAsset, GitHubAsset,
InstalledVersion, InstalledVersion,
} from '@/types/electron'; } from '@/types/electron';
import { sortDownloadsByType } from '@/utils/assets';
interface CachedReleaseData { interface CachedReleaseData {
releases: DownloadItem[]; releases: DownloadItem[];
@ -137,15 +138,7 @@ interface UseKoboldVersionsReturn {
loadingRemote: boolean; loadingRemote: boolean;
downloading: string | null; downloading: string | null;
downloadProgress: Record<string, number>; downloadProgress: Record<string, number>;
loadRemoteVersions: () => Promise<void>;
refresh: () => Promise<void>;
handleDownload: (params: HandleDownloadParams) => Promise<boolean>; handleDownload: (params: HandleDownloadParams) => Promise<boolean>;
setDownloading: (value: string | null) => void;
setDownloadProgress: (
value:
| Record<string, number>
| ((prev: Record<string, number>) => Record<string, number>)
) => void;
getLatestReleaseWithDownloadStatus: () => Promise<ReleaseWithStatus | null>; getLatestReleaseWithDownloadStatus: () => Promise<ReleaseWithStatus | null>;
} }
@ -186,7 +179,7 @@ export const useKoboldVersions = (): UseKoboldVersionsReturn => {
if (rocm) { if (rocm) {
allDownloads.push(rocm); allDownloads.push(rocm);
} }
setAvailableDownloads(allDownloads); setAvailableDownloads(sortDownloadsByType(allDownloads));
setLoadingRemote(false); setLoadingRemote(false);
return; return;
} }
@ -203,7 +196,7 @@ export const useKoboldVersions = (): UseKoboldVersionsReturn => {
allDownloads.push(rocm); allDownloads.push(rocm);
} }
setAvailableDownloads(allDownloads); setAvailableDownloads(sortDownloadsByType(allDownloads));
} catch (err) { } catch (err) {
error('Failed to load remote versions:', err as Error); error('Failed to load remote versions:', err as Error);
const cached = loadFromCache(); const cached = loadFromCache();
@ -213,7 +206,7 @@ export const useKoboldVersions = (): UseKoboldVersionsReturn => {
if (rocm) { if (rocm) {
allDownloads.push(rocm); allDownloads.push(rocm);
} }
setAvailableDownloads(allDownloads); setAvailableDownloads(sortDownloadsByType(allDownloads));
} }
} finally { } finally {
setLoadingRemote(false); setLoadingRemote(false);
@ -257,11 +250,6 @@ export const useKoboldVersions = (): UseKoboldVersionsReturn => {
[] []
); );
const refresh = useCallback(async () => {
localStorage.removeItem(CACHE_KEY);
await loadRemoteVersions();
}, [loadRemoteVersions]);
useEffect(() => { useEffect(() => {
loadPlatform(); loadPlatform();
}, [loadPlatform]); }, [loadPlatform]);
@ -296,11 +284,7 @@ export const useKoboldVersions = (): UseKoboldVersionsReturn => {
loadingRemote, loadingRemote,
downloading, downloading,
downloadProgress, downloadProgress,
loadRemoteVersions,
refresh,
handleDownload, handleDownload,
setDownloading,
setDownloadProgress,
getLatestReleaseWithDownloadStatus, getLatestReleaseWithDownloadStatus,
}; };
}; };

View file

@ -169,20 +169,6 @@ export class IPCHandlers {
arch: process.arch, arch: process.arch,
})); }));
ipcMain.handle('app:openExternal', async (_, url) => {
try {
await shell.openExternal(url);
} catch (error) {
this.logManager.logError(
'Failed to open external URL:',
error as Error
);
throw new Error(
`Failed to open external URL: ${(error as Error).message}`
);
}
});
ipcMain.handle('app:showLogsFolder', async () => { ipcMain.handle('app:showLogsFolder', async () => {
try { try {
const logsDir = this.logManager.getLogsDirectory(); const logsDir = this.logManager.getLogsDirectory();

View file

@ -87,20 +87,9 @@ export class WindowManager {
} }
}); });
this.mainWindow.webContents.setWindowOpenHandler(({ url }) => { this.mainWindow.webContents.setWindowOpenHandler(() => ({
const parsedUrl = new URL(url); action: 'allow',
}));
if (
parsedUrl.hostname === 'localhost' ||
parsedUrl.hostname === '127.0.0.1'
) {
return { action: 'allow' };
}
shell.openExternal(url);
return { action: 'deny' };
});
this.mainWindow.on('close', () => { this.mainWindow.on('close', () => {
app.quit(); app.quit();

View file

@ -76,7 +76,6 @@ const koboldAPI: KoboldAPI = {
}; };
const appAPI: AppAPI = { const appAPI: AppAPI = {
openExternal: (url) => ipcRenderer.invoke('app:openExternal', url),
showLogsFolder: () => ipcRenderer.invoke('app:showLogsFolder'), showLogsFolder: () => ipcRenderer.invoke('app:showLogsFolder'),
getVersion: () => ipcRenderer.invoke('app:getVersion'), getVersion: () => ipcRenderer.invoke('app:getVersion'),
getVersionInfo: () => ipcRenderer.invoke('app:getVersionInfo'), getVersionInfo: () => ipcRenderer.invoke('app:getVersionInfo'),

View file

@ -142,7 +142,6 @@ export interface VersionInfo {
} }
export interface AppAPI { export interface AppAPI {
openExternal: (url: string) => Promise<void>;
showLogsFolder: () => Promise<void>; showLogsFolder: () => Promise<void>;
getVersion: () => Promise<string>; getVersion: () => Promise<string>;
getVersionInfo: () => Promise<VersionInfo>; getVersionInfo: () => Promise<VersionInfo>;

View file

@ -3083,15 +3083,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"eslint-plugin-react-refresh@npm:^0.4.20":
version: 0.4.20
resolution: "eslint-plugin-react-refresh@npm:0.4.20"
peerDependencies:
eslint: ">=8.40"
checksum: 10c0/2ccf4ba28f1dcbcb9e773e46eae1e61e568bba69281a700eb26fd762152e4e90a78c991f9c8173342a7cd2a82f3f52fedb40a1e81360cef9c40ea5b814fa3613
languageName: node
linkType: hard
"eslint-plugin-react@npm:^7.37.5": "eslint-plugin-react@npm:^7.37.5":
version: 7.37.5 version: 7.37.5
resolution: "eslint-plugin-react@npm:7.37.5" resolution: "eslint-plugin-react@npm:7.37.5"
@ -3654,7 +3645,6 @@ __metadata:
eslint-plugin-no-comments: "npm:^1.1.10" eslint-plugin-no-comments: "npm:^1.1.10"
eslint-plugin-react: "npm:^7.37.5" eslint-plugin-react: "npm:^7.37.5"
eslint-plugin-react-hooks: "npm:^5.2.0" eslint-plugin-react-hooks: "npm:^5.2.0"
eslint-plugin-react-refresh: "npm:^0.4.20"
eslint-plugin-sonarjs: "npm:^3.0.5" eslint-plugin-sonarjs: "npm:^3.0.5"
execa: "npm:^9.6.0" execa: "npm:^9.6.0"
globals: "npm:^16.3.0" globals: "npm:^16.3.0"