import { app, BrowserWindow, Menu, Tray, shell, ipcMain, nativeImage } from 'electron' import { join } from 'node:path' import { startWebUiServer, stopWebUiServer, getToken } from './webui-server' import { desktopIcon, desktopTrayTemplateIcon, hermesBinExists, hermesBin } from './paths' import { checkForDesktopUpdates, initAutoUpdater } from './updater' import { t } from './desktop-i18n' const PORT = Number(process.env.HERMES_DESKTOP_PORT) || 8748 const START_HIDDEN = process.argv.includes('--hidden') let mainWindow: BrowserWindow | null = null let serverUrl: string | null = null let tray: Tray | null = null let isQuitting = false function showMainWindow() { if (!mainWindow) { createWindow() } if (!mainWindow) return if (mainWindow.isMinimized()) mainWindow.restore() mainWindow.show() mainWindow.focus() } function quitApp() { isQuitting = true app.quit() } function getOpenAtLogin(): boolean { return app.getLoginItemSettings().openAtLogin } function setOpenAtLogin(openAtLogin: boolean) { app.setLoginItemSettings({ openAtLogin, openAsHidden: true, path: process.execPath, args: ['--hidden'], }) } function updateTrayMenu() { if (!tray) return const isVisible = !!mainWindow && mainWindow.isVisible() const menu = Menu.buildFromTemplate([ { label: isVisible ? t('tray.hide') : t('tray.show'), click: () => { if (mainWindow?.isVisible()) { mainWindow.hide() } else { showMainWindow() } updateTrayMenu() }, }, { label: t('tray.checkForUpdates'), click: () => { checkForDesktopUpdates(true).catch(err => { console.error('[tray] update check failed:', err) }) }, }, { label: t('tray.openAtLogin'), type: 'checkbox', checked: getOpenAtLogin(), click: (item) => { setOpenAtLogin(item.checked) updateTrayMenu() }, }, { type: 'separator' }, { label: t('tray.quit'), click: quitApp, }, ]) tray.setContextMenu(menu) } function createTray() { if (tray) return const source = process.platform === 'darwin' ? desktopTrayTemplateIcon() : desktopIcon() const icon = nativeImage.createFromPath(source).resize({ width: process.platform === 'darwin' ? 18 : 16, height: process.platform === 'darwin' ? 18 : 16, quality: 'best', }) if (process.platform === 'darwin') { icon.setTemplateImage(true) } tray = new Tray(icon) tray.setToolTip('Hermes Studio') tray.on('click', () => { showMainWindow() updateTrayMenu() }) updateTrayMenu() } function createWindow() { mainWindow = new BrowserWindow({ width: 1280, height: 820, minWidth: 960, minHeight: 600, title: 'Hermes Studio', backgroundColor: '#1a1a1a', autoHideMenuBar: true, show: !START_HIDDEN, ...(process.platform === 'linux' ? { icon: desktopIcon() } : {}), webPreferences: { preload: join(__dirname, '..', 'preload', 'index.js'), contextIsolation: true, nodeIntegration: false, sandbox: false, }, }) mainWindow.on('close', (event) => { if (isQuitting) return event.preventDefault() mainWindow?.hide() updateTrayMenu() }) mainWindow.on('show', updateTrayMenu) mainWindow.on('hide', updateTrayMenu) // External links → system browser mainWindow.webContents.setWindowOpenHandler(({ url }) => { if (url.startsWith('http://127.0.0.1') || url.startsWith('http://localhost')) { return { action: 'allow' } } shell.openExternal(url).catch(() => undefined) return { action: 'deny' } }) // If the Web UI server is already up (re-opening window after close on // macOS), go straight to it. Otherwise show a loading splash; bootstrap() // will swap in the real URL once the server is ready. if (serverUrl) { mainWindow.loadURL(serverUrl) } else { mainWindow.loadURL(splashHtml()) } updateTrayMenu() } function splashHtml(): string { const html = `Hermes Studio

Hermes Studio

Starting local services…
` return 'data:text/html;charset=utf-8,' + encodeURIComponent(html) } async function bootstrap() { if (!hermesBinExists()) { console.error(`hermes binary missing at ${hermesBin()}`) console.error('Run: npm run prepare:python (to bundle Python + hermes-agent)') } try { const url = await startWebUiServer(PORT) serverUrl = url if (mainWindow) await mainWindow.loadURL(url) } catch (err) { console.error('Failed to start Web UI server:', err) if (mainWindow) { const msg = String(err instanceof Error ? err.message : err).replace(/[<>]/g, '') mainWindow.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent( `

Failed to start local services

${msg}
`, )) } } } ipcMain.handle('hermes-desktop:get-token', () => getToken()) const gotLock = app.requestSingleInstanceLock() if (!gotLock) { app.quit() } else { app.on('second-instance', () => { showMainWindow() }) app.whenReady().then(() => { // Drop the default File/Edit/View/Window menu on Windows/Linux. The web // UI provides its own in-page controls, so the native menu bar is just // visual clutter. macOS keeps a menu (system requirement) but Electron's // default is fine there. if (process.platform !== 'darwin') Menu.setApplicationMenu(null) createTray() createWindow() bootstrap() initAutoUpdater({ beforeQuitAndInstall: () => { isQuitting = true }, }) app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow() } else if (mainWindow) { showMainWindow() } }) }) app.on('window-all-closed', () => { if (isQuitting && process.platform !== 'darwin') app.quit() }) app.on('before-quit', async (e) => { if (!isQuitting && process.platform !== 'darwin') { e.preventDefault() mainWindow?.hide() updateTrayMenu() return } e.preventDefault() await stopWebUiServer().catch(() => undefined) app.exit(0) }) }