/** * System Tray Management * Creates and manages the system tray icon and menu */ import { Tray, Menu, app, nativeImage } from 'electron'; import { join } from 'path'; import { resolveMainWindow, sendWhenWindowReady, showMainWindow, type EnsureMainWindow, type MainWindowProvider, } from './window-provider'; let tray: Tray | null = null; let windowProvider: MainWindowProvider = () => null; let ensureWindow: EnsureMainWindow | undefined; /** * Resolve the icons directory path (works in both dev and packaged mode) */ function getIconsDir(): string { if (app.isPackaged) { return join(process.resourcesPath, 'resources', 'icons'); } return join(__dirname, '../../resources/icons'); } /** * Create system tray icon and menu */ export function createTray(mainWindow: MainWindowProvider, ensureMainWindow?: EnsureMainWindow): Tray { windowProvider = mainWindow; ensureWindow = ensureMainWindow; if (tray && !tray.isDestroyed()) return tray; // Use platform-appropriate icon for system tray const iconsDir = getIconsDir(); let iconPath: string; if (process.platform === 'win32') { // Windows: use .ico for best quality in system tray iconPath = join(iconsDir, 'icon.ico'); } else if (process.platform === 'darwin') { // macOS: use Template.png for proper status bar icon // The "Template" suffix tells macOS to treat it as a template image iconPath = join(iconsDir, 'tray-icon-Template.png'); } else { // Linux: use 32x32 PNG iconPath = join(iconsDir, '32x32.png'); } let icon = nativeImage.createFromPath(iconPath); // Fallback to icon.png if platform-specific icon not found if (icon.isEmpty()) { icon = nativeImage.createFromPath(join(iconsDir, 'icon.png')); // Still try to set as template for macOS if (process.platform === 'darwin') { icon.setTemplateImage(true); } } // Note: Using "Template" suffix in filename automatically marks it as template image // But we can also explicitly set it for safety if (process.platform === 'darwin') { icon.setTemplateImage(true); } tray = new Tray(icon); // Set tooltip tray.setToolTip('Makelore - AI Assistant'); const showWindow = () => { const window = resolveMainWindow(windowProvider) ?? ensureWindow?.() ?? null; showMainWindow(window); return window; }; // Create context menu const contextMenu = Menu.buildFromTemplate([ { label: 'Show Makelore', click: showWindow, }, { type: 'separator', }, { label: 'Makelore Runtime', enabled: false, }, { label: ' Running', type: 'checkbox', checked: true, enabled: false, }, { type: 'separator', }, { label: 'Quick Actions', submenu: [ { label: 'Open Chat', click: () => { sendWhenWindowReady(showWindow(), 'navigate', '/'); }, }, { label: 'Open Settings', click: () => { sendWhenWindowReady(showWindow(), 'navigate', '/settings'); }, }, ], }, { type: 'separator', }, { label: 'Check for Updates...', click: () => { sendWhenWindowReady(showWindow(), 'update:check'); }, }, { type: 'separator', }, { label: 'Quit Makelore', click: () => { app.quit(); }, }, ]); tray.setContextMenu(contextMenu); // Click to show window (Windows/Linux) tray.on('click', () => { const window = resolveMainWindow(windowProvider); if (window?.isVisible()) { window.hide(); } else { showWindow(); } }); // Double-click to show window (Windows) tray.on('double-click', showWindow); return tray; } /** * Update tray tooltip with runtime status */ export function updateTrayStatus(status: string): void { if (tray) { tray.setToolTip(`Makelore - ${status}`); } } /** * Destroy tray icon */ export function destroyTray(): void { if (tray) { tray.destroy(); tray = null; windowProvider = () => null; ensureWindow = undefined; } }