import { dirname, join } from "node:path"; import { app, BrowserWindow, dialog, Menu, nativeImage, shell, Tray, } from "electron"; import type { IpcMainInvokeEvent } from "electron"; import { IPC_CHANNELS } from "@shared/contracts"; import type { DesktopResult } from "@shared/contracts"; import { ok } from "./ipc-core-common"; export interface DesktopShellMenuDependencies { iconPath: string; isDev: boolean; onCheckForUpdates: () => Promise; onBackupProfile: () => Promise; onShortcut: (action: "toggle-command-palette" | "refresh") => void; getMainWindow: () => BrowserWindow | null; ensureWindow: () => Promise; requestQuitWithConfirmation: () => Promise; registerHandle: ( channel: string, handler: (event: IpcMainInvokeEvent, ...args: unknown[]) => Promise> | DesktopResult, ) => void; } export function createDesktopShellMenu(deps: DesktopShellMenuDependencies) { const { iconPath, isDev, onCheckForUpdates, onBackupProfile, onShortcut, getMainWindow, ensureWindow, requestQuitWithConfirmation, registerHandle, } = deps; let appTray: Tray | null = null; function buildMenu() { const macAppMenu: Electron.MenuItemConstructorOptions = { label: app.name, submenu: [ { role: "about" }, { type: "separator" }, { role: "services" }, { type: "separator" }, { role: "hide" }, { role: "hideOthers" }, { role: "unhide" }, { type: "separator" }, { label: `Quit ${app.name}`, accelerator: "CmdOrCtrl+Q", click: () => void requestQuitWithConfirmation(), }, ], }; const template: Electron.MenuItemConstructorOptions[] = [ ...(process.platform === "darwin" ? [macAppMenu] : []), { label: "File", submenu: [ { label: "Open Workspace Folder", accelerator: "CmdOrCtrl+O", click: async () => { const result = await dialog.showOpenDialog({ properties: ["openDirectory"], }); const targetPath = result.filePaths[0]; if (result.canceled || !targetPath) return; shell.openPath(targetPath); }, }, { type: "separator" }, { label: "Backup Profile", accelerator: "CmdOrCtrl+Shift+B", click: async () => { await onBackupProfile(); }, }, ...(process.platform === "darwin" ? [] : [ { type: "separator" as const }, { label: "Quit", accelerator: "Alt+F4", click: () => void requestQuitWithConfirmation(), }, ]), ], }, { label: "Edit", submenu: [ { role: "undo" }, { role: "redo" }, { type: "separator" }, { role: "cut" }, { role: "copy" }, { role: "paste" }, { role: "selectAll" }, ], }, { label: "View", submenu: [ { label: "Command Palette", accelerator: "CmdOrCtrl+K", click: () => onShortcut("toggle-command-palette"), }, { label: "Refresh Data", accelerator: "CmdOrCtrl+R", click: () => onShortcut("refresh"), }, { type: "separator" }, { role: "toggleDevTools", // Hidden in packaged builds unless opted in via env var so power // users can still inspect rendered state without a dev build. visible: isDev || process.env.AGENTCOMPANY_ENABLE_DEVTOOLS === "true", }, ], }, { label: "Run", submenu: [ { label: "Check For Updates", click: async () => { await onCheckForUpdates(); }, }, ], }, ]; const menu = Menu.buildFromTemplate(template); const mainWindow = getMainWindow(); if (process.platform === "darwin") { Menu.setApplicationMenu(menu); } else { // Frameless windows: hide native menu bar but keep keyboard accelerators active Menu.setApplicationMenu(menu); mainWindow?.setMenuBarVisibility(false); mainWindow?.setAutoHideMenuBar(true); } } function createTray() { if (appTray) { return; } const assetsDir = dirname(iconPath); let trayIcon: Electron.NativeImage; if (process.platform === "darwin") { // macOS menu bar: use Template images (monochrome black + alpha). // Electron auto-detects the @2x variant when the base name includes "Template". const templatePath = join(assetsDir, "tray-iconTemplate.png"); trayIcon = nativeImage.createFromPath(templatePath); if (!trayIcon.isEmpty()) { trayIcon.setTemplateImage(true); } } else { // Windows / Linux: use the color tray icon const trayPath = join(assetsDir, "tray-icon.png"); trayIcon = nativeImage.createFromPath(trayPath); } // Fallback: resize the full app icon if (trayIcon.isEmpty()) { const fallback = nativeImage.createFromPath(iconPath); if (!fallback.isEmpty()) { trayIcon = fallback.resize({ width: 32, height: 32 }); if (process.platform === "darwin") { trayIcon.setTemplateImage(true); } } } appTray = new Tray(trayIcon.isEmpty() ? nativeImage.createEmpty() : trayIcon); appTray.setToolTip("AgentCompany"); appTray.setContextMenu( Menu.buildFromTemplate([ { label: "Show AgentCompany", click: () => { void ensureWindow().then((win) => { if (win) { win.show(); win.focus(); } }); }, }, { label: "Hide AgentCompany", click: () => { const mainWindow = getMainWindow(); if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.hide(); } }, }, { type: "separator" }, { label: "Quit", click: () => void requestQuitWithConfirmation(), }, ]), ); appTray.on("click", () => { void ensureWindow().then((win) => { if (!win) return; if (win.isVisible()) { win.hide(); return; } win.show(); win.focus(); }); }); } function registerWindowControlIpc() { // Window control IPC for custom title bar (Windows/Linux frameless windows). // Routed through registerHandle so the trusted-sender check blocks // embedded webContents from closing/minimizing the app. registerHandle(IPC_CHANNELS.windowMinimize, () => { getMainWindow()?.minimize(); return ok(null); }); registerHandle(IPC_CHANNELS.windowMaximize, () => { const mainWindow = getMainWindow(); if (mainWindow?.isMaximized()) { mainWindow.unmaximize(); } else { mainWindow?.maximize(); } return ok(null); }); registerHandle(IPC_CHANNELS.windowClose, () => { getMainWindow()?.close(); return ok(null); }); registerHandle(IPC_CHANNELS.windowIsMaximized, () => { return ok(getMainWindow()?.isMaximized() ?? false); }); } function destroy() { appTray?.destroy(); appTray = null; } return { buildMenu, createTray, registerWindowControlIpc, destroy, }; }