feat: add general assist feature and enhance shortcut management

- Introduced a new "General Assist" shortcut, allowing users to open SurfSense from anywhere.
- Updated shortcut management to include the new general assist functionality in both the desktop and web applications.
- Enhanced the UI to reflect changes in shortcut labels and descriptions for better clarity.
- Improved the Electron API to support the new shortcut configuration.
This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-04-07 03:42:46 -07:00
parent e574b5ec4a
commit 27e9e8d873
10 changed files with 159 additions and 33 deletions

View file

@ -19,6 +19,9 @@ files:
- "!scripts"
- "!release"
extraResources:
- from: assets/
to: assets/
filter: ["*.ico", "*.png", "*.icns"]
- from: ../surfsense_web/.next/standalone/surfsense_web/
to: standalone/
filter:

View file

@ -23,6 +23,7 @@ import {
import { getShortcuts, setShortcuts, type ShortcutConfig } from '../modules/shortcuts';
import { reregisterQuickAsk } from '../modules/quick-ask';
import { reregisterAutocomplete } from '../modules/autocomplete';
import { reregisterGeneralAssist } from '../modules/tray';
let authTokens: { bearer: string; refresh: string } | null = null;
@ -107,6 +108,7 @@ export function registerIpcHandlers(): void {
ipcMain.handle(IPC_CHANNELS.SET_SHORTCUTS, async (_event, config: Partial<ShortcutConfig>) => {
const updated = await setShortcuts(config);
if (config.generalAssist) await reregisterGeneralAssist();
if (config.quickAsk) await reregisterQuickAsk();
if (config.autocomplete) await reregisterAutocomplete();
return updated;

View file

@ -1,7 +1,9 @@
import { app, BrowserWindow } from 'electron';
let isQuitting = false;
import { registerGlobalErrorHandlers, showErrorDialog } from './modules/errors';
import { startNextServer } from './modules/server';
import { createMainWindow } from './modules/window';
import { createMainWindow, getMainWindow } from './modules/window';
import { setupDeepLinks, handlePendingDeepLink } from './modules/deep-links';
import { setupAutoUpdater } from './modules/auto-updater';
import { setupMenu } from './modules/menu';
@ -9,6 +11,7 @@ import { registerQuickAsk, unregisterQuickAsk } from './modules/quick-ask';
import { registerAutocomplete, unregisterAutocomplete } from './modules/autocomplete';
import { registerFolderWatcher, unregisterFolderWatcher } from './modules/folder-watcher';
import { registerIpcHandlers } from './ipc/handlers';
import { createTray, destroyTray } from './modules/tray';
registerGlobalErrorHandlers();
@ -28,7 +31,18 @@ app.whenReady().then(async () => {
return;
}
createMainWindow('/dashboard');
await createTray();
const win = createMainWindow('/dashboard');
// Minimize to tray instead of closing the app
win.on('close', (e) => {
if (!isQuitting) {
e.preventDefault();
win.hide();
}
});
await registerQuickAsk();
await registerAutocomplete();
registerFolderWatcher();
@ -37,20 +51,28 @@ app.whenReady().then(async () => {
handlePendingDeepLink();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
const mw = getMainWindow();
if (!mw || mw.isDestroyed()) {
createMainWindow('/dashboard');
} else {
mw.show();
mw.focus();
}
});
});
// Keep running in the background — the tray "Quit" calls app.exit()
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
// Do nothing: the app stays alive in the tray
});
app.on('before-quit', () => {
isQuitting = true;
});
app.on('will-quit', () => {
unregisterQuickAsk();
unregisterAutocomplete();
unregisterFolderWatcher();
destroyTray();
});

View file

@ -114,7 +114,7 @@ async function quickAskHandler(): Promise<void> {
const text = selected || savedClipboard.trim();
sourceApp = getFrontmostApp();
console.log('[quick-ask] Source app:', sourceApp, '| Opening Quick Ask with', text.length, 'chars', selected ? '(selected)' : text ? '(clipboard fallback)' : '(empty)');
console.log('[quick-ask] Source app:', sourceApp, '| Opening Quick Assist with', text.length, 'chars', selected ? '(selected)' : text ? '(clipboard fallback)' : '(empty)');
openQuickAsk(text);
}

View file

@ -1,9 +1,11 @@
export interface ShortcutConfig {
generalAssist: string;
quickAsk: string;
autocomplete: string;
}
const DEFAULTS: ShortcutConfig = {
generalAssist: 'CommandOrControl+Shift+S',
quickAsk: 'CommandOrControl+Alt+S',
autocomplete: 'CommandOrControl+Shift+Space',
};

View file

@ -0,0 +1,77 @@
import { app, globalShortcut, Menu, nativeImage, Tray } from 'electron';
import path from 'path';
import { getMainWindow, createMainWindow } from './window';
import { getShortcuts } from './shortcuts';
let tray: Tray | null = null;
let currentShortcut: string | null = null;
function getTrayIcon(): nativeImage {
const iconName = process.platform === 'win32' ? 'icon.ico' : 'icon.png';
const iconPath = app.isPackaged
? path.join(process.resourcesPath, 'assets', iconName)
: path.join(__dirname, '..', 'assets', iconName);
const img = nativeImage.createFromPath(iconPath);
return img.resize({ width: 16, height: 16 });
}
function showMainWindow(): void {
let win = getMainWindow();
if (!win || win.isDestroyed()) {
win = createMainWindow('/dashboard');
} else {
win.show();
win.focus();
}
}
function registerShortcut(accelerator: string): void {
if (currentShortcut) {
globalShortcut.unregister(currentShortcut);
currentShortcut = null;
}
if (!accelerator) return;
try {
const ok = globalShortcut.register(accelerator, showMainWindow);
if (ok) {
currentShortcut = accelerator;
} else {
console.warn(`[tray] Failed to register General Assist shortcut: ${accelerator}`);
}
} catch (err) {
console.error(`[tray] Error registering General Assist shortcut:`, err);
}
}
export async function createTray(): Promise<void> {
if (tray) return;
tray = new Tray(getTrayIcon());
tray.setToolTip('SurfSense');
const contextMenu = Menu.buildFromTemplate([
{ label: 'Open SurfSense', click: showMainWindow },
{ type: 'separator' },
{ label: 'Quit', click: () => { app.exit(0); } },
]);
tray.setContextMenu(contextMenu);
tray.on('double-click', showMainWindow);
const shortcuts = await getShortcuts();
registerShortcut(shortcuts.generalAssist);
}
export async function reregisterGeneralAssist(): Promise<void> {
const shortcuts = await getShortcuts();
registerShortcut(shortcuts.generalAssist);
}
export function destroyTray(): void {
if (currentShortcut) {
globalShortcut.unregister(currentShortcut);
currentShortcut = null;
}
tray?.destroy();
tray = null;
}