@amd-gaia/agent-ui 0.17.1 → 0.17.3
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/bin/gaia-ui.cjs +370 -0
- package/dist/assets/index-B4Qzv7Ys.js +443 -0
- package/dist/assets/index-eQemgF08.css +1 -0
- package/dist/index.html +3 -3
- package/main.cjs +209 -54
- package/package.json +8 -11
- package/preload.cjs +42 -0
- package/services/agent-seeder.cjs +301 -0
- package/services/auto-updater.cjs +437 -0
- package/services/backend-installer-progress-dialog.cjs +429 -0
- package/services/backend-installer.cjs +1082 -0
- package/services/tray-manager.cjs +1 -1
- package/bin/gaia-ui.mjs +0 -571
- package/dist/assets/index-DFaWywBV.js +0 -432
- package/dist/assets/index-TyWv9Ej0.css +0 -1
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
// Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* auto-updater.cjs — GAIA Agent UI auto-update service.
|
|
6
|
+
*
|
|
7
|
+
* Wraps `electron-updater` for the GitHub Releases auto-update flow.
|
|
8
|
+
* Implements §4 Layer 3 + §7 Phase F of docs/plans/desktop-installer.mdx.
|
|
9
|
+
*
|
|
10
|
+
* Behavior:
|
|
11
|
+
* - First check 10 seconds after `init()` is called (typically from
|
|
12
|
+
* `app.whenReady`)
|
|
13
|
+
* - Subsequent checks every 4 hours via re-scheduling setTimeout
|
|
14
|
+
* - Concurrent-check guard so checks never overlap
|
|
15
|
+
* - Download silently in the background
|
|
16
|
+
* - On `update-downloaded`, show a native dialog: "Update ready — restart?"
|
|
17
|
+
* - Renderer integration via IPC channel `gaia:update:status`
|
|
18
|
+
* - Disabled entirely via GAIA_DISABLE_UPDATE=1 env var (CI / dev / corp)
|
|
19
|
+
*
|
|
20
|
+
* Exports:
|
|
21
|
+
* - init(mainWindow) → set up handlers, schedule checks
|
|
22
|
+
* - destroy() → tear down timers and IPC handlers
|
|
23
|
+
* - checkForUpdates() → manually trigger a check
|
|
24
|
+
* - getState() → returns a copy of the current state
|
|
25
|
+
* - STATES → string constants for valid states
|
|
26
|
+
*
|
|
27
|
+
* Design note: `electron` and `electron-updater` are lazy-required inside
|
|
28
|
+
* `init()`. Accessing `electronUpdater.autoUpdater` outside of an Electron
|
|
29
|
+
* runtime throws synchronously (it reads `app.getVersion()` eagerly), so
|
|
30
|
+
* we keep the module load pure. This also makes `GAIA_DISABLE_UPDATE=1`
|
|
31
|
+
* safely short-circuit before touching any Electron APIs — useful in tests
|
|
32
|
+
* and in environments where the Electron app isn't wired up.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
"use strict";
|
|
36
|
+
|
|
37
|
+
const path = require("path");
|
|
38
|
+
const fs = require("fs");
|
|
39
|
+
const os = require("os");
|
|
40
|
+
|
|
41
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
const CHECK_DELAY_MS = 10 * 1000; // First check 10s after init
|
|
44
|
+
const CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000; // Subsequent checks every 4h
|
|
45
|
+
const LOG_PATH = path.join(os.homedir(), ".gaia", "electron-updater.log");
|
|
46
|
+
|
|
47
|
+
const STATES = Object.freeze({
|
|
48
|
+
IDLE: "idle",
|
|
49
|
+
CHECKING: "checking",
|
|
50
|
+
AVAILABLE: "available",
|
|
51
|
+
DOWNLOADING: "downloading",
|
|
52
|
+
DOWNLOADED: "downloaded",
|
|
53
|
+
ERROR: "error",
|
|
54
|
+
DISABLED: "disabled",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ── Module state ─────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/** Shape broadcast to the renderer via `gaia:update:status`. */
|
|
60
|
+
const state = {
|
|
61
|
+
status: STATES.IDLE,
|
|
62
|
+
version: null,
|
|
63
|
+
progress: 0,
|
|
64
|
+
releaseNotes: null,
|
|
65
|
+
error: null,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
let mainWindowRef = null;
|
|
69
|
+
let checkInProgress = false;
|
|
70
|
+
let scheduledTimeout = null;
|
|
71
|
+
let initialCheckTimeout = null;
|
|
72
|
+
let ipcHandlersRegistered = false;
|
|
73
|
+
let initialized = false;
|
|
74
|
+
|
|
75
|
+
// Lazy-loaded Electron references (populated inside init()).
|
|
76
|
+
let electronApi = null; // { dialog, ipcMain }
|
|
77
|
+
let autoUpdaterRef = null; // electron-updater's singleton
|
|
78
|
+
|
|
79
|
+
// ── Logging ──────────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
function log(level, message, ...args) {
|
|
82
|
+
const ts = new Date().toISOString();
|
|
83
|
+
const extra = args.length ? " " + safeStringify(args) : "";
|
|
84
|
+
const line = `[${ts}] [${level}] ${message}${extra}\n`;
|
|
85
|
+
try {
|
|
86
|
+
fs.mkdirSync(path.dirname(LOG_PATH), { recursive: true });
|
|
87
|
+
fs.appendFileSync(LOG_PATH, line);
|
|
88
|
+
} catch {
|
|
89
|
+
// Non-fatal — logging must never crash the app.
|
|
90
|
+
}
|
|
91
|
+
// Also mirror to stdout so devs see it in `npm start` output.
|
|
92
|
+
try {
|
|
93
|
+
// eslint-disable-next-line no-console
|
|
94
|
+
console.log(`[auto-updater] ${level} ${message}`, ...args);
|
|
95
|
+
} catch {
|
|
96
|
+
// ignore
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function safeStringify(value) {
|
|
101
|
+
try {
|
|
102
|
+
return JSON.stringify(value);
|
|
103
|
+
} catch {
|
|
104
|
+
return String(value);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── State management ─────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
function broadcastState() {
|
|
111
|
+
if (!mainWindowRef) return;
|
|
112
|
+
try {
|
|
113
|
+
if (mainWindowRef.isDestroyed && mainWindowRef.isDestroyed()) return;
|
|
114
|
+
if (mainWindowRef.webContents && !mainWindowRef.webContents.isDestroyed()) {
|
|
115
|
+
mainWindowRef.webContents.send("gaia:update:status", { ...state });
|
|
116
|
+
}
|
|
117
|
+
} catch (err) {
|
|
118
|
+
log("warn", "Failed to broadcast state:", err && err.message);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function setState(patch) {
|
|
123
|
+
Object.assign(state, patch);
|
|
124
|
+
broadcastState();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getState() {
|
|
128
|
+
return { ...state };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Env gate ─────────────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
function isDisabled() {
|
|
134
|
+
return process.env.GAIA_DISABLE_UPDATE === "1";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── Core check ───────────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
async function checkForUpdates() {
|
|
140
|
+
if (isDisabled()) {
|
|
141
|
+
setState({ status: STATES.DISABLED });
|
|
142
|
+
log("info", "Update check skipped — GAIA_DISABLE_UPDATE=1");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (!autoUpdaterRef) {
|
|
146
|
+
log("warn", "checkForUpdates called before init — ignoring");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (checkInProgress) {
|
|
150
|
+
log("info", "Skipping check — another check is already in progress");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
checkInProgress = true;
|
|
154
|
+
setState({ status: STATES.CHECKING, error: null });
|
|
155
|
+
try {
|
|
156
|
+
log("info", "Checking for updates...");
|
|
157
|
+
await autoUpdaterRef.checkForUpdates();
|
|
158
|
+
} catch (err) {
|
|
159
|
+
setState({
|
|
160
|
+
status: STATES.ERROR,
|
|
161
|
+
error: (err && err.message) || String(err),
|
|
162
|
+
});
|
|
163
|
+
log("error", "Update check failed:", err && err.message);
|
|
164
|
+
} finally {
|
|
165
|
+
checkInProgress = false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function scheduleNextCheck() {
|
|
170
|
+
if (scheduledTimeout) {
|
|
171
|
+
clearTimeout(scheduledTimeout);
|
|
172
|
+
scheduledTimeout = null;
|
|
173
|
+
}
|
|
174
|
+
scheduledTimeout = setTimeout(async () => {
|
|
175
|
+
try {
|
|
176
|
+
await checkForUpdates();
|
|
177
|
+
} catch (err) {
|
|
178
|
+
log("error", "Scheduled check threw:", err && err.message);
|
|
179
|
+
}
|
|
180
|
+
scheduleNextCheck();
|
|
181
|
+
}, CHECK_INTERVAL_MS);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── Event wiring ─────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
function wireAutoUpdaterEvents() {
|
|
187
|
+
autoUpdaterRef.on("checking-for-update", () => {
|
|
188
|
+
setState({ status: STATES.CHECKING });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
autoUpdaterRef.on("update-available", (info) => {
|
|
192
|
+
const releaseNotes =
|
|
193
|
+
typeof info.releaseNotes === "string" ? info.releaseNotes : null;
|
|
194
|
+
setState({
|
|
195
|
+
status: STATES.AVAILABLE,
|
|
196
|
+
version: info.version || null,
|
|
197
|
+
releaseNotes,
|
|
198
|
+
error: null,
|
|
199
|
+
});
|
|
200
|
+
log("info", `Update available: ${info.version}`);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
autoUpdaterRef.on("update-not-available", (info) => {
|
|
204
|
+
// Reset to idle so the UI hides any stale "available" chip.
|
|
205
|
+
setState({
|
|
206
|
+
status: STATES.IDLE,
|
|
207
|
+
version: null,
|
|
208
|
+
progress: 0,
|
|
209
|
+
releaseNotes: null,
|
|
210
|
+
error: null,
|
|
211
|
+
});
|
|
212
|
+
log("info", `No update available (current ${info && info.version})`);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
autoUpdaterRef.on("download-progress", (progress) => {
|
|
216
|
+
const percent =
|
|
217
|
+
progress && typeof progress.percent === "number"
|
|
218
|
+
? Math.max(0, Math.min(100, Math.round(progress.percent)))
|
|
219
|
+
: 0;
|
|
220
|
+
setState({
|
|
221
|
+
status: STATES.DOWNLOADING,
|
|
222
|
+
progress: percent,
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
autoUpdaterRef.on("update-downloaded", async (info) => {
|
|
227
|
+
setState({
|
|
228
|
+
status: STATES.DOWNLOADED,
|
|
229
|
+
version: (info && info.version) || state.version,
|
|
230
|
+
progress: 100,
|
|
231
|
+
error: null,
|
|
232
|
+
});
|
|
233
|
+
log("info", `Update downloaded: ${info && info.version}`);
|
|
234
|
+
|
|
235
|
+
if (!electronApi || !electronApi.dialog) {
|
|
236
|
+
log("warn", "No dialog available — skipping restart prompt");
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const choice = await electronApi.dialog.showMessageBox(
|
|
241
|
+
mainWindowRef && !mainWindowRef.isDestroyed() ? mainWindowRef : null,
|
|
242
|
+
{
|
|
243
|
+
type: "info",
|
|
244
|
+
buttons: ["Restart now", "Later"],
|
|
245
|
+
defaultId: 0,
|
|
246
|
+
cancelId: 1,
|
|
247
|
+
title: "Update ready",
|
|
248
|
+
message: `GAIA ${info && info.version ? info.version : ""} has been downloaded.`,
|
|
249
|
+
detail:
|
|
250
|
+
"Restart the app to apply the update. Your chat history will be preserved.",
|
|
251
|
+
}
|
|
252
|
+
);
|
|
253
|
+
if (choice && choice.response === 0) {
|
|
254
|
+
log("info", "User chose to restart — calling quitAndInstall");
|
|
255
|
+
// (isSilent=false, isForceRunAfter=true) — run the installer UI on
|
|
256
|
+
// Windows and relaunch after the update is applied.
|
|
257
|
+
autoUpdaterRef.quitAndInstall(false, true);
|
|
258
|
+
} else {
|
|
259
|
+
log("info", "User deferred restart — will install on next quit");
|
|
260
|
+
}
|
|
261
|
+
} catch (err) {
|
|
262
|
+
log("error", "Failed to show restart dialog:", err && err.message);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
autoUpdaterRef.on("error", (err) => {
|
|
267
|
+
setState({
|
|
268
|
+
status: STATES.ERROR,
|
|
269
|
+
error: (err && err.message) || String(err),
|
|
270
|
+
});
|
|
271
|
+
log("error", "electron-updater error:", err && err.message);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function registerIpcHandlers() {
|
|
276
|
+
if (ipcHandlersRegistered || !electronApi || !electronApi.ipcMain) return;
|
|
277
|
+
const { ipcMain } = electronApi;
|
|
278
|
+
|
|
279
|
+
ipcMain.handle("gaia:update:get-status", () => getState());
|
|
280
|
+
ipcMain.handle("gaia:update:check", async () => {
|
|
281
|
+
await checkForUpdates();
|
|
282
|
+
return getState();
|
|
283
|
+
});
|
|
284
|
+
ipcHandlersRegistered = true;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function unregisterIpcHandlers() {
|
|
288
|
+
if (!ipcHandlersRegistered || !electronApi || !electronApi.ipcMain) return;
|
|
289
|
+
const { ipcMain } = electronApi;
|
|
290
|
+
try {
|
|
291
|
+
ipcMain.removeHandler("gaia:update:get-status");
|
|
292
|
+
} catch {
|
|
293
|
+
// ignore
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
ipcMain.removeHandler("gaia:update:check");
|
|
297
|
+
} catch {
|
|
298
|
+
// ignore
|
|
299
|
+
}
|
|
300
|
+
ipcHandlersRegistered = false;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Initialize the auto-updater. Safe to call multiple times — subsequent
|
|
307
|
+
* calls update the window reference only.
|
|
308
|
+
*
|
|
309
|
+
* Must NOT block app launch: if anything goes wrong the caller catches and
|
|
310
|
+
* continues, and this function itself never throws.
|
|
311
|
+
*
|
|
312
|
+
* @param {Electron.BrowserWindow | null} mainWindow
|
|
313
|
+
*/
|
|
314
|
+
function init(mainWindow) {
|
|
315
|
+
mainWindowRef = mainWindow || null;
|
|
316
|
+
|
|
317
|
+
// Disabled path — short-circuit BEFORE touching any Electron APIs so
|
|
318
|
+
// this works in plain Node tests where require('electron') returns a
|
|
319
|
+
// string and the electron-updater singleton throws on access.
|
|
320
|
+
if (isDisabled()) {
|
|
321
|
+
setState({ status: STATES.DISABLED });
|
|
322
|
+
log("info", "Auto-updater disabled via GAIA_DISABLE_UPDATE=1");
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (initialized) {
|
|
327
|
+
// Just refresh the window reference and push the current state down.
|
|
328
|
+
broadcastState();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Lazy-load Electron and electron-updater. Any failure here is logged
|
|
333
|
+
// and the updater stays in `idle` — we never crash the app.
|
|
334
|
+
try {
|
|
335
|
+
// eslint-disable-next-line global-require
|
|
336
|
+
const electron = require("electron");
|
|
337
|
+
// eslint-disable-next-line global-require
|
|
338
|
+
const electronUpdater = require("electron-updater");
|
|
339
|
+
|
|
340
|
+
if (!electron || !electron.app || !electron.ipcMain || !electron.dialog) {
|
|
341
|
+
log(
|
|
342
|
+
"warn",
|
|
343
|
+
"Electron APIs unavailable — auto-updater will not be active"
|
|
344
|
+
);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
electronApi = {
|
|
349
|
+
dialog: electron.dialog,
|
|
350
|
+
ipcMain: electron.ipcMain,
|
|
351
|
+
};
|
|
352
|
+
autoUpdaterRef = electronUpdater.autoUpdater;
|
|
353
|
+
} catch (err) {
|
|
354
|
+
log("error", "Failed to load electron-updater:", err && err.message);
|
|
355
|
+
setState({
|
|
356
|
+
status: STATES.ERROR,
|
|
357
|
+
error: (err && err.message) || "Failed to load electron-updater",
|
|
358
|
+
});
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Configure electron-updater. Provider config comes from
|
|
363
|
+
// electron-builder.yml (`publish:` block) at build time; we only set
|
|
364
|
+
// behavioral flags here.
|
|
365
|
+
try {
|
|
366
|
+
autoUpdaterRef.autoDownload = true;
|
|
367
|
+
autoUpdaterRef.autoInstallOnAppQuit = true;
|
|
368
|
+
autoUpdaterRef.disableWebInstaller = true;
|
|
369
|
+
autoUpdaterRef.allowDowngrade = false;
|
|
370
|
+
// Allow pre-releases only if explicitly opted in (for beta channels later).
|
|
371
|
+
autoUpdaterRef.allowPrerelease =
|
|
372
|
+
process.env.GAIA_UPDATE_PRERELEASE === "1";
|
|
373
|
+
|
|
374
|
+
autoUpdaterRef.logger = {
|
|
375
|
+
info: (m) => log("info", String(m)),
|
|
376
|
+
warn: (m) => log("warn", String(m)),
|
|
377
|
+
error: (m) => log("error", String(m)),
|
|
378
|
+
debug: (m) => log("debug", String(m)),
|
|
379
|
+
};
|
|
380
|
+
} catch (err) {
|
|
381
|
+
log("warn", "Failed to configure autoUpdater flags:", err && err.message);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
wireAutoUpdaterEvents();
|
|
386
|
+
} catch (err) {
|
|
387
|
+
log("error", "Failed to wire autoUpdater events:", err && err.message);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
registerIpcHandlers();
|
|
393
|
+
} catch (err) {
|
|
394
|
+
log("warn", "Failed to register IPC handlers:", err && err.message);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// First check after CHECK_DELAY_MS, then every CHECK_INTERVAL_MS.
|
|
398
|
+
initialCheckTimeout = setTimeout(async () => {
|
|
399
|
+
try {
|
|
400
|
+
await checkForUpdates();
|
|
401
|
+
} catch (err) {
|
|
402
|
+
log("error", "Initial check threw:", err && err.message);
|
|
403
|
+
}
|
|
404
|
+
scheduleNextCheck();
|
|
405
|
+
}, CHECK_DELAY_MS);
|
|
406
|
+
|
|
407
|
+
initialized = true;
|
|
408
|
+
log(
|
|
409
|
+
"info",
|
|
410
|
+
`Auto-updater initialized; first check in ${CHECK_DELAY_MS}ms`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Tear down timers and IPC handlers. Safe to call multiple times. */
|
|
415
|
+
function destroy() {
|
|
416
|
+
if (initialCheckTimeout) {
|
|
417
|
+
clearTimeout(initialCheckTimeout);
|
|
418
|
+
initialCheckTimeout = null;
|
|
419
|
+
}
|
|
420
|
+
if (scheduledTimeout) {
|
|
421
|
+
clearTimeout(scheduledTimeout);
|
|
422
|
+
scheduledTimeout = null;
|
|
423
|
+
}
|
|
424
|
+
unregisterIpcHandlers();
|
|
425
|
+
mainWindowRef = null;
|
|
426
|
+
// Keep `initialized` true — calling init() again after destroy() is not a
|
|
427
|
+
// supported lifecycle and we'd need to re-wire electron-updater events
|
|
428
|
+
// which cannot be reliably cleaned up via its public API.
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
module.exports = {
|
|
432
|
+
init,
|
|
433
|
+
destroy,
|
|
434
|
+
checkForUpdates,
|
|
435
|
+
getState,
|
|
436
|
+
STATES,
|
|
437
|
+
};
|