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 = `
${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)
})
}