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

View file

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

View file

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

View file

@ -43,7 +43,7 @@ export const TitleBar = ({
const computedColorScheme = useComputedColorScheme('light', {
getInitialValueInEffect: false,
});
const { hasUpdate, openReleasePage } = useAppUpdateChecker();
const { hasUpdate, releaseUrl } = useAppUpdateChecker();
const { isImageGenerationMode } = useLaunchConfigStore();
const [logoClickCount, setLogoClickCount] = useState(0);
const [isElephantMode, setIsElephantMode] = useState(false);
@ -214,12 +214,15 @@ export const TitleBar = ({
</Box>
<Group gap="0" style={{ WebkitAppRegion: 'no-drag' }}>
{hasUpdate && (
{hasUpdate && releaseUrl && (
<ActionIcon
component="a"
href={releaseUrl}
target="_blank"
rel="noopener noreferrer"
variant="subtle"
color="orange"
size={TITLEBAR_HEIGHT}
onClick={openReleasePage}
aria-label="New release available"
tabIndex={-1}
style={{
@ -240,6 +243,7 @@ export const TitleBar = ({
style={{
borderRadius: '0.25rem',
margin: 0,
outline: 'none',
}}
>
<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}`}
target="_blank"
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">
<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 { getPlatformDisplayName } from '@/utils/platform';
import { formatDownloadSize } from '@/utils/download';
import { getAssetDescription, sortDownloadsByType } from '@/utils/assets';
import { getAssetDescription } from '@/utils/assets';
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
import { safeExecute } from '@/utils/logger';
import type { DownloadItem } from '@/types/electron';
@ -28,8 +28,6 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
const loading = loadingPlatform || loadingRemote;
const sortedDownloads = sortDownloadsByType(availableDownloads);
const handleDownload = useCallback(
async (download: DownloadItem) => {
setDownloadingAsset(download.name);
@ -80,7 +78,7 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
<>
{availableDownloads.length > 0 ? (
<Stack gap="sm">
{sortedDownloads.map((download) => {
{availableDownloads.map((download) => {
const isDownloading =
Boolean(downloading) &&
downloadingAsset === download.name;

View file

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

View file

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

View file

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

View file

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

View file

@ -140,7 +140,6 @@ export const KLITE_AUTOSCROLL_PATCHES = `
const currentHeight = el.scrollHeight;
const lastHeight = lastScrollHeights[id] || currentHeight;
// Calculate dynamic threshold based on recent growth
const heightGrowth = Math.max(0, currentHeight - lastHeight);
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(() => {
checkForAppUpdates();
}, [checkForAppUpdates]);
@ -72,7 +66,7 @@ export const useAppUpdateChecker = () => {
isChecking,
lastChecked,
checkForAppUpdates,
openReleasePage,
releaseUrl: updateInfo?.releaseUrl,
hasUpdate: updateInfo?.hasUpdate || false,
};
};

View file

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

View file

@ -169,20 +169,6 @@ export class IPCHandlers {
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 () => {
try {
const logsDir = this.logManager.getLogsDirectory();

View file

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

View file

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

View file

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

View file

@ -3083,15 +3083,6 @@ __metadata:
languageName: node
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":
version: 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-react: "npm:^7.37.5"
eslint-plugin-react-hooks: "npm:^5.2.0"
eslint-plugin-react-refresh: "npm:^0.4.20"
eslint-plugin-sonarjs: "npm:^3.0.5"
execa: "npm:^9.6.0"
globals: "npm:^16.3.0"