@amd-gaia/agent-ui 0.25.0 → 0.25.1-rc.2
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.
- package/dist/assets/AdvancedSettings-BsZzYCUX.js +1 -0
- package/dist/assets/AdvancedSettings-D5VKpkPY.css +1 -0
- package/dist/assets/ConnectorsPane-Dzu2DRTc.css +1 -0
- package/dist/assets/ConnectorsPane-sXLzWlmi.js +1 -0
- package/dist/assets/DocumentLibrary-C8MaQVoZ.js +1 -0
- package/dist/assets/DocumentLibrary-Cj9MJ9yW.css +1 -0
- package/dist/assets/FileBrowser-D04I8Pvv.css +1 -0
- package/dist/assets/FileBrowser-DAjSQXvM.js +4 -0
- package/dist/assets/MemoryDashboard-CANryTDV.js +1 -0
- package/dist/assets/MemoryDashboard-DgugVDad.css +1 -0
- package/dist/assets/MobileAccessModal-BukNLr4c.js +2 -0
- package/dist/assets/MobileAccessModal-DuVy9F1u.css +1 -0
- package/dist/assets/ScheduleManager-GwVTsKQO.css +1 -0
- package/dist/assets/ScheduleManager-o_9LgcpS.js +2 -0
- package/dist/assets/{jsx-runtime-DVeaULmT.js → apiBase-y00JgRBP.js} +1 -1
- package/dist/assets/eye-B6FejHNG.js +1 -0
- package/dist/assets/index-B3UtHCtQ.css +1 -0
- package/dist/assets/index-CqsM0gYR.js +71 -0
- package/dist/assets/inter-latin-C2S99t-D.woff2 +0 -0
- package/dist/assets/inter-latin-ext-CFHvXkgd.woff2 +0 -0
- package/dist/assets/message-square-Dwm7y3C0.js +1 -0
- package/dist/index.html +6 -6
- package/main-safety-net.cjs +54 -7
- package/main.cjs +217 -58
- package/package.json +3 -1
- package/preload.cjs +24 -0
- package/services/agent-process-manager.cjs +101 -13
- package/services/auto-updater.cjs +26 -2
- package/services/backend-installer-progress-dialog.cjs +9 -9
- package/services/keep-awake.cjs +52 -0
- package/services/notification-service.cjs +62 -0
- package/services/port-manager.cjs +14 -70
- package/services/process-tree.cjs +305 -0
- package/services/quit-cleanup.cjs +77 -0
- package/services/quit-guard.cjs +242 -0
- package/services/tray-icons.cjs +174 -0
- package/services/tray-manager.cjs +582 -83
- package/services/tray-state.cjs +231 -0
- package/services/window-keys.cjs +43 -0
- package/assets/tray-icon.ico +0 -0
- package/dist/assets/AdvancedSettings-5cX0pGZc.js +0 -1
- package/dist/assets/AdvancedSettings-Ce6EvcAe.css +0 -1
- package/dist/assets/ConnectorsPane-CG7atLb2.css +0 -1
- package/dist/assets/ConnectorsPane-CuWxzcBY.js +0 -1
- package/dist/assets/DocumentLibrary-BdAKDe8H.js +0 -1
- package/dist/assets/DocumentLibrary-DrIP_032.css +0 -1
- package/dist/assets/FileBrowser-B8s6hKC5.js +0 -4
- package/dist/assets/FileBrowser-Bj6aB6Ro.css +0 -1
- package/dist/assets/MemoryDashboard-DQxtAW90.css +0 -1
- package/dist/assets/MemoryDashboard-DtolA-HZ.js +0 -1
- package/dist/assets/MobileAccessModal-C3nWkpWA.js +0 -2
- package/dist/assets/MobileAccessModal-C_t8DG0n.css +0 -1
- package/dist/assets/ScheduleManager-CIescbX6.css +0 -1
- package/dist/assets/ScheduleManager-HHdD6sko.js +0 -2
- package/dist/assets/dm-sans-latin-Xz1IZZA0.woff2 +0 -0
- package/dist/assets/index-CFVpPQal.css +0 -1
- package/dist/assets/index-DEPbenxC.js +0 -60
- package/dist/assets/message-square-Ccmbk96j.js +0 -1
- package/dist/assets/space-mono-400-Co7bH5Hm.woff2 +0 -0
- package/dist/assets/useEscapeToClose-B8fw5mn-.js +0 -1
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{l as e}from"./apiBase-y00JgRBP.js";var t={name:`message-square`,size:24,node:[[`path`,{d:`M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z`,key:`18887p`}]]};t.node;var n=e(t);export{n as t};
|
package/dist/index.html
CHANGED
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<!--
|
|
7
|
-
|
|
8
|
-
<meta name="theme-color" content="#
|
|
6
|
+
<!-- Canvas color; rewritten per theme and palette by applyTheme() and
|
|
7
|
+
syncTitleBar(). With no saved preference the app follows the OS. -->
|
|
8
|
+
<meta name="theme-color" content="#1A1917" />
|
|
9
9
|
<link rel="icon" type="image/png" href="./favicon.png" />
|
|
10
10
|
<title>GAIA Agent UI</title>
|
|
11
|
-
<script type="module" crossorigin src="./assets/index-
|
|
11
|
+
<script type="module" crossorigin src="./assets/index-CqsM0gYR.js"></script>
|
|
12
12
|
<link rel="modulepreload" crossorigin href="./assets/rolldown-runtime-hePW80VL.js">
|
|
13
|
-
<link rel="modulepreload" crossorigin href="./assets/
|
|
14
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
13
|
+
<link rel="modulepreload" crossorigin href="./assets/apiBase-y00JgRBP.js">
|
|
14
|
+
<link rel="stylesheet" crossorigin href="./assets/index-B3UtHCtQ.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
package/main-safety-net.cjs
CHANGED
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
* - process.on('unhandledRejection') catches rejected app.whenReady() chain.
|
|
14
14
|
* - installLogTee() attaches stream.on('error') so stream errors are handled
|
|
15
15
|
* before they can become uncaughtException (root-cause fix).
|
|
16
|
+
*
|
|
17
|
+
* A fatal error quits through app.quit(), not process.exit(): only the quit
|
|
18
|
+
* path (will-quit in main.cjs) stops the backend and the agents, so exiting
|
|
19
|
+
* directly left them running after the window was gone (#3498). A timer
|
|
20
|
+
* still forces the exit if that cleanup hangs.
|
|
16
21
|
*/
|
|
17
22
|
|
|
18
23
|
"use strict";
|
|
@@ -60,6 +65,9 @@ function appendLog(logPath, msg) {
|
|
|
60
65
|
}
|
|
61
66
|
}
|
|
62
67
|
|
|
68
|
+
/** How long a fatal error's quit may take before the process is ended outright (ms). */
|
|
69
|
+
const FATAL_EXIT_GRACE_MS = 25000;
|
|
70
|
+
|
|
63
71
|
// ── Core installer ────────────────────────────────────────────────────────────
|
|
64
72
|
|
|
65
73
|
/**
|
|
@@ -70,24 +78,63 @@ function appendLog(logPath, msg) {
|
|
|
70
78
|
* @param {object} opts.dialogModule - Electron dialog (injected for tests).
|
|
71
79
|
* @param {object} opts.appModule - Electron app EventEmitter (injected).
|
|
72
80
|
* @param {Function} [opts.homedirFn] - Override for os.homedir (tests).
|
|
81
|
+
* @param {Function} [opts.beforeQuit] - Called before the fatal quit, e.g.
|
|
82
|
+
* so the quit guard does not ask the user to confirm it.
|
|
83
|
+
* @param {number} [opts.exitGraceMs] - Override for FATAL_EXIT_GRACE_MS.
|
|
73
84
|
*/
|
|
74
|
-
function installSafetyNet({
|
|
85
|
+
function installSafetyNet({
|
|
86
|
+
logPath,
|
|
87
|
+
dialogModule,
|
|
88
|
+
appModule,
|
|
89
|
+
homedirFn,
|
|
90
|
+
beforeQuit,
|
|
91
|
+
exitGraceMs = FATAL_EXIT_GRACE_MS,
|
|
92
|
+
}) {
|
|
75
93
|
const homedir = homedirFn || (() => os.homedir());
|
|
76
94
|
|
|
77
95
|
// Per-handler re-entry guard (closure-scoped — each installSafetyNet
|
|
78
96
|
// call gets its own, intentionally; see test_main_error_handling.js).
|
|
79
97
|
let _inFatalHandler = false;
|
|
80
98
|
|
|
99
|
+
// Quit through will-quit so its cleanup stops the backend and agents; the
|
|
100
|
+
// timer ends the process if that cleanup never finishes.
|
|
101
|
+
function quitAfterFatal() {
|
|
102
|
+
const timer = setTimeout(() => {
|
|
103
|
+
appendLog(
|
|
104
|
+
logPath,
|
|
105
|
+
`[${new Date().toISOString()}] FATAL_EXIT cleanup did not finish within ${exitGraceMs} ms; exiting`
|
|
106
|
+
);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}, exitGraceMs);
|
|
109
|
+
if (typeof beforeQuit === "function") {
|
|
110
|
+
try {
|
|
111
|
+
beforeQuit();
|
|
112
|
+
} catch (hookErr) {
|
|
113
|
+
appendLog(logPath, `[${new Date().toISOString()}] FATAL_QUIT beforeQuit failed: ${hookErr.stack || hookErr}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
appModule.quit();
|
|
118
|
+
} catch (quitErr) {
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
appendLog(logPath, `[${new Date().toISOString()}] FATAL_QUIT app.quit() failed, exiting now: ${quitErr.stack || quitErr}`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
81
125
|
function fatal(err) {
|
|
126
|
+
const stack = (err && err.stack) ? err.stack : String(err);
|
|
127
|
+
const ts = new Date().toISOString();
|
|
128
|
+
|
|
129
|
+
// A second fatal while the first is still showing its dialog or quitting
|
|
130
|
+
// (an EPIPE from a child that died during cleanup, say) is logged only:
|
|
131
|
+
// exiting here would skip the cleanup that is already under way.
|
|
82
132
|
if (_inFatalHandler) {
|
|
83
|
-
|
|
133
|
+
appendLog(logPath, `[${ts}] FATAL (while already quitting) ${stack}`);
|
|
84
134
|
return;
|
|
85
135
|
}
|
|
86
136
|
_inFatalHandler = true;
|
|
87
137
|
|
|
88
|
-
const stack = (err && err.stack) ? err.stack : String(err);
|
|
89
|
-
const ts = new Date().toISOString();
|
|
90
|
-
|
|
91
138
|
// Write to log BEFORE showing dialog so the entry survives even if
|
|
92
139
|
// dialog.showErrorBox itself crashes.
|
|
93
140
|
appendLog(logPath, `[${ts}] FATAL ${stack}`);
|
|
@@ -112,7 +159,7 @@ function installSafetyNet({ logPath, dialogModule, appModule, homedirFn }) {
|
|
|
112
159
|
}
|
|
113
160
|
} catch { } // intentional: fatal path, no upstream
|
|
114
161
|
|
|
115
|
-
|
|
162
|
+
quitAfterFatal();
|
|
116
163
|
}
|
|
117
164
|
|
|
118
165
|
// Wire process-level handlers.
|
|
@@ -179,4 +226,4 @@ function installLogTee({ stream, logPath }) {
|
|
|
179
226
|
});
|
|
180
227
|
}
|
|
181
228
|
|
|
182
|
-
module.exports = { installSafetyNet, installLogTee };
|
|
229
|
+
module.exports = { installSafetyNet, installLogTee, FATAL_EXIT_GRACE_MS };
|
package/main.cjs
CHANGED
|
@@ -13,7 +13,16 @@
|
|
|
13
13
|
// services/notification-service.js — Desktop notifications + permission prompts (T5)
|
|
14
14
|
// preload.cjs — contextBridge for IPC channels (T0/T1)
|
|
15
15
|
|
|
16
|
-
const {
|
|
16
|
+
const {
|
|
17
|
+
app,
|
|
18
|
+
autoUpdater: electronAutoUpdater,
|
|
19
|
+
BrowserWindow,
|
|
20
|
+
Menu,
|
|
21
|
+
dialog,
|
|
22
|
+
ipcMain,
|
|
23
|
+
powerMonitor,
|
|
24
|
+
shell,
|
|
25
|
+
} = require("electron");
|
|
17
26
|
const path = require("path");
|
|
18
27
|
const fs = require("fs");
|
|
19
28
|
const os = require("os");
|
|
@@ -37,12 +46,18 @@ const _MAIN_LOG_PATH = path.join(_GAIA_DIR, "electron-main.log");
|
|
|
37
46
|
// very handler we are trying to install, falling through to Electron's bare
|
|
38
47
|
// JS-error dialog.
|
|
39
48
|
let installSafetyNet, installLogTee, _fatalHandler;
|
|
49
|
+
// Set when a fatal error quits the app: the crash dialog was the question,
|
|
50
|
+
// so the quit guard must not ask again.
|
|
51
|
+
let fatalQuitting = false;
|
|
40
52
|
try {
|
|
41
53
|
({ installSafetyNet, installLogTee } = require("./main-safety-net.cjs"));
|
|
42
54
|
({ fatal: _fatalHandler } = installSafetyNet({
|
|
43
55
|
logPath: _MAIN_LOG_PATH,
|
|
44
56
|
dialogModule: dialog,
|
|
45
57
|
appModule: app,
|
|
58
|
+
beforeQuit: () => {
|
|
59
|
+
fatalQuitting = true;
|
|
60
|
+
},
|
|
46
61
|
}));
|
|
47
62
|
} catch (err) {
|
|
48
63
|
try { process.stderr.write(`[main] safety-net load failed: ${err.message}\n`); } catch { }
|
|
@@ -54,8 +69,17 @@ try {
|
|
|
54
69
|
|
|
55
70
|
// Services (loaded after app.whenReady)
|
|
56
71
|
const TrayManager = require("./services/tray-manager.cjs");
|
|
72
|
+
const {
|
|
73
|
+
attachWindowGuards,
|
|
74
|
+
confirmQuit,
|
|
75
|
+
fetchWorkInProgress,
|
|
76
|
+
installQuitGuard,
|
|
77
|
+
QUIT,
|
|
78
|
+
RESTART_FOR_UPDATE,
|
|
79
|
+
} = require("./services/quit-guard.cjs");
|
|
57
80
|
const AgentProcessManager = require("./services/agent-process-manager.cjs");
|
|
58
81
|
const NotificationService = require("./services/notification-service.cjs");
|
|
82
|
+
const KeepAwake = require("./services/keep-awake.cjs");
|
|
59
83
|
const PortManager = require("./services/port-manager.cjs");
|
|
60
84
|
const { isGaiaBackendProcess } = require("./services/backend-orphan.cjs");
|
|
61
85
|
const { buildIndexQuery } = require("./services/index-query.cjs");
|
|
@@ -76,6 +100,8 @@ const {
|
|
|
76
100
|
describeBlockedUrl,
|
|
77
101
|
} = require("./services/link-policy.cjs");
|
|
78
102
|
const { createStartupDeepLinkQueue } = require("./services/deep-link-queue.cjs");
|
|
103
|
+
const { installWindowKeys } = require("./services/window-keys.cjs");
|
|
104
|
+
const { installQuitCleanup } = require("./services/quit-cleanup.cjs");
|
|
79
105
|
|
|
80
106
|
// ── F7: Ozone hint (issue #782) ─────────────────────────────────────────────
|
|
81
107
|
// Electron-recommended switch for distro-agnostic Linux behaviour: picks
|
|
@@ -205,6 +231,11 @@ const windowConfig = appConfig.window || {
|
|
|
205
231
|
// ── State ──────────────────────────────────────────────────────────────────
|
|
206
232
|
|
|
207
233
|
let backendProcess = null;
|
|
234
|
+
// The in-flight startBackend(), so a quit during startup can wait for the
|
|
235
|
+
// spawn and stop it instead of leaving it behind.
|
|
236
|
+
let backendStarting = null;
|
|
237
|
+
// Set as cleanup() begins; startup stops at its next step once it is.
|
|
238
|
+
let shutdownStarted = false;
|
|
208
239
|
let backendPort = DEFAULT_BACKEND_PORT;
|
|
209
240
|
let healthCheckUrl = `http://localhost:${backendPort}/api/health`;
|
|
210
241
|
let backendStderrTail = [];
|
|
@@ -225,6 +256,9 @@ let agentProcessManager = null;
|
|
|
225
256
|
/** @type {NotificationService | null} */
|
|
226
257
|
let notificationService = null;
|
|
227
258
|
|
|
259
|
+
/** @type {KeepAwake | null} */
|
|
260
|
+
let keepAwake = null;
|
|
261
|
+
|
|
228
262
|
/** @type {PortManager} */
|
|
229
263
|
const portManager = new PortManager({
|
|
230
264
|
logger: { log: console.log.bind(console), error: console.error.bind(console) },
|
|
@@ -386,6 +420,7 @@ async function startBackend() {
|
|
|
386
420
|
}
|
|
387
421
|
const crashed = !isIntentionalKill && code !== 0 && code !== null;
|
|
388
422
|
backendProcess = null;
|
|
423
|
+
if (trayManager && !isQuitting) trayManager.setBackendAlive(false);
|
|
389
424
|
|
|
390
425
|
if (crashed && !isQuitting) {
|
|
391
426
|
// Fire-and-forget — don't block the event loop.
|
|
@@ -523,8 +558,40 @@ function reportBlockedLink(url) {
|
|
|
523
558
|
);
|
|
524
559
|
}
|
|
525
560
|
|
|
561
|
+
// No system title bar: the app's own top row is the window's top edge, and
|
|
562
|
+
// the native window controls are drawn into it (traffic lights on macOS).
|
|
563
|
+
// Keep the height in sync with --titlebar-h in src/styles/index.css. The
|
|
564
|
+
// renderer sends its canvas and text colors once it paints; until then the
|
|
565
|
+
// controls use the dark canvas, like the window background.
|
|
566
|
+
const TITLE_BAR_HEIGHT = 48;
|
|
567
|
+
const TITLE_BAR_INITIAL = { color: "#1A1917", symbolColor: "#EFECE6" };
|
|
568
|
+
const IS_MAC = process.platform === "darwin";
|
|
569
|
+
const HEX_COLOR = /^#[0-9a-fA-F]{6}$/;
|
|
570
|
+
|
|
571
|
+
function titleBarOptions() {
|
|
572
|
+
return IS_MAC
|
|
573
|
+
? { titleBarStyle: "hidden", trafficLightPosition: { x: 16, y: 16 } }
|
|
574
|
+
: {
|
|
575
|
+
titleBarStyle: "hidden",
|
|
576
|
+
titleBarOverlay: { ...TITLE_BAR_INITIAL, height: TITLE_BAR_HEIGHT },
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// The renderer sends its canvas and text colors so the controls match the theme.
|
|
581
|
+
ipcMain.handle("window:set-titlebar-colors", (_event, colors) => {
|
|
582
|
+
const { color, symbolColor } = colors || {};
|
|
583
|
+
if (!HEX_COLOR.test(color) || !HEX_COLOR.test(symbolColor)) {
|
|
584
|
+
throw new Error(`Title bar colors must be #rrggbb, got ${JSON.stringify(colors)}`);
|
|
585
|
+
}
|
|
586
|
+
if (!IS_MAC && mainWindow && !mainWindow.isDestroyed()) {
|
|
587
|
+
mainWindow.setTitleBarOverlay({ color, symbolColor });
|
|
588
|
+
}
|
|
589
|
+
return true;
|
|
590
|
+
});
|
|
591
|
+
|
|
526
592
|
function createWindow() {
|
|
527
593
|
mainWindow = new BrowserWindow({
|
|
594
|
+
...titleBarOptions(),
|
|
528
595
|
width: windowConfig.width,
|
|
529
596
|
height: windowConfig.height,
|
|
530
597
|
minWidth: windowConfig.minWidth,
|
|
@@ -539,7 +606,12 @@ function createWindow() {
|
|
|
539
606
|
},
|
|
540
607
|
});
|
|
541
608
|
|
|
542
|
-
//
|
|
609
|
+
// No File/Edit/View menu: hiding the bar alone still lets Alt bring it back.
|
|
610
|
+
// macOS keeps its menu, which carries copy, paste and quit.
|
|
611
|
+
if (process.platform !== "darwin") {
|
|
612
|
+
Menu.setApplicationMenu(null);
|
|
613
|
+
installWindowKeys(mainWindow.webContents, { devTools: !app.isPackaged });
|
|
614
|
+
}
|
|
543
615
|
mainWindow.setMenuBarVisibility(false);
|
|
544
616
|
|
|
545
617
|
// Open external links in the default browser. The URL here is already
|
|
@@ -567,18 +639,27 @@ function createWindow() {
|
|
|
567
639
|
reportBlockedLink(url);
|
|
568
640
|
});
|
|
569
641
|
|
|
570
|
-
// ── Minimize-to-tray on close (C4 fix)
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
642
|
+
// ── Minimize-to-tray on close (C4 fix), first-close notice, quit guard ──
|
|
643
|
+
attachWindowGuards(mainWindow, {
|
|
644
|
+
app,
|
|
645
|
+
guard: quitGuard,
|
|
646
|
+
getTray: () => trayManager,
|
|
647
|
+
isQuitting: () => isQuitting,
|
|
648
|
+
platform: process.platform,
|
|
649
|
+
reportNoticeError: (err) => {
|
|
650
|
+
console.error("[main] First-close notice failed:", err.message);
|
|
651
|
+
const where = process.platform === "darwin" ? "menu bar" : "tray";
|
|
652
|
+
dialog.showErrorBox(
|
|
653
|
+
"GAIA is still running",
|
|
654
|
+
`GAIA is still running in the ${where}. Quit from the ${where} menu to stop it.\n\n` +
|
|
655
|
+
`(The usual notice failed: ${err.message})`
|
|
656
|
+
);
|
|
657
|
+
},
|
|
578
658
|
});
|
|
579
659
|
|
|
580
660
|
mainWindow.on("closed", () => {
|
|
581
661
|
mainWindow = null;
|
|
662
|
+
if (trayManager) trayManager.windowClosed();
|
|
582
663
|
});
|
|
583
664
|
|
|
584
665
|
// Show window when ready (unless --minimized or startMinimized config)
|
|
@@ -631,9 +712,9 @@ async function loadApp() {
|
|
|
631
712
|
* src/styles/index.css, so the dark theme's role values are
|
|
632
713
|
* mirrored here. Keep them in step with that file. */
|
|
633
714
|
:root {
|
|
634
|
-
--bg-primary: #
|
|
635
|
-
--text-primary: #
|
|
636
|
-
--text-secondary: #
|
|
715
|
+
--bg-primary: #1A1917;
|
|
716
|
+
--text-primary: #EFECE6;
|
|
717
|
+
--text-secondary: #B3ADA5;
|
|
637
718
|
}
|
|
638
719
|
body {
|
|
639
720
|
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
|
@@ -664,6 +745,25 @@ async function loadApp() {
|
|
|
664
745
|
|
|
665
746
|
// ── Services Setup ─────────────────────────────────────────────────────────
|
|
666
747
|
|
|
748
|
+
/**
|
|
749
|
+
* Recreate the window after it was closed (macOS keeps the app running).
|
|
750
|
+
* Services are re-pointed at it, not re-created: their IPC handlers stay.
|
|
751
|
+
* @returns {Electron.BrowserWindow}
|
|
752
|
+
*/
|
|
753
|
+
function reopenWindow() {
|
|
754
|
+
createWindow();
|
|
755
|
+
// Asked for from the tray or the dock: show it even when starting minimized.
|
|
756
|
+
mainWindow.once("ready-to-show", () => mainWindow && mainWindow.show());
|
|
757
|
+
if (agentProcessManager) agentProcessManager.mainWindow = mainWindow;
|
|
758
|
+
if (trayManager) trayManager.mainWindow = mainWindow;
|
|
759
|
+
if (notificationService) notificationService.mainWindow = mainWindow;
|
|
760
|
+
loadApp().catch((err) => {
|
|
761
|
+
console.error("[main] Failed to load the reopened window:", err.message);
|
|
762
|
+
dialog.showErrorBox("GAIA", `GAIA couldn't reopen its window: ${err.message}`);
|
|
763
|
+
});
|
|
764
|
+
return mainWindow;
|
|
765
|
+
}
|
|
766
|
+
|
|
667
767
|
function initializeServices() {
|
|
668
768
|
console.log("[main] Initializing services...");
|
|
669
769
|
|
|
@@ -676,8 +776,16 @@ function initializeServices() {
|
|
|
676
776
|
});
|
|
677
777
|
|
|
678
778
|
// T1: Tray Manager (system tray icon + context menu)
|
|
679
|
-
trayManager = new TrayManager(mainWindow, {
|
|
779
|
+
trayManager = new TrayManager(mainWindow, {
|
|
780
|
+
backendPort,
|
|
781
|
+
reopenWindow: () => reopenWindow(),
|
|
782
|
+
});
|
|
680
783
|
trayManager.create();
|
|
784
|
+
try {
|
|
785
|
+
trayManager.syncLoginItem();
|
|
786
|
+
} catch (err) {
|
|
787
|
+
console.error(`[main] Could not read the sign-in start setting: ${err.message}`);
|
|
788
|
+
}
|
|
681
789
|
|
|
682
790
|
// T5: Notification Service (routes agent notifications to OS + renderer)
|
|
683
791
|
notificationService = new NotificationService(
|
|
@@ -686,6 +794,9 @@ function initializeServices() {
|
|
|
686
794
|
trayManager
|
|
687
795
|
);
|
|
688
796
|
|
|
797
|
+
// #5226: hold off idle sleep while a chat turn runs
|
|
798
|
+
keepAwake = new KeepAwake();
|
|
799
|
+
|
|
689
800
|
// #2007: System metrics for the observability dashboard
|
|
690
801
|
systemMetrics.registerIpcHandlers();
|
|
691
802
|
|
|
@@ -1111,7 +1222,13 @@ app.whenReady().then(async () => {
|
|
|
1111
1222
|
}
|
|
1112
1223
|
|
|
1113
1224
|
// Start the Python backend
|
|
1114
|
-
|
|
1225
|
+
if (shutdownStarted) return;
|
|
1226
|
+
backendStarting = startBackend();
|
|
1227
|
+
backendProcess = await backendStarting;
|
|
1228
|
+
if (shutdownStarted) {
|
|
1229
|
+
console.log("[main] Quit during startup — not opening the window");
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1115
1232
|
|
|
1116
1233
|
// Create the window (hidden until ready-to-show)
|
|
1117
1234
|
createWindow();
|
|
@@ -1125,7 +1242,7 @@ app.whenReady().then(async () => {
|
|
|
1125
1242
|
// Any failure here is logged and swallowed — the app continues to run
|
|
1126
1243
|
// even if auto-update is unavailable.
|
|
1127
1244
|
try {
|
|
1128
|
-
autoUpdater.init(mainWindow);
|
|
1245
|
+
autoUpdater.init(mainWindow, { confirmRestart: confirmUpdateRestart });
|
|
1129
1246
|
} catch (err) {
|
|
1130
1247
|
console.error(
|
|
1131
1248
|
"[main] Failed to init auto-updater:",
|
|
@@ -1169,18 +1286,12 @@ app.whenReady().then(async () => {
|
|
|
1169
1286
|
}
|
|
1170
1287
|
}
|
|
1171
1288
|
|
|
1172
|
-
|
|
1289
|
+
// macOS and Linux: the shutdown is the OS's, so don't ask.
|
|
1290
|
+
powerMonitor.on("shutdown", () => quitGuard.allow());
|
|
1291
|
+
|
|
1292
|
+
app.on("activate", () => {
|
|
1173
1293
|
if (BrowserWindow.getAllWindows().length === 0) {
|
|
1174
|
-
|
|
1175
|
-
// Re-wire existing services to the new window (don't re-create — IPC handlers are already registered)
|
|
1176
|
-
if (agentProcessManager) agentProcessManager.mainWindow = mainWindow;
|
|
1177
|
-
if (trayManager) trayManager.mainWindow = mainWindow;
|
|
1178
|
-
if (notificationService) notificationService.mainWindow = mainWindow;
|
|
1179
|
-
try {
|
|
1180
|
-
await loadApp();
|
|
1181
|
-
} catch (err) {
|
|
1182
|
-
console.error("[main] Failed to load app on activate:", err.message);
|
|
1183
|
-
}
|
|
1294
|
+
reopenWindow();
|
|
1184
1295
|
} else if (mainWindow) {
|
|
1185
1296
|
mainWindow.show();
|
|
1186
1297
|
}
|
|
@@ -1215,33 +1326,57 @@ app.on("window-all-closed", () => {
|
|
|
1215
1326
|
// Electron's before-quit does NOT await async handlers.
|
|
1216
1327
|
// We use will-quit + event.preventDefault() to perform async cleanup, then re-quit.
|
|
1217
1328
|
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
// Mark any subsequent backend exit as intentional so we don't pop the
|
|
1223
|
-
// crash dialog during normal shutdown.
|
|
1224
|
-
isIntentionalKill = true;
|
|
1225
|
-
});
|
|
1329
|
+
/** Only while the GAIA service runs: without it, no answer is running. */
|
|
1330
|
+
function workCanBeLost() {
|
|
1331
|
+
return Boolean(trayManager && backendProcess);
|
|
1332
|
+
}
|
|
1226
1333
|
|
|
1227
|
-
|
|
1228
|
-
|
|
1334
|
+
/** Ask about losing work in front of the window, even when it was hidden in the tray. */
|
|
1335
|
+
function askAboutRunningWork(action) {
|
|
1336
|
+
return confirmQuit({
|
|
1337
|
+
action,
|
|
1338
|
+
getWork: () => fetchWorkInProgress(backendPort),
|
|
1339
|
+
showMessageBox: (opts) => {
|
|
1340
|
+
if (!mainWindow || mainWindow.isDestroyed()) return dialog.showMessageBox(opts);
|
|
1341
|
+
if (!mainWindow.isVisible()) mainWindow.show();
|
|
1342
|
+
return dialog.showMessageBox(mainWindow, opts);
|
|
1343
|
+
},
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1229
1346
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1347
|
+
/** The update dialog's "Restart now": the installer starts at once, so ask first. */
|
|
1348
|
+
function confirmUpdateRestart() {
|
|
1349
|
+
return workCanBeLost() ? askAboutRunningWork(RESTART_FOR_UPDATE) : Promise.resolve(true);
|
|
1350
|
+
}
|
|
1232
1351
|
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1352
|
+
// Every quit asks first while an answer or index job would be lost; an
|
|
1353
|
+
// update restart (asked in confirmUpdateRestart) and an OS shutdown don't.
|
|
1354
|
+
const quitGuard = installQuitGuard({
|
|
1355
|
+
app,
|
|
1356
|
+
updater: electronAutoUpdater,
|
|
1357
|
+
shouldAsk: () => !fatalQuitting && workCanBeLost(),
|
|
1358
|
+
confirm: () => askAboutRunningWork(QUIT),
|
|
1359
|
+
onQuitting: () => {
|
|
1360
|
+
isQuitting = true;
|
|
1361
|
+
// Mark any subsequent backend exit as intentional so we don't pop the
|
|
1362
|
+
// crash dialog during normal shutdown.
|
|
1363
|
+
isIntentionalKill = true;
|
|
1364
|
+
},
|
|
1365
|
+
onError: (err) => {
|
|
1366
|
+
console.error("[main] Quit check failed:", err);
|
|
1367
|
+
dialog.showErrorBox(
|
|
1368
|
+
"GAIA",
|
|
1369
|
+
`GAIA couldn't ask whether to stop the running work: ${err.message}\n\n` +
|
|
1370
|
+
"Quit again within a minute to quit anyway; any answer still being written will be lost."
|
|
1371
|
+
);
|
|
1372
|
+
},
|
|
1242
1373
|
});
|
|
1243
1374
|
|
|
1375
|
+
installQuitCleanup({ app, cleanup });
|
|
1376
|
+
|
|
1244
1377
|
async function cleanup() {
|
|
1378
|
+
shutdownStarted = true;
|
|
1379
|
+
|
|
1245
1380
|
// Phase F: tear down auto-updater timers and IPC handlers.
|
|
1246
1381
|
try {
|
|
1247
1382
|
autoUpdater.destroy();
|
|
@@ -1258,24 +1393,48 @@ async function cleanup() {
|
|
|
1258
1393
|
notificationService = null;
|
|
1259
1394
|
}
|
|
1260
1395
|
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
try {
|
|
1265
|
-
await agentProcessManager.stopAll();
|
|
1266
|
-
} catch (err) {
|
|
1267
|
-
console.error("Error stopping agents:", err.message);
|
|
1268
|
-
}
|
|
1269
|
-
agentProcessManager = null;
|
|
1396
|
+
if (keepAwake) {
|
|
1397
|
+
keepAwake.destroy();
|
|
1398
|
+
keepAwake = null;
|
|
1270
1399
|
}
|
|
1271
1400
|
|
|
1401
|
+
// The agents and the backend stop side by side: each waits out its own
|
|
1402
|
+
// grace period, and in series they could overrun the quit's time limit.
|
|
1403
|
+
await Promise.all([stopAgentsForQuit(), stopBackendForQuit()]);
|
|
1404
|
+
|
|
1272
1405
|
// Destroy tray icon
|
|
1273
1406
|
if (trayManager) {
|
|
1274
1407
|
trayManager.destroy();
|
|
1275
1408
|
trayManager = null;
|
|
1276
1409
|
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
async function stopAgentsForQuit() {
|
|
1413
|
+
if (!agentProcessManager) return;
|
|
1414
|
+
console.log("Stopping all managed agents...");
|
|
1415
|
+
try {
|
|
1416
|
+
await agentProcessManager.stopAll();
|
|
1417
|
+
} catch (err) {
|
|
1418
|
+
console.error("Error stopping agents:", err.message);
|
|
1419
|
+
}
|
|
1420
|
+
agentProcessManager = null;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
async function stopBackendForQuit() {
|
|
1424
|
+
// A quit during startup: wait for the spawn so it can be stopped below.
|
|
1425
|
+
if (backendStarting) {
|
|
1426
|
+
try {
|
|
1427
|
+
const started = await backendStarting;
|
|
1428
|
+
if (started && !backendProcess && started.exitCode === null && started.signalCode === null) {
|
|
1429
|
+
backendProcess = started;
|
|
1430
|
+
}
|
|
1431
|
+
} catch (err) {
|
|
1432
|
+
console.error("[main] Backend start failed during quit:", err.message);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1277
1435
|
|
|
1278
|
-
// Stop the Python backend via the port-manager
|
|
1436
|
+
// Stop the Python backend and everything it started via the port-manager
|
|
1437
|
+
// (SIGTERM → wait 3 s → SIGKILL).
|
|
1279
1438
|
// F5: previously we did this inline; extracted so main.cjs stays lean and
|
|
1280
1439
|
// to prevent orphan leaks across runs (issue #782).
|
|
1281
1440
|
if (backendProcess) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amd-gaia/agent-ui",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.1-rc.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"productName": "GAIA Agent UI",
|
|
6
6
|
"description": "Chat with the GAIA agent - local and private by default on AMD Ryzen AI, cloud models when you choose them",
|
|
@@ -59,12 +59,14 @@
|
|
|
59
59
|
"prepublishOnly": "npm run build",
|
|
60
60
|
"test": "vitest run",
|
|
61
61
|
"test:watch": "vitest",
|
|
62
|
+
"test:visual": "vite build && playwright test -c visual/playwright.config.ts",
|
|
62
63
|
"gen:api-types": "node scripts/gen-api-types.mjs"
|
|
63
64
|
},
|
|
64
65
|
"dependencies": {
|
|
65
66
|
"electron-updater": "^6.8.3"
|
|
66
67
|
},
|
|
67
68
|
"devDependencies": {
|
|
69
|
+
"@playwright/test": "1.63.0",
|
|
68
70
|
"@testing-library/jest-dom": "^7.0.0",
|
|
69
71
|
"@testing-library/react": "^16.1.0",
|
|
70
72
|
"@testing-library/user-event": "^14.5.2",
|
package/preload.cjs
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* install:* — First-run backend install progress (Phase A)
|
|
15
15
|
* gaia:update:* — Auto-update status & manual check (Phase F)
|
|
16
16
|
* system:* — System metrics for the observability dashboard (#2007)
|
|
17
|
+
* power:* — Keep the PC awake while a chat turn runs (#5226)
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
20
|
const { contextBridge, ipcRenderer } = require("electron");
|
|
@@ -53,6 +54,10 @@ contextBridge.exposeInMainWorld("gaiaAPI", {
|
|
|
53
54
|
tray: {
|
|
54
55
|
getConfig: () => ipcRenderer.invoke("tray:get-config"),
|
|
55
56
|
setConfig: (cfg) => ipcRenderer.invoke("tray:set-config", cfg),
|
|
57
|
+
// What GAIA is doing, for the tray icon, tooltip and menu.
|
|
58
|
+
setActivity: (activity) => ipcRenderer.invoke("tray:set-activity", activity),
|
|
59
|
+
// Tray menu actions: open-chat, new-chat, open-settings, refresh-status.
|
|
60
|
+
onCommand: (cb) => onEvent("tray:command", cb),
|
|
56
61
|
onNavigate: (cb) => onEvent("tray:navigate", cb),
|
|
57
62
|
},
|
|
58
63
|
|
|
@@ -63,6 +68,16 @@ contextBridge.exposeInMainWorld("gaiaAPI", {
|
|
|
63
68
|
respondPermission: (id, action, remember) =>
|
|
64
69
|
ipcRenderer.invoke("notification:respond", id, action, remember),
|
|
65
70
|
onNotification: (cb) => onEvent("notification:new", cb),
|
|
71
|
+
// Chat alerts (#5226)
|
|
72
|
+
show: (payload) => ipcRenderer.invoke("notification:show", payload),
|
|
73
|
+
flash: () => ipcRenderer.invoke("notification:flash"),
|
|
74
|
+
setAttentionCount: (count) =>
|
|
75
|
+
ipcRenderer.invoke("notification:attention", count),
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
// ── Keep the PC awake while GAIA works (#5226) ────────────────────────
|
|
79
|
+
power: {
|
|
80
|
+
setKeepAwake: (on) => ipcRenderer.invoke("power:keep-awake", on),
|
|
66
81
|
},
|
|
67
82
|
|
|
68
83
|
// ── System metrics for the observability dashboard (#2007) ────────────
|
|
@@ -71,6 +86,15 @@ contextBridge.exposeInMainWorld("gaiaAPI", {
|
|
|
71
86
|
},
|
|
72
87
|
});
|
|
73
88
|
|
|
89
|
+
// ── Window frame ─────────────────────────────────────────────────────────
|
|
90
|
+
// The app draws its own top row; this says how the native controls sit in it.
|
|
91
|
+
contextBridge.exposeInMainWorld("gaiaWindow", {
|
|
92
|
+
/** "mac": traffic lights at the top left; "overlay": controls at the top right. */
|
|
93
|
+
titleBar: process.platform === "darwin" ? "mac" : "overlay",
|
|
94
|
+
/** Match the native window controls to the app: { color, symbolColor } as #rrggbb. */
|
|
95
|
+
setColors: (colors) => ipcRenderer.invoke("window:set-titlebar-colors", colors),
|
|
96
|
+
});
|
|
97
|
+
|
|
74
98
|
// ── Install progress (Phase A) ──────────────────────────────────────────
|
|
75
99
|
// Exposed as a separate global so the progress window can use it without
|
|
76
100
|
// pulling in the full gaiaAPI surface (and so it keeps working if an
|