@wrongstack/desktop 0.280.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2293 @@
1
+ import {
2
+ DesktopAgentBridge
3
+ } from "./chunk-F3LGFYUK.js";
4
+
5
+ // src/main/main.ts
6
+ import * as path3 from "path";
7
+ import * as fs3 from "fs/promises";
8
+ import { wstackGlobalRoot as wstackGlobalRoot3 } from "@wrongstack/core/utils";
9
+ import {
10
+ app,
11
+ BaseWindow,
12
+ dialog,
13
+ ipcMain,
14
+ Menu,
15
+ screen,
16
+ shell,
17
+ WebContentsView
18
+ } from "electron";
19
+
20
+ // src/main/ipc.ts
21
+ var IPC = {
22
+ getState: "desktop:get-state",
23
+ getConversation: "desktop:get-conversation",
24
+ getWebuiStatus: "desktop:get-webui-status",
25
+ openProject: "desktop:open-project",
26
+ registerProject: "desktop:register-project",
27
+ unregisterProject: "desktop:unregister-project",
28
+ openProjectSession: "desktop:open-project-session",
29
+ activateRuntime: "desktop:activate-runtime",
30
+ closeRuntime: "desktop:close-runtime",
31
+ navigateWebui: "desktop:navigate-webui",
32
+ reloadWebui: "desktop:reload-webui",
33
+ setShellSidebarCollapsed: "desktop:set-shell-sidebar-collapsed",
34
+ openSettings: "desktop:open-settings",
35
+ sendMessage: "desktop:send-message",
36
+ abortRuntime: "desktop:abort-runtime",
37
+ openRuntimeInBrowser: "desktop:open-runtime-in-browser",
38
+ revealRuntimeRoot: "desktop:reveal-runtime-root",
39
+ stateChanged: "desktop:state-changed",
40
+ conversationChanged: "desktop:conversation-changed",
41
+ webuiStatusChanged: "desktop:webui-status-changed",
42
+ webuiReadyChanged: "desktop:webui-ready-changed",
43
+ webuiPrefsChanged: "desktop:webui-prefs-changed",
44
+ webuiCommandAck: "desktop:webui-command-ack",
45
+ webuiCommand: "desktop:webui-command",
46
+ shellSidebarCollapsedChanged: "desktop:shell-sidebar-collapsed-changed",
47
+ setLocale: "desktop:set-locale",
48
+ localeChanged: "desktop:locale-changed",
49
+ // Embedded WebUI view side — the desktop shell pushes locale changes here
50
+ // so the React WebUI inside Electron can swap i18n instantly, without waiting
51
+ // for the config-file watcher → WS prefs.updated round-trip.
52
+ webuiLocaleChanged: "desktop:webui-locale-changed"
53
+ };
54
+
55
+ // src/main/i18n-main.ts
56
+ var en = {
57
+ windowTitle: "WrongStack Desktop",
58
+ noOpenProjectSessions: "No open project sessions",
59
+ revealProjectFolder: "Reveal Project Folder",
60
+ quickView: "Quick View",
61
+ chat: "Chat",
62
+ terminal: "Terminal",
63
+ session: "Session",
64
+ newSession: "New Session",
65
+ file: "File",
66
+ view: "View",
67
+ workspace: "Workspace",
68
+ projects: "Projects",
69
+ openProject: "Open Project",
70
+ registerProject: "Register Project",
71
+ openProjectEllipsis: "Open Project\u2026",
72
+ registerProjectEllipsis: "Register Project\u2026",
73
+ removeActiveFromRegistry: "Remove Active Project from Registry",
74
+ newSessionForActive: "New Session for Active Project",
75
+ settings: "Settings",
76
+ closeActiveRuntime: "Close Active Runtime",
77
+ openChat: "Open Chat",
78
+ focusPrompt: "Focus Prompt",
79
+ toggleTerminal: "Toggle Terminal",
80
+ newTerminal: "New Terminal",
81
+ files: "Files",
82
+ changes: "Changes",
83
+ sessions: "Sessions",
84
+ fleetHQ: "Fleet HQ",
85
+ commandPalette: "Command Palette",
86
+ modelSwitcher: "Model Switcher",
87
+ quickModelSwitcher: "Quick Model Switcher",
88
+ openInBrowser: "Open in Browser",
89
+ reloadWebui: "Reload WebUI",
90
+ reloadActiveWebui: "Reload Active WebUI",
91
+ closeSession: "Close Session",
92
+ yoloMode: "YOLO Mode",
93
+ nextPrediction: "Next Prediction",
94
+ contextAutoCompact: "Context Auto Compact",
95
+ compactDesktopSidebar: "Compact Desktop Sidebar"
96
+ };
97
+ var tr = {
98
+ windowTitle: "WrongStack Desktop",
99
+ noOpenProjectSessions: "A\xE7\u0131k proje oturumu yok",
100
+ revealProjectFolder: "Proje klas\xF6r\xFCn\xFC g\xF6ster",
101
+ quickView: "H\u0131zl\u0131 g\xF6r\xFCn\xFCm",
102
+ chat: "Sohbet",
103
+ terminal: "Terminal",
104
+ session: "Oturum",
105
+ newSession: "Yeni Oturum",
106
+ file: "Dosya",
107
+ view: "G\xF6r\xFCn\xFCm",
108
+ workspace: "\xC7al\u0131\u015Fma alan\u0131",
109
+ projects: "Projeler",
110
+ openProject: "Proje A\xE7",
111
+ registerProject: "Proje Kaydet",
112
+ openProjectEllipsis: "Proje A\xE7\u2026",
113
+ registerProjectEllipsis: "Proje Kaydet\u2026",
114
+ removeActiveFromRegistry: "Aktif Projeyi Kay\u0131ttan Kald\u0131r",
115
+ newSessionForActive: "Aktif Proje i\xE7in Yeni Oturum",
116
+ settings: "Ayarlar",
117
+ closeActiveRuntime: "Aktif \xC7al\u0131\u015Fma Zaman\u0131n\u0131 Kapat",
118
+ openChat: "Sohbeti A\xE7",
119
+ focusPrompt: "\u0130steme Odaklan",
120
+ toggleTerminal: "Terminali A\xE7/Kapat",
121
+ newTerminal: "Yeni Terminal",
122
+ files: "Dosyalar",
123
+ changes: "De\u011Fi\u015Fiklikler",
124
+ sessions: "Oturumlar",
125
+ fleetHQ: "Filo HQ",
126
+ commandPalette: "Komut Paleti",
127
+ modelSwitcher: "Model De\u011Fi\u015Ftirici",
128
+ quickModelSwitcher: "H\u0131zl\u0131 Model De\u011Fi\u015Ftirici",
129
+ openInBrowser: "Taray\u0131c\u0131da A\xE7",
130
+ reloadWebui: "WebUI Yeniden Y\xFCkle",
131
+ reloadActiveWebui: "Aktif WebUI Yeniden Y\xFCkle",
132
+ closeSession: "Oturumu Kapat",
133
+ yoloMode: "YOLO Modu",
134
+ nextPrediction: "Sonraki Tahmin",
135
+ contextAutoCompact: "Ba\u011Flam\u0131 Otomatik S\u0131k\u0131\u015Ft\u0131r",
136
+ compactDesktopSidebar: "Masa\xFCst\xFC Kenar \xC7ubu\u011Funu Daralt"
137
+ };
138
+ var de = {
139
+ windowTitle: "WrongStack Desktop",
140
+ noOpenProjectSessions: "Keine offenen Projektsessions",
141
+ revealProjectFolder: "Projektordner anzeigen",
142
+ quickView: "Schnellansicht",
143
+ chat: "Chat",
144
+ terminal: "Terminal",
145
+ session: "Sitzung",
146
+ newSession: "Neue Sitzung",
147
+ file: "Datei",
148
+ view: "Ansicht",
149
+ workspace: "Workspace",
150
+ projects: "Projekte",
151
+ openProject: "Projekt \xF6ffnen",
152
+ registerProject: "Projekt registrieren",
153
+ openProjectEllipsis: "Projekt \xF6ffnen\u2026",
154
+ registerProjectEllipsis: "Projekt registrieren\u2026",
155
+ removeActiveFromRegistry: "Aktives Projekt aus Registry entfernen",
156
+ newSessionForActive: "Neue Sitzung f\xFCr aktives Projekt",
157
+ settings: "Einstellungen",
158
+ closeActiveRuntime: "Aktive Runtime schlie\xDFen",
159
+ openChat: "Chat \xF6ffnen",
160
+ focusPrompt: "Prompt fokussieren",
161
+ toggleTerminal: "Terminal umschalten",
162
+ newTerminal: "Neues Terminal",
163
+ files: "Dateien",
164
+ changes: "\xC4nderungen",
165
+ sessions: "Sitzungen",
166
+ fleetHQ: "Fleet HQ",
167
+ commandPalette: "Command Palette",
168
+ modelSwitcher: "Modell-Umschalter",
169
+ quickModelSwitcher: "Schneller Modell-Umschalter",
170
+ openInBrowser: "Im Browser \xF6ffnen",
171
+ reloadWebui: "WebUI neu laden",
172
+ reloadActiveWebui: "Aktive WebUI neu laden",
173
+ closeSession: "Sitzung schlie\xDFen",
174
+ yoloMode: "YOLO-Modus",
175
+ nextPrediction: "Next-Step-Vorhersage",
176
+ contextAutoCompact: "Kontext auto-kompaktieren",
177
+ compactDesktopSidebar: "Desktop-Seitenleiste einklappen"
178
+ };
179
+ var fr = {
180
+ windowTitle: "WrongStack Desktop",
181
+ noOpenProjectSessions: "Aucune session de projet ouverte",
182
+ revealProjectFolder: "R\xE9v\xE9ler le dossier du projet",
183
+ quickView: "Vue rapide",
184
+ chat: "Chat",
185
+ terminal: "Terminal",
186
+ session: "Session",
187
+ newSession: "Nouvelle session",
188
+ file: "Fichier",
189
+ view: "Affichage",
190
+ workspace: "Espace de travail",
191
+ projects: "Projets",
192
+ openProject: "Ouvrir un projet",
193
+ registerProject: "Enregistrer le projet",
194
+ openProjectEllipsis: "Ouvrir un projet\u2026",
195
+ registerProjectEllipsis: "Enregistrer le projet\u2026",
196
+ removeActiveFromRegistry: "Retirer le projet actif du registre",
197
+ newSessionForActive: "Nouvelle session pour le projet actif",
198
+ settings: "Param\xE8tres",
199
+ closeActiveRuntime: "Fermer l'ex\xE9cution active",
200
+ openChat: "Ouvrir le chat",
201
+ focusPrompt: "Focaliser le prompt",
202
+ toggleTerminal: "Basculer le terminal",
203
+ newTerminal: "Nouveau terminal",
204
+ files: "Fichiers",
205
+ changes: "Modifications",
206
+ sessions: "Sessions",
207
+ fleetHQ: "Fleet HQ",
208
+ commandPalette: "Palette de commandes",
209
+ modelSwitcher: "S\xE9lecteur de mod\xE8le",
210
+ quickModelSwitcher: "S\xE9lecteur de mod\xE8le rapide",
211
+ openInBrowser: "Ouvrir dans le navigateur",
212
+ reloadWebui: "Recharger WebUI",
213
+ reloadActiveWebui: "Recharger WebUI actif",
214
+ closeSession: "Fermer la session",
215
+ yoloMode: "Mode YOLO",
216
+ nextPrediction: "Pr\xE9diction du prochain pas",
217
+ contextAutoCompact: "Compacter le contexte auto",
218
+ compactDesktopSidebar: "R\xE9duire la barre lat\xE9rale"
219
+ };
220
+ var it = {
221
+ windowTitle: "WrongStack Desktop",
222
+ noOpenProjectSessions: "Nessuna sessione di progetto aperta",
223
+ revealProjectFolder: "Rivela cartella progetto",
224
+ quickView: "Vista rapida",
225
+ chat: "Chat",
226
+ terminal: "Terminale",
227
+ session: "Sessione",
228
+ newSession: "Nuova sessione",
229
+ file: "File",
230
+ view: "Visualizza",
231
+ workspace: "Workspace",
232
+ projects: "Progetti",
233
+ openProject: "Apri progetto",
234
+ registerProject: "Registra progetto",
235
+ openProjectEllipsis: "Apri progetto\u2026",
236
+ registerProjectEllipsis: "Registra progetto\u2026",
237
+ removeActiveFromRegistry: "Rimuovi progetto attivo dal registro",
238
+ newSessionForActive: "Nuova sessione per il progetto attivo",
239
+ settings: "Impostazioni",
240
+ closeActiveRuntime: "Chiudi runtime attivo",
241
+ openChat: "Apri chat",
242
+ focusPrompt: "Focalizza prompt",
243
+ toggleTerminal: "Attiva/disattiva terminale",
244
+ newTerminal: "Nuovo terminale",
245
+ files: "File",
246
+ changes: "Modifiche",
247
+ sessions: "Sessioni",
248
+ fleetHQ: "Fleet HQ",
249
+ commandPalette: "Palette comandi",
250
+ modelSwitcher: "Selettore modello",
251
+ quickModelSwitcher: "Selettore modello rapido",
252
+ openInBrowser: "Apri nel browser",
253
+ reloadWebui: "Ricarica WebUI",
254
+ reloadActiveWebui: "Ricarica WebUI attivo",
255
+ closeSession: "Chiudi sessione",
256
+ yoloMode: "Modalit\xE0 YOLO",
257
+ nextPrediction: "Predizione prossimo passo",
258
+ contextAutoCompact: "Compatta contesto auto",
259
+ compactDesktopSidebar: "Comprimi barra laterale desktop"
260
+ };
261
+ var es = {
262
+ windowTitle: "WrongStack Desktop",
263
+ noOpenProjectSessions: "Sin sesiones de proyecto abiertas",
264
+ revealProjectFolder: "Revelar carpeta del proyecto",
265
+ quickView: "Vista r\xE1pida",
266
+ chat: "Chat",
267
+ terminal: "Terminal",
268
+ session: "Sesi\xF3n",
269
+ newSession: "Nueva sesi\xF3n",
270
+ file: "Archivo",
271
+ view: "Ver",
272
+ workspace: "Espacio de trabajo",
273
+ projects: "Proyectos",
274
+ openProject: "Abrir proyecto",
275
+ registerProject: "Registrar proyecto",
276
+ openProjectEllipsis: "Abrir proyecto\u2026",
277
+ registerProjectEllipsis: "Registrar proyecto\u2026",
278
+ removeActiveFromRegistry: "Quitar proyecto activo del registro",
279
+ newSessionForActive: "Nueva sesi\xF3n para el proyecto activo",
280
+ settings: "Ajustes",
281
+ closeActiveRuntime: "Cerrar runtime activo",
282
+ openChat: "Abrir chat",
283
+ focusPrompt: "Focalizar indicaci\xF3n",
284
+ toggleTerminal: "Alternar terminal",
285
+ newTerminal: "Nuevo terminal",
286
+ files: "Archivos",
287
+ changes: "Cambios",
288
+ sessions: "Sesiones",
289
+ fleetHQ: "Fleet HQ",
290
+ commandPalette: "Paleta de comandos",
291
+ modelSwitcher: "Selector de modelo",
292
+ quickModelSwitcher: "Selector de modelo r\xE1pido",
293
+ openInBrowser: "Abrir en el navegador",
294
+ reloadWebui: "Recargar WebUI",
295
+ reloadActiveWebui: "Recargar WebUI activo",
296
+ closeSession: "Cerrar sesi\xF3n",
297
+ yoloMode: "Modo YOLO",
298
+ nextPrediction: "Predicci\xF3n del siguiente paso",
299
+ contextAutoCompact: "Auto-compactar contexto",
300
+ compactDesktopSidebar: "Contraer barra lateral de escritorio"
301
+ };
302
+ var ptBR = {
303
+ windowTitle: "WrongStack Desktop",
304
+ noOpenProjectSessions: "Sem sess\xF5es de projeto abertas",
305
+ revealProjectFolder: "Revelar pasta do projeto",
306
+ quickView: "Vista r\xE1pida",
307
+ chat: "Chat",
308
+ terminal: "Terminal",
309
+ session: "Sess\xE3o",
310
+ newSession: "Nova sess\xE3o",
311
+ file: "Arquivo",
312
+ view: "Ver",
313
+ workspace: "Workspace",
314
+ projects: "Projetos",
315
+ openProject: "Abrir projeto",
316
+ registerProject: "Registrar projeto",
317
+ openProjectEllipsis: "Abrir projeto\u2026",
318
+ registerProjectEllipsis: "Registrar projeto\u2026",
319
+ removeActiveFromRegistry: "Remover projeto ativo do registro",
320
+ newSessionForActive: "Nova sess\xE3o para o projeto ativo",
321
+ settings: "Configura\xE7\xF5es",
322
+ closeActiveRuntime: "Fechar runtime ativo",
323
+ openChat: "Abrir chat",
324
+ focusPrompt: "Focar prompt",
325
+ toggleTerminal: "Alternar terminal",
326
+ newTerminal: "Novo terminal",
327
+ files: "Arquivos",
328
+ changes: "Altera\xE7\xF5es",
329
+ sessions: "Sess\xF5es",
330
+ fleetHQ: "Fleet HQ",
331
+ commandPalette: "Paleta de comandos",
332
+ modelSwitcher: "Seletor de modelo",
333
+ quickModelSwitcher: "Seletor de modelo r\xE1pido",
334
+ openInBrowser: "Abrir no navegador",
335
+ reloadWebui: "Recarregar WebUI",
336
+ reloadActiveWebui: "Recarregar WebUI ativo",
337
+ closeSession: "Fechar sess\xE3o",
338
+ yoloMode: "Modo YOLO",
339
+ nextPrediction: "Previs\xE3o do pr\xF3ximo passo",
340
+ contextAutoCompact: "Auto-compactar contexto",
341
+ compactDesktopSidebar: "Recolher barra lateral do desktop"
342
+ };
343
+ var CATALOGS = { en, tr, de, fr, it, es, "pt-BR": ptBR };
344
+ var mainLocale = "en";
345
+ function setMainLocale(locale) {
346
+ if (locale in CATALOGS) mainLocale = locale;
347
+ }
348
+ function getMainLocale() {
349
+ return mainLocale;
350
+ }
351
+ function tMain(key) {
352
+ return CATALOGS[mainLocale]?.[key] ?? CATALOGS.en[key] ?? key;
353
+ }
354
+
355
+ // src/main/desktop-config-io.ts
356
+ import * as fs from "fs/promises";
357
+ import * as path from "path";
358
+ import { DefaultSecretVault } from "@wrongstack/core";
359
+ import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
360
+ import { atomicWrite, wstackGlobalRoot } from "@wrongstack/core/utils";
361
+ var globalConfigPath = path.join(wstackGlobalRoot(), "config.json");
362
+ var vault = new DefaultSecretVault({
363
+ keyFile: path.join(wstackGlobalRoot(), ".key")
364
+ });
365
+ async function readUiLocale() {
366
+ let raw;
367
+ try {
368
+ raw = await fs.readFile(globalConfigPath, "utf8");
369
+ } catch {
370
+ return void 0;
371
+ }
372
+ try {
373
+ const decrypted = decryptConfigSecrets(
374
+ JSON.parse(raw),
375
+ vault
376
+ );
377
+ const value = decrypted.uiLocale;
378
+ return typeof value === "string" && value ? value : void 0;
379
+ } catch {
380
+ return void 0;
381
+ }
382
+ }
383
+ var desktopConfigPaths = { globalConfigPath, vault };
384
+ async function writeUiLocale(code) {
385
+ let raw;
386
+ try {
387
+ raw = await fs.readFile(globalConfigPath, "utf8");
388
+ } catch {
389
+ raw = "{}";
390
+ }
391
+ let parsed;
392
+ try {
393
+ parsed = JSON.parse(raw);
394
+ } catch {
395
+ return;
396
+ }
397
+ const decrypted = decryptConfigSecrets(parsed, vault);
398
+ decrypted.uiLocale = code;
399
+ const encrypted = encryptConfigSecrets(decrypted, vault);
400
+ await atomicWrite(globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
401
+ }
402
+
403
+ // src/main/runtime-manager.ts
404
+ import { spawn } from "child_process";
405
+ import { randomBytes } from "crypto";
406
+ import { EventEmitter } from "events";
407
+ import * as fs2 from "fs/promises";
408
+ import * as http from "http";
409
+ import { createRequire } from "module";
410
+ import * as net from "net";
411
+ import * as os from "os";
412
+ import * as path2 from "path";
413
+ import { fileURLToPath } from "url";
414
+ import { atomicWrite as atomicWrite2, projectSlug, toErrorMessage, wstackGlobalRoot as wstackGlobalRoot2 } from "@wrongstack/core/utils";
415
+ var HTTP_PORT_START = 34560;
416
+ var WS_PORT_START = 34660;
417
+ var START_TIMEOUT_MS = 3e4;
418
+ var MIN_WINDOW_WIDTH = 760;
419
+ var MIN_WINDOW_HEIGHT = 520;
420
+ var DesktopRuntimeManager = class extends EventEmitter {
421
+ runtimes = /* @__PURE__ */ new Map();
422
+ stateFile = path2.join(wstackGlobalRoot2(), "desktop.json");
423
+ recentProjects = [];
424
+ registeredProjects = [];
425
+ restoreProjectSessions = [];
426
+ restoreActiveRuntimeId = null;
427
+ restoreActiveProjectRoot = null;
428
+ lastActiveProjectRoot = null;
429
+ windowState = null;
430
+ activeRuntimeId = null;
431
+ restoring = false;
432
+ workspaceRestoreCompleted = false;
433
+ async init() {
434
+ const state = await this.loadDesktopState();
435
+ this.recentProjects = state.recentProjects;
436
+ this.registeredProjects = await readGlobalProjectManifest();
437
+ this.restoreProjectSessions = state.openProjectSessions;
438
+ this.restoreActiveRuntimeId = state.activeRuntimeId;
439
+ this.restoreActiveProjectRoot = state.activeProjectRoot;
440
+ this.lastActiveProjectRoot = state.activeProjectRoot;
441
+ this.windowState = state.window;
442
+ }
443
+ snapshot() {
444
+ return {
445
+ activeRuntimeId: this.activeRuntimeId,
446
+ runtimes: Array.from(this.runtimes.values()).map(publicRuntime),
447
+ recentProjects: [...this.recentProjects],
448
+ registeredProjects: [...this.registeredProjects],
449
+ restoring: this.restoring
450
+ };
451
+ }
452
+ getWindowState() {
453
+ return this.windowState ? { ...this.windowState } : null;
454
+ }
455
+ async saveWindowState(window) {
456
+ this.windowState = { ...window };
457
+ await this.saveDesktopState();
458
+ }
459
+ async restoreLastWorkspace() {
460
+ const sessions = this.restoreProjectSessions.filter(
461
+ (session) => typeof session.root === "string" && session.root.trim()
462
+ );
463
+ if (sessions.length === 0 || this.restoring || this.runtimes.size > 0) {
464
+ this.workspaceRestoreCompleted = true;
465
+ return;
466
+ }
467
+ this.restoring = true;
468
+ this.emitChanged();
469
+ try {
470
+ const seen = /* @__PURE__ */ new Map();
471
+ for (const session of sessions) {
472
+ const key = pathKey(session.root);
473
+ const seenCount = seen.get(key) ?? 0;
474
+ seen.set(key, seenCount + 1);
475
+ await this.openProject(session.root, {
476
+ forceNew: seenCount > 0,
477
+ name: session.name,
478
+ runtimeId: session.runtimeId
479
+ }).catch((err) => {
480
+ process.stderr.write(
481
+ `[desktop:restore] Failed to restore ${session.root}: ${toErrorMessage(err)}
482
+ `
483
+ );
484
+ });
485
+ }
486
+ let restoredActive = false;
487
+ if (this.restoreActiveRuntimeId) {
488
+ const active = this.runtimes.get(this.restoreActiveRuntimeId);
489
+ if (active) {
490
+ await this.activateRuntime(active.id);
491
+ restoredActive = true;
492
+ }
493
+ }
494
+ if (!restoredActive && this.restoreActiveProjectRoot) {
495
+ const active = Array.from(this.runtimes.values()).find(
496
+ (runtime) => samePath(runtime.root, this.restoreActiveProjectRoot ?? "")
497
+ );
498
+ if (active) await this.activateRuntime(active.id);
499
+ }
500
+ } finally {
501
+ this.restoring = false;
502
+ this.workspaceRestoreCompleted = true;
503
+ this.emitChanged();
504
+ await this.saveDesktopState();
505
+ }
506
+ }
507
+ getRuntime(id) {
508
+ const runtime = this.runtimes.get(id);
509
+ return runtime ? publicRuntime(runtime) : void 0;
510
+ }
511
+ getRuntimeUrlWithToken(id) {
512
+ const runtime = this.runtimes.get(id);
513
+ if (!runtime) return void 0;
514
+ const url = new URL(runtime.url);
515
+ url.searchParams.set("token", runtime.token);
516
+ url.searchParams.set("shell", "desktop");
517
+ return url.toString();
518
+ }
519
+ getRuntimeWsUrlWithToken(id) {
520
+ const runtime = this.runtimes.get(id);
521
+ if (!runtime) return void 0;
522
+ const url = new URL(`ws://127.0.0.1:${runtime.wsPort}`);
523
+ url.searchParams.set("token", runtime.token);
524
+ return url.toString();
525
+ }
526
+ async openProject(projectRoot, options = {}) {
527
+ const resolved = path2.resolve(projectRoot);
528
+ const stat2 = await fs2.stat(resolved).catch(() => null);
529
+ if (!stat2?.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
530
+ const kind = options.kind ?? "project";
531
+ const touchRecent = options.touchRecent ?? kind === "project";
532
+ const forceNew = options.forceNew === true;
533
+ if (!forceNew) {
534
+ const existing = Array.from(this.runtimes.values()).find(
535
+ (runtime2) => samePath(runtime2.root, resolved) && runtime2.kind === kind && (runtime2.status === "starting" || runtime2.status === "running")
536
+ );
537
+ if (existing) {
538
+ this.activeRuntimeId = existing.id;
539
+ if (existing.kind === "project") this.lastActiveProjectRoot = existing.root;
540
+ if (touchRecent) {
541
+ await this.touchProject(existing.root);
542
+ } else {
543
+ await this.persistWorkspaceState();
544
+ }
545
+ this.emitChanged();
546
+ return publicRuntime(existing);
547
+ }
548
+ const staleSameRoot = Array.from(this.runtimes.values()).filter(
549
+ (runtime2) => samePath(runtime2.root, resolved) && runtime2.kind === kind
550
+ );
551
+ for (const stale of staleSameRoot) {
552
+ await this.closeRuntimeInternal(stale.id, { persistWorkspace: false });
553
+ }
554
+ }
555
+ const slug = projectSlug(resolved);
556
+ const requestedRuntimeId = normalizeRuntimeId(options.runtimeId);
557
+ const runtimeId = requestedRuntimeId && !this.runtimes.has(requestedRuntimeId) ? requestedRuntimeId : `${slug}-${randomBytes(3).toString("hex")}`;
558
+ const name = options.name ?? nextRuntimeName(this.runtimes, resolved, kind);
559
+ const httpPort = await findFreePort(HTTP_PORT_START, usedPorts(this.runtimes));
560
+ const wsPort = await findFreePort(
561
+ WS_PORT_START,
562
+ /* @__PURE__ */ new Set([...usedPorts(this.runtimes), httpPort])
563
+ );
564
+ const token = randomBytes(24).toString("hex");
565
+ const runtime = {
566
+ id: runtimeId,
567
+ name,
568
+ root: resolved,
569
+ slug,
570
+ kind,
571
+ status: "starting",
572
+ httpPort,
573
+ wsPort,
574
+ url: `http://127.0.0.1:${httpPort}`,
575
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
576
+ token,
577
+ child: null,
578
+ logs: [],
579
+ logNotifyTimer: null
580
+ };
581
+ this.runtimes.set(runtimeId, runtime);
582
+ this.activeRuntimeId = runtimeId;
583
+ if (kind === "project") this.lastActiveProjectRoot = resolved;
584
+ if (touchRecent) {
585
+ await this.touchProject(resolved);
586
+ } else {
587
+ await this.persistWorkspaceState();
588
+ }
589
+ this.emitChanged();
590
+ try {
591
+ const entry = resolveWebUiEntry();
592
+ const child = spawn(
593
+ process.execPath,
594
+ [
595
+ entry,
596
+ "--host",
597
+ "127.0.0.1",
598
+ "--port",
599
+ String(httpPort),
600
+ "--ws-port",
601
+ String(wsPort),
602
+ "--token",
603
+ token,
604
+ "--require-token"
605
+ ],
606
+ {
607
+ cwd: resolved,
608
+ env: {
609
+ ...process.env,
610
+ ELECTRON_RUN_AS_NODE: "1",
611
+ WEBUI_STRICT_PORT: "1",
612
+ WRONGSTACK_DESKTOP: "1"
613
+ },
614
+ stdio: ["ignore", "pipe", "pipe"],
615
+ windowsHide: true
616
+ }
617
+ );
618
+ runtime.child = child;
619
+ runtime.pid = child.pid;
620
+ child.stdout?.on("data", (chunk) => {
621
+ const text = chunk.toString();
622
+ appendRuntimeLog(runtime, "stdout", text);
623
+ this.scheduleLogChanged(runtime);
624
+ process.stdout.write(`[desktop:${runtime.id}] ${text}`);
625
+ });
626
+ child.stderr?.on("data", (chunk) => {
627
+ const text = chunk.toString();
628
+ appendRuntimeLog(runtime, "stderr", text);
629
+ this.scheduleLogChanged(runtime);
630
+ process.stderr.write(`[desktop:${runtime.id}] ${text}`);
631
+ });
632
+ child.once("exit", (code, signal) => {
633
+ runtime.status = runtime.status === "error" ? "error" : "stopped";
634
+ runtime.error = runtime.status === "error" ? runtime.error : code === 0 ? void 0 : `Exited with ${signal ?? `code ${code ?? "unknown"}`}`;
635
+ runtime.child = null;
636
+ if (this.activeRuntimeId === runtime.id) {
637
+ this.activeRuntimeId = firstRunningRuntimeId(this.runtimes);
638
+ }
639
+ this.emitChanged();
640
+ });
641
+ await waitForHttpReady(runtime.url, token, START_TIMEOUT_MS);
642
+ runtime.status = "running";
643
+ await this.persistWorkspaceState();
644
+ this.emitChanged();
645
+ return publicRuntime(runtime);
646
+ } catch (err) {
647
+ runtime.status = "error";
648
+ runtime.error = toErrorMessage(err);
649
+ await terminateProcessTree(runtime.child);
650
+ runtime.child = null;
651
+ this.emitChanged();
652
+ throw err;
653
+ }
654
+ }
655
+ async activateRuntime(id) {
656
+ const runtime = this.runtimes.get(id);
657
+ if (!runtime) throw new Error(`Runtime not found: ${id}`);
658
+ this.activeRuntimeId = id;
659
+ if (runtime.kind === "project") this.lastActiveProjectRoot = runtime.root;
660
+ if (runtime.kind === "project") {
661
+ await this.touchProject(runtime.root);
662
+ } else {
663
+ await this.persistWorkspaceState();
664
+ }
665
+ this.emitChanged();
666
+ }
667
+ async closeRuntime(id) {
668
+ await this.closeRuntimeInternal(id, { persistWorkspace: true });
669
+ }
670
+ async closeAll(options = {}) {
671
+ const persistWorkspace = options.persistWorkspace ?? true;
672
+ await Promise.all(
673
+ Array.from(this.runtimes.keys()).map(
674
+ (id) => this.closeRuntimeInternal(id, { persistWorkspace })
675
+ )
676
+ );
677
+ }
678
+ async registerProject(projectRoot) {
679
+ const resolved = path2.resolve(projectRoot);
680
+ const stat2 = await fs2.stat(resolved).catch(() => null);
681
+ if (!stat2?.isDirectory()) throw new Error(`Not a directory: ${resolved}`);
682
+ const now = (/* @__PURE__ */ new Date()).toISOString();
683
+ const entry = {
684
+ name: path2.basename(resolved) || resolved,
685
+ root: resolved,
686
+ slug: projectSlug(resolved),
687
+ lastSeen: now,
688
+ lastWorkingDir: resolved
689
+ };
690
+ this.registeredProjects = await touchGlobalProjectManifest(entry);
691
+ this.emitChanged();
692
+ }
693
+ async unregisterProject(projectRoot) {
694
+ const resolved = path2.resolve(projectRoot);
695
+ this.registeredProjects = await removeGlobalProjectManifest(resolved);
696
+ this.recentProjects = this.recentProjects.filter((project) => !samePath(project.root, resolved));
697
+ await this.saveDesktopState();
698
+ this.emitChanged();
699
+ }
700
+ async closeRuntimeInternal(id, options) {
701
+ const runtime = this.runtimes.get(id);
702
+ if (!runtime) return;
703
+ runtime.status = "stopped";
704
+ const child = runtime.child;
705
+ runtime.child = null;
706
+ await terminateProcessTree(child);
707
+ if (runtime.logNotifyTimer) {
708
+ clearTimeout(runtime.logNotifyTimer);
709
+ runtime.logNotifyTimer = null;
710
+ }
711
+ this.runtimes.delete(id);
712
+ if (this.activeRuntimeId === id) this.activeRuntimeId = firstRunningRuntimeId(this.runtimes);
713
+ if (this.lastActiveProjectRoot && samePath(this.lastActiveProjectRoot, runtime.root)) {
714
+ this.lastActiveProjectRoot = firstProjectRuntimeRoot(this.runtimes);
715
+ }
716
+ if (options.persistWorkspace) {
717
+ await this.persistWorkspaceState();
718
+ }
719
+ this.emitChanged();
720
+ }
721
+ async touchProject(projectRoot) {
722
+ const resolved = path2.resolve(projectRoot);
723
+ const now = (/* @__PURE__ */ new Date()).toISOString();
724
+ const entry = {
725
+ name: path2.basename(resolved) || resolved,
726
+ root: resolved,
727
+ slug: projectSlug(resolved),
728
+ lastSeen: now,
729
+ lastWorkingDir: resolved
730
+ };
731
+ this.recentProjects = [
732
+ entry,
733
+ ...this.recentProjects.filter((p) => !samePath(p.root, resolved))
734
+ ].slice(0, 24);
735
+ const [, registeredProjects] = await Promise.all([
736
+ this.saveDesktopState(),
737
+ touchGlobalProjectManifest(entry)
738
+ ]);
739
+ this.registeredProjects = registeredProjects;
740
+ }
741
+ async persistWorkspaceState() {
742
+ await this.saveDesktopState();
743
+ }
744
+ async loadDesktopState() {
745
+ try {
746
+ const raw = await fs2.readFile(this.stateFile, "utf8");
747
+ const parsed = JSON.parse(raw);
748
+ const openProjects = normalizePathList(parsed.openProjects);
749
+ const openProjectSessions = normalizeSessionStateList(
750
+ parsed.openProjectSessions,
751
+ openProjects
752
+ );
753
+ return {
754
+ recentProjects: Array.isArray(parsed.recentProjects) ? parsed.recentProjects : [],
755
+ openProjects,
756
+ openProjectSessions,
757
+ activeRuntimeId: normalizeRuntimeId(parsed.activeRuntimeId) ?? null,
758
+ activeProjectRoot: typeof parsed.activeProjectRoot === "string" && parsed.activeProjectRoot.trim() ? path2.resolve(parsed.activeProjectRoot) : null,
759
+ window: normalizeWindowState(parsed.window)
760
+ };
761
+ } catch {
762
+ return {
763
+ recentProjects: [],
764
+ openProjects: [],
765
+ openProjectSessions: [],
766
+ activeRuntimeId: null,
767
+ activeProjectRoot: null,
768
+ window: null
769
+ };
770
+ }
771
+ }
772
+ async saveDesktopState() {
773
+ await fs2.mkdir(path2.dirname(this.stateFile), { recursive: true });
774
+ const liveProjectSessions = Array.from(this.runtimes.values()).filter((runtime) => runtime.status !== "stopped" && runtime.kind === "project").map((runtime) => runtimeToSessionState(runtime));
775
+ const openProjectSessions = liveProjectSessions.length === 0 && !this.workspaceRestoreCompleted ? [...this.restoreProjectSessions] : liveProjectSessions;
776
+ const openProjects = openProjectSessions.map((session) => session.root);
777
+ const activeRuntime = this.activeRuntimeId ? this.runtimes.get(this.activeRuntimeId) : null;
778
+ const lastActiveProjectRoot = this.lastActiveProjectRoot;
779
+ const fallbackSession = lastActiveProjectRoot ? openProjectSessions.find((session) => samePath(session.root, lastActiveProjectRoot)) : void 0;
780
+ const activeSession = activeRuntime?.kind === "project" ? runtimeToSessionState(activeRuntime) : fallbackSession ?? openProjectSessions[0];
781
+ const activeRoot = activeSession?.root;
782
+ const activeRuntimeId2 = activeSession?.runtimeId ?? null;
783
+ await atomicWrite2(
784
+ this.stateFile,
785
+ `${JSON.stringify(
786
+ {
787
+ recentProjects: this.recentProjects,
788
+ openProjects,
789
+ openProjectSessions,
790
+ activeRuntimeId: activeRuntimeId2,
791
+ activeProjectRoot: activeRoot ?? null,
792
+ window: this.windowState
793
+ },
794
+ null,
795
+ 2
796
+ )}
797
+ `,
798
+ { mode: 384 }
799
+ );
800
+ }
801
+ emitChanged() {
802
+ this.emit("changed");
803
+ }
804
+ scheduleLogChanged(runtime) {
805
+ if (runtime.logNotifyTimer) return;
806
+ runtime.logNotifyTimer = setTimeout(() => {
807
+ runtime.logNotifyTimer = null;
808
+ if (this.runtimes.get(runtime.id) === runtime) {
809
+ this.emitChanged();
810
+ }
811
+ }, 250);
812
+ }
813
+ };
814
+ async function terminateProcessTree(child) {
815
+ if (!child || child.killed || !child.pid) return;
816
+ if (process.platform !== "win32") {
817
+ child.kill("SIGTERM");
818
+ return;
819
+ }
820
+ await new Promise((resolve3) => {
821
+ let settled = false;
822
+ const finish = () => {
823
+ if (settled) return;
824
+ settled = true;
825
+ resolve3();
826
+ };
827
+ const timer = setTimeout(finish, 3e3);
828
+ timer.unref?.();
829
+ const killer = spawn("taskkill", ["/PID", String(child.pid), "/T", "/F"], {
830
+ stdio: "ignore",
831
+ windowsHide: true
832
+ });
833
+ killer.once("exit", () => {
834
+ clearTimeout(timer);
835
+ finish();
836
+ });
837
+ killer.once("error", () => {
838
+ clearTimeout(timer);
839
+ child.kill();
840
+ finish();
841
+ });
842
+ });
843
+ }
844
+ function publicRuntime(runtime) {
845
+ const {
846
+ child: _child,
847
+ token: _token,
848
+ logs,
849
+ logNotifyTimer: _logNotifyTimer,
850
+ ...record
851
+ } = runtime;
852
+ void _child;
853
+ void _token;
854
+ void _logNotifyTimer;
855
+ return {
856
+ ...record,
857
+ recentLogs: logs.slice(-40)
858
+ };
859
+ }
860
+ function runtimeToSessionState(runtime) {
861
+ return {
862
+ runtimeId: runtime.id,
863
+ name: runtime.name,
864
+ root: runtime.root,
865
+ startedAt: runtime.startedAt
866
+ };
867
+ }
868
+ function appendRuntimeLog(runtime, stream, text) {
869
+ for (const rawLine of text.split(/\r?\n/)) {
870
+ const line = rawLine.trimEnd();
871
+ if (!line) continue;
872
+ runtime.logs.push(`[${stream}] ${line}`);
873
+ }
874
+ if (runtime.logs.length > 120) {
875
+ runtime.logs.splice(0, runtime.logs.length - 120);
876
+ }
877
+ }
878
+ function normalizePathList(value) {
879
+ if (!Array.isArray(value)) return [];
880
+ const roots = [];
881
+ for (const item of value) {
882
+ if (typeof item !== "string" || !item.trim()) continue;
883
+ const resolved = path2.resolve(item);
884
+ roots.push(resolved);
885
+ }
886
+ return roots.slice(0, 12);
887
+ }
888
+ function normalizeSessionStateList(value, fallbackRoots) {
889
+ if (!Array.isArray(value)) {
890
+ return fallbackRoots.map((root) => ({ root })).slice(0, 12);
891
+ }
892
+ const sessions = [];
893
+ for (const item of value) {
894
+ if (!item || typeof item !== "object") continue;
895
+ const candidate = item;
896
+ if (typeof candidate.root !== "string" || !candidate.root.trim()) continue;
897
+ const session = {
898
+ root: path2.resolve(candidate.root)
899
+ };
900
+ const runtimeId = normalizeRuntimeId(candidate.runtimeId);
901
+ if (runtimeId) session.runtimeId = runtimeId;
902
+ if (typeof candidate.name === "string" && candidate.name.trim()) {
903
+ session.name = candidate.name.trim().slice(0, 120);
904
+ }
905
+ if (typeof candidate.startedAt === "string" && candidate.startedAt.trim()) {
906
+ session.startedAt = candidate.startedAt.trim();
907
+ }
908
+ sessions.push(session);
909
+ }
910
+ return sessions.slice(0, 12);
911
+ }
912
+ function normalizeRuntimeId(value) {
913
+ if (typeof value !== "string") return void 0;
914
+ const trimmed = value.trim();
915
+ if (!/^[a-zA-Z0-9._:-]{3,120}$/.test(trimmed)) return void 0;
916
+ return trimmed;
917
+ }
918
+ function normalizeWindowState(value) {
919
+ if (!value || typeof value !== "object") return null;
920
+ const candidate = value;
921
+ const width = Number(candidate.width);
922
+ const height = Number(candidate.height);
923
+ if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
924
+ if (width < MIN_WINDOW_WIDTH || height < MIN_WINDOW_HEIGHT) return null;
925
+ const state = {
926
+ width: Math.round(width),
927
+ height: Math.round(height),
928
+ maximized: Boolean(candidate.maximized)
929
+ };
930
+ if (Number.isFinite(Number(candidate.x))) state.x = Math.round(Number(candidate.x));
931
+ if (Number.isFinite(Number(candidate.y))) state.y = Math.round(Number(candidate.y));
932
+ return state;
933
+ }
934
+ function firstRunningRuntimeId(runtimes) {
935
+ return Array.from(runtimes.values()).find((runtime) => runtime.status === "running")?.id ?? null;
936
+ }
937
+ function firstProjectRuntimeRoot(runtimes) {
938
+ return Array.from(runtimes.values()).find(
939
+ (runtime) => runtime.status === "running" && runtime.kind === "project"
940
+ )?.root ?? null;
941
+ }
942
+ function usedPorts(runtimes) {
943
+ const ports = /* @__PURE__ */ new Set();
944
+ for (const runtime of runtimes.values()) {
945
+ ports.add(runtime.httpPort);
946
+ ports.add(runtime.wsPort);
947
+ }
948
+ return ports;
949
+ }
950
+ function nextRuntimeName(runtimes, root, kind) {
951
+ const baseName = path2.basename(root) || root;
952
+ if (kind !== "project") return baseName;
953
+ const liveSameRoot = Array.from(runtimes.values()).filter(
954
+ (runtime) => runtime.kind === "project" && samePath(runtime.root, root) && runtime.status !== "stopped"
955
+ ).length;
956
+ return liveSameRoot === 0 ? baseName : `${baseName} #${liveSameRoot + 1}`;
957
+ }
958
+ function pathKey(value) {
959
+ const resolved = path2.resolve(value);
960
+ return os.platform() === "win32" ? resolved.toLowerCase() : resolved;
961
+ }
962
+ async function findFreePort(startPort, exclude) {
963
+ for (let port = startPort; port < startPort + 200; port++) {
964
+ if (exclude.has(port)) continue;
965
+ if (await isPortFree(port)) return port;
966
+ }
967
+ throw new Error(`No free local port found near ${startPort}`);
968
+ }
969
+ function isPortFree(port) {
970
+ return new Promise((resolve3) => {
971
+ const server = net.createServer();
972
+ server.once("error", () => resolve3(false));
973
+ server.once("listening", () => {
974
+ server.close(() => resolve3(true));
975
+ });
976
+ server.listen(port, "127.0.0.1");
977
+ });
978
+ }
979
+ function waitForHttpReady(baseUrl, token, timeoutMs) {
980
+ const deadline = Date.now() + timeoutMs;
981
+ const url = new URL(baseUrl);
982
+ url.searchParams.set("token", token);
983
+ url.searchParams.set("shell", "desktop");
984
+ return new Promise((resolve3, reject) => {
985
+ const probe = () => {
986
+ const req = http.get(url, (res) => {
987
+ res.resume();
988
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 500) {
989
+ resolve3();
990
+ return;
991
+ }
992
+ retry();
993
+ });
994
+ req.once("error", retry);
995
+ req.setTimeout(1e3, () => {
996
+ req.destroy();
997
+ retry();
998
+ });
999
+ };
1000
+ const retry = () => {
1001
+ if (Date.now() >= deadline) {
1002
+ reject(new Error(`WebUI did not become ready at ${baseUrl}`));
1003
+ return;
1004
+ }
1005
+ setTimeout(probe, 250);
1006
+ };
1007
+ probe();
1008
+ });
1009
+ }
1010
+ function resolveWebUiEntry() {
1011
+ if (process.env["WRONGSTACK_WEBUI_ENTRY"]) {
1012
+ return path2.resolve(process.env["WRONGSTACK_WEBUI_ENTRY"]);
1013
+ }
1014
+ const require2 = createRequire(import.meta.url);
1015
+ const serverIndex = require2.resolve("@wrongstack/webui/server");
1016
+ return path2.join(path2.dirname(serverIndex), "entry.js");
1017
+ }
1018
+ async function readGlobalProjectManifest() {
1019
+ const manifestFile = path2.join(wstackGlobalRoot2(), "projects.json");
1020
+ try {
1021
+ const raw = await fs2.readFile(manifestFile, "utf8");
1022
+ const parsed = JSON.parse(raw);
1023
+ const projects = Array.isArray(parsed.projects) ? parsed.projects : [];
1024
+ return projects.filter((project) => typeof project?.root === "string" && project.root.trim()).map((project) => ({
1025
+ ...project,
1026
+ name: project.name || path2.basename(project.root) || project.root,
1027
+ root: path2.resolve(project.root),
1028
+ slug: project.slug || projectSlug(path2.resolve(project.root))
1029
+ })).sort((a, b) => (b.lastSeen ?? b.createdAt ?? "").localeCompare(a.lastSeen ?? a.createdAt ?? "")).slice(0, 80);
1030
+ } catch {
1031
+ return [];
1032
+ }
1033
+ }
1034
+ async function touchGlobalProjectManifest(entry) {
1035
+ const manifestFile = path2.join(wstackGlobalRoot2(), "projects.json");
1036
+ const projects = await readGlobalProjectManifest();
1037
+ const existing = projects.find((p) => samePath(p.root, entry.root));
1038
+ if (existing) {
1039
+ existing.name = entry.name;
1040
+ existing.slug = entry.slug;
1041
+ existing.lastSeen = entry.lastSeen;
1042
+ existing.lastWorkingDir = entry.lastWorkingDir;
1043
+ } else {
1044
+ projects.push({ ...entry, createdAt: entry.lastSeen });
1045
+ }
1046
+ const sorted = projects.sort((a, b) => (b.lastSeen ?? b.createdAt ?? "").localeCompare(a.lastSeen ?? a.createdAt ?? "")).slice(0, 80);
1047
+ await fs2.mkdir(path2.dirname(manifestFile), { recursive: true });
1048
+ await atomicWrite2(manifestFile, `${JSON.stringify({ projects: sorted }, null, 2)}
1049
+ `, { mode: 384 });
1050
+ return sorted;
1051
+ }
1052
+ async function removeGlobalProjectManifest(projectRoot) {
1053
+ const manifestFile = path2.join(wstackGlobalRoot2(), "projects.json");
1054
+ const resolved = path2.resolve(projectRoot);
1055
+ const projects = (await readGlobalProjectManifest()).filter(
1056
+ (project) => !samePath(project.root, resolved)
1057
+ );
1058
+ await fs2.mkdir(path2.dirname(manifestFile), { recursive: true });
1059
+ await atomicWrite2(manifestFile, `${JSON.stringify({ projects }, null, 2)}
1060
+ `, { mode: 384 });
1061
+ return projects;
1062
+ }
1063
+ function samePath(left, right) {
1064
+ const a = path2.resolve(left);
1065
+ const b = path2.resolve(right);
1066
+ return os.platform() === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
1067
+ }
1068
+ function rendererIndexPath() {
1069
+ return fileURLToPath(new URL("../renderer/index.html", import.meta.url));
1070
+ }
1071
+ function preloadPath() {
1072
+ return fileURLToPath(new URL("../preload/preload.cjs", import.meta.url));
1073
+ }
1074
+ function webuiPreloadPath() {
1075
+ return fileURLToPath(new URL("../preload/webui-preload.cjs", import.meta.url));
1076
+ }
1077
+
1078
+ // src/main/main.ts
1079
+ import { watchProviderConfig } from "@wrongstack/core/storage";
1080
+
1081
+ // src/main/webui-command-bridge.ts
1082
+ var DESKTOP_WEBUI_ACTIONS = /* @__PURE__ */ new Set([
1083
+ "new-session",
1084
+ "clear-context",
1085
+ "compact-context",
1086
+ "repair-context",
1087
+ "download-chat",
1088
+ "focus-chat",
1089
+ "open-command-palette",
1090
+ "open-shortcuts",
1091
+ "search-chat",
1092
+ "open-model-switcher",
1093
+ "open-prompt-library"
1094
+ ]);
1095
+ var DESKTOP_WEBUI_VIEWS = /* @__PURE__ */ new Set([
1096
+ "chat",
1097
+ "settings",
1098
+ "autophase",
1099
+ "specs",
1100
+ "sddboard",
1101
+ "sddwizard",
1102
+ "files",
1103
+ "changes",
1104
+ "sessions",
1105
+ "setup",
1106
+ "skill",
1107
+ "officemap",
1108
+ "mailbox",
1109
+ "debug",
1110
+ "design-gallery",
1111
+ "refresh-debug",
1112
+ "analytics"
1113
+ ]);
1114
+ var DESKTOP_WEBUI_ACTIVITIES = /* @__PURE__ */ new Set([
1115
+ "chat",
1116
+ "agents",
1117
+ "history",
1118
+ "files",
1119
+ "changes",
1120
+ "mailbox",
1121
+ "skills",
1122
+ "design",
1123
+ "worktrees",
1124
+ "officemap"
1125
+ ]);
1126
+ var DESKTOP_WEBUI_OVERLAYS = /* @__PURE__ */ new Set([
1127
+ "fleet",
1128
+ "agents-monitor",
1129
+ "processes",
1130
+ "queue"
1131
+ ]);
1132
+ var DESKTOP_WEBUI_DOCKS = /* @__PURE__ */ new Set([
1133
+ "autophase",
1134
+ "goal",
1135
+ "fleet",
1136
+ "work",
1137
+ "worktrees",
1138
+ "collab"
1139
+ ]);
1140
+ var DESKTOP_WEBUI_WORK_TABS = /* @__PURE__ */ new Set(["todos", "tasks", "plan"]);
1141
+ var DESKTOP_WEBUI_PREF_KEYS = /* @__PURE__ */ new Set([
1142
+ "yolo",
1143
+ "nextPrediction",
1144
+ "contextAutoCompact"
1145
+ ]);
1146
+ function normalizeDesktopWebuiCommand(value) {
1147
+ if (!isRecord(value)) return null;
1148
+ const command = {};
1149
+ let hasCommand = false;
1150
+ const action = value["action"];
1151
+ if (action !== void 0) {
1152
+ if (typeof action !== "string" || !DESKTOP_WEBUI_ACTIONS.has(action)) {
1153
+ return null;
1154
+ }
1155
+ command.action = action;
1156
+ hasCommand = true;
1157
+ }
1158
+ const view = value["view"];
1159
+ if (view !== void 0) {
1160
+ if (typeof view !== "string" || !DESKTOP_WEBUI_VIEWS.has(view)) {
1161
+ return null;
1162
+ }
1163
+ command.view = view;
1164
+ hasCommand = true;
1165
+ }
1166
+ const activity = value["activity"];
1167
+ if (activity !== void 0) {
1168
+ if (typeof activity !== "string" || !DESKTOP_WEBUI_ACTIVITIES.has(activity)) {
1169
+ return null;
1170
+ }
1171
+ command.activity = activity;
1172
+ hasCommand = true;
1173
+ }
1174
+ const overlay = value["overlay"];
1175
+ if (overlay !== void 0) {
1176
+ if (typeof overlay !== "string" || !DESKTOP_WEBUI_OVERLAYS.has(overlay)) {
1177
+ return null;
1178
+ }
1179
+ command.overlay = overlay;
1180
+ hasCommand = true;
1181
+ }
1182
+ const dockSection = value["dockSection"];
1183
+ if (dockSection !== void 0) {
1184
+ if (typeof dockSection !== "string" || !DESKTOP_WEBUI_DOCKS.has(dockSection)) {
1185
+ return null;
1186
+ }
1187
+ command.dockSection = dockSection;
1188
+ hasCommand = true;
1189
+ }
1190
+ const workTab = value["workTab"];
1191
+ if (workTab !== void 0) {
1192
+ if (typeof workTab !== "string" || !DESKTOP_WEBUI_WORK_TABS.has(workTab)) {
1193
+ return null;
1194
+ }
1195
+ command.workTab = workTab;
1196
+ hasCommand = true;
1197
+ }
1198
+ const terminal = value["terminal"];
1199
+ if (terminal !== void 0) {
1200
+ if (terminal !== true && terminal !== false && terminal !== "toggle" && terminal !== "new") {
1201
+ return null;
1202
+ }
1203
+ command.terminal = terminal;
1204
+ hasCommand = true;
1205
+ }
1206
+ const pref = value["pref"];
1207
+ if (pref !== void 0) {
1208
+ if (!isRecord(pref)) return null;
1209
+ const key = pref["key"];
1210
+ if (typeof key !== "string" || !DESKTOP_WEBUI_PREF_KEYS.has(key)) {
1211
+ return null;
1212
+ }
1213
+ const toggle = pref["toggle"];
1214
+ const prefValue = pref["value"];
1215
+ if (toggle !== void 0 && typeof toggle !== "boolean") return null;
1216
+ if (prefValue !== void 0 && typeof prefValue !== "boolean") return null;
1217
+ if (toggle === void 0 && prefValue === void 0) return null;
1218
+ command.pref = {
1219
+ key,
1220
+ ...typeof prefValue === "boolean" ? { value: prefValue } : {},
1221
+ ...typeof toggle === "boolean" ? { toggle } : {}
1222
+ };
1223
+ hasCommand = true;
1224
+ }
1225
+ return hasCommand ? command : null;
1226
+ }
1227
+ function buildWebuiCommandFallbackScript(command) {
1228
+ const payload = JSON.stringify(command).replace(/</g, "\\u003c");
1229
+ return `window.dispatchEvent(new CustomEvent('wrongstack:desktop-command', { detail: ${payload} })); true;`;
1230
+ }
1231
+ function isRecord(value) {
1232
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1233
+ }
1234
+
1235
+ // src/main/main.ts
1236
+ var manager = new DesktopRuntimeManager();
1237
+ var bridge = new DesktopAgentBridge();
1238
+ var OPEN_EXTERNAL_ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:"]);
1239
+ var SIDEBAR_WIDTH_WIDE = 292;
1240
+ var SIDEBAR_WIDTH_MEDIUM = 276;
1241
+ var SIDEBAR_WIDTH_NARROW = 252;
1242
+ var SIDEBAR_WIDTH_COLLAPSED = 56;
1243
+ var MIN_WINDOW_WIDTH2 = 760;
1244
+ var MIN_WINDOW_HEIGHT2 = 520;
1245
+ var MAX_PENDING_WEBUI_COMMANDS = 50;
1246
+ var MAX_PENDING_FLUSH_ATTEMPTS = 80;
1247
+ var WEBUI_COMMAND_FALLBACK_MS = 350;
1248
+ var WEBUI_COMMAND_ACK_TIMEOUT_MS = 2e3;
1249
+ app.setAppUserModelId("com.wrongstack.desktop");
1250
+ app.setPath("userData", path3.join(wstackGlobalRoot3(), "desktop", "electron-profile"));
1251
+ function safeOpenExternal(target) {
1252
+ let protocol;
1253
+ try {
1254
+ protocol = new URL(target).protocol;
1255
+ } catch {
1256
+ return;
1257
+ }
1258
+ if (OPEN_EXTERNAL_ALLOWED_PROTOCOLS.has(protocol)) {
1259
+ void shell.openExternal(target);
1260
+ }
1261
+ }
1262
+ var mainWindow = null;
1263
+ var shellView = null;
1264
+ var webuiViews = /* @__PURE__ */ new Map();
1265
+ var activeWebuiRuntimeId = null;
1266
+ var webuiStatus = { runtimeId: null, status: "idle" };
1267
+ var webuiCommandSequence = 0;
1268
+ var shellSidebarCollapsed = false;
1269
+ var pendingWebuiCommandAcks = /* @__PURE__ */ new Map();
1270
+ var saveWindowStateTimer = null;
1271
+ var quittingAfterCleanup = false;
1272
+ async function createWindow() {
1273
+ await manager.init();
1274
+ const bootLocale = await readUiLocale();
1275
+ if (bootLocale) setMainLocale(bootLocale);
1276
+ configureApplicationMenu();
1277
+ const windowState = validatedWindowState(manager.getWindowState());
1278
+ const windowOptions = {
1279
+ width: windowState?.width ?? 1320,
1280
+ height: windowState?.height ?? 860,
1281
+ minWidth: MIN_WINDOW_WIDTH2,
1282
+ minHeight: MIN_WINDOW_HEIGHT2,
1283
+ title: tMain("windowTitle"),
1284
+ backgroundColor: "#111217"
1285
+ };
1286
+ if (windowState?.x !== void 0) windowOptions.x = windowState.x;
1287
+ if (windowState?.y !== void 0) windowOptions.y = windowState.y;
1288
+ mainWindow = new BaseWindow(windowOptions);
1289
+ if (windowState?.maximized) {
1290
+ mainWindow.maximize();
1291
+ }
1292
+ shellView = new WebContentsView({
1293
+ webPreferences: {
1294
+ preload: preloadPath(),
1295
+ contextIsolation: true,
1296
+ nodeIntegration: false,
1297
+ sandbox: false
1298
+ }
1299
+ });
1300
+ mainWindow.contentView.addChildView(shellView);
1301
+ shellView.webContents.once("did-finish-load", () => {
1302
+ shellView?.webContents.send(IPC.localeChanged, getMainLocale());
1303
+ });
1304
+ shellView.webContents.setWindowOpenHandler(({ url }) => {
1305
+ safeOpenExternal(url);
1306
+ return { action: "deny" };
1307
+ });
1308
+ await shellView.webContents.loadFile(rendererIndexPath());
1309
+ mainWindow.on("resize", layoutViews);
1310
+ mainWindow.on("resize", scheduleWindowStateSave);
1311
+ mainWindow.on("move", scheduleWindowStateSave);
1312
+ mainWindow.on("maximize", scheduleWindowStateSave);
1313
+ mainWindow.on("unmaximize", scheduleWindowStateSave);
1314
+ mainWindow.on("close", () => {
1315
+ if (saveWindowStateTimer) {
1316
+ clearTimeout(saveWindowStateTimer);
1317
+ saveWindowStateTimer = null;
1318
+ }
1319
+ void saveWindowState();
1320
+ });
1321
+ mainWindow.on("closed", () => {
1322
+ if (saveWindowStateTimer) {
1323
+ clearTimeout(saveWindowStateTimer);
1324
+ saveWindowStateTimer = null;
1325
+ }
1326
+ mainWindow = null;
1327
+ disposeAllWebuiEntries();
1328
+ shellView = null;
1329
+ activeWebuiRuntimeId = null;
1330
+ webuiStatus = { runtimeId: null, status: "idle" };
1331
+ });
1332
+ layoutViews();
1333
+ syncActiveWebuiView();
1334
+ void restoreLastWorkspace();
1335
+ }
1336
+ function layoutViews() {
1337
+ if (!mainWindow || !shellView) return;
1338
+ const size = mainWindow.getContentSize();
1339
+ const width = size[0] ?? 0;
1340
+ const height = size[1] ?? 0;
1341
+ shellView.setBounds({ x: 0, y: 0, width, height });
1342
+ layoutWebuiViews(width, height);
1343
+ }
1344
+ function scheduleWindowStateSave() {
1345
+ if (saveWindowStateTimer) clearTimeout(saveWindowStateTimer);
1346
+ saveWindowStateTimer = setTimeout(() => {
1347
+ saveWindowStateTimer = null;
1348
+ void saveWindowState();
1349
+ }, 350);
1350
+ }
1351
+ async function saveWindowState() {
1352
+ if (!mainWindow) return;
1353
+ const bounds = mainWindow.getNormalBounds();
1354
+ await manager.saveWindowState({
1355
+ x: bounds.x,
1356
+ y: bounds.y,
1357
+ width: bounds.width,
1358
+ height: bounds.height,
1359
+ maximized: mainWindow.isMaximized()
1360
+ });
1361
+ }
1362
+ function layoutWebuiViews(windowWidth, windowHeight) {
1363
+ if (!mainWindow) return;
1364
+ const size = mainWindow.getContentSize();
1365
+ const width = windowWidth ?? size[0] ?? 0;
1366
+ const height = windowHeight ?? size[1] ?? 0;
1367
+ const snapshot = manager.snapshot();
1368
+ const active = snapshot.runtimes.find((runtime) => runtime.id === snapshot.activeRuntimeId);
1369
+ const sidebarWidth = desktopSidebarWidth(width);
1370
+ const contentWidth = Math.max(0, width - sidebarWidth);
1371
+ for (const entry of webuiViews.values()) {
1372
+ if (active?.id === entry.runtimeId && active.status === "running") {
1373
+ entry.view.setBounds({ x: sidebarWidth, y: 0, width: contentWidth, height });
1374
+ } else {
1375
+ entry.view.setBounds({ x: sidebarWidth, y: 0, width: 0, height });
1376
+ }
1377
+ }
1378
+ }
1379
+ function desktopSidebarWidth(windowWidth) {
1380
+ if (shellSidebarCollapsed) return SIDEBAR_WIDTH_COLLAPSED;
1381
+ if (windowWidth < 900) return SIDEBAR_WIDTH_NARROW;
1382
+ if (windowWidth < 1180) return SIDEBAR_WIDTH_MEDIUM;
1383
+ return SIDEBAR_WIDTH_WIDE;
1384
+ }
1385
+ function setShellSidebarCollapsed(collapsed) {
1386
+ shellSidebarCollapsed = collapsed;
1387
+ layoutWebuiViews();
1388
+ configureApplicationMenu();
1389
+ if (!shellView || shellView.webContents.isDestroyed()) return;
1390
+ shellView.webContents.send(IPC.shellSidebarCollapsedChanged, shellSidebarCollapsed);
1391
+ }
1392
+ function ensureWebuiEntry(runtimeId) {
1393
+ if (!mainWindow) return null;
1394
+ const existing = webuiViews.get(runtimeId);
1395
+ if (existing) return existing;
1396
+ const view = new WebContentsView({
1397
+ webPreferences: {
1398
+ preload: webuiPreloadPath(),
1399
+ contextIsolation: true,
1400
+ nodeIntegration: false,
1401
+ sandbox: false
1402
+ }
1403
+ });
1404
+ const entry = {
1405
+ runtimeId,
1406
+ view,
1407
+ url: null,
1408
+ status: { runtimeId, status: "idle" },
1409
+ bridgeReady: false,
1410
+ attached: false,
1411
+ pendingCommands: [],
1412
+ pendingFlushTimer: null,
1413
+ pendingFlushAttempts: 0
1414
+ };
1415
+ view.webContents.setWindowOpenHandler(({ url }) => {
1416
+ safeOpenExternal(url);
1417
+ return { action: "deny" };
1418
+ });
1419
+ view.webContents.on("will-navigate", (event, url) => {
1420
+ if (sameOrigin(url, entry.url)) return;
1421
+ event.preventDefault();
1422
+ safeOpenExternal(url);
1423
+ });
1424
+ view.webContents.on("did-start-loading", () => {
1425
+ if (webuiViews.get(runtimeId) !== entry) return;
1426
+ entry.bridgeReady = false;
1427
+ setEntryWebuiStatus(entry, { runtimeId, status: "loading" });
1428
+ });
1429
+ view.webContents.on("did-finish-load", () => {
1430
+ if (webuiViews.get(runtimeId) !== entry) return;
1431
+ schedulePendingWebuiFlush(entry);
1432
+ try {
1433
+ entry.view.webContents.send(IPC.webuiLocaleChanged, getMainLocale());
1434
+ } catch {
1435
+ }
1436
+ });
1437
+ view.webContents.on("did-fail-load", (_event, errorCode, errorDescription) => {
1438
+ if (webuiViews.get(runtimeId) !== entry || errorCode === -3) return;
1439
+ setEntryWebuiStatus(entry, {
1440
+ runtimeId,
1441
+ status: "error",
1442
+ error: errorDescription
1443
+ });
1444
+ });
1445
+ view.webContents.on("render-process-gone", (_event, details) => {
1446
+ if (webuiViews.get(runtimeId) !== entry) return;
1447
+ setEntryWebuiStatus(entry, {
1448
+ runtimeId,
1449
+ status: "error",
1450
+ error: `WebUI renderer exited: ${details.reason}`
1451
+ });
1452
+ });
1453
+ webuiViews.set(runtimeId, entry);
1454
+ return entry;
1455
+ }
1456
+ function syncActiveWebuiView() {
1457
+ if (!mainWindow) return;
1458
+ const snapshot = manager.snapshot();
1459
+ pruneWebuiEntries(
1460
+ snapshot.runtimes.filter((runtime) => runtime.status === "running").map((runtime) => runtime.id)
1461
+ );
1462
+ const active = snapshot.runtimes.find((runtime) => runtime.id === snapshot.activeRuntimeId);
1463
+ if (active?.status !== "running") {
1464
+ activeWebuiRuntimeId = active?.id ?? null;
1465
+ publishWebuiStatus({ runtimeId: active?.id ?? null, status: "idle" });
1466
+ layoutWebuiViews();
1467
+ return;
1468
+ }
1469
+ const url = manager.getRuntimeUrlWithToken(active.id);
1470
+ if (!url) {
1471
+ activeWebuiRuntimeId = active.id;
1472
+ publishWebuiStatus({ runtimeId: active.id, status: "idle" });
1473
+ layoutWebuiViews();
1474
+ return;
1475
+ }
1476
+ const entry = ensureWebuiEntry(active.id);
1477
+ if (!entry) return;
1478
+ activeWebuiRuntimeId = active.id;
1479
+ attachWebuiEntry(entry);
1480
+ layoutWebuiViews();
1481
+ publishWebuiStatus(entry.status);
1482
+ if (entry.url !== url) {
1483
+ entry.url = url;
1484
+ entry.bridgeReady = false;
1485
+ setEntryWebuiStatus(entry, { runtimeId: active.id, status: "loading" });
1486
+ void entry.view.webContents.loadURL(url).catch((err) => {
1487
+ setEntryWebuiStatus(entry, {
1488
+ runtimeId: active.id,
1489
+ status: "error",
1490
+ error: err instanceof Error ? err.message : String(err)
1491
+ });
1492
+ console.error("Failed to load desktop WebUI view:", err);
1493
+ });
1494
+ }
1495
+ }
1496
+ function broadcastState() {
1497
+ if (!shellView || shellView.webContents.isDestroyed()) return;
1498
+ shellView.webContents.send(IPC.stateChanged, manager.snapshot());
1499
+ }
1500
+ function publishWebuiStatus(next) {
1501
+ webuiStatus = next;
1502
+ if (!shellView || shellView.webContents.isDestroyed()) return;
1503
+ shellView.webContents.send(IPC.webuiStatusChanged, webuiStatus);
1504
+ }
1505
+ function setEntryWebuiStatus(entry, next) {
1506
+ const previousPrefs = entry.status.prefs;
1507
+ entry.status = {
1508
+ ...next,
1509
+ prefs: next.prefs ?? entry.status.prefs,
1510
+ pendingCommands: entry.pendingCommands.length || void 0
1511
+ };
1512
+ if (activeWebuiRuntimeId === entry.runtimeId) {
1513
+ publishWebuiStatus(entry.status);
1514
+ if (menuRelevantPrefsChanged(previousPrefs, entry.status.prefs)) {
1515
+ configureApplicationMenu();
1516
+ }
1517
+ }
1518
+ }
1519
+ function menuRelevantPrefsChanged(previous, next) {
1520
+ return previous?.yolo !== next?.yolo || previous?.nextPrediction !== next?.nextPrediction || previous?.contextAutoCompact !== next?.contextAutoCompact;
1521
+ }
1522
+ function runtimeWsUrlOrThrow(runtimeId) {
1523
+ const wsUrl = manager.getRuntimeWsUrlWithToken(runtimeId);
1524
+ if (!wsUrl) throw new Error(`Runtime not found: ${runtimeId}`);
1525
+ return wsUrl;
1526
+ }
1527
+ function broadcastLocaleToEmbeddedWebuis(locale) {
1528
+ for (const entry of webuiViews.values()) {
1529
+ if (entry.view.webContents.isDestroyed()) continue;
1530
+ try {
1531
+ entry.view.webContents.send(IPC.webuiLocaleChanged, locale);
1532
+ } catch {
1533
+ }
1534
+ }
1535
+ }
1536
+ function registerIpc() {
1537
+ ipcMain.handle(IPC.getState, () => manager.snapshot());
1538
+ ipcMain.handle(IPC.getConversation, (_event, runtimeId) => bridge.snapshot(runtimeId));
1539
+ ipcMain.handle(IPC.getWebuiStatus, () => webuiStatus);
1540
+ ipcMain.handle(
1541
+ IPC.navigateWebui,
1542
+ async (_event, command) => dispatchWebuiCommand(command)
1543
+ );
1544
+ ipcMain.handle(IPC.reloadWebui, async () => reloadActiveWebuiView());
1545
+ ipcMain.handle(IPC.setShellSidebarCollapsed, (_event, collapsed) => {
1546
+ setShellSidebarCollapsed(collapsed === true);
1547
+ return true;
1548
+ });
1549
+ ipcMain.handle(IPC.openSettings, async () => openSettings());
1550
+ ipcMain.handle(IPC.openProjectSession, async (_event, runtimeId) => {
1551
+ return openProjectSession(runtimeId);
1552
+ });
1553
+ ipcMain.handle(IPC.openProject, async (_event, requestedRoot) => {
1554
+ return openProject(requestedRoot);
1555
+ });
1556
+ ipcMain.handle(IPC.registerProject, async (_event, requestedRoot) => {
1557
+ return registerProject(requestedRoot);
1558
+ });
1559
+ ipcMain.handle(IPC.unregisterProject, async (_event, root) => {
1560
+ return unregisterProject(root);
1561
+ });
1562
+ ipcMain.handle(IPC.activateRuntime, async (_event, id) => {
1563
+ return activateRuntime(id);
1564
+ });
1565
+ ipcMain.handle(IPC.closeRuntime, async (_event, id) => {
1566
+ return closeRuntime(id);
1567
+ });
1568
+ ipcMain.handle(
1569
+ IPC.sendMessage,
1570
+ async (_event, id, content) => bridge.sendMessage(id, runtimeWsUrlOrThrow(id), content)
1571
+ );
1572
+ ipcMain.handle(
1573
+ IPC.abortRuntime,
1574
+ async (_event, id) => bridge.abort(id, runtimeWsUrlOrThrow(id))
1575
+ );
1576
+ ipcMain.handle(IPC.openRuntimeInBrowser, async (_event, id) => {
1577
+ const url = manager.getRuntimeUrlWithToken(id);
1578
+ if (url) safeOpenExternal(url);
1579
+ });
1580
+ ipcMain.handle(IPC.revealRuntimeRoot, async (_event, id) => {
1581
+ const runtime = manager.getRuntime(id);
1582
+ if (runtime) await shell.openPath(runtime.root);
1583
+ });
1584
+ ipcMain.on(IPC.webuiReadyChanged, (event, ready) => {
1585
+ const entry = findWebuiEntryBySenderId(event.sender.id);
1586
+ if (!entry) return;
1587
+ entry.bridgeReady = ready === true;
1588
+ if (entry.bridgeReady) {
1589
+ setEntryWebuiStatus(entry, { ...entry.status, status: "ready" });
1590
+ schedulePendingWebuiFlush(entry);
1591
+ } else if (entry.status.status === "ready") {
1592
+ setEntryWebuiStatus(entry, { ...entry.status, status: "loading" });
1593
+ }
1594
+ });
1595
+ ipcMain.on(IPC.webuiPrefsChanged, (event, prefs) => {
1596
+ const entry = findWebuiEntryBySenderId(event.sender.id);
1597
+ if (!entry) return;
1598
+ const sanitized = sanitizeWebuiPrefs(prefs);
1599
+ if (Object.keys(sanitized).length === 0) return;
1600
+ setEntryWebuiStatus(entry, {
1601
+ ...entry.status,
1602
+ prefs: { ...entry.status.prefs ?? {}, ...sanitized }
1603
+ });
1604
+ });
1605
+ ipcMain.on(
1606
+ IPC.webuiCommandAck,
1607
+ (event, requestId, handled, _message) => {
1608
+ const entry = findWebuiEntryBySenderId(event.sender.id);
1609
+ if (!entry || typeof requestId !== "string") return;
1610
+ const pending = pendingWebuiCommandAcks.get(requestId);
1611
+ if (!pending || pending.runtimeId !== entry.runtimeId) return;
1612
+ settlePendingWebuiCommandAck(requestId, handled === true);
1613
+ }
1614
+ );
1615
+ }
1616
+ function findWebuiEntryBySenderId(senderId) {
1617
+ return Array.from(webuiViews.values()).find(
1618
+ (candidate) => candidate.view.webContents.id === senderId
1619
+ );
1620
+ }
1621
+ function sanitizeWebuiPrefs(prefs) {
1622
+ const next = {};
1623
+ if (!isRecord2(prefs)) return next;
1624
+ if (typeof prefs["yolo"] === "boolean") next.yolo = prefs["yolo"];
1625
+ if (typeof prefs["nextPrediction"] === "boolean") next.nextPrediction = prefs["nextPrediction"];
1626
+ if (typeof prefs["contextAutoCompact"] === "boolean") {
1627
+ next.contextAutoCompact = prefs["contextAutoCompact"];
1628
+ }
1629
+ return next;
1630
+ }
1631
+ function isRecord2(value) {
1632
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1633
+ }
1634
+ async function dispatchWebuiCommand(commandInput) {
1635
+ const command = normalizeDesktopWebuiCommand(commandInput);
1636
+ if (!command) return false;
1637
+ const entry = getActiveWebuiEntry();
1638
+ if (!entry?.url) return false;
1639
+ if (entry.status.status !== "ready") {
1640
+ if (!entry.view.webContents.isLoading() && entry.status.status !== "error") {
1641
+ return dispatchWebuiCommandNow(entry, command);
1642
+ }
1643
+ queueWebuiCommand(entry, command);
1644
+ schedulePendingWebuiFlush(entry);
1645
+ return true;
1646
+ }
1647
+ if (!await isWebuiCommandBridgeReady(entry)) {
1648
+ if (!entry.view.webContents.isLoading()) {
1649
+ return dispatchWebuiCommandNow(entry, command);
1650
+ }
1651
+ queueWebuiCommand(entry, command);
1652
+ schedulePendingWebuiFlush(entry);
1653
+ return true;
1654
+ }
1655
+ return dispatchWebuiCommandNow(entry, command);
1656
+ }
1657
+ async function reloadActiveWebuiView() {
1658
+ const entry = getActiveWebuiEntry();
1659
+ if (!entry?.url) return false;
1660
+ entry.bridgeReady = false;
1661
+ setEntryWebuiStatus(entry, { runtimeId: entry.runtimeId, status: "loading" });
1662
+ return entry.view.webContents.loadURL(entry.url).then(() => true).catch((err) => {
1663
+ setEntryWebuiStatus(entry, {
1664
+ runtimeId: entry.runtimeId,
1665
+ status: "error",
1666
+ error: err instanceof Error ? err.message : String(err)
1667
+ });
1668
+ console.error("Failed to reload desktop WebUI view:", err);
1669
+ return false;
1670
+ });
1671
+ }
1672
+ async function dispatchWebuiCommandNow(entry, command) {
1673
+ if (webuiViews.get(entry.runtimeId) !== entry || !entry.url) return false;
1674
+ const requestId = nextWebuiCommandRequestId(entry.runtimeId);
1675
+ const commandWithRequestId = { ...command, requestId };
1676
+ return new Promise((resolve3) => {
1677
+ const fallbackTimer = setTimeout(() => {
1678
+ const pending = pendingWebuiCommandAcks.get(requestId);
1679
+ if (!pending) return;
1680
+ sendWebuiCommandDomFallback(entry, commandWithRequestId);
1681
+ }, WEBUI_COMMAND_FALLBACK_MS);
1682
+ const timer = setTimeout(() => {
1683
+ settlePendingWebuiCommandAck(requestId, false);
1684
+ }, WEBUI_COMMAND_ACK_TIMEOUT_MS);
1685
+ pendingWebuiCommandAcks.set(requestId, {
1686
+ runtimeId: entry.runtimeId,
1687
+ timer,
1688
+ fallbackTimer,
1689
+ resolve: resolve3
1690
+ });
1691
+ try {
1692
+ entry.view.webContents.send(IPC.webuiCommand, commandWithRequestId);
1693
+ if (activeWebuiRuntimeId === entry.runtimeId) {
1694
+ entry.view.webContents.focus();
1695
+ }
1696
+ } catch {
1697
+ settlePendingWebuiCommandAck(requestId, false);
1698
+ }
1699
+ });
1700
+ }
1701
+ function sendWebuiCommandDomFallback(entry, command) {
1702
+ if (webuiViews.get(entry.runtimeId) !== entry || entry.view.webContents.isDestroyed()) return;
1703
+ void entry.view.webContents.executeJavaScript(buildWebuiCommandFallbackScript(command), true).catch(() => void 0);
1704
+ }
1705
+ function nextWebuiCommandRequestId(runtimeId) {
1706
+ webuiCommandSequence += 1;
1707
+ return `${runtimeId}:${Date.now()}:${webuiCommandSequence}`;
1708
+ }
1709
+ function settlePendingWebuiCommandAck(requestId, handled) {
1710
+ const pending = pendingWebuiCommandAcks.get(requestId);
1711
+ if (!pending) return;
1712
+ pendingWebuiCommandAcks.delete(requestId);
1713
+ clearTimeout(pending.timer);
1714
+ if (pending.fallbackTimer) clearTimeout(pending.fallbackTimer);
1715
+ if (handled) {
1716
+ const entry = webuiViews.get(pending.runtimeId);
1717
+ if (entry) {
1718
+ entry.bridgeReady = true;
1719
+ setEntryWebuiStatus(entry, { ...entry.status, status: "ready" });
1720
+ }
1721
+ }
1722
+ pending.resolve(handled);
1723
+ }
1724
+ function settlePendingWebuiCommandAcksForRuntime(runtimeId, handled) {
1725
+ for (const [requestId, pending] of [...pendingWebuiCommandAcks]) {
1726
+ if (pending.runtimeId === runtimeId) {
1727
+ settlePendingWebuiCommandAck(requestId, handled);
1728
+ }
1729
+ }
1730
+ }
1731
+ async function flushPendingWebuiCommands(entry) {
1732
+ if (webuiViews.get(entry.runtimeId) !== entry) return;
1733
+ if (entry.pendingCommands.length === 0) return;
1734
+ if (!await isWebuiCommandBridgeReady(entry)) {
1735
+ entry.pendingFlushAttempts += 1;
1736
+ const canFallback = !entry.view.webContents.isLoading() && entry.pendingFlushAttempts >= 4;
1737
+ if (!canFallback && entry.pendingFlushAttempts <= MAX_PENDING_FLUSH_ATTEMPTS) {
1738
+ schedulePendingWebuiFlush(entry);
1739
+ setEntryWebuiStatus(entry, entry.status);
1740
+ return;
1741
+ }
1742
+ if (!canFallback) {
1743
+ entry.pendingCommands.length = 0;
1744
+ setEntryWebuiStatus(entry, {
1745
+ runtimeId: entry.runtimeId,
1746
+ status: "error",
1747
+ error: "WebUI command bridge did not become ready."
1748
+ });
1749
+ return;
1750
+ }
1751
+ }
1752
+ entry.pendingFlushAttempts = 0;
1753
+ const commands = entry.pendingCommands.splice(0, entry.pendingCommands.length);
1754
+ setEntryWebuiStatus(entry, entry.status);
1755
+ for (const command of commands) {
1756
+ await dispatchWebuiCommandNow(entry, command).catch(() => void 0);
1757
+ }
1758
+ }
1759
+ function schedulePendingWebuiFlush(entry) {
1760
+ if (entry.pendingFlushTimer) return;
1761
+ entry.pendingFlushTimer = setTimeout(() => {
1762
+ entry.pendingFlushTimer = null;
1763
+ void flushPendingWebuiCommands(entry);
1764
+ }, 250);
1765
+ }
1766
+ async function isWebuiCommandBridgeReady(entry) {
1767
+ if (webuiViews.get(entry.runtimeId) !== entry || !entry.url) return false;
1768
+ return entry.bridgeReady;
1769
+ }
1770
+ function queueWebuiCommand(entry, command) {
1771
+ entry.pendingCommands.push(command);
1772
+ if (entry.pendingCommands.length > MAX_PENDING_WEBUI_COMMANDS) {
1773
+ entry.pendingCommands.splice(0, entry.pendingCommands.length - MAX_PENDING_WEBUI_COMMANDS);
1774
+ }
1775
+ entry.pendingFlushAttempts = 0;
1776
+ setEntryWebuiStatus(entry, entry.status);
1777
+ }
1778
+ function getActiveWebuiEntry() {
1779
+ const activeId = manager.snapshot().activeRuntimeId;
1780
+ return activeId ? webuiViews.get(activeId) : void 0;
1781
+ }
1782
+ function attachWebuiEntry(entry) {
1783
+ if (!mainWindow) return;
1784
+ if (entry.attached) return;
1785
+ mainWindow.contentView.addChildView(entry.view);
1786
+ entry.attached = true;
1787
+ }
1788
+ function pruneWebuiEntries(runtimeIds) {
1789
+ const live = new Set(runtimeIds);
1790
+ for (const [id, entry] of webuiViews) {
1791
+ if (!live.has(id)) {
1792
+ disposeWebuiEntry(entry);
1793
+ }
1794
+ }
1795
+ }
1796
+ function disposeWebuiEntry(entry) {
1797
+ webuiViews.delete(entry.runtimeId);
1798
+ entry.pendingCommands.length = 0;
1799
+ settlePendingWebuiCommandAcksForRuntime(entry.runtimeId, false);
1800
+ if (entry.pendingFlushTimer) {
1801
+ clearTimeout(entry.pendingFlushTimer);
1802
+ entry.pendingFlushTimer = null;
1803
+ }
1804
+ if (mainWindow && entry.attached) {
1805
+ mainWindow.contentView.removeChildView(entry.view);
1806
+ }
1807
+ entry.attached = false;
1808
+ if (!entry.view.webContents.isDestroyed()) {
1809
+ entry.view.webContents.close();
1810
+ }
1811
+ if (activeWebuiRuntimeId === entry.runtimeId) activeWebuiRuntimeId = null;
1812
+ }
1813
+ function disposeAllWebuiEntries() {
1814
+ for (const entry of Array.from(webuiViews.values())) {
1815
+ disposeWebuiEntry(entry);
1816
+ }
1817
+ webuiViews.clear();
1818
+ }
1819
+ async function openProject(requestedRoot) {
1820
+ let projectRoot = requestedRoot;
1821
+ if (!projectRoot) {
1822
+ const result = await dialog.showOpenDialog({
1823
+ title: tMain("openProject"),
1824
+ properties: ["openDirectory"]
1825
+ });
1826
+ projectRoot = result.filePaths[0];
1827
+ }
1828
+ if (!projectRoot) return manager.snapshot();
1829
+ await manager.openProject(projectRoot);
1830
+ syncActiveWebuiView();
1831
+ broadcastState();
1832
+ return manager.snapshot();
1833
+ }
1834
+ async function registerProject(requestedRoot) {
1835
+ let projectRoot = requestedRoot;
1836
+ if (!projectRoot) {
1837
+ const result = await dialog.showOpenDialog({
1838
+ title: tMain("registerProject"),
1839
+ properties: ["openDirectory"]
1840
+ });
1841
+ projectRoot = result.filePaths[0];
1842
+ }
1843
+ if (!projectRoot) return manager.snapshot();
1844
+ await manager.registerProject(projectRoot);
1845
+ broadcastState();
1846
+ return manager.snapshot();
1847
+ }
1848
+ async function unregisterProject(root) {
1849
+ if (!root || typeof root !== "string") return manager.snapshot();
1850
+ await manager.unregisterProject(root);
1851
+ broadcastState();
1852
+ return manager.snapshot();
1853
+ }
1854
+ async function openProjectSession(runtimeId) {
1855
+ const snapshot = manager.snapshot();
1856
+ const runtime = (runtimeId ? snapshot.runtimes.find((candidate) => candidate.id === runtimeId) : void 0) ?? snapshot.runtimes.find((candidate) => candidate.id === snapshot.activeRuntimeId);
1857
+ if (runtime?.kind !== "project") {
1858
+ return openProject();
1859
+ }
1860
+ await manager.openProject(runtime.root, { forceNew: true });
1861
+ syncActiveWebuiView();
1862
+ broadcastState();
1863
+ return manager.snapshot();
1864
+ }
1865
+ async function openSettings() {
1866
+ const snapshot = manager.snapshot();
1867
+ const active = snapshot.runtimes.find((runtime) => runtime.id === snapshot.activeRuntimeId);
1868
+ if (!active || active.kind === "global-settings" || active.status !== "running") {
1869
+ const root = desktopSettingsWorkspaceRoot();
1870
+ await fs3.mkdir(root, { recursive: true });
1871
+ await manager.openProject(root, {
1872
+ name: "Global Settings",
1873
+ kind: "global-settings",
1874
+ touchRecent: false
1875
+ });
1876
+ syncActiveWebuiView();
1877
+ broadcastState();
1878
+ }
1879
+ await dispatchWebuiCommand({ view: "settings" });
1880
+ return manager.snapshot();
1881
+ }
1882
+ async function activateRuntime(id) {
1883
+ await manager.activateRuntime(id);
1884
+ syncActiveWebuiView();
1885
+ broadcastState();
1886
+ return manager.snapshot();
1887
+ }
1888
+ async function closeRuntime(id) {
1889
+ bridge.close(id);
1890
+ await manager.closeRuntime(id);
1891
+ const entry = webuiViews.get(id);
1892
+ if (entry) disposeWebuiEntry(entry);
1893
+ syncActiveWebuiView();
1894
+ broadcastState();
1895
+ return manager.snapshot();
1896
+ }
1897
+ async function restoreLastWorkspace() {
1898
+ await manager.restoreLastWorkspace();
1899
+ syncActiveWebuiView();
1900
+ broadcastState();
1901
+ }
1902
+ function activeRuntimeId() {
1903
+ return manager.snapshot().activeRuntimeId;
1904
+ }
1905
+ function buildProjectsMenu(runtimes, actions) {
1906
+ const projectGroups = groupProjectRuntimesForMenu(runtimes);
1907
+ const menu = [
1908
+ {
1909
+ label: tMain("openProjectEllipsis"),
1910
+ accelerator: "CmdOrCtrl+O",
1911
+ click: () => void openProject()
1912
+ },
1913
+ { label: tMain("registerProjectEllipsis"), click: () => void registerProject() },
1914
+ { type: "separator" }
1915
+ ];
1916
+ if (projectGroups.length === 0) {
1917
+ menu.push({ label: tMain("noOpenProjectSessions"), enabled: false });
1918
+ return menu;
1919
+ }
1920
+ for (const group of projectGroups) {
1921
+ menu.push({
1922
+ label: group.name,
1923
+ submenu: [
1924
+ {
1925
+ label: tMain("newSession"),
1926
+ click: () => actions.newSession(group.sessions[0]?.id ?? ""),
1927
+ enabled: Boolean(group.sessions[0])
1928
+ },
1929
+ {
1930
+ label: tMain("revealProjectFolder"),
1931
+ click: () => actions.reveal(group.sessions[0]?.id ?? ""),
1932
+ enabled: Boolean(group.sessions[0])
1933
+ },
1934
+ { type: "separator" },
1935
+ ...group.sessions.map((runtime, index) => buildSessionMenu(runtime, index + 1, actions))
1936
+ ]
1937
+ });
1938
+ }
1939
+ return menu;
1940
+ }
1941
+ function buildSessionMenu(runtime, index, actions) {
1942
+ const running = runtime.status === "running";
1943
+ const label = `${tMain("session")} ${index} \xB7 ${runtime.status}`;
1944
+ return {
1945
+ label,
1946
+ submenu: [
1947
+ {
1948
+ label: tMain("quickView"),
1949
+ click: () => actions.activate(runtime.id)
1950
+ },
1951
+ {
1952
+ label: "WebUI",
1953
+ enabled: running,
1954
+ submenu: [
1955
+ {
1956
+ label: tMain("chat"),
1957
+ click: () => actions.activateAndNavigate(runtime.id, { activity: "chat", view: "chat" })
1958
+ },
1959
+ {
1960
+ label: tMain("focusPrompt"),
1961
+ click: () => actions.activateAndNavigate(runtime.id, { action: "focus-chat" })
1962
+ },
1963
+ {
1964
+ label: tMain("terminal"),
1965
+ click: () => actions.activateAndNavigate(runtime.id, { terminal: "toggle" })
1966
+ },
1967
+ {
1968
+ label: tMain("newTerminal"),
1969
+ click: () => actions.activateAndNavigate(runtime.id, { terminal: "new" })
1970
+ },
1971
+ { type: "separator" },
1972
+ {
1973
+ label: tMain("files"),
1974
+ click: () => actions.activateAndNavigate(runtime.id, { activity: "files", view: "files" })
1975
+ },
1976
+ {
1977
+ label: tMain("changes"),
1978
+ click: () => actions.activateAndNavigate(runtime.id, { activity: "changes", view: "changes" })
1979
+ },
1980
+ {
1981
+ label: tMain("sessions"),
1982
+ click: () => actions.activateAndNavigate(runtime.id, { view: "sessions" })
1983
+ },
1984
+ {
1985
+ label: tMain("fleetHQ"),
1986
+ click: () => actions.activateAndNavigate(runtime.id, { activity: "officemap", view: "officemap" })
1987
+ },
1988
+ {
1989
+ label: tMain("settings"),
1990
+ click: () => actions.activateAndNavigate(runtime.id, { view: "settings" })
1991
+ },
1992
+ { type: "separator" },
1993
+ {
1994
+ label: tMain("commandPalette"),
1995
+ click: () => actions.activateAndNavigate(runtime.id, { action: "open-command-palette" })
1996
+ },
1997
+ {
1998
+ label: tMain("modelSwitcher"),
1999
+ click: () => actions.activateAndNavigate(runtime.id, { action: "open-model-switcher" })
2000
+ }
2001
+ ]
2002
+ },
2003
+ { type: "separator" },
2004
+ {
2005
+ label: tMain("openInBrowser"),
2006
+ enabled: running,
2007
+ click: () => actions.openBrowser(runtime.id)
2008
+ },
2009
+ {
2010
+ label: tMain("reloadWebui"),
2011
+ enabled: running,
2012
+ click: () => actions.reload(runtime.id)
2013
+ },
2014
+ {
2015
+ label: tMain("closeSession"),
2016
+ click: () => actions.close(runtime.id)
2017
+ }
2018
+ ]
2019
+ };
2020
+ }
2021
+ function groupProjectRuntimesForMenu(runtimes) {
2022
+ const groups = /* @__PURE__ */ new Map();
2023
+ for (const runtime of runtimes) {
2024
+ if (runtime.kind !== "project") continue;
2025
+ const key = normalizeMenuRoot(runtime.root);
2026
+ const existing = groups.get(key);
2027
+ if (existing) {
2028
+ existing.sessions.push(runtime);
2029
+ continue;
2030
+ }
2031
+ groups.set(key, {
2032
+ key,
2033
+ name: path3.basename(runtime.root) || runtime.name,
2034
+ root: runtime.root,
2035
+ sessions: [runtime]
2036
+ });
2037
+ }
2038
+ return [...groups.values()].sort((a, b) => a.name.localeCompare(b.name));
2039
+ }
2040
+ function normalizeMenuRoot(root) {
2041
+ return path3.resolve(root).replace(/\\/g, "/").replace(/\/+$/g, "").toLowerCase();
2042
+ }
2043
+ function configureApplicationMenu() {
2044
+ const navigate = (command) => {
2045
+ void dispatchWebuiCommand(command);
2046
+ };
2047
+ const activateAndNavigate = (runtimeId, command) => {
2048
+ void activateRuntime(runtimeId).then(() => dispatchWebuiCommand(command));
2049
+ };
2050
+ const reloadRuntimeWebui = (runtimeId) => {
2051
+ void activateRuntime(runtimeId).then(() => reloadActiveWebuiView());
2052
+ };
2053
+ const snapshot = manager.snapshot();
2054
+ const active = snapshot.runtimes.find((runtime) => runtime.id === snapshot.activeRuntimeId);
2055
+ const hasActiveRuntime = Boolean(active);
2056
+ const hasActiveWebui = active?.status === "running";
2057
+ const hasActiveProjectWebui = hasActiveWebui && active?.kind === "project";
2058
+ const activeWebuiPrefs = active ? webuiViews.get(active.id)?.status.prefs : void 0;
2059
+ const yoloChecked = activeWebuiPrefs?.yolo === true;
2060
+ const nextPredictionChecked = activeWebuiPrefs?.nextPrediction === true;
2061
+ const contextAutoCompactChecked = activeWebuiPrefs?.contextAutoCompact === true;
2062
+ const webuiItem = (item) => ({
2063
+ ...item,
2064
+ enabled: item.enabled ?? hasActiveWebui
2065
+ });
2066
+ const template = [
2067
+ {
2068
+ label: tMain("file"),
2069
+ submenu: [
2070
+ {
2071
+ label: tMain("openProjectEllipsis"),
2072
+ accelerator: "CmdOrCtrl+O",
2073
+ click: () => void openProject()
2074
+ },
2075
+ { label: tMain("registerProjectEllipsis"), click: () => void registerProject() },
2076
+ {
2077
+ label: tMain("removeActiveFromRegistry"),
2078
+ enabled: hasActiveProjectWebui,
2079
+ click: () => {
2080
+ if (active?.kind === "project") void unregisterProject(active.root);
2081
+ }
2082
+ },
2083
+ { type: "separator" },
2084
+ {
2085
+ label: tMain("newSessionForActive"),
2086
+ accelerator: "CmdOrCtrl+N",
2087
+ enabled: hasActiveProjectWebui,
2088
+ click: () => void openProjectSession(active?.id)
2089
+ },
2090
+ {
2091
+ label: tMain("settings"),
2092
+ accelerator: "CmdOrCtrl+,",
2093
+ click: () => {
2094
+ if (hasActiveWebui) navigate({ view: "settings" });
2095
+ else void openSettings();
2096
+ }
2097
+ },
2098
+ { type: "separator" },
2099
+ {
2100
+ label: tMain("closeActiveRuntime"),
2101
+ accelerator: "CmdOrCtrl+W",
2102
+ enabled: hasActiveRuntime,
2103
+ click: () => {
2104
+ const id = activeRuntimeId();
2105
+ if (id) void closeRuntime(id);
2106
+ }
2107
+ },
2108
+ { type: "separator" },
2109
+ { role: process.platform === "darwin" ? "close" : "quit" }
2110
+ ]
2111
+ },
2112
+ {
2113
+ label: tMain("projects"),
2114
+ submenu: buildProjectsMenu(snapshot.runtimes, {
2115
+ activate: (runtimeId) => void activateRuntime(runtimeId),
2116
+ activateAndNavigate,
2117
+ newSession: (runtimeId) => void openProjectSession(runtimeId),
2118
+ openBrowser: (runtimeId) => {
2119
+ const url = manager.getRuntimeUrlWithToken(runtimeId);
2120
+ if (url) safeOpenExternal(url);
2121
+ },
2122
+ reload: reloadRuntimeWebui,
2123
+ close: (runtimeId) => void closeRuntime(runtimeId),
2124
+ reveal: (runtimeId) => {
2125
+ const runtime = manager.getRuntime(runtimeId);
2126
+ if (runtime) void shell.openPath(runtime.root);
2127
+ }
2128
+ })
2129
+ },
2130
+ {
2131
+ label: tMain("workspace"),
2132
+ submenu: [
2133
+ webuiItem({
2134
+ label: tMain("openChat"),
2135
+ accelerator: "CmdOrCtrl+1",
2136
+ click: () => navigate({ activity: "chat", view: "chat" })
2137
+ }),
2138
+ webuiItem({
2139
+ label: tMain("focusPrompt"),
2140
+ accelerator: "CmdOrCtrl+/",
2141
+ click: () => navigate({ action: "focus-chat" })
2142
+ }),
2143
+ webuiItem({
2144
+ label: tMain("toggleTerminal"),
2145
+ accelerator: "CmdOrCtrl+`",
2146
+ click: () => navigate({ terminal: "toggle" })
2147
+ }),
2148
+ webuiItem({ label: tMain("newTerminal"), click: () => navigate({ terminal: "new" }) }),
2149
+ { type: "separator" },
2150
+ webuiItem({
2151
+ label: tMain("commandPalette"),
2152
+ accelerator: "CmdOrCtrl+K",
2153
+ click: () => navigate({ action: "open-command-palette" })
2154
+ }),
2155
+ webuiItem({
2156
+ label: tMain("quickModelSwitcher"),
2157
+ accelerator: "CmdOrCtrl+M",
2158
+ click: () => navigate({ action: "open-model-switcher" })
2159
+ }),
2160
+ webuiItem({
2161
+ type: "checkbox",
2162
+ label: tMain("yoloMode"),
2163
+ checked: yoloChecked,
2164
+ accelerator: "CmdOrCtrl+Shift+Y",
2165
+ click: () => navigate({ pref: { key: "yolo", toggle: true } })
2166
+ }),
2167
+ webuiItem({
2168
+ type: "checkbox",
2169
+ label: tMain("nextPrediction"),
2170
+ checked: nextPredictionChecked,
2171
+ click: () => navigate({ pref: { key: "nextPrediction", toggle: true } })
2172
+ }),
2173
+ webuiItem({
2174
+ type: "checkbox",
2175
+ label: tMain("contextAutoCompact"),
2176
+ checked: contextAutoCompactChecked,
2177
+ click: () => navigate({ pref: { key: "contextAutoCompact", toggle: true } })
2178
+ }),
2179
+ { type: "separator" },
2180
+ webuiItem({
2181
+ label: tMain("reloadActiveWebui"),
2182
+ accelerator: "CmdOrCtrl+Shift+R",
2183
+ click: () => void reloadActiveWebuiView()
2184
+ })
2185
+ ]
2186
+ },
2187
+ {
2188
+ label: tMain("view"),
2189
+ submenu: [
2190
+ {
2191
+ type: "checkbox",
2192
+ label: tMain("compactDesktopSidebar"),
2193
+ accelerator: "CmdOrCtrl+B",
2194
+ checked: shellSidebarCollapsed,
2195
+ click: () => setShellSidebarCollapsed(!shellSidebarCollapsed)
2196
+ },
2197
+ { type: "separator" },
2198
+ { role: "reload" },
2199
+ { role: "toggleDevTools" },
2200
+ { type: "separator" },
2201
+ { role: "resetZoom" },
2202
+ { role: "zoomIn" },
2203
+ { role: "zoomOut" },
2204
+ { type: "separator" },
2205
+ { role: "togglefullscreen" }
2206
+ ]
2207
+ }
2208
+ ];
2209
+ Menu.setApplicationMenu(Menu.buildFromTemplate(template));
2210
+ }
2211
+ manager.on("changed", () => {
2212
+ configureApplicationMenu();
2213
+ syncActiveWebuiView();
2214
+ broadcastState();
2215
+ });
2216
+ ipcMain.on(IPC.setLocale, (_event, locale) => {
2217
+ setMainLocale(locale);
2218
+ configureApplicationMenu();
2219
+ broadcastLocaleToEmbeddedWebuis(locale);
2220
+ void writeUiLocale(locale);
2221
+ });
2222
+ watchProviderConfig(
2223
+ desktopConfigPaths.globalConfigPath,
2224
+ desktopConfigPaths.vault,
2225
+ (snapshot) => {
2226
+ if (snapshot.uiLocale === void 0) return;
2227
+ setMainLocale(snapshot.uiLocale);
2228
+ configureApplicationMenu();
2229
+ shellView?.webContents.send(IPC.localeChanged, snapshot.uiLocale);
2230
+ broadcastLocaleToEmbeddedWebuis(snapshot.uiLocale);
2231
+ },
2232
+ { warn: (m) => console.warn(`Config watcher: ${m}`) }
2233
+ );
2234
+ bridge.on("changed", (conversation) => {
2235
+ shellView?.webContents.send(IPC.conversationChanged, conversation);
2236
+ });
2237
+ app.on("window-all-closed", () => {
2238
+ if (process.platform !== "darwin") app.quit();
2239
+ });
2240
+ app.on("before-quit", (event) => {
2241
+ if (quittingAfterCleanup) return;
2242
+ event.preventDefault();
2243
+ quittingAfterCleanup = true;
2244
+ if (saveWindowStateTimer) {
2245
+ clearTimeout(saveWindowStateTimer);
2246
+ saveWindowStateTimer = null;
2247
+ }
2248
+ bridge.closeAll();
2249
+ void saveWindowState().catch(() => void 0).finally(() => manager.closeAll({ persistWorkspace: false }).finally(() => app.quit()));
2250
+ });
2251
+ app.whenReady().then(async () => {
2252
+ registerIpc();
2253
+ await createWindow();
2254
+ app.on("activate", () => {
2255
+ if (mainWindow === null) void createWindow();
2256
+ });
2257
+ }).catch((err) => {
2258
+ console.error(err);
2259
+ app.exit(1);
2260
+ });
2261
+ function sameOrigin(candidate, base) {
2262
+ if (!base) return false;
2263
+ try {
2264
+ const candidateUrl = new URL(candidate);
2265
+ const baseUrl = new URL(base);
2266
+ return candidateUrl.origin === baseUrl.origin;
2267
+ } catch {
2268
+ return false;
2269
+ }
2270
+ }
2271
+ function desktopSettingsWorkspaceRoot() {
2272
+ return path3.join(wstackGlobalRoot3(), "desktop", "global-settings-workspace");
2273
+ }
2274
+ function validatedWindowState(state) {
2275
+ if (!state) return null;
2276
+ if (state.width < MIN_WINDOW_WIDTH2 || state.height < MIN_WINDOW_HEIGHT2) return null;
2277
+ if (state.x === void 0 || state.y === void 0) return state;
2278
+ const candidate = {
2279
+ x: state.x,
2280
+ y: state.y,
2281
+ width: state.width,
2282
+ height: state.height
2283
+ };
2284
+ const visibleOnSomeDisplay = screen.getAllDisplays().some((display) => {
2285
+ const area = display.workArea;
2286
+ return rectanglesIntersect(candidate, area);
2287
+ });
2288
+ return visibleOnSomeDisplay ? state : null;
2289
+ }
2290
+ function rectanglesIntersect(left, right) {
2291
+ return left.x < right.x + right.width && left.x + left.width > right.x && left.y < right.y + right.height && left.y + left.height > right.y;
2292
+ }
2293
+ //# sourceMappingURL=main.js.map