@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,429 @@
|
|
|
1
|
+
// Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* backend-installer-progress-dialog.cjs — Electron progress UI for the
|
|
6
|
+
* first-run backend install.
|
|
7
|
+
*
|
|
8
|
+
* Owns the Electron-specific presentation layer for the install flow.
|
|
9
|
+
* The core install logic lives in `backend-installer.cjs`, which is pure
|
|
10
|
+
* Node.js; this module renders a borderless BrowserWindow with embedded
|
|
11
|
+
* HTML/JS and wires up IPC events so the install module's progress
|
|
12
|
+
* callbacks stream through to the renderer.
|
|
13
|
+
*
|
|
14
|
+
* Responsibilities:
|
|
15
|
+
* - Show a borderless progress window (stage + percent + message)
|
|
16
|
+
* - Expose an `onProgress(stage, percent, message)` callback for the
|
|
17
|
+
* install module
|
|
18
|
+
* - Show a failure dialog with Retry / Manual / Quit buttons
|
|
19
|
+
* - Surface the log file path (copy / open)
|
|
20
|
+
*
|
|
21
|
+
* Exposed API:
|
|
22
|
+
* - createProgressWindow() → { window, onProgress, close }
|
|
23
|
+
* - showFailureDialog(parentWindow, errorInfo) → Promise<'retry'|'manual'|'quit'>
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
"use strict";
|
|
27
|
+
|
|
28
|
+
const path = require("path");
|
|
29
|
+
const { BrowserWindow, dialog, ipcMain, shell, clipboard } = require("electron");
|
|
30
|
+
|
|
31
|
+
const installer = require("./backend-installer.cjs");
|
|
32
|
+
|
|
33
|
+
// IPC channels. Keep these in sync with preload.cjs.
|
|
34
|
+
const IPC_PROGRESS_EVENT = "install:progress";
|
|
35
|
+
const IPC_STATUS_REQUEST = "install:status";
|
|
36
|
+
const IPC_COPY_LOG_PATH = "install:copy-log-path";
|
|
37
|
+
const IPC_OPEN_LOG_FILE = "install:open-log-file";
|
|
38
|
+
|
|
39
|
+
// Track whether one-time IPC handlers have been registered so we don't
|
|
40
|
+
// attach them twice if createProgressWindow() runs multiple times.
|
|
41
|
+
let ipcHandlersRegistered = false;
|
|
42
|
+
|
|
43
|
+
function registerIpcHandlers() {
|
|
44
|
+
if (ipcHandlersRegistered) return;
|
|
45
|
+
ipcHandlersRegistered = true;
|
|
46
|
+
|
|
47
|
+
ipcMain.handle(IPC_STATUS_REQUEST, () => {
|
|
48
|
+
return {
|
|
49
|
+
state: installer.getState(),
|
|
50
|
+
logPath: installer.getLogPath(),
|
|
51
|
+
statePath: installer.getStatePath(),
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
ipcMain.handle(IPC_COPY_LOG_PATH, () => {
|
|
56
|
+
try {
|
|
57
|
+
clipboard.writeText(installer.getLogPath());
|
|
58
|
+
return { ok: true };
|
|
59
|
+
} catch (err) {
|
|
60
|
+
return { ok: false, message: err.message };
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
ipcMain.handle(IPC_OPEN_LOG_FILE, async () => {
|
|
65
|
+
try {
|
|
66
|
+
const logPath = installer.getLogPath();
|
|
67
|
+
const result = await shell.openPath(logPath);
|
|
68
|
+
return { ok: !result, message: result || null };
|
|
69
|
+
} catch (err) {
|
|
70
|
+
return { ok: false, message: err.message };
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Embedded HTML ───────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* HTML payload for the progress window. Rendered via `loadURL('data:...')`.
|
|
79
|
+
* The script uses `window.gaiaInstall` exposed by preload.cjs.
|
|
80
|
+
*/
|
|
81
|
+
function buildProgressHtml({ logPath }) {
|
|
82
|
+
const escapedLog = String(logPath || "").replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
83
|
+
return `<!DOCTYPE html>
|
|
84
|
+
<html lang="en">
|
|
85
|
+
<head>
|
|
86
|
+
<meta charset="UTF-8" />
|
|
87
|
+
<title>Installing GAIA</title>
|
|
88
|
+
<style>
|
|
89
|
+
html, body {
|
|
90
|
+
margin: 0;
|
|
91
|
+
padding: 0;
|
|
92
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
93
|
+
background: #1a1a2e;
|
|
94
|
+
color: #eee;
|
|
95
|
+
user-select: none;
|
|
96
|
+
-webkit-user-select: none;
|
|
97
|
+
overflow: hidden;
|
|
98
|
+
}
|
|
99
|
+
body {
|
|
100
|
+
display: flex;
|
|
101
|
+
flex-direction: column;
|
|
102
|
+
align-items: center;
|
|
103
|
+
justify-content: center;
|
|
104
|
+
height: 100vh;
|
|
105
|
+
padding: 32px;
|
|
106
|
+
box-sizing: border-box;
|
|
107
|
+
text-align: center;
|
|
108
|
+
}
|
|
109
|
+
h1 {
|
|
110
|
+
font-size: 18px;
|
|
111
|
+
font-weight: 600;
|
|
112
|
+
margin: 0 0 8px 0;
|
|
113
|
+
color: #fff;
|
|
114
|
+
}
|
|
115
|
+
.subtitle {
|
|
116
|
+
font-size: 13px;
|
|
117
|
+
color: #9aa0c8;
|
|
118
|
+
margin: 0 0 24px 0;
|
|
119
|
+
}
|
|
120
|
+
.stage {
|
|
121
|
+
font-size: 12px;
|
|
122
|
+
color: #7a7f9f;
|
|
123
|
+
text-transform: uppercase;
|
|
124
|
+
letter-spacing: 0.5px;
|
|
125
|
+
margin-bottom: 6px;
|
|
126
|
+
min-height: 15px;
|
|
127
|
+
}
|
|
128
|
+
.bar {
|
|
129
|
+
width: 360px;
|
|
130
|
+
max-width: 100%;
|
|
131
|
+
height: 10px;
|
|
132
|
+
background: #2a2a47;
|
|
133
|
+
border-radius: 5px;
|
|
134
|
+
overflow: hidden;
|
|
135
|
+
margin-bottom: 12px;
|
|
136
|
+
box-shadow: inset 0 1px 2px rgba(0,0,0,0.3);
|
|
137
|
+
}
|
|
138
|
+
.fill {
|
|
139
|
+
height: 100%;
|
|
140
|
+
background: linear-gradient(90deg, #4a7dff 0%, #6b9aff 100%);
|
|
141
|
+
border-radius: 5px;
|
|
142
|
+
transition: width 300ms ease-out;
|
|
143
|
+
width: 0%;
|
|
144
|
+
}
|
|
145
|
+
.fill.indeterminate {
|
|
146
|
+
width: 30% !important;
|
|
147
|
+
animation: slide 1.4s ease-in-out infinite;
|
|
148
|
+
}
|
|
149
|
+
@keyframes slide {
|
|
150
|
+
0% { margin-left: 0%; }
|
|
151
|
+
50% { margin-left: 70%; }
|
|
152
|
+
100% { margin-left: 0%; }
|
|
153
|
+
}
|
|
154
|
+
.percent {
|
|
155
|
+
font-size: 12px;
|
|
156
|
+
color: #9aa0c8;
|
|
157
|
+
margin-bottom: 16px;
|
|
158
|
+
min-height: 14px;
|
|
159
|
+
}
|
|
160
|
+
.message {
|
|
161
|
+
font-size: 13px;
|
|
162
|
+
color: #c6cae4;
|
|
163
|
+
max-width: 420px;
|
|
164
|
+
min-height: 36px;
|
|
165
|
+
line-height: 1.4;
|
|
166
|
+
}
|
|
167
|
+
.footer {
|
|
168
|
+
position: absolute;
|
|
169
|
+
bottom: 10px;
|
|
170
|
+
left: 0;
|
|
171
|
+
right: 0;
|
|
172
|
+
text-align: center;
|
|
173
|
+
font-size: 10px;
|
|
174
|
+
color: #5a5f7a;
|
|
175
|
+
}
|
|
176
|
+
code {
|
|
177
|
+
font-family: "SF Mono", Monaco, Consolas, monospace;
|
|
178
|
+
font-size: 10px;
|
|
179
|
+
color: #7a7f9f;
|
|
180
|
+
}
|
|
181
|
+
</style>
|
|
182
|
+
</head>
|
|
183
|
+
<body>
|
|
184
|
+
<h1>Setting up GAIA</h1>
|
|
185
|
+
<p class="subtitle">First-launch backend install — this can take a few minutes</p>
|
|
186
|
+
<div class="stage" id="stage">Starting…</div>
|
|
187
|
+
<div class="bar"><div class="fill indeterminate" id="fill"></div></div>
|
|
188
|
+
<div class="percent" id="percent"></div>
|
|
189
|
+
<div class="message" id="message">Running pre-flight checks…</div>
|
|
190
|
+
<div class="footer">Log: <code>${escapedLog}</code></div>
|
|
191
|
+
<script>
|
|
192
|
+
(function() {
|
|
193
|
+
const stageEl = document.getElementById('stage');
|
|
194
|
+
const fillEl = document.getElementById('fill');
|
|
195
|
+
const percentEl = document.getElementById('percent');
|
|
196
|
+
const messageEl = document.getElementById('message');
|
|
197
|
+
|
|
198
|
+
function onProgress(payload) {
|
|
199
|
+
if (!payload) return;
|
|
200
|
+
const stage = payload.stage || '';
|
|
201
|
+
const percent = typeof payload.percent === 'number' ? payload.percent : null;
|
|
202
|
+
const message = payload.message || '';
|
|
203
|
+
|
|
204
|
+
if (stage) stageEl.textContent = stage;
|
|
205
|
+
if (message) messageEl.textContent = message;
|
|
206
|
+
|
|
207
|
+
if (percent != null && percent >= 0) {
|
|
208
|
+
fillEl.classList.remove('indeterminate');
|
|
209
|
+
fillEl.style.width = Math.max(0, Math.min(100, percent)) + '%';
|
|
210
|
+
percentEl.textContent = percent + '%';
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (window.gaiaInstall && typeof window.gaiaInstall.onProgress === 'function') {
|
|
215
|
+
window.gaiaInstall.onProgress(onProgress);
|
|
216
|
+
} else {
|
|
217
|
+
// Fallback: listen directly via ipcRenderer shim if preload didn't load.
|
|
218
|
+
console.warn('gaiaInstall API not available in progress window');
|
|
219
|
+
}
|
|
220
|
+
})();
|
|
221
|
+
</script>
|
|
222
|
+
</body>
|
|
223
|
+
</html>`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ── Progress window factory ─────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Create a borderless progress window and return an `onProgress` callback
|
|
230
|
+
* suitable for passing to `backend-installer.ensureBackend({ onProgress })`.
|
|
231
|
+
*
|
|
232
|
+
* Returns:
|
|
233
|
+
* {
|
|
234
|
+
* window: BrowserWindow,
|
|
235
|
+
* onProgress: (stage, percent, message) => void,
|
|
236
|
+
* close: () => void,
|
|
237
|
+
* }
|
|
238
|
+
*/
|
|
239
|
+
function createProgressWindow() {
|
|
240
|
+
registerIpcHandlers();
|
|
241
|
+
|
|
242
|
+
const window = new BrowserWindow({
|
|
243
|
+
width: 480,
|
|
244
|
+
height: 280,
|
|
245
|
+
resizable: false,
|
|
246
|
+
minimizable: true,
|
|
247
|
+
maximizable: false,
|
|
248
|
+
fullscreenable: false,
|
|
249
|
+
frame: false,
|
|
250
|
+
transparent: false,
|
|
251
|
+
show: false,
|
|
252
|
+
backgroundColor: "#1a1a2e",
|
|
253
|
+
title: "Installing GAIA",
|
|
254
|
+
webPreferences: {
|
|
255
|
+
nodeIntegration: false,
|
|
256
|
+
contextIsolation: true,
|
|
257
|
+
preload: path.join(__dirname, "..", "preload.cjs"),
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
window.setMenuBarVisibility(false);
|
|
262
|
+
|
|
263
|
+
const html = buildProgressHtml({ logPath: installer.getLogPath() });
|
|
264
|
+
window.loadURL("data:text/html;charset=utf-8," + encodeURIComponent(html));
|
|
265
|
+
|
|
266
|
+
window.once("ready-to-show", () => {
|
|
267
|
+
if (!window.isDestroyed()) window.show();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Buffer progress events until the renderer has finished loading — events
|
|
271
|
+
// sent before `did-finish-load` are lost because the renderer's IPC
|
|
272
|
+
// listener hasn't attached yet. Once loaded, the most recent state is
|
|
273
|
+
// replayed (and flushed for subsequent delivery).
|
|
274
|
+
let rendererReady = false;
|
|
275
|
+
let lastProgress = null;
|
|
276
|
+
|
|
277
|
+
window.webContents.once("did-finish-load", () => {
|
|
278
|
+
rendererReady = true;
|
|
279
|
+
if (lastProgress && !window.isDestroyed()) {
|
|
280
|
+
try {
|
|
281
|
+
window.webContents.send(IPC_PROGRESS_EVENT, lastProgress);
|
|
282
|
+
} catch {
|
|
283
|
+
// non-fatal
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const onProgress = (stage, percent, message) => {
|
|
289
|
+
lastProgress = { stage, percent, message };
|
|
290
|
+
if (window.isDestroyed()) return;
|
|
291
|
+
if (!rendererReady) return; // will be replayed on did-finish-load
|
|
292
|
+
try {
|
|
293
|
+
window.webContents.send(IPC_PROGRESS_EVENT, lastProgress);
|
|
294
|
+
} catch (err) {
|
|
295
|
+
// Non-fatal
|
|
296
|
+
// eslint-disable-next-line no-console
|
|
297
|
+
console.error("[install-progress] send failed:", err.message);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const close = () => {
|
|
302
|
+
if (!window.isDestroyed()) {
|
|
303
|
+
try {
|
|
304
|
+
window.destroy();
|
|
305
|
+
} catch {
|
|
306
|
+
// ignore
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
window.on("closed", () => {
|
|
312
|
+
if (lastProgress) {
|
|
313
|
+
// eslint-disable-next-line no-console
|
|
314
|
+
console.log(
|
|
315
|
+
`[install-progress] window closed at stage=${lastProgress.stage} percent=${lastProgress.percent}`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return { window, onProgress, close };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ── Failure dialog ──────────────────────────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Show a modal failure dialog with Retry / Manual / Quit options.
|
|
327
|
+
* Returns 'retry', 'manual', or 'quit'.
|
|
328
|
+
*/
|
|
329
|
+
async function showFailureDialog(parentWindow, errorInfo = {}) {
|
|
330
|
+
const {
|
|
331
|
+
message = "GAIA backend install failed.",
|
|
332
|
+
stage = null,
|
|
333
|
+
suggestion = null,
|
|
334
|
+
} = errorInfo;
|
|
335
|
+
|
|
336
|
+
const logPath = installer.getLogPath();
|
|
337
|
+
const statePath = installer.getStatePath();
|
|
338
|
+
|
|
339
|
+
const detail = [
|
|
340
|
+
stage ? `Stage: ${stage}` : null,
|
|
341
|
+
suggestion ? `\n${suggestion}` : null,
|
|
342
|
+
`\nLog file: ${logPath}`,
|
|
343
|
+
`State file: ${statePath}`,
|
|
344
|
+
]
|
|
345
|
+
.filter(Boolean)
|
|
346
|
+
.join("\n");
|
|
347
|
+
|
|
348
|
+
const result = await dialog.showMessageBox(parentWindow || null, {
|
|
349
|
+
type: "error",
|
|
350
|
+
title: "GAIA install failed",
|
|
351
|
+
message,
|
|
352
|
+
detail,
|
|
353
|
+
buttons: [
|
|
354
|
+
"Retry",
|
|
355
|
+
"Manual install instructions",
|
|
356
|
+
"Copy log path",
|
|
357
|
+
"Open log file",
|
|
358
|
+
"Quit",
|
|
359
|
+
],
|
|
360
|
+
defaultId: 0,
|
|
361
|
+
cancelId: 4,
|
|
362
|
+
noLink: true,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
switch (result.response) {
|
|
366
|
+
case 0:
|
|
367
|
+
return "retry";
|
|
368
|
+
case 1: {
|
|
369
|
+
try {
|
|
370
|
+
await shell.openExternal("https://amd-gaia.ai/quickstart#cli-install");
|
|
371
|
+
} catch {
|
|
372
|
+
// ignore
|
|
373
|
+
}
|
|
374
|
+
return "manual";
|
|
375
|
+
}
|
|
376
|
+
case 2: {
|
|
377
|
+
try {
|
|
378
|
+
clipboard.writeText(logPath);
|
|
379
|
+
} catch {
|
|
380
|
+
// ignore
|
|
381
|
+
}
|
|
382
|
+
// Keep the user in the loop — re-show the dialog so they can pick an action.
|
|
383
|
+
return showFailureDialog(parentWindow, errorInfo);
|
|
384
|
+
}
|
|
385
|
+
case 3: {
|
|
386
|
+
try {
|
|
387
|
+
await shell.openPath(logPath);
|
|
388
|
+
} catch {
|
|
389
|
+
// ignore
|
|
390
|
+
}
|
|
391
|
+
return showFailureDialog(parentWindow, errorInfo);
|
|
392
|
+
}
|
|
393
|
+
case 4:
|
|
394
|
+
default:
|
|
395
|
+
return "quit";
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ── Pre-check failure dialogs ───────────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Show a simple retryable error dialog. Used for disk-space / offline errors
|
|
403
|
+
* before install begins.
|
|
404
|
+
* Returns 'retry' or 'quit'.
|
|
405
|
+
*/
|
|
406
|
+
async function showPreCheckFailureDialog(parentWindow, { title, message, detail }) {
|
|
407
|
+
const result = await dialog.showMessageBox(parentWindow || null, {
|
|
408
|
+
type: "warning",
|
|
409
|
+
title: title || "GAIA cannot start",
|
|
410
|
+
message: message || "A pre-flight check failed.",
|
|
411
|
+
detail: detail || "",
|
|
412
|
+
buttons: ["Retry", "Quit"],
|
|
413
|
+
defaultId: 0,
|
|
414
|
+
cancelId: 1,
|
|
415
|
+
noLink: true,
|
|
416
|
+
});
|
|
417
|
+
return result.response === 0 ? "retry" : "quit";
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
module.exports = {
|
|
421
|
+
createProgressWindow,
|
|
422
|
+
showFailureDialog,
|
|
423
|
+
showPreCheckFailureDialog,
|
|
424
|
+
// IPC channel names for preload.cjs
|
|
425
|
+
IPC_PROGRESS_EVENT,
|
|
426
|
+
IPC_STATUS_REQUEST,
|
|
427
|
+
IPC_COPY_LOG_PATH,
|
|
428
|
+
IPC_OPEN_LOG_FILE,
|
|
429
|
+
};
|