Files
AgentCompany/src/main/desktop-shell-service.ts
T
ZacharyZhang-NYandClaude Opus 4.6 667ac48aa5 feat(04-01): refactor all notify() call sites to use structured NotificationOptions
- Change ipc-support.ts notify() from (title, body) to (options: NotificationOptions)
- Wire createNotificationService in bootstrap-application.ts with lazy service graph refs
- Update all 12 call sites with entity context, urgency, navigation, and batch keys
- Update dependency interfaces in service-graph, worker, orchestration, automation, updater, desktop-shell
- No string-based notify() calls remain in src/main/

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 16:37:07 -04:00

353 lines
9.7 KiB
TypeScript

import { existsSync } from "node:fs";
import { extname } from "node:path";
import { pathToFileURL } from "node:url";
import { resolve, sep } from "node:path";
import {
app,
BrowserWindow,
dialog,
Menu,
nativeImage,
net,
protocol,
session,
shell,
Tray,
} from "electron";
import { attachWindowStatePersistence, loadWindowState } from "./window-state";
import type { NotificationOptions } from "./notification-service";
interface DesktopShellDependencies {
preloadPath: string;
rendererDist: string;
iconPath: string;
isDev: boolean;
devServerUrl?: string;
windowStatePath: string;
shouldQuit: () => boolean;
onRequestQuit: () => void;
onCheckForUpdates: () => Promise<void>;
onBackupProfile: () => Promise<void>;
onShortcut: (action: "toggle-command-palette" | "toggle-console" | "refresh") => void;
notify: (options: NotificationOptions) => void;
}
function isSocialBrowserSession(contents: Electron.WebContents) {
return contents.session !== session.defaultSession;
}
function isWithinDirectory(rootPath: string, targetPath: string) {
const normalizedRoot = resolve(rootPath);
const normalizedTarget = resolve(targetPath);
return normalizedTarget === normalizedRoot || normalizedTarget.startsWith(`${normalizedRoot}${sep}`);
}
export function createDesktopShellService({
preloadPath,
rendererDist,
iconPath,
isDev,
devServerUrl,
windowStatePath,
shouldQuit,
onRequestQuit,
onCheckForUpdates,
onBackupProfile,
onShortcut,
notify,
}: DesktopShellDependencies) {
let mainWindow: BrowserWindow | null = null;
let appTray: Tray | null = null;
let backgroundCloseNoticeShown = false;
let windowCreationPromise: Promise<BrowserWindow | null> | null = null;
function getMainWindow() {
return mainWindow;
}
function installSecurityGuards() {
session.defaultSession.setPermissionCheckHandler(() => false);
session.defaultSession.setPermissionRequestHandler((_webContents, _permission, callback) => {
callback(false);
});
app.on("web-contents-created", (_event, contents) => {
contents.setWindowOpenHandler(() => ({ action: "deny" }));
contents.on("will-attach-webview", (event) => {
event.preventDefault();
});
contents.on("will-navigate", (event) => {
if (isSocialBrowserSession(contents)) return;
event.preventDefault();
});
});
}
function registerProtocol() {
const textMimeTypes: Record<string, string> = {
".html": "text/html; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".mjs": "text/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".json": "application/json; charset=utf-8",
".svg": "image/svg+xml; charset=utf-8",
};
protocol.handle("app", async (request) => {
const url = new URL(request.url);
const relativePath = decodeURIComponent(url.pathname === "/" ? "index.html" : url.pathname.replace(/^\/+/, ""));
const targetPath = resolve(rendererDist, relativePath);
if (!isWithinDirectory(rendererDist, targetPath) || !existsSync(targetPath)) {
return new Response("Not found", { status: 404 });
}
const response = await net.fetch(pathToFileURL(targetPath).toString());
const mime = textMimeTypes[extname(targetPath).toLowerCase()];
if (mime) {
const headers = new Headers(response.headers);
headers.set("Content-Type", mime);
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
});
}
return response;
});
}
async function createWindow() {
const windowIcon = nativeImage.createFromPath(iconPath);
const savedWindowState = loadWindowState(windowStatePath);
mainWindow = new BrowserWindow({
title: "AgentCompany",
icon: windowIcon.isEmpty() ? undefined : windowIcon,
width: savedWindowState.bounds.width,
height: savedWindowState.bounds.height,
x: savedWindowState.bounds.x,
y: savedWindowState.bounds.y,
minWidth: 1180,
minHeight: 760,
backgroundColor: "#13110f",
titleBarStyle: process.platform === "darwin" ? "hiddenInset" : "default",
webPreferences: {
preload: preloadPath,
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
webviewTag: false,
devTools: isDev,
webSecurity: true,
allowRunningInsecureContent: false,
},
});
if (isDev && devServerUrl) {
await mainWindow.loadURL(devServerUrl);
} else {
await mainWindow.loadURL("app://-/index.html");
}
attachWindowStatePersistence(mainWindow, windowStatePath);
if (savedWindowState.maximized) {
mainWindow.maximize();
}
mainWindow.on("close", (event) => {
if (process.platform === "darwin" || shouldQuit()) {
return;
}
event.preventDefault();
mainWindow?.hide();
if (!backgroundCloseNoticeShown) {
backgroundCloseNoticeShown = true;
notify({
title: "Running in Background",
body: "AgentCompany is still running in the system tray",
urgency: "informational",
navigation: { section: "overview" },
});
}
});
mainWindow.on("closed", () => {
mainWindow = null;
});
}
async function ensureWindow() {
if (!mainWindow || mainWindow.isDestroyed()) {
if (!windowCreationPromise) {
windowCreationPromise = createWindow()
.then(() => mainWindow)
.finally(() => {
windowCreationPromise = null;
});
}
return windowCreationPromise;
}
return mainWindow;
}
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" },
{ role: "quit" },
],
};
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"],
});
if (result.canceled || result.filePaths.length === 0) return;
shell.openPath(result.filePaths[0] ?? "");
},
},
{ type: "separator" },
{
label: "Backup Profile",
accelerator: "CmdOrCtrl+Shift+B",
click: async () => {
await onBackupProfile();
},
},
...(process.platform === "darwin" ? [] : [{ type: "separator" as const }, { role: "quit" as const }]),
],
},
{
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: "Toggle Console",
accelerator: "CmdOrCtrl+`",
click: () => onShortcut("toggle-console"),
},
{
label: "Refresh Data",
accelerator: "CmdOrCtrl+R",
click: () => onShortcut("refresh"),
},
{ type: "separator" },
{ role: "toggleDevTools", visible: isDev },
],
},
{
label: "Run",
submenu: [
{
label: "Check For Updates",
click: async () => {
await onCheckForUpdates();
},
},
],
},
];
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
}
function createTray() {
if (process.platform === "darwin" || appTray) {
return;
}
const trayIcon = nativeImage.createFromPath(iconPath);
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: () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.hide();
}
},
},
{ type: "separator" },
{
label: "Quit",
click: () => {
onRequestQuit();
app.quit();
},
},
]),
);
appTray.on("click", () => {
void ensureWindow().then((win) => {
if (!win) return;
if (win.isVisible()) {
win.hide();
return;
}
win.show();
win.focus();
});
});
}
function destroy() {
appTray?.destroy();
appTray = null;
}
function initialize() {
installSecurityGuards();
registerProtocol();
buildMenu();
createTray();
}
return {
initialize,
installSecurityGuards,
registerProtocol,
buildMenu,
createTray,
createWindow,
ensureWindow,
getMainWindow,
destroy,
};
}