much better context menu, titlebar bug fixes, bringing back error log viewing

This commit is contained in:
Egor 2025-08-31 13:29:51 -07:00
parent 2413f66c41
commit 204560117e
9 changed files with 153 additions and 49 deletions

View file

@ -1,7 +1,7 @@
{
"name": "gerbil",
"productName": "Gerbil",
"version": "0.9.2",
"version": "0.9.3",
"description": "Run Large Language Models locally",
"main": "out/main/index.js",
"homepage": "./",

View file

@ -147,6 +147,8 @@ export const App = () => {
}
};
const isAnyModalOpen = settingsOpened || showEjectModal || showUpdateModal;
return (
<AppShell
header={{ height: TITLEBAR_HEIGHT }}
@ -159,6 +161,7 @@ export const App = () => {
onTabChange={setActiveInterfaceTab}
onEject={handleEject}
onOpenSettings={() => setSettingsOpened(true)}
isModalOpen={isAnyModalOpen}
/>
</AppShell.Header>
<AppShell.Main

View file

@ -27,6 +27,7 @@ interface TitleBarProps {
onTabChange?: (tab: InterfaceTab) => void;
onEject?: () => void;
onOpenSettings?: () => void;
isModalOpen?: boolean;
}
export const TitleBar = ({
@ -35,6 +36,7 @@ export const TitleBar = ({
onTabChange,
onEject,
onOpenSettings,
isModalOpen = false,
}: TitleBarProps) => {
const computedColorScheme = useComputedColorScheme('light', {
getInitialValueInEffect: false,
@ -97,7 +99,7 @@ export const TitleBar = ({
? 'var(--mantine-color-dark-7)'
: 'var(--mantine-color-gray-0)',
borderBottom: '1px solid var(--mantine-color-default-border)',
WebkitAppRegion: 'drag',
WebkitAppRegion: isModalOpen ? 'no-drag' : 'drag',
userSelect: 'none',
position: 'relative',
}}

View file

@ -8,9 +8,10 @@ import {
Image,
Center,
Badge,
Button,
rem,
} from '@mantine/core';
import { Github } from 'lucide-react';
import { Github, FolderOpen } from 'lucide-react';
import type { VersionInfo } from '@/types/electron';
import { PRODUCT_NAME } from '@/constants';
import iconUrl from '/icon.png';
@ -76,26 +77,48 @@ export const AboutTab = () => {
</Badge>
</Group>
<Text size="sm" c="dimmed" mt="xs">
A desktop app to easily run Large Language Models locally.
Run Large Language Models locally
</Text>
<Anchor
href="https://github.com/lone-cloud/gerbil"
target="_blank"
onClick={(e) => {
e.preventDefault();
window.electronAPI.app.openExternal(
'https://github.com/lone-cloud/gerbil'
);
}}
style={{ textDecoration: 'none' }}
>
<Group gap="xs" align="center">
<Github style={{ width: rem(16), height: rem(16) }} />
<Text size="sm" fw={500}>
GitHub
</Text>
</Group>
</Anchor>
<Group gap="md" mt="md">
<Anchor
href="https://github.com/lone-cloud/gerbil"
target="_blank"
onClick={(e) => {
e.preventDefault();
window.electronAPI.app.openExternal(
'https://github.com/lone-cloud/gerbil'
);
}}
style={{ textDecoration: 'none' }}
>
<Group gap="xs" align="center">
<Github style={{ width: rem(16), height: rem(16) }} />
<Text size="sm" fw={500}>
GitHub
</Text>
</Group>
</Anchor>
<Button
variant="light"
size="compact-sm"
leftSection={
<FolderOpen style={{ width: rem(16), height: rem(16) }} />
}
onClick={async () => {
try {
await window.electronAPI.app.showLogsFolder();
} catch (error) {
window.electronAPI.logs.logError(
'Failed to open logs folder',
error as Error
);
}
}}
>
Show Logs
</Button>
</Group>
</div>
</Group>
</Card>

View file

@ -169,7 +169,7 @@ export const SettingsModal = ({
}}
>
<Button onClick={onClose} variant="filled">
Done
Close
</Button>
</Box>
</Modal>

View file

@ -171,6 +171,16 @@ export class IPCHandlers {
}
});
ipcMain.handle('app:showLogsFolder', async () => {
try {
const logsDir = this.logManager.getLogsDirectory();
await shell.openPath(logsDir);
return { success: true };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});
ipcMain.handle('app:minimizeWindow', () => {
const mainWindow = this.windowManager.getMainWindow();
mainWindow?.minimize();

View file

@ -1,4 +1,12 @@
import { BrowserWindow, app, shell, nativeImage, screen, Menu } from 'electron';
import {
BrowserWindow,
app,
shell,
nativeImage,
screen,
Menu,
clipboard,
} from 'electron';
import { join } from 'path';
import { stripVTControlCharacters } from 'util';
import { PRODUCT_NAME } from '../../constants';
@ -104,40 +112,96 @@ export class WindowManager {
this.mainWindow.webContents.on('context-menu', (_, params) => {
const hasLinkURL = !!params.linkURL;
const hasSelection = !!params.selectionText;
const isEditable = params.isEditable;
const isDev = this.isDevelopment();
const menuTemplate = [
...(isDev
? [
{
label: 'Inspect Element',
click: () => {
this.mainWindow?.webContents.inspectElement(
params.x,
params.y
);
},
},
{ type: 'separator' as const },
]
: []),
{ label: 'Cut', role: 'cut' as const },
{ label: 'Copy', role: 'copy' as const },
{ label: 'Paste', role: 'paste' as const },
...(hasLinkURL ? [{ type: 'separator' as const }] : []),
{
const canCut = hasSelection && isEditable;
const canCopy = hasSelection;
const canPaste = isEditable;
const canSelectAll = isEditable || params.mediaType === 'none';
const canUndo = isEditable && params.editFlags?.canUndo;
const canRedo = isEditable && params.editFlags?.canRedo;
const hasEditOperations =
canCut || canCopy || canPaste || canSelectAll || canUndo || canRedo;
const menuItems = [];
if (isDev) {
menuItems.push({
label: 'Inspect Element',
click: () => {
this.mainWindow?.webContents.inspectElement(params.x, params.y);
},
});
}
if (hasEditOperations) {
if (isDev) {
menuItems.push({ type: 'separator' as const });
}
if (canUndo) {
menuItems.push({ label: 'Undo', role: 'undo' as const });
}
if (canRedo) {
menuItems.push({ label: 'Redo', role: 'redo' as const });
}
if (
(canUndo || canRedo) &&
(canCut || canCopy || canPaste || canSelectAll)
) {
menuItems.push({ type: 'separator' as const });
}
if (canCut) {
menuItems.push({ label: 'Cut', role: 'cut' as const });
}
if (canCopy) {
menuItems.push({ label: 'Copy', role: 'copy' as const });
}
if (canPaste) {
menuItems.push({ label: 'Paste', role: 'paste' as const });
}
if ((canCut || canCopy || canPaste) && canSelectAll) {
menuItems.push({ type: 'separator' as const });
}
if (canSelectAll) {
menuItems.push({ label: 'Select All', role: 'selectAll' as const });
}
}
if (hasLinkURL) {
if (isDev || hasEditOperations) {
menuItems.push({ type: 'separator' as const });
}
menuItems.push({
label: 'Open Link in Browser',
visible: hasLinkURL,
click: () => {
if (params.linkURL) {
shell.openExternal(params.linkURL);
}
},
},
];
});
const menu = Menu.buildFromTemplate(menuTemplate);
menu.popup({ window: this.mainWindow! });
menuItems.push({
label: 'Copy Link Address',
click: () => {
if (params.linkURL) {
clipboard.writeText(params.linkURL);
}
},
});
}
if (menuItems.length > 0) {
const menu = Menu.buildFromTemplate(menuItems);
menu.popup({ window: this.mainWindow! });
}
});
}

View file

@ -77,6 +77,7 @@ 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'),
minimizeWindow: () => ipcRenderer.invoke('app:minimizeWindow'),

View file

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