@amd-gaia/agent-ui 0.25.0 → 0.25.1-rc.10
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/assets/tray-icon.png +0 -0
- package/assets/tray-icon@2x.png +0 -0
- package/dist/assets/AdvancedSettings-CtYKNNWc.js +1 -0
- package/dist/assets/AdvancedSettings-D5VKpkPY.css +1 -0
- package/dist/assets/ConnectorsPane-Ctfp9d5s.js +1 -0
- package/dist/assets/ConnectorsPane-Dzu2DRTc.css +1 -0
- package/dist/assets/DocumentLibrary-CIKp6qld.css +1 -0
- package/dist/assets/DocumentLibrary-Fp70O1-8.js +4 -0
- package/dist/assets/FileBrowser-3H3DoJIm.js +4 -0
- package/dist/assets/FileBrowser-D04I8Pvv.css +1 -0
- package/dist/assets/MemoryDashboard-D5HzmtDi.js +1 -0
- package/dist/assets/MemoryDashboard-Mo721-BI.css +1 -0
- package/dist/assets/MobileAccessModal-B9peVPY4.js +2 -0
- package/dist/assets/MobileAccessModal-DuVy9F1u.css +1 -0
- package/dist/assets/ScheduleManager-DotJl5JW.css +1 -0
- package/dist/assets/ScheduleManager-HcGB9P6m.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/fraunces-latin-BwwriW9Y.woff2 +0 -0
- package/dist/assets/index-D2UMLtwY.js +74 -0
- package/dist/assets/index-DF5qku9L.css +1 -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/fonts-OFL.txt +101 -0
- package/dist/index.html +6 -6
- package/main-safety-net.cjs +54 -7
- package/main.cjs +276 -120
- package/package.json +3 -1
- package/preload.cjs +24 -0
- package/services/agent-process-manager.cjs +116 -13
- package/services/app-icon.cjs +29 -0
- package/services/auto-updater.cjs +26 -2
- package/services/backend-crash-dialog.cjs +102 -0
- package/services/backend-installer-progress-dialog.cjs +71 -43
- package/services/backend-installer.cjs +233 -25
- package/services/chromium-switches.cjs +29 -0
- package/services/keep-awake.cjs +52 -0
- package/services/notification-service.cjs +62 -0
- package/services/port-manager.cjs +19 -70
- package/services/process-tree.cjs +328 -0
- package/services/quit-cleanup.cjs +86 -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
|
@@ -26,6 +26,12 @@ const path = require("path");
|
|
|
26
26
|
const fs = require("fs");
|
|
27
27
|
const os = require("os");
|
|
28
28
|
const { EventEmitter } = require("events");
|
|
29
|
+
const {
|
|
30
|
+
killProcessTree,
|
|
31
|
+
listDescendants,
|
|
32
|
+
listTimeoutMs,
|
|
33
|
+
worstCaseKillMs,
|
|
34
|
+
} = require("./process-tree.cjs");
|
|
29
35
|
|
|
30
36
|
// ── Constants ────────────────────────────────────────────────────────────
|
|
31
37
|
|
|
@@ -38,6 +44,9 @@ const MANIFEST_FILENAME = "agent-manifest.json";
|
|
|
38
44
|
/** Graceful shutdown timeout before force kill (ms) */
|
|
39
45
|
const SHUTDOWN_TIMEOUT = 5000;
|
|
40
46
|
|
|
47
|
+
/** After the graceful shutdown, how long the tree gets between SIGTERM and SIGKILL (ms) */
|
|
48
|
+
const TREE_KILL_GRACE = 2000;
|
|
49
|
+
|
|
41
50
|
/** Health check interval (ms) — uses MCP "ping", not "initialize" */
|
|
42
51
|
const HEALTH_CHECK_INTERVAL = 30000;
|
|
43
52
|
|
|
@@ -82,6 +91,7 @@ class AgentProcessManager extends EventEmitter {
|
|
|
82
91
|
* handlers proxy to that backend's `/api/agents/*` routes (the real install
|
|
83
92
|
* runtime), so without a resolver those handlers fail loudly rather than
|
|
84
93
|
* duplicating download/verify/uv logic in the main process (issue #1721).
|
|
94
|
+
* killTree / listDescendants replace the process-tree helpers (tests).
|
|
85
95
|
*/
|
|
86
96
|
constructor(mainWindow, options = {}) {
|
|
87
97
|
super();
|
|
@@ -110,6 +120,12 @@ class AgentProcessManager extends EventEmitter {
|
|
|
110
120
|
*/
|
|
111
121
|
this.processes = {};
|
|
112
122
|
|
|
123
|
+
this._killTree = options.killTree || killProcessTree;
|
|
124
|
+
this._listDescendants = options.listDescendants || listDescendants;
|
|
125
|
+
|
|
126
|
+
/** Set by stopAll(): GAIA is quitting, so nothing may start another agent. */
|
|
127
|
+
this._shuttingDown = false;
|
|
128
|
+
|
|
113
129
|
/** Crash timestamps per agent for rate-limiting restart attempts */
|
|
114
130
|
this._crashTimes = {};
|
|
115
131
|
|
|
@@ -130,6 +146,11 @@ class AgentProcessManager extends EventEmitter {
|
|
|
130
146
|
* @returns {Promise<{ pid: number }>}
|
|
131
147
|
*/
|
|
132
148
|
async startAgent(agentId) {
|
|
149
|
+
if (this._shuttingDown) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`Not starting agent "${agentId}": GAIA is quitting and has already stopped its agents.`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
133
154
|
if (this.processes[agentId]) {
|
|
134
155
|
console.log(`[agent-mgr] Agent ${agentId} is already running (PID ${this.processes[agentId].process.pid})`);
|
|
135
156
|
return { pid: this.processes[agentId].process.pid };
|
|
@@ -166,10 +187,18 @@ class AgentProcessManager extends EventEmitter {
|
|
|
166
187
|
pendingRpc: {},
|
|
167
188
|
healthTimer: null,
|
|
168
189
|
stopping: false, // Set to true during intentional shutdown to suppress crash recovery
|
|
190
|
+
stopPromise: null, // The running stopAgent(), shared by duplicate calls
|
|
191
|
+
stdinError: null, // The error that broke stdin; nothing more can be written once set
|
|
169
192
|
};
|
|
170
193
|
|
|
171
194
|
this.processes[agentId] = entry;
|
|
172
195
|
|
|
196
|
+
// A write to a child that has died fails later, as an 'error' on the
|
|
197
|
+
// stream; without a listener that EPIPE crashes the whole app (#3498).
|
|
198
|
+
child.stdin.on("error", (err) => {
|
|
199
|
+
this._handleStdinError(agentId, child, err);
|
|
200
|
+
});
|
|
201
|
+
|
|
173
202
|
// ── stdout: JSON-RPC message stream ──
|
|
174
203
|
child.stdout.on("data", (data) => {
|
|
175
204
|
this._handleStdout(agentId, data);
|
|
@@ -207,6 +236,7 @@ class AgentProcessManager extends EventEmitter {
|
|
|
207
236
|
/**
|
|
208
237
|
* Stop an agent gracefully using JSON-RPC shutdown protocol.
|
|
209
238
|
* Cross-platform (works on Windows where SIGTERM = TerminateProcess).
|
|
239
|
+
* A second call while the first runs waits for that same stop.
|
|
210
240
|
* @param {string} agentId
|
|
211
241
|
* @returns {Promise<void>}
|
|
212
242
|
*/
|
|
@@ -217,23 +247,40 @@ class AgentProcessManager extends EventEmitter {
|
|
|
217
247
|
return;
|
|
218
248
|
}
|
|
219
249
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return;
|
|
250
|
+
if (entry.stopPromise) {
|
|
251
|
+
console.log(`[agent-mgr] Agent ${agentId} is already being stopped — waiting for that stop`);
|
|
252
|
+
return entry.stopPromise;
|
|
224
253
|
}
|
|
225
254
|
|
|
226
255
|
console.log(`[agent-mgr] Stopping agent ${agentId} (PID ${entry.process.pid})...`);
|
|
227
256
|
|
|
228
257
|
// Mark as intentionally stopping — suppresses crash recovery in _handleProcessExit
|
|
229
258
|
entry.stopping = true;
|
|
259
|
+
entry.stopPromise = this._stopAgent(agentId, entry);
|
|
260
|
+
return entry.stopPromise;
|
|
261
|
+
}
|
|
230
262
|
|
|
263
|
+
async _stopAgent(agentId, entry) {
|
|
231
264
|
// Clear health check timer
|
|
232
265
|
if (entry.healthTimer) {
|
|
233
266
|
clearInterval(entry.healthTimer);
|
|
234
267
|
entry.healthTimer = null;
|
|
235
268
|
}
|
|
236
269
|
|
|
270
|
+
// Read the tree while the agent is alive: once it exits, its children
|
|
271
|
+
// are re-parented on Linux and macOS and can no longer be found from it.
|
|
272
|
+
let knownDescendants = [];
|
|
273
|
+
if (entry.process.exitCode === null && entry.process.signalCode === null) {
|
|
274
|
+
try {
|
|
275
|
+
({ descendants: knownDescendants } = await this._listDescendants(entry.process.pid));
|
|
276
|
+
} catch (err) {
|
|
277
|
+
console.error(
|
|
278
|
+
`[agent-mgr] Could not list the processes agent ${agentId} started (${err.message}); ` +
|
|
279
|
+
"any it leaves behind will keep running."
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
237
284
|
// Step 1: Send JSON-RPC shutdown request via stdin
|
|
238
285
|
try {
|
|
239
286
|
this._sendJsonRpcRaw(agentId, "shutdown", {});
|
|
@@ -247,17 +294,17 @@ class AgentProcessManager extends EventEmitter {
|
|
|
247
294
|
// Step 2: Wait up to SHUTDOWN_TIMEOUT for clean exit
|
|
248
295
|
const exited = await this._waitForExit(agentId, SHUTDOWN_TIMEOUT);
|
|
249
296
|
|
|
250
|
-
// Step 3:
|
|
251
|
-
if (!exited
|
|
297
|
+
// Step 3: Stop the agent if it ignored the request, and anything it started either way.
|
|
298
|
+
if (!exited) {
|
|
252
299
|
console.warn(
|
|
253
300
|
`[agent-mgr] Agent ${agentId} did not exit within ${SHUTDOWN_TIMEOUT}ms, force killing...`
|
|
254
301
|
);
|
|
255
|
-
try {
|
|
256
|
-
entry.process.kill(); // SIGKILL on Unix, TerminateProcess on Windows
|
|
257
|
-
} catch {
|
|
258
|
-
// Already dead
|
|
259
|
-
}
|
|
260
302
|
}
|
|
303
|
+
await this._killTree(entry.process, {
|
|
304
|
+
knownDescendants,
|
|
305
|
+
graceMs: TREE_KILL_GRACE,
|
|
306
|
+
label: `agent ${agentId}`,
|
|
307
|
+
});
|
|
261
308
|
|
|
262
309
|
// Note: _handleProcessExit may have already cleaned up if the process exited.
|
|
263
310
|
// _cleanupProcess is idempotent, so calling it again is safe.
|
|
@@ -626,7 +673,13 @@ class AgentProcessManager extends EventEmitter {
|
|
|
626
673
|
|
|
627
674
|
entry.pendingRpc[id] = { resolve, reject, timer };
|
|
628
675
|
|
|
629
|
-
|
|
676
|
+
try {
|
|
677
|
+
this._sendJsonRpcRaw(agentId, method, params, id);
|
|
678
|
+
} catch (err) {
|
|
679
|
+
clearTimeout(timer);
|
|
680
|
+
delete entry.pendingRpc[id];
|
|
681
|
+
reject(err);
|
|
682
|
+
}
|
|
630
683
|
});
|
|
631
684
|
}
|
|
632
685
|
|
|
@@ -639,6 +692,7 @@ class AgentProcessManager extends EventEmitter {
|
|
|
639
692
|
const agentConfigs = this.config.agents || {};
|
|
640
693
|
|
|
641
694
|
for (const [agentId, agentCfg] of Object.entries(agentConfigs)) {
|
|
695
|
+
if (this._shuttingDown) return;
|
|
642
696
|
if (agentCfg.autoStart && !this.processes[agentId]) {
|
|
643
697
|
try {
|
|
644
698
|
await this.startAgent(agentId);
|
|
@@ -656,13 +710,16 @@ class AgentProcessManager extends EventEmitter {
|
|
|
656
710
|
}
|
|
657
711
|
|
|
658
712
|
/**
|
|
659
|
-
* Stop all running agents gracefully.
|
|
713
|
+
* Stop all running agents gracefully. For quitting only: afterwards no
|
|
714
|
+
* agent can be started, so a pending auto-start or crash restart can't
|
|
715
|
+
* spawn one the app will never stop.
|
|
660
716
|
*/
|
|
661
717
|
async stopAll() {
|
|
662
718
|
const agentIds = Object.keys(this.processes);
|
|
663
719
|
console.log(
|
|
664
720
|
`[agent-mgr] Stopping all agents: ${agentIds.join(", ") || "(none)"}`
|
|
665
721
|
);
|
|
722
|
+
this._shuttingDown = true;
|
|
666
723
|
await Promise.all(agentIds.map((id) => this.stopAgent(id)));
|
|
667
724
|
}
|
|
668
725
|
|
|
@@ -832,6 +889,7 @@ class AgentProcessManager extends EventEmitter {
|
|
|
832
889
|
);
|
|
833
890
|
|
|
834
891
|
setTimeout(async () => {
|
|
892
|
+
if (this._shuttingDown) return;
|
|
835
893
|
try {
|
|
836
894
|
await this.startAgent(agentId);
|
|
837
895
|
console.log(`[agent-mgr] Agent ${agentId} restarted after crash`);
|
|
@@ -858,6 +916,11 @@ class AgentProcessManager extends EventEmitter {
|
|
|
858
916
|
if (!entry || !entry.process.stdin || entry.process.stdin.destroyed) {
|
|
859
917
|
throw new Error(`Cannot write to stdin of agent "${agentId}"`);
|
|
860
918
|
}
|
|
919
|
+
if (entry.stdinError) {
|
|
920
|
+
throw new Error(
|
|
921
|
+
`Cannot write to stdin of agent "${agentId}": the pipe broke earlier (${entry.stdinError.message})`
|
|
922
|
+
);
|
|
923
|
+
}
|
|
861
924
|
|
|
862
925
|
const msg = {
|
|
863
926
|
jsonrpc: "2.0",
|
|
@@ -870,6 +933,36 @@ class AgentProcessManager extends EventEmitter {
|
|
|
870
933
|
entry.process.stdin.write(payload);
|
|
871
934
|
}
|
|
872
935
|
|
|
936
|
+
/**
|
|
937
|
+
* The agent's stdin failed — almost always EPIPE because the process is
|
|
938
|
+
* gone. Fail its pending calls now instead of letting them time out, and
|
|
939
|
+
* make sure the death is reported: a child that only closed its stdin is
|
|
940
|
+
* unusable, so it is stopped and its exit goes through the crash path.
|
|
941
|
+
*/
|
|
942
|
+
_handleStdinError(agentId, child, err) {
|
|
943
|
+
const entry = this.processes[agentId];
|
|
944
|
+
const current = entry && entry.process === child ? entry : null;
|
|
945
|
+
const exited = child.exitCode !== null || child.signalCode !== null;
|
|
946
|
+
console.error(
|
|
947
|
+
`[agent-mgr] stdin of agent ${agentId} (PID ${child.pid}) failed: ${err.code || "error"} ${err.message}` +
|
|
948
|
+
(exited ? " — the agent had already exited" : "")
|
|
949
|
+
);
|
|
950
|
+
if (!current) return;
|
|
951
|
+
current.stdinError = err;
|
|
952
|
+
|
|
953
|
+
for (const [rpcId, pending] of Object.entries(current.pendingRpc)) {
|
|
954
|
+
clearTimeout(pending.timer);
|
|
955
|
+
delete current.pendingRpc[rpcId];
|
|
956
|
+
pending.reject(new Error(`Agent "${agentId}" stopped reading its input (${err.code || err.message})`));
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (exited || current.stopping) return;
|
|
960
|
+
console.error(`[agent-mgr] Agent ${agentId} can no longer receive requests — stopping it`);
|
|
961
|
+
this._killTree(child, { graceMs: TREE_KILL_GRACE, label: `agent ${agentId}` }).catch((killErr) => {
|
|
962
|
+
console.error(`[agent-mgr] Could not stop agent ${agentId} after its stdin broke: ${killErr.message}`);
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
|
|
873
966
|
// ── Private: Health check ────────────────────────────────────────────
|
|
874
967
|
|
|
875
968
|
async _healthCheck(agentId) {
|
|
@@ -1096,4 +1189,14 @@ class AgentProcessManager extends EventEmitter {
|
|
|
1096
1189
|
}
|
|
1097
1190
|
}
|
|
1098
1191
|
|
|
1192
|
+
/**
|
|
1193
|
+
* Longest stopAgent can take on `platform` (ms): the tree read while the
|
|
1194
|
+
* agent is alive, its shutdown wait, then killProcessTree. The quit's
|
|
1195
|
+
* cleanup deadline must cover it.
|
|
1196
|
+
*/
|
|
1197
|
+
AgentProcessManager.worstCaseStopMs = (platform = process.platform) =>
|
|
1198
|
+
listTimeoutMs(platform) +
|
|
1199
|
+
SHUTDOWN_TIMEOUT +
|
|
1200
|
+
worstCaseKillMs({ platform, graceMs: TREE_KILL_GRACE });
|
|
1201
|
+
|
|
1099
1202
|
module.exports = AgentProcessManager;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Copyright(C) 2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The GAIA window icon. Every BrowserWindow must pass it: a source build runs
|
|
8
|
+
* as electron.exe, so a window without one shows Electron's logo.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} [platform] Defaults to process.platform.
|
|
11
|
+
* @returns {string} Absolute path to the icon asset.
|
|
12
|
+
*/
|
|
13
|
+
function appIconPath(platform = process.platform) {
|
|
14
|
+
return path.join(__dirname, "..", "assets", platform === "win32" ? "icon.ico" : "icon.png");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The Windows AppUserModelID. Without one, the taskbar groups GAIA with every
|
|
19
|
+
* other window of the same electron.exe and shows Electron's icon. Packaged
|
|
20
|
+
* builds match the installer's shortcut (`appId` in electron-builder.yml).
|
|
21
|
+
*
|
|
22
|
+
* @param {boolean} isPackaged app.isPackaged
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
function appUserModelId(isPackaged) {
|
|
26
|
+
return isPackaged ? "ai.amd.gaia" : "ai.amd.gaia.dev";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = { appIconPath, appUserModelId };
|
|
@@ -106,6 +106,8 @@ const state = {
|
|
|
106
106
|
};
|
|
107
107
|
|
|
108
108
|
let mainWindowRef = null;
|
|
109
|
+
/** Asks before an update restart would cut off running work (set by init). */
|
|
110
|
+
let confirmRestartRef = null;
|
|
109
111
|
let checkInProgress = false;
|
|
110
112
|
let scheduledTimeout = null;
|
|
111
113
|
let initialCheckTimeout = null;
|
|
@@ -616,13 +618,25 @@ function wireAutoUpdaterEvents() {
|
|
|
616
618
|
}
|
|
617
619
|
);
|
|
618
620
|
if (choice && choice.response === 0) {
|
|
621
|
+
// quitAndInstall starts the installer at once, so ask about running
|
|
622
|
+
// work here; the quit that follows doesn't ask again.
|
|
623
|
+
if (!(await confirmRestartRef())) {
|
|
624
|
+
log("info", "Restart put off: work is still running — will install on next quit");
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
619
627
|
log("info", "User chose to restart — calling quitAndInstall");
|
|
620
628
|
autoUpdaterRef.quitAndInstall(false, true);
|
|
621
629
|
} else {
|
|
622
630
|
log("info", "User deferred restart — will install on next quit");
|
|
623
631
|
}
|
|
624
632
|
} catch (err) {
|
|
625
|
-
|
|
633
|
+
const message = (err && err.message) || String(err);
|
|
634
|
+
log("error", "Failed to show restart dialog:", message);
|
|
635
|
+
electronApi.dialog.showErrorBox(
|
|
636
|
+
"GAIA update",
|
|
637
|
+
`GAIA couldn't finish asking about the update restart: ${message}\n\n` +
|
|
638
|
+
"The update installs the next time you quit GAIA."
|
|
639
|
+
);
|
|
626
640
|
}
|
|
627
641
|
});
|
|
628
642
|
|
|
@@ -689,9 +703,19 @@ function unregisterIpcHandlers() {
|
|
|
689
703
|
* continues, and this function itself never throws.
|
|
690
704
|
*
|
|
691
705
|
* @param {Electron.BrowserWindow | null} mainWindow
|
|
706
|
+
* @param {object} options
|
|
707
|
+
* @param {() => Promise<boolean>} options.confirmRestart - Whether "Restart now"
|
|
708
|
+
* may go ahead; asked before the installer starts. Required: without it an
|
|
709
|
+
* update restart would silently cut off a running answer.
|
|
692
710
|
*/
|
|
693
|
-
function init(mainWindow) {
|
|
711
|
+
function init(mainWindow, options = {}) {
|
|
712
|
+
if (typeof options.confirmRestart !== "function") {
|
|
713
|
+
throw new TypeError(
|
|
714
|
+
"auto-updater init needs options.confirmRestart, so Restart now can ask before it stops running work"
|
|
715
|
+
);
|
|
716
|
+
}
|
|
694
717
|
mainWindowRef = mainWindow || null;
|
|
718
|
+
confirmRestartRef = options.confirmRestart;
|
|
695
719
|
|
|
696
720
|
// Disabled path — short-circuit BEFORE touching any Electron APIs so
|
|
697
721
|
// this works in plain Node tests where require('electron') returns a
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Copyright(C) 2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The dialog shown when GAIA's backend exits on its own. Every choice ends
|
|
6
|
+
* somewhere: Restart relaunches the app, Quit quits it, and Save diagnostics
|
|
7
|
+
* writes the bundle and comes back to the same choice.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const RESTART = 0;
|
|
11
|
+
const SAVE_DIAGNOSTICS = 1;
|
|
12
|
+
const QUIT = 2;
|
|
13
|
+
|
|
14
|
+
// Colour codes the backend's logger writes; a dialog shows them as junk.
|
|
15
|
+
const ANSI_ESCAPE = /\u001b\[[0-9;]*[A-Za-z]/g;
|
|
16
|
+
|
|
17
|
+
function stripAnsi(text) {
|
|
18
|
+
return String(text).replace(ANSI_ESCAPE, "");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The dialog's detail text: what happened, what to do, then the log tail. */
|
|
22
|
+
function crashDetail(code, signal, tailLines) {
|
|
23
|
+
const how = signal ? `signal ${signal}` : `exit code ${code}`;
|
|
24
|
+
const tail = (tailLines || []).map(stripAnsi).join("\n") || "(nothing was logged)";
|
|
25
|
+
return (
|
|
26
|
+
`GAIA's background service stopped unexpectedly (${how}). Your chats are saved.\n` +
|
|
27
|
+
"Restart GAIA to continue. If it keeps happening, save diagnostics and attach " +
|
|
28
|
+
"them to a bug report.\n\n" +
|
|
29
|
+
`Last log lines:\n${tail}`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* An AppImage runs from a mount that goes away when the app exits, so a
|
|
35
|
+
* relaunch must start the AppImage file itself, not process.execPath.
|
|
36
|
+
*/
|
|
37
|
+
function relaunchOptions(env = process.env, argv = process.argv) {
|
|
38
|
+
return env.APPIMAGE ? { execPath: env.APPIMAGE, args: argv.slice(1) } : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Show the crash dialog until the user restarts or quits.
|
|
43
|
+
*
|
|
44
|
+
* @param {object} deps
|
|
45
|
+
* @param {object} deps.dialog Electron's dialog module
|
|
46
|
+
* @param {object} deps.app Electron's app module
|
|
47
|
+
* @param {() => Promise<string>} deps.writeDiagnostics resolves to the bundle path
|
|
48
|
+
* @param {() => void} deps.beforeQuit called before app.quit(), e.g. to skip the quit guard
|
|
49
|
+
* @param {object} [deps.env] where an AppImage's own path is read from
|
|
50
|
+
* @param {object} [deps.parent] window the dialog is modal to, e.g. mainWindow
|
|
51
|
+
* @returns {Promise<"restart"|"quit">}
|
|
52
|
+
*/
|
|
53
|
+
async function showBackendCrashDialog({ dialog, app, code, signal, tailLines, writeDiagnostics, beforeQuit, env = process.env, parent }) {
|
|
54
|
+
const detail = crashDetail(code, signal, tailLines);
|
|
55
|
+
// Modal to GAIA's window and in front: unparented, it can open behind other apps.
|
|
56
|
+
const owner = parent && !parent.isDestroyed() ? parent : null;
|
|
57
|
+
if (owner) {
|
|
58
|
+
if (owner.isMinimized()) owner.restore();
|
|
59
|
+
owner.show();
|
|
60
|
+
owner.focus();
|
|
61
|
+
}
|
|
62
|
+
const show = (box) => (owner && !owner.isDestroyed() ? dialog.showMessageBox(owner, box) : dialog.showMessageBox(box));
|
|
63
|
+
for (;;) {
|
|
64
|
+
const { response } = await show({
|
|
65
|
+
type: "error",
|
|
66
|
+
title: "GAIA stopped",
|
|
67
|
+
message: "GAIA's background service stopped",
|
|
68
|
+
detail,
|
|
69
|
+
buttons: ["Restart GAIA", "Save diagnostics", "Quit"],
|
|
70
|
+
defaultId: RESTART,
|
|
71
|
+
cancelId: QUIT,
|
|
72
|
+
noLink: true,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (response === SAVE_DIAGNOSTICS) {
|
|
76
|
+
try {
|
|
77
|
+
const bundlePath = await writeDiagnostics();
|
|
78
|
+
await show({
|
|
79
|
+
type: "info",
|
|
80
|
+
title: "Diagnostics saved",
|
|
81
|
+
message: "Diagnostics saved",
|
|
82
|
+
detail: `Attach this file to your bug report:\n${bundlePath}`,
|
|
83
|
+
buttons: ["OK"],
|
|
84
|
+
});
|
|
85
|
+
} catch (err) {
|
|
86
|
+
dialog.showErrorBox(
|
|
87
|
+
"Could not save diagnostics",
|
|
88
|
+
`Writing the diagnostics bundle failed: ${err.message}`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const choice = response === RESTART ? "restart" : "quit";
|
|
95
|
+
if (choice === "restart") app.relaunch(relaunchOptions(env));
|
|
96
|
+
if (beforeQuit) beforeQuit();
|
|
97
|
+
app.quit();
|
|
98
|
+
return choice;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = { showBackendCrashDialog, crashDetail, stripAnsi, relaunchOptions };
|
|
@@ -29,6 +29,7 @@ const path = require("path");
|
|
|
29
29
|
const { BrowserWindow, dialog, ipcMain, shell, clipboard } = require("electron");
|
|
30
30
|
|
|
31
31
|
const installer = require("./backend-installer.cjs");
|
|
32
|
+
const { appIconPath } = require("./app-icon.cjs");
|
|
32
33
|
|
|
33
34
|
// IPC channels. Keep these in sync with preload.cjs.
|
|
34
35
|
const IPC_PROGRESS_EVENT = "install:progress";
|
|
@@ -72,14 +73,45 @@ function registerIpcHandlers() {
|
|
|
72
73
|
});
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
// ── Stage labels ────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
// Keyed by installer.STAGES values; the ids themselves stay in the install log.
|
|
79
|
+
const STAGE_LABELS = Object.freeze({
|
|
80
|
+
"pre-checks": "Checking your PC",
|
|
81
|
+
"ensure-uv": "Getting the installer ready",
|
|
82
|
+
"create-venv": "Preparing GAIA's workspace",
|
|
83
|
+
"install-package": "Installing GAIA",
|
|
84
|
+
"gaia-init": "Getting the AI model ready",
|
|
85
|
+
verify: "Finishing up",
|
|
86
|
+
});
|
|
87
|
+
const NEUTRAL_STAGE_LABEL = "Setting up GAIA";
|
|
88
|
+
|
|
89
|
+
const unlabeledStages = new Set();
|
|
90
|
+
|
|
91
|
+
/** What the window shows for an installer stage id. */
|
|
92
|
+
function stageLabel(stage) {
|
|
93
|
+
if (Object.prototype.hasOwnProperty.call(STAGE_LABELS, stage)) return STAGE_LABELS[stage];
|
|
94
|
+
if (stage && !unlabeledStages.has(stage)) {
|
|
95
|
+
unlabeledStages.add(stage);
|
|
96
|
+
installer.log(
|
|
97
|
+
`Warning: install stage "${stage}" has no label; the window shows "${NEUTRAL_STAGE_LABEL}"`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return NEUTRAL_STAGE_LABEL;
|
|
101
|
+
}
|
|
102
|
+
|
|
75
103
|
// ── Embedded HTML ───────────────────────────────────────────────────────────
|
|
76
104
|
|
|
105
|
+
function escapeHtml(text) {
|
|
106
|
+
return String(text).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
107
|
+
}
|
|
108
|
+
|
|
77
109
|
/**
|
|
78
110
|
* HTML payload for the progress window. Rendered via `loadURL('data:...')`.
|
|
79
111
|
* The script uses `window.gaiaInstall` exposed by preload.cjs.
|
|
80
112
|
*/
|
|
81
113
|
function buildProgressHtml({ logPath }) {
|
|
82
|
-
const escapedLog =
|
|
114
|
+
const escapedLog = escapeHtml(logPath || "");
|
|
83
115
|
return `<!DOCTYPE html>
|
|
84
116
|
<html lang="en">
|
|
85
117
|
<head>
|
|
@@ -90,13 +122,13 @@ function buildProgressHtml({ logPath }) {
|
|
|
90
122
|
* src/styles/index.css, so the dark theme's role values are mirrored
|
|
91
123
|
* here. Keep them in step with that file; nothing below names a hex. */
|
|
92
124
|
:root {
|
|
93
|
-
--bg-primary: #
|
|
94
|
-
--bg-tertiary: #
|
|
95
|
-
--border: #
|
|
96
|
-
--text-primary: #
|
|
97
|
-
--text-secondary: #
|
|
98
|
-
--text-muted: #
|
|
99
|
-
--accent-
|
|
125
|
+
--bg-primary: #1A1917;
|
|
126
|
+
--bg-tertiary: #2C2B27;
|
|
127
|
+
--border: #4A4740;
|
|
128
|
+
--text-primary: #EFECE6;
|
|
129
|
+
--text-secondary: #B3ADA5;
|
|
130
|
+
--text-muted: #958F87;
|
|
131
|
+
--accent-solid: #D4B47E;
|
|
100
132
|
}
|
|
101
133
|
html, body {
|
|
102
134
|
margin: 0;
|
|
@@ -132,8 +164,6 @@ function buildProgressHtml({ logPath }) {
|
|
|
132
164
|
.stage {
|
|
133
165
|
font-size: 12px;
|
|
134
166
|
color: var(--text-secondary);
|
|
135
|
-
text-transform: uppercase;
|
|
136
|
-
letter-spacing: 0.5px;
|
|
137
167
|
margin-bottom: 6px;
|
|
138
168
|
min-height: 15px;
|
|
139
169
|
}
|
|
@@ -152,7 +182,7 @@ function buildProgressHtml({ logPath }) {
|
|
|
152
182
|
}
|
|
153
183
|
.fill {
|
|
154
184
|
height: 100%;
|
|
155
|
-
background: var(--accent-
|
|
185
|
+
background: var(--accent-solid);
|
|
156
186
|
border-radius: 9999px;
|
|
157
187
|
transition: width 300ms ease-out;
|
|
158
188
|
width: 0%;
|
|
@@ -201,11 +231,11 @@ function buildProgressHtml({ logPath }) {
|
|
|
201
231
|
</head>
|
|
202
232
|
<body>
|
|
203
233
|
<h1>Setting up GAIA</h1>
|
|
204
|
-
<p class="subtitle">First-
|
|
234
|
+
<p class="subtitle">First-time setup — this can take a few minutes</p>
|
|
205
235
|
<div class="stage" id="stage">Starting…</div>
|
|
206
236
|
<div class="bar"><div class="fill indeterminate" id="fill"></div></div>
|
|
207
237
|
<div class="percent" id="percent"></div>
|
|
208
|
-
<div class="message" id="message">
|
|
238
|
+
<div class="message" id="message">Checking your PC…</div>
|
|
209
239
|
<div class="footer">Log: <code>${escapedLog}</code></div>
|
|
210
240
|
<script>
|
|
211
241
|
(function() {
|
|
@@ -216,7 +246,7 @@ function buildProgressHtml({ logPath }) {
|
|
|
216
246
|
|
|
217
247
|
function onProgress(payload) {
|
|
218
248
|
if (!payload) return;
|
|
219
|
-
const stage = payload.
|
|
249
|
+
const stage = payload.label || '';
|
|
220
250
|
const percent = typeof payload.percent === 'number' ? payload.percent : null;
|
|
221
251
|
const message = payload.message || '';
|
|
222
252
|
|
|
@@ -268,8 +298,9 @@ function createProgressWindow() {
|
|
|
268
298
|
frame: false,
|
|
269
299
|
transparent: false,
|
|
270
300
|
show: false,
|
|
271
|
-
backgroundColor: "#
|
|
301
|
+
backgroundColor: "#1A1917", // --bg-primary, dark theme (see the :root block above)
|
|
272
302
|
title: "Installing GAIA",
|
|
303
|
+
icon: appIconPath(),
|
|
273
304
|
webPreferences: {
|
|
274
305
|
nodeIntegration: false,
|
|
275
306
|
contextIsolation: true,
|
|
@@ -305,7 +336,7 @@ function createProgressWindow() {
|
|
|
305
336
|
});
|
|
306
337
|
|
|
307
338
|
const onProgress = (stage, percent, message) => {
|
|
308
|
-
lastProgress = { stage, percent, message };
|
|
339
|
+
lastProgress = { stage, label: stageLabel(stage), percent, message };
|
|
309
340
|
if (window.isDestroyed()) return;
|
|
310
341
|
if (!rendererReady) return; // will be replayed on did-finish-load
|
|
311
342
|
try {
|
|
@@ -364,21 +395,19 @@ async function showFailureDialog(parentWindow, errorInfo = {}) {
|
|
|
364
395
|
.filter(Boolean)
|
|
365
396
|
.join("\n");
|
|
366
397
|
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
"
|
|
374
|
-
"
|
|
375
|
-
"
|
|
376
|
-
"
|
|
377
|
-
"Open log file",
|
|
378
|
-
"Quit",
|
|
398
|
+
// Copy/Open log return to the same dialog. "Install uv" is offered only
|
|
399
|
+
// when uv is what failed; for any other stage it would just repeat Retry.
|
|
400
|
+
const uvFailed = stage === "ensure-uv";
|
|
401
|
+
const actions = [
|
|
402
|
+
...(uvFailed ? [["install-uv", "Install uv (auto)"]] : []),
|
|
403
|
+
["retry", "Retry"],
|
|
404
|
+
["manual", "Manual install instructions"],
|
|
405
|
+
["copy-log", "Copy log path"],
|
|
406
|
+
["open-log", "Open log file"],
|
|
407
|
+
["quit", "Quit"],
|
|
379
408
|
];
|
|
409
|
+
const buttons = actions.map(([, label]) => label);
|
|
380
410
|
|
|
381
|
-
// Helper to show the dialog and handle the response.
|
|
382
411
|
const show = async () => {
|
|
383
412
|
const result = await dialog.showMessageBox(parentWindow || null, {
|
|
384
413
|
type: "error",
|
|
@@ -391,15 +420,12 @@ async function showFailureDialog(parentWindow, errorInfo = {}) {
|
|
|
391
420
|
noLink: true,
|
|
392
421
|
});
|
|
393
422
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
|
|
400
|
-
// (which will detect READY and proceed); on failure, re-show the
|
|
401
|
-
// dialog with augmented details.
|
|
402
|
-
const { window, onProgress, close } = createProgressWindow();
|
|
423
|
+
const action = (actions[result.response] || ["quit"])[0];
|
|
424
|
+
switch (action) {
|
|
425
|
+
case "install-uv": {
|
|
426
|
+
// Run the full packaged install (ensureBackend), with progress, so one
|
|
427
|
+
// click retries everything, not only uv; re-show with the new error.
|
|
428
|
+
const { onProgress, close } = createProgressWindow();
|
|
403
429
|
try {
|
|
404
430
|
await installer.ensureBackend({ onProgress, isPackaged: true });
|
|
405
431
|
try { close(); } catch {}
|
|
@@ -414,9 +440,9 @@ async function showFailureDialog(parentWindow, errorInfo = {}) {
|
|
|
414
440
|
return showFailureDialog(parentWindow, nextInfo);
|
|
415
441
|
}
|
|
416
442
|
}
|
|
417
|
-
case
|
|
443
|
+
case "retry":
|
|
418
444
|
return "retry";
|
|
419
|
-
case
|
|
445
|
+
case "manual": {
|
|
420
446
|
try {
|
|
421
447
|
await shell.openExternal("https://amd-gaia.ai/docs/quickstart#cli-install");
|
|
422
448
|
} catch {
|
|
@@ -424,7 +450,7 @@ async function showFailureDialog(parentWindow, errorInfo = {}) {
|
|
|
424
450
|
}
|
|
425
451
|
return "manual";
|
|
426
452
|
}
|
|
427
|
-
case
|
|
453
|
+
case "copy-log": {
|
|
428
454
|
try {
|
|
429
455
|
clipboard.writeText(logPath);
|
|
430
456
|
} catch {
|
|
@@ -432,7 +458,7 @@ async function showFailureDialog(parentWindow, errorInfo = {}) {
|
|
|
432
458
|
}
|
|
433
459
|
return showFailureDialog(parentWindow, errorInfo);
|
|
434
460
|
}
|
|
435
|
-
case
|
|
461
|
+
case "open-log": {
|
|
436
462
|
try {
|
|
437
463
|
await shell.openPath(logPath);
|
|
438
464
|
} catch {
|
|
@@ -440,7 +466,6 @@ async function showFailureDialog(parentWindow, errorInfo = {}) {
|
|
|
440
466
|
}
|
|
441
467
|
return showFailureDialog(parentWindow, errorInfo);
|
|
442
468
|
}
|
|
443
|
-
case 5:
|
|
444
469
|
default:
|
|
445
470
|
return "quit";
|
|
446
471
|
}
|
|
@@ -472,6 +497,9 @@ async function showPreCheckFailureDialog(parentWindow, { title, message, detail
|
|
|
472
497
|
|
|
473
498
|
module.exports = {
|
|
474
499
|
createProgressWindow,
|
|
500
|
+
buildProgressHtml,
|
|
501
|
+
stageLabel,
|
|
502
|
+
STAGE_LABELS,
|
|
475
503
|
showFailureDialog,
|
|
476
504
|
showPreCheckFailureDialog,
|
|
477
505
|
// IPC channel names for preload.cjs
|