@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.
@@ -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
+ };