1import fs from "node:fs/promises";2import { createRequire } from "node:module";3import path from "node:path";4import { fileURLToPath } from "node:url";5 6import type
*
as
Electron
from
"electron"
;
7import { hostPlatform } from "../../../src/platform/host";
8import { CloudSyncService } from "../../../src/sync/service";
9import type { PersistedState } from "../src/app/types";
10import { MAIN_WINDOW_MIN_WIDTH } from "../src/lib/adaptiveLayout";
11import {
12 getCanvasCaptionSymbolTone,
13 getCanvasNativeBackgroundColor,
14} from "../src/lib/canvasAppearance";
15import {
16 DESKTOP_EVENT_CHANNELS,
17 type DesktopMenuCommand,
18 type ShowCanvasWindowInput,
19 type ShowQuickChatWindowInput,
20 type SystemAppearance,
21 type ThemeSource,
22 type UpdaterState,
23} from "../src/lib/desktopApi";
24import { registerDesktopIpc } from "./ipc";
25import { WorkspaceRootsController } from "./ipc/workspaceRoots";
26import {
27 applySystemAppearanceToWindow,
28 applyThemeSourcePreference,
29 getInitialWindowAppearanceOptions,
30 getSystemAppearanceSnapshot,
31 registerSystemAppearanceListener,
32 registerWindowAppearanceProfile,
33 syncWindowAppearance,
34} from "./services/appearance";
35import { AppearancePreferences } from "./services/appearancePreferences";
36import {
37 captureCrashReportingError,
38 initElectronMainCrashReporting,
39} from "./services/crashReporting";
40import { runDesktopSmokePromptLoadCheck } from "./services/desktopSmoke";
41import { DiagnosticsService } from "./services/diagnostics";
42import { logError, logInfo, logWarn } from "./services/localLogs";
43import {
44 registerDesktopMediaProtocolHandler,
45 registerDesktopMediaSchemePrivileges,
46} from "./services/mediaProtocol";
47import { installDesktopApplicationMenu } from "./services/menu";
48import { createMenuCommandDispatcher } from "./services/menuCommandDispatcher";
49import { MobileRelayBridge } from "./services/mobileRelayBridge";
50import { isPathEqualOrInside } from "./services/pathBoundary";
51import { PersistenceService } from "./services/persistence";
52import { DesktopProductAnalyticsService } from "./services/productAnalytics";
53import { applyPublicTelemetryEnv } from "./services/publicTelemetryEnv";
54import { QuickChatController } from "./services/quickChatController";
55import { resolveElectronRemoteDebugConfig } from "./services/remoteDebug";
56import { resolveDesktopRendererUrl } from "./services/rendererUrl";
57import { ServerManager } from "./services/serverManager";
58import { createBeforeQuitHandler } from "./services/shutdown";
59import { resolveTrayIconPath } from "./services/trayIcon";
60import { DesktopUpdaterService } from "./services/updater";
61import { applyElectronUserDataDirOverride } from "./services/userDataOverride";
62import { revealAndActivateWindow } from "./services/windowActivation";
63import {
64 type NativeCloseWindow,
65 NativeWindowCloseCoordinator,
66} from "./services/windowCloseCoordinator";
67import {
68 applyPlatformWindowCreated,
69 getPlatformBrowserWindowOptions,
70 shouldUseMacosNativeGlass,
71} from "./services/windowEnhancements";
72import { loadMainWindowBounds, trackMainWindowBounds } from "./services/windowState";
73
74const require = createRequire(import.meta.url);
75const { app, BrowserWindow, Menu, Notification, net, protocol, screen, shell } =
76 require("electron") as typeof Electron;
77
78const __filename = fileURLToPath(import.meta.url);
79const __dirname = path.dirname(__filename);
80const PACKAGED_RENDERER_DIR = path.resolve(path.join(__dirname, "../renderer"));
81const DESKTOP_SMOKE_WORKSPACE_ENV = "COWORK_DESKTOP_SMOKE_WORKSPACE";
82const DESKTOP_SMOKE_OUTPUT_ENV = "COWORK_DESKTOP_SMOKE_OUTPUT";
83const DESKTOP_APP_NAME = "Cowork";
84const WINDOWS_APP_USER_MODEL_ID = "com.cowork.desktop";
85
86// App identity must be established before any service resolves `userData`.
87app.setName(DESKTOP_APP_NAME);
88const electronUserDataDirOverride = applyElectronUserDataDirOverride(app, process.env);
89
90if (process.platform === "win32") {
91 app.setAppUserModelId(WINDOWS_APP_USER_MODEL_ID);
92}
93
94// Keep packaged-mode feature resolution consistent across main and preload.
95process.env.COWORK_IS_PACKAGED = String(app.isPackaged);
96applyPublicTelemetryEnv(process.env);
97
98// Must run before app ready so cowork-media images load in renderer <img> tags.
99registerDesktopMediaSchemePrivileges(protocol);
100
101const productAnalytics = new DesktopProductAnalyticsService();
102const cloudSync = new CloudSyncService({
103 env: process.env,
104 log: (level, message, meta) => {
105 if (level === "error") {
106 logError("cloud-sync", message, meta);
107 return;
108 }
109 if (level === "warn") {
110 logWarn("cloud-sync", message, meta);
111 return;
112 }
113 logInfo("cloud-sync", message, meta);
114 },
115});
116const serverManager = new ServerManager({
117 getProductAnalyticsState: () => productAnalytics.getPersistedState(),
118 onWorkspaceServerExited: (event) => {
119 emitDesktopEvent(DESKTOP_EVENT_CHANNELS.workspaceServerExited, event);
120 },
121});
122const mobileRelayBridge = new MobileRelayBridge({ serverManager });
123const persistence = new PersistenceService();
124const appearancePreferences = new AppearancePreferences(app);
125// Shared between the cowork-media protocol handler and desktop IPC so both
126// enforce (and observe approvals against) the same workspace-root boundary.
127const workspaceRoots = new WorkspaceRootsController(persistence);
128const updater = new DesktopUpdaterService({
129 currentVersion: app.getVersion(),
130 isPackaged: app.isPackaged,
131 onStateChange: (state) => {
132 emitDesktopEvent(DESKTOP_EVENT_CHANNELS.updateStateChanged, state);
133 },
134 notifyUpdateReady: (state) => {
135 showUpdateReadyNotification(state);
136 },
137 captureError: (error, context) => {
138 captureCrashReportingError(error, {
139 tags: {
140 operation: context.operation,
141 },
142 });
143 },
144});
145const diagnostics = new DiagnosticsService({
146 persistence,
147 updater,
148 serverDiagnostics: () => serverManager.getDiagnostics(),
149});
150const windowCloseCoordinator = new NativeWindowCloseCoordinator();
151let unregisterAppearanceListener = () => {};
152let mainWindow: Electron.BrowserWindow | null = null;
153let quickChatController: QuickChatController | null = null;
154const menuCommandDispatcher = createMenuCommandDispatcher();
155const WINDOW_SHOW_FALLBACK_TIMEOUT_MS = 2_000;
156
157const electronRemoteDebug = resolveElectronRemoteDebugConfig({
158 isPackaged: app.isPackaged,
159 env: process.env,
160});
161
162if (electronRemoteDebug.enabled) {
163 app.commandLine.appendSwitch("remote-debugging-port", electronRemoteDebug.port);
164 app.commandLine.appendSwitch("remote-debugging-address", "127.0.0.1");
165}
166
167logInfo("main", "desktop process starting", {
168 version: app.getVersion(),
169 packaged: app.isPackaged,
170 platform: process.platform,
171 arch: process.arch,
172 ...(electronUserDataDirOverride.applied
173 ? { userDataDirOverride: electronUserDataDirOverride.path }
174 : {}),
175});
176
177function emitDesktopEvent(channel: string, payload: unknown): void {
178 for (const win of BrowserWindow.getAllWindows()) {
179 if (win.isDestroyed()) {
180 continue;
181 }
182 win.webContents.send(channel, payload);
183 }
184}
185
186function emitSystemAppearance(): void {
187 emitDesktopEvent(DESKTOP_EVENT_CHANNELS.systemAppearanceChanged, getSystemAppearanceSnapshot());
188}
189
190function showUpdateReadyNotification(state: UpdaterState): void {
191 if (!Notification.isSupported()) {
192 return;
193 }
194 const version = state.release?.version;
195 const isWindows = process.platform === "win32";
196 const notification = new Notification({
197 title: "Update ready",
198 body: version
199 ? `Cowork ${version} is ready. Restart to install.`
200 : "Cowork update is ready. Restart to install.",
201 silent: false,
202 ...(isWindows
203 ? {
204 actions: [
205 { type: "button" as const, text: "Restart Now" },
206 { type: "button" as const, text: "Later" },
207 ],
208 }
209 : {}),
210 });
211
212 if (isWindows) {
213 notification.on("action", (_event: Electron.Event, index: number) => {
214 if (index === 0) {
215 updater.quitAndInstall();
216 }
217 });
218 }
219
220 notification.on("click", () => {
221 const win = mainWindow ?? BrowserWindow.getAllWindows()[0];
222 if (win && !win.isDestroyed()) {
223 if (win.isMinimized()) win.restore();
224 win.focus();
225 void sendMenuCommand("openUpdates");
226 }
227 });
228
229 notification.show();
230}
231
232async function sendMenuCommand(command: DesktopMenuCommand): Promise<void> {
233 const existingMainWindow = mainWindow && !mainWindow.isDestroyed() ? mainWindow : null;
234 const target = await ensureMainWindow();
235 menuCommandDispatcher.dispatch(
236 command,
237 existingMainWindow
238 ? {
239 send(nextCommand) {
240 target.webContents.send(DESKTOP_EVENT_CHANNELS.menuCommand, nextCommand);
241 },
242 }
243 : null,
244 );
245}
246
247function isExternalUrl(rawUrl: string): boolean {
248 try {
249 const parsed = new URL(rawUrl);
250 return (
251 parsed.protocol === "http:" || parsed.protocol === "https:" || parsed.protocol === "mailto:"
252 );
253 } catch {
254 return false;
255 }
256}
257
258function isTrustedRendererNavigation(rawUrl: string): boolean {
259 if (app.isPackaged) {
260 try {
261 const parsed = new URL(rawUrl);
262 if (parsed.protocol !== "file:") {
263 return false;
264 }
265 const resolvedPath = path.resolve(fileURLToPath(parsed));
266 return isPathEqualOrInside(PACKAGED_RENDERER_DIR, resolvedPath);
267 } catch {
268 return false;
269 }
270 }
271
272 const { url } = resolveDesktopRendererUrl(
273 process.env.ELECTRON_RENDERER_URL,
274 process.env.COWORK_DESKTOP_RENDERER_PORT,
275 );
276
277 try {
278 const trusted = new URL(url);
279 const parsed = new URL(rawUrl);
280 return (
281 trusted.protocol === parsed.protocol &&
282 trusted.hostname === parsed.hostname &&
283 trusted.port === parsed.port
284 );
285 } catch {
286 return false;
287 }
288}
289
290function parseTrustedCanvasWindowUrl(rawUrl: string): ShowCanvasWindowInput | null {
291 if (!isTrustedRendererNavigation(rawUrl)) {
292 return null;
293 }
294
295 try {
296 const parsed = new URL(rawUrl);
297 if (parsed.searchParams.get("window") !== "canvas") {
298 return null;
299 }
300 const targetPath = parsed.searchParams.get("path");
301 if (!targetPath) {
302 return null;
303 }
304 return { path: targetPath };
305 } catch {
306 return null;
307 }
308}
309
310function applyWindowSecurity(win: Electron.BrowserWindow): void {
311 win.webContents.setWindowOpenHandler(({ url }) => {
312 const canvasWindow = parseTrustedCanvasWindowUrl(url);
313 if (canvasWindow) {
314 void createCanvasWindow(canvasWindow);
315 } else if (isExternalUrl(url)) {
316 void shell.openExternal(url);
317 }
318 return { action: "deny" };
319 });
320
321 win.webContents.on("will-navigate", (event: Electron.Event, navigationUrl: string) => {
322 if (isTrustedRendererNavigation(navigationUrl)) {
323 return;
324 }
325
326 event.preventDefault();
327 if (isExternalUrl(navigationUrl)) {
328 void shell.openExternal(navigationUrl);
329 }
330 });
331
332 win.webContents.on("will-attach-webview", (event: Electron.Event) => {
333 event.preventDefault();
334 });
335
336 win.webContents.session.setPermissionRequestHandler(
337 (
338 _webContents: Electron.WebContents,
339 _permission: string,
340 callback: (permissionGranted: boolean) => void,
341 ) => {
342 callback(false);
343 },
344 );
345}
346
347function resolveDesktopSmokeConfig(): { workspacePath: string; outputPath: string } | null {
348 const workspacePath = process.env[DESKTOP_SMOKE_WORKSPACE_ENV]?.trim();
349 const outputPath = process.env[DESKTOP_SMOKE_OUTPUT_ENV]?.trim();
350 if (!workspacePath || !outputPath) {
351 return null;
352 }
353 return { workspacePath, outputPath };
354}
355
356async function maybeRunPackagedSmoke(initialState: PersistedState | null): Promise<boolean> {
357 const smokeConfig = resolveDesktopSmokeConfig();
358 if (!smokeConfig) {
359 return false;
360 }
361
362 const workspaceId = "__desktop_smoke__";
363
364 try {
365 const listening = await serverManager.startWorkspaceServer({
366 workspaceId,
367 workspacePath: smokeConfig.workspacePath,
368 yolo: true,
369 privacyTelemetrySettings: initialState?.privacyTelemetrySettings,
370 });
371 await runDesktopSmokePromptLoadCheck({
372 url: listening.url,
373 workspacePath: smokeConfig.workspacePath,
374 clientVersion: app.getVersion(),
375 });
376
377 await fs.mkdir(path.dirname(smokeConfig.outputPath), { recursive: true });
378 await fs.writeFile(
379 smokeConfig.outputPath,
380 `${JSON.stringify(
381 {
382 ok: true,
383 type: "server_listening",
384 promptLoaded: true,
385 turnCompleted: true,
386 url: listening.url,
387 platform: process.platform,
388 arch: process.arch,
389 },
390 null,
391 2,
392 )}\n`,
393 "utf8",
394 );
395 await serverManager.stopWorkspaceServer(workspaceId);
396 app.exit(0);
397 return true;
398 } catch (error) {
399 captureCrashReportingError(error, {
400 tags: { operation: "desktop_smoke" },
401 });
402 await fs.mkdir(path.dirname(smokeConfig.outputPath), { recursive: true });
403 await fs.writeFile(
404 smokeConfig.outputPath,
405 `${JSON.stringify(
406 {
407 ok: false,
408 error: error instanceof Error ? error.message : String(error),
409 platform: process.platform,
410 arch: process.arch,
411 },
412 null,
413 2,
414 )}\n`,
415 "utf8",
416 );
417 try {
418 await serverManager.stopWorkspaceServer(workspaceId);
419 } catch {
420 // Ignore cleanup failures in smoke mode.
421 }
422 app.exit(1);
423 return true;
424 }
425}
426
427async function loadRendererWindow(
428 win: Electron.BrowserWindow,
429 windowMode: "main" | "quick-chat" | "utility" | "canvas",
430 query: Record<string, string> = {},
431): Promise<void> {
432 const appearance = getSystemAppearanceSnapshot();
433 const initialAppearanceQuery = {
434 platform: appearance.platform,
435 themeSource: appearance.themeSource,
436 resolvedTheme: appearance.shouldUseDarkColors ? "dark" : "light",
437 highContrast:
438 appearance.shouldUseHighContrastColors || appearance.inForcedColorsMode ? "true" : "false",
439 reducedTransparency: appearance.prefersReducedTransparency ? "true" : "false",
440 };
441 if (!app.isPackaged) {
442 const { url, warning } = resolveDesktopRendererUrl(
443 process.env.ELECTRON_RENDERER_URL,
444 process.env.COWORK_DESKTOP_RENDERER_PORT,
445 );
446 if (warning) {
447 logWarn("renderer", "renderer URL warning", { warning });
448 console.warn(`[desktop] ${warning}`);
449 }
450 const target = new URL(url);
451 if (windowMode !== "main") {
452 target.searchParams.set("window", windowMode);
453 }
454 for (const [key, value] of Object.entries({ ...initialAppearanceQuery, ...query })) {
455 target.searchParams.set(key, value);
456 }
457 await win.loadURL(target.toString());
458 return;
459 }
460
461 await win.loadFile(path.join(__dirname, "../renderer/index.html"), {
462 query: {
463 ...(windowMode === "main" ? {} : { window: windowMode }),
464 ...initialAppearanceQuery,
465 ...query,
466 },
467 });
468}
469
470async function createMainWindow(): Promise<Electron.BrowserWindow> {
471 const useMacosNativeGlass = shouldUseMacosNativeGlass(process.platform, process.env, {
472 prefersReducedTransparency: getSystemAppearanceSnapshot().prefersReducedTransparency,
473 });
474 const useDarkColors = getSystemAppearanceSnapshot().shouldUseDarkColors;
475
476 // Restore last-saved window bounds (clamped to a visible display) so the
477 // window reopens where the user left it. Falls back to defaults on first
478 // launch or if the saved state is unusable.
479 const savedBounds = await loadMainWindowBounds(app, screen);
480
481 const win = new BrowserWindow({
482 title: "Cowork",
483 width: savedBounds?.width ?? 1240,
484 height: savedBounds?.height ?? 820,
485 minWidth: MAIN_WINDOW_MIN_WIDTH,
486 ...(savedBounds ? { x: savedBounds.x, y: savedBounds.y } : {}),
487 ...getInitialWindowAppearanceOptions({ useDarkColors, useMacosNativeGlass }),
488 ...getPlatformBrowserWindowOptions(process.platform, { useDarkColors, useMacosNativeGlass }),
489 webPreferences: {
490 preload: path.join(__dirname, "../preload/preload.js"),
491 contextIsolation: true,
492 nodeIntegration: false,
493 sandbox: true,
494 webSecurity: true,
495 allowRunningInsecureContent: false,
496 webviewTag: false,
497 safeDialogs: true,
498 devTools: !app.isPackaged,
499 },
500 });
501 if (savedBounds?.isMaximized) {
502 win.maximize();
503 }
504 mainWindow = win;
505 windowCloseCoordinator.track(win as unknown as NativeCloseWindow);
506 // Persist bounds on resize/move so the next launch restores them.
507 const stopTrackingBounds = trackMainWindowBounds(app, win);
508 applyPlatformWindowCreated(win, process.platform);
509 applyWindowSecurity(win);
510 syncWindowAppearance(win, {
511 platform: process.platform,
512 useDarkColors,
513 useMacosNativeGlass,
514 });
515 const showWindow = () => {
516 if (win.isDestroyed()) {
517 return;
518 }
519 win.show();
520 };
521 const readyToShowTimeout = setTimeout(showWindow, WINDOW_SHOW_FALLBACK_TIMEOUT_MS);
522
523 win.once("ready-to-show", () => {
524 clearTimeout(readyToShowTimeout);
525 showWindow();
526 });
527
528 win.webContents.on(
529 "context-menu",
530 (_event: Electron.Event, params: Electron.ContextMenuParams) => {
531 const hasSelection = params.selectionText.trim().length > 0;
532 const isEditable = params.isEditable;
533
534 const menuItems: Electron.MenuItemConstructorOptions[] = [];
535
536 if (isEditable) {
537 menuItems.push(
538 { role: "cut", enabled: hasSelection },
539 { role: "copy", enabled: hasSelection },
540 { role: "paste" },
541 { type: "separator" },
542 { role: "selectAll" },
543 );
544 } else if (hasSelection) {
545 menuItems.push({ role: "copy" }, { type: "separator" }, { role: "selectAll" });
546 } else {
547 menuItems.push({ role: "selectAll" });
548 }
549
550 Menu.buildFromTemplate(menuItems).popup();
551 },
552 );
553
554 win.on("closed", () => {
555 clearTimeout(readyToShowTimeout);
556 // Flush final bounds to disk before the window reference is dropped.
557 stopTrackingBounds();
558 if (mainWindow === win) {
559 mainWindow = null;
560 }
561 });
562
563 win.webContents.once("did-finish-load", () => {
564 emitSystemAppearance();
565 });
566
567 await loadRendererWindow(win, "main");
568 return win;
569}
570
571async function createQuickChatWindow(
572 opts?: ShowQuickChatWindowInput,
573): Promise<Electron.BrowserWindow> {
574 const useMacosNativeGlass = shouldUseMacosNativeGlass(process.platform, process.env, {
575 prefersReducedTransparency: getSystemAppearanceSnapshot().prefersReducedTransparency,
576 });
577 const useDarkColors = getSystemAppearanceSnapshot().shouldUseDarkColors;
578 const isDarwin = process.platform === "darwin";
579
580 const win = new BrowserWindow({
581 title: "Cowork Quick Chat",
582 width: 337,
583 height: 552,
584 minWidth: 275,
585 minHeight: 449,
586 show: false,
587 frame: false,
588 resizable: true,
589 minimizable: false,
590 maximizable: false,
591 fullscreenable: false,
592 skipTaskbar: true,
593 autoHideMenuBar: true,
594 // Let the native window clip the transparent host instead of drawing a
595 // second CSS radius at the renderer edge.
596 roundedCorners: true,
597 hasShadow: true,
598 backgroundColor: useDarkColors ? "#1f1d1a" : "#f5f0e5",
599 ...getInitialWindowAppearanceOptions({ useDarkColors, useMacosNativeGlass }),
600 ...(isDarwin ? { transparent: true, backgroundColor: "#00000000" } : {}),
601 webPreferences: {
602 preload: path.join(__dirname, "../preload/preload.js"),
603 contextIsolation: true,
604 nodeIntegration: false,
605 sandbox: true,
606 webSecurity: true,
607 allowRunningInsecureContent: false,
608 webviewTag: false,
609 safeDialogs: true,
610 devTools: !app.isPackaged,
611 },
612 });
613
614 applyPlatformWindowCreated(win, process.platform);
615 if (process.platform === "darwin") {
616 win.setWindowButtonVisibility(false);
617 }
618 applyWindowSecurity(win);
619 syncWindowAppearance(win, {
620 platform: process.platform,
621 useDarkColors,
622 useMacosNativeGlass,
623 });
624 if (isDarwin) {
625 win.setBackgroundColor("#00000000");
626 }
627 win.setAlwaysOnTop(true, process.platform === "darwin" ? "pop-up-menu" : "normal");
628 await loadRendererWindow(win, "quick-chat", quickChatWindowQuery(opts));
629 return win;
630}
631
632async function retargetQuickChatWindow(
633 win: Electron.BrowserWindow,
634 opts?: ShowQuickChatWindowInput,
635): Promise<void> {
636 await loadRendererWindow(win, "quick-chat", quickChatWindowQuery(opts));
637}
638
639function quickChatWindowQuery(opts?: ShowQuickChatWindowInput): Record<string, string> {
640 return {
641 ...(opts?.threadId ? { threadId: opts.threadId } : {}),
642 ...(opts?.newThread ? { newThread: "true" } : {}),
643 };
644}
645
646async function createCanvasWindow(opts: ShowCanvasWindowInput): Promise<Electron.BrowserWindow> {
647 const platform = hostPlatform();
648 const useDarkColors = getSystemAppearanceSnapshot().shouldUseDarkColors;
649 const useMacosNativeGlass = false;
650 const backgroundColor = getCanvasNativeBackgroundColor(opts.path, useDarkColors);
651 const appearanceOptions = {
652 useDarkColors,
653 useMacosNativeGlass,
654 backgroundColor,
655 captionSymbolTone: getCanvasCaptionSymbolTone(opts.path, useDarkColors),
656 ...(platform === "win32" ? { backgroundMaterial: "none" as const } : {}),
657 };
658
659 const win = new BrowserWindow({
660 title: "Cowork Canvas",
661 width: 800,
662 height: 600,
663 minWidth: 400,
664 minHeight: 400,
665 ...getInitialWindowAppearanceOptions(appearanceOptions),
666 ...getPlatformBrowserWindowOptions(platform, appearanceOptions),
667 webPreferences: {
668 preload: path.join(__dirname, "../preload/preload.js"),
669 contextIsolation: true,
670 nodeIntegration: false,
671 sandbox: true,
672 webSecurity: true,
673 allowRunningInsecureContent: false,
674 webviewTag: false,
675 safeDialogs: true,
676 devTools: !app.isPackaged,
677 },
678 });
679
680 registerWindowAppearanceProfile(win, {
681 backgroundColor: (nextUseDarkColors) =>
682 getCanvasNativeBackgroundColor(opts.path, nextUseDarkColors),
683 captionSymbolTone: (nextUseDarkColors) =>
684 getCanvasCaptionSymbolTone(opts.path, nextUseDarkColors),
685 useMacosNativeGlass,
686 ...(platform === "win32" ? { backgroundMaterial: "none" } : {}),
687 });
688 windowCloseCoordinator.track(win as unknown as NativeCloseWindow);
689 applyPlatformWindowCreated(win, platform);
690 applyWindowSecurity(win);
691 syncWindowAppearance(win, {
692 platform,
693 ...appearanceOptions,
694 });
695
696 win.show();
697 await loadRendererWindow(win, "canvas", { path: opts.path });
698 return win;
699}
700
701async function createUtilityWindow(): Promise<Electron.BrowserWindow> {
702 const useMacosNativeGlass = shouldUseMacosNativeGlass(process.platform, process.env, {
703 prefersReducedTransparency: getSystemAppearanceSnapshot().prefersReducedTransparency,
704 });
705 const useDarkColors = getSystemAppearanceSnapshot().shouldUseDarkColors;
706 const isDarwin = process.platform === "darwin";
707
708 const win = new BrowserWindow({
709 title: "Cowork Menu",
710 width: 252,
711 height: 400,
712 minWidth: 252,
713 minHeight: 300,
714 maxWidth: 312,
715 maxHeight: 560,
716 show: false,
717 frame: false,
718 resizable: false,
719 minimizable: false,
720 maximizable: false,
721 fullscreenable: false,
722 skipTaskbar: true,
723 autoHideMenuBar: true,
724 // macOS: match the system rounded (menu-style) window shape so a transparent
725 // frameless host does not show ragged/empty crescents; renderer draws edge-to-edge.
726 roundedCorners: true,
727 hasShadow: true,
728 backgroundColor: useDarkColors ? "#1f1d1a" : "#f5f0e5",
729 ...getInitialWindowAppearanceOptions({ useDarkColors, useMacosNativeGlass }),
730 ...(isDarwin ? { transparent: true, backgroundColor: "#00000000" } : {}),
731 webPreferences: {
732 preload: path.join(__dirname, "../preload/preload.js"),
733 contextIsolation: true,
734 nodeIntegration: false,
735 sandbox: true,
736 webSecurity: true,
737 allowRunningInsecureContent: false,
738 webviewTag: false,
739 safeDialogs: true,
740 devTools: !app.isPackaged,
741 },
742 });
743
744 applyPlatformWindowCreated(win, process.platform);
745 if (process.platform === "darwin") {
746 win.setWindowButtonVisibility(false);
747 }
748 applyWindowSecurity(win);
749 syncWindowAppearance(win, {
750 platform: process.platform,
751 useDarkColors,
752 useMacosNativeGlass,
753 });
754 if (isDarwin) {
755 win.setBackgroundColor("#00000000");
756 }
757 win.setAlwaysOnTop(true, process.platform === "darwin" ? "pop-up-menu" : "normal");
758 await loadRendererWindow(win, "utility");
759 return win;
760}
761
762async function ensureMainWindow(): Promise<Electron.BrowserWindow> {
763 if (mainWindow && !mainWindow.isDestroyed()) {
764 revealAndActivateWindow(app, mainWindow);
765 return mainWindow;
766 }
767 const win = await createMainWindow();
768 revealAndActivateWindow(app, win);
769 return win;
770}
771
772const gotSingleInstanceLock = app.requestSingleInstanceLock();
773if (!gotSingleInstanceLock) {
774 app.quit();
775} else {
776 app.on("second-instance", () => {
777 void quickChatController?.showMainWindow();
778 });
779
780 app
781 .whenReady()
782 .then(async () => {
783 registerDesktopMediaProtocolHandler(protocol, net, workspaceRoots);
784 const initialState: PersistedState | null = await persistence.loadState().catch(() => null);
785 const initialThemeSource = await appearancePreferences
786 .loadThemeSource()
787 .catch((): ThemeSource => "system");
788 applyThemeSourcePreference(initialThemeSource);
789 await initElectronMainCrashReporting(initialState?.privacyTelemetrySettings);
790 let preparedInitialState = initialState;
791 if (preparedInitialState) {
792 const prepared = productAnalytics.preparePersistedState(preparedInitialState);
793 preparedInitialState = prepared.state;
794 if (prepared.changed) {
795 await persistence.saveState(preparedInitialState);
796 }
797 await productAnalytics.applyPersistedState(preparedInitialState);
798 } else {
799 await productAnalytics.applyPersistedState({
800 version: 2,
801 workspaces: [],
802 threads: [],
803 privacyTelemetrySettings: undefined,
804 });
805 }
806
807 if (await maybeRunPackagedSmoke(preparedInitialState)) {
808 return;
809 }
810
811 quickChatController = new QuickChatController({
812 appName: DESKTOP_APP_NAME,
813 trayIconPath: resolveTrayIconPath(__dirname),
814 getMainWindow: () => mainWindow,
815 createMainWindow,
816 createQuickChatWindow,
817 retargetQuickChatWindow,
818 createUtilityWindow,
819 });
820 if (preparedInitialState) {
821 quickChatController.applyPersistedState(preparedInitialState);
822 }
823 quickChatController.initialize();
824
825 registerDesktopIpc({
826 appearancePreferences,
827 mobileRelayBridge,
828 persistence,
829 workspaceRoots,
830 productAnalytics,
831 cloudSync,
832 diagnostics,
833 serverManager,
834 updater,
835 showMainWindow: () => quickChatController?.showMainWindow(),
836 consumePendingMenuCommands: () => menuCommandDispatcher.drainPending(),
837 showQuickChatWindow: (opts?: ShowQuickChatWindowInput) =>
838 quickChatController?.showQuickChatWindow(opts),
839 showCanvasWindow: (opts: ShowCanvasWindowInput) => {
840 void createCanvasWindow(opts);
841 },
842 resolveWindowCloseRequest: (sender, response) => {
843 windowCloseCoordinator.resolve(sender, response);
844 },
845 shouldKeepPopupWindowsAlive: () =>
846 quickChatController?.shouldKeepPopupWindowsAlive() === true,
847 applyPersistedState: (state: PersistedState) => {
848 quickChatController?.applyPersistedState(state);
849 void productAnalytics.applyPersistedState(state).then(async (prepared) => {
850 if (prepared.changed) {
851 await persistence.saveState(prepared.state);
852 }
853 });
854 },
855 });
856 unregisterAppearanceListener = registerSystemAppearanceListener(
857 (appearance: SystemAppearance) => {
858 for (const win of BrowserWindow.getAllWindows()) {
859 if (win.isDestroyed()) {
860 continue;
861 }
862 applySystemAppearanceToWindow(win, appearance);
863 }
864 emitDesktopEvent(DESKTOP_EVENT_CHANNELS.systemAppearanceChanged, appearance);
865 },
866 );
867
868 installDesktopApplicationMenu({
869 includeDevTools: !app.isPackaged,
870 openExternal: (url: string) => {
871 void shell.openExternal(url);
872 },
873 openQuickChat: () => {
874 void quickChatController?.showQuickChatWindow();
875 },
876 sendCommand: (command: DesktopMenuCommand) => {
877 void sendMenuCommand(command);
878 },
879 });
880
881 updater.start();
882 void ensureMainWindow();
883
884 app.on("activate", () => {
885 void ensureMainWindow();
886 });
887 })
888 .catch((error) => {
889 logError("main", error, { operation: "desktop_startup" });
890 captureCrashReportingError(error, {
891 tags: { operation: "desktop_startup" },
892 });
893 console.error(error);
894 app.exit(1);
895 });
896
897 app.on(
898 "before-quit",
899 createBeforeQuitHandler({
900 unregisterAppearanceListener: () => unregisterAppearanceListener(),
901 stopUpdater: () => updater.dispose(),
902 stopMobileRelayBridge: async () => {
903 mobileRelayBridge.stopForShutdown();
904 },
905 stopQuickChat: () => quickChatController?.dispose(),
906 stopProductAnalytics: () => productAnalytics.shutdown(),
907 stopCloudSync: () => cloudSync.shutdown(),
908 stopAllServers: () => serverManager.stopAll(),
909 quit: () => app.quit(),
910 onError: (error) => {
911 logError("shutdown", error, { operation: "stop_workspace_servers" });
912 console.error(
913 `[desktop] Failed to stop workspace servers during shutdown: ${String(error)}`,
914 );
915 },
916 }),
917 );
918
919 app.on("window-all-closed", () => {
920 if (
921 process.platform === "linux" ||
922 (process.platform === "win32" && !quickChatController?.hasTray())
923 ) {
924 app.quit();
925 }
926 });
927}
928