@amd-gaia/agent-ui 0.25.4-rc.2 → 0.25.4-rc.4

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.
Files changed (46) hide show
  1. package/bin/gaia-ui.cjs +2 -23
  2. package/dist/assets/AdvancedSettings-DoIDd09m.js +1 -0
  3. package/dist/assets/ConnectorsPane-CK3h7C7E.js +1 -0
  4. package/dist/assets/ConnectorsPane-CzLYXGMl.css +1 -0
  5. package/dist/assets/DocumentLibrary-BWvkvxTu.js +4 -0
  6. package/dist/assets/FileBrowser-DhqWGirJ.js +4 -0
  7. package/dist/assets/MemoryDashboard-BF-o8284.js +1 -0
  8. package/dist/assets/MobileAccessModal-51t2kQaJ.js +2 -0
  9. package/dist/assets/MobileAccessModal-BkVDm3YG.css +1 -0
  10. package/dist/assets/ScheduleManager-2xjZsnLK.js +2 -0
  11. package/dist/assets/api-BzlFpP4T.js +4 -0
  12. package/dist/assets/apiBase-BMkGbdXt.js +1 -0
  13. package/dist/assets/index-C_Cx5IlD.css +1 -0
  14. package/dist/assets/index-hAcJijAU.js +78 -0
  15. package/dist/assets/{zap-BdhDcjhB.js → zap-D7aLu_f0.js} +1 -1
  16. package/dist/icons/apple-touch-icon.png +0 -0
  17. package/dist/icons/icon-192.png +0 -0
  18. package/dist/icons/icon-512.png +0 -0
  19. package/dist/icons/icon-maskable-192.png +0 -0
  20. package/dist/icons/icon-maskable-512.png +0 -0
  21. package/dist/index.html +8 -3
  22. package/dist/manifest.json +16 -0
  23. package/dist/sw.js +49 -0
  24. package/main-safety-net.cjs +106 -8
  25. package/main.cjs +139 -159
  26. package/package.json +2 -1
  27. package/preload.cjs +16 -9
  28. package/services/auto-updater.cjs +780 -216
  29. package/services/backend-installer.cjs +233 -344
  30. package/services/engine-install.cjs +142 -0
  31. package/services/index-query.cjs +7 -1
  32. package/services/quit-guard.cjs +10 -3
  33. package/services/tray-manager.cjs +12 -7
  34. package/dist/assets/AdvancedSettings-B4MY5XPH.js +0 -1
  35. package/dist/assets/ConnectorsPane-B71QL-Kl.js +0 -1
  36. package/dist/assets/ConnectorsPane-Dzu2DRTc.css +0 -1
  37. package/dist/assets/DocumentLibrary-c78Y6arz.js +0 -4
  38. package/dist/assets/FileBrowser-DwEPODjV.js +0 -4
  39. package/dist/assets/MemoryDashboard-BQhg3t2B.js +0 -1
  40. package/dist/assets/MobileAccessModal-DuVy9F1u.css +0 -1
  41. package/dist/assets/MobileAccessModal-RI4HtcYe.js +0 -2
  42. package/dist/assets/ScheduleManager-tcyjbGY9.js +0 -2
  43. package/dist/assets/apiBase-DRtaTd8u.js +0 -1
  44. package/dist/assets/index-C9zLn8zo.css +0 -1
  45. package/dist/assets/index-CKOG_8jk.js +0 -78
  46. package/services/backend-installer-progress-dialog.cjs +0 -510
package/main.cjs CHANGED
@@ -47,11 +47,18 @@ const _MAIN_LOG_PATH = path.join(_GAIA_DIR, "electron-main.log");
47
47
  // very handler we are trying to install, falling through to Electron's bare
48
48
  // JS-error dialog.
49
49
  let installSafetyNet, installLogTee, _fatalHandler;
50
+ let readCount, runSafeModePrompt, SAFE_MODE_THRESHOLD;
50
51
  // Set when a fatal error quits the app: the crash dialog was the question,
51
52
  // so the quit guard must not ask again.
52
53
  let fatalQuitting = false;
53
54
  try {
54
- ({ installSafetyNet, installLogTee } = require("./main-safety-net.cjs"));
55
+ ({
56
+ installSafetyNet,
57
+ installLogTee,
58
+ readCount,
59
+ runSafeModePrompt,
60
+ SAFE_MODE_THRESHOLD,
61
+ } = require("./main-safety-net.cjs"));
55
62
  ({ fatal: _fatalHandler } = installSafetyNet({
56
63
  logPath: _MAIN_LOG_PATH,
57
64
  dialogModule: dialog,
@@ -59,6 +66,8 @@ try {
59
66
  beforeQuit: () => {
60
67
  fatalQuitting = true;
61
68
  },
69
+ // The install screen takes focus before anything has proved it can run.
70
+ holdCrashCount: () => showingInstallScreen,
62
71
  }));
63
72
  } catch (err) {
64
73
  try { process.stderr.write(`[main] safety-net load failed: ${err.message}\n`); } catch { }
@@ -68,6 +77,12 @@ try {
68
77
  process.exit(1);
69
78
  }
70
79
 
80
+ // ── Safe mode (issue #938) ────────────────────────────────────────────────────
81
+ // After SAFE_MODE_THRESHOLD crashes in a row, skip the log tee (the #934 crash
82
+ // path) and ask the user what to do before any other start-up work runs.
83
+ const _crashCount = readCount(() => os.homedir());
84
+ const _inSafeMode = _crashCount >= SAFE_MODE_THRESHOLD;
85
+
71
86
  // Services (loaded after app.whenReady)
72
87
  const TrayManager = require("./services/tray-manager.cjs");
73
88
  const { appIconPath, appUserModelId } = require("./services/app-icon.cjs");
@@ -85,9 +100,9 @@ const KeepAwake = require("./services/keep-awake.cjs");
85
100
  const PortManager = require("./services/port-manager.cjs");
86
101
  const { isGaiaBackendProcess } = require("./services/backend-orphan.cjs");
87
102
  const { showBackendCrashDialog } = require("./services/backend-crash-dialog.cjs");
88
- const { buildIndexQuery } = require("./services/index-query.cjs");
103
+ const { buildIndexQuery, INSTALL_SCREEN_QUERY } = require("./services/index-query.cjs");
89
104
  const backendInstaller = require("./services/backend-installer.cjs");
90
- const installerProgressDialog = require("./services/backend-installer-progress-dialog.cjs");
105
+ const { createEngineInstall } = require("./services/engine-install.cjs");
91
106
  const autoUpdater = require("./services/auto-updater.cjs");
92
107
  const agentSeeder = require("./services/agent-seeder.cjs");
93
108
  const systemMetrics = require("./services/system-metrics.cjs");
@@ -119,7 +134,7 @@ for (const [name, value] of chromiumSwitches({ platform: process.platform, env:
119
134
  // Users often launch AppImages by double-click, not from a terminal, so
120
135
  // console output vanishes. Mirror console.log/error to a file so the
121
136
  // diagnostics bundler has something to attach.
122
- (function installMainLogTee() {
137
+ function installMainLogTee() {
123
138
  try {
124
139
  try { fs.mkdirSync(_GAIA_DIR, { recursive: true }); } catch { /* ignore */ }
125
140
  const logPath = _MAIN_LOG_PATH;
@@ -186,7 +201,8 @@ for (const [name, value] of chromiumSwitches({ platform: process.platform, env:
186
201
  // not refuse to launch over a log-tee failure.
187
202
  process.stderr.write(`[main] log tee failed: ${err.message}\n`);
188
203
  }
189
- })();
204
+ }
205
+ if (!_inSafeMode) installMainLogTee();
190
206
 
191
207
  // ── Configuration ──────────────────────────────────────────────────────────
192
208
 
@@ -232,10 +248,11 @@ let backendStderrTail = [];
232
248
  let isIntentionalKill = false;
233
249
  let mainWindow = null;
234
250
 
235
- // True until createWindow() runs. Guards window-all-closed from firing app.quit()
236
- // while the backend-installer progress dialog is open (it's the only window during
237
- // bootstrap, so destroying it would trigger a premature quit — issue #934).
238
- let isBootstrapping = true;
251
+ // The window shows EngineInstallScreen, not the app, until the backend answers.
252
+ let showingInstallScreen = false;
253
+
254
+ /** @type {ReturnType<typeof createEngineInstall> | null} */
255
+ let engineInstall = null;
239
256
 
240
257
  /** @type {TrayManager | null} */
241
258
  let trayManager = null;
@@ -269,7 +286,7 @@ let isQuitting = false;
269
286
  * orphaned `gaia chat --ui` process. On Windows that process keeps an open
270
287
  * handle on `gaia.exe`, so the next upgrade's `uv pip install --refresh` fails
271
288
  * with `os error 32` (file in use) — issue #1388. We MUST run this BEFORE the
272
- * installer (`bootstrapBackend()`), not just before spawning the new backend,
289
+ * installer (`engineInstall.run()`), not just before spawning the new backend,
273
290
  * so the executable can be replaced.
274
291
  *
275
292
  * The PID is read from ~/.gaia/backend.pid and verified to be a GAIA backend
@@ -316,7 +333,7 @@ async function cleanupOrphanedBackend() {
316
333
  /**
317
334
  * Start the GAIA Python backend. Expects the backend installer to have
318
335
  * already ensured the venv is populated — callers should await
319
- * `bootstrapBackend()` first.
336
+ * `engineInstall.run()` first.
320
337
  *
321
338
  * Returns the ChildProcess, or null if the gaia binary cannot be found
322
339
  * (shouldn't happen post-ensureBackend, but we guard just in case).
@@ -363,7 +380,15 @@ async function startBackend() {
363
380
  stdio: ["ignore", "pipe", "pipe"],
364
381
  // GAIA_APP_PID: GAIA's daemon unloads its models once this app exits
365
382
  // and no other GAIA client is using them.
366
- env: { ...process.env, GAIA_APP_PID: String(process.pid) },
383
+ // GAIA_BUNDLED_LEMONADE_DIR: where EmbeddedLemonade.install() finds the
384
+ // archive this app ships (electron-builder.yml) instead of downloading it.
385
+ env: {
386
+ ...process.env,
387
+ GAIA_APP_PID: String(process.pid),
388
+ ...(app.isPackaged && {
389
+ GAIA_BUNDLED_LEMONADE_DIR: path.join(process.resourcesPath, "vendor", "lemonade"),
390
+ }),
391
+ },
367
392
  detached: false,
368
393
  windowsHide: true, // Prevent console window flash on Windows
369
394
  }
@@ -440,6 +465,7 @@ async function startBackend() {
440
465
  * Quit. Invoked from child.on("exit") when the kill was NOT initiated by us.
441
466
  */
442
467
  async function handleBackendCrash(code, signal) {
468
+ autoUpdater.reportStartFailure();
443
469
  try {
444
470
  const choice = await showBackendCrashDialog({
445
471
  dialog,
@@ -548,11 +574,21 @@ function titleDownloadSaveDialogs() {
548
574
  session.defaultSession.on("will-download", (_event, item) => {
549
575
  item.setSaveDialogOptions({
550
576
  title: "Save from GAIA",
551
- defaultPath: path.join(app.getPath("downloads"), item.getFilename()),
577
+ defaultPath: saveFolderFor(item.getFilename()),
552
578
  });
553
579
  });
554
580
  }
555
581
 
582
+ // Windows throws when the user's Downloads folder is missing or redirected away; the dialog still lets them pick one.
583
+ function saveFolderFor(filename) {
584
+ try {
585
+ return path.join(app.getPath("downloads"), filename);
586
+ } catch (err) {
587
+ console.warn(`[GAIA] No Downloads folder to suggest for ${filename}: ${err.message}`);
588
+ return filename;
589
+ }
590
+ }
591
+
556
592
  function createWindow() {
557
593
  mainWindow = new BrowserWindow({
558
594
  ...titleBarOptions(process.platform),
@@ -656,8 +692,8 @@ async function loadApp() {
656
692
  // renderer (apiBase.ts) validates this value against an allowlist
657
693
  // before using it — keep buildIndexQuery in sync with TRUSTED_API_RE.
658
694
  const indexPath = path.join(distPath, "index.html");
659
- const indexQuery = buildIndexQuery(backendPort);
660
- console.log("Loading app from:", indexPath, "api:", indexQuery.api);
695
+ const indexQuery = showingInstallScreen ? INSTALL_SCREEN_QUERY : buildIndexQuery(backendPort);
696
+ console.log("Loading app from:", indexPath, "query:", indexQuery);
661
697
  // Use pathToFileURL so the file:// URL always has forward slashes on
662
698
  // Windows — Chromium 130+ (Electron 40) rejects backslash file URLs
663
699
  // that Node's url.format() (used by loadFile) produces on Windows.
@@ -808,117 +844,30 @@ function setupJumpList() {
808
844
 
809
845
  // ── Backend Bootstrap (Phase A) ───────────────────────────────────────────
810
846
 
811
- /** First install found a Lemonade already running: use it, or install GAIA's own? */
812
- async function chooseLemonadeInDialog(found, parent) {
813
- // Modal to the progress window and in front: otherwise it can open behind
814
- // other windows while the install sits at 70% waiting for it.
815
- const owner = parent && !parent.isDestroyed() ? parent : null;
816
- if (owner) {
817
- if (owner.isMinimized()) owner.restore();
818
- owner.show();
819
- owner.moveTop();
820
- owner.focus();
821
- // Windows can refuse focus to a background app; the taskbar flash is the hint.
822
- if (!owner.isFocused()) owner.flashFrame(true);
847
+ /** A failed install is on screen, even for a launch that started in the tray. */
848
+ function showInstallFailure(err) {
849
+ // No frontend build to show it in: a source checkout that never ran `npm run build`.
850
+ if (!findDistPath()) {
851
+ _fatalHandler(err);
852
+ return;
823
853
  }
824
- const box = {
825
- type: "question",
826
- title: "Lemonade Server found",
827
- message: `A Lemonade Server (v${found.version}) is already running at ${found.url}. Use it instead of installing GAIA's own?`,
828
- detail: "This avoids running two servers and keeping two copies of each model.",
829
- buttons: ["Use my Lemonade", "Install GAIA's own"],
830
- defaultId: 0,
831
- cancelId: 1,
832
- noLink: true,
833
- };
834
- const { response } = owner
835
- ? await dialog.showMessageBox(owner, box)
836
- : await dialog.showMessageBox(box);
837
- if (owner && !owner.isDestroyed()) owner.flashFrame(false);
838
- return response === 0 ? "external" : "gaia";
854
+ const win = mainWindow && !mainWindow.isDestroyed() ? mainWindow : reopenWindow();
855
+ if (win.isMinimized()) win.restore();
856
+ win.show();
839
857
  }
840
858
 
841
- /**
842
- * Ensure the Python backend is installed before the main window loads.
843
- *
844
- * Shows a borderless progress window while the install runs. On failure,
845
- * surfaces a retry / manual / quit dialog. Loops until the user either
846
- * succeeds, chooses manual install, or quits.
847
- *
848
- * Returns true if the backend is ready, false if the user chose to quit.
849
- */
850
- async function bootstrapBackend() {
851
- // Fast-path: if an install is obviously not needed (binary present and
852
- // version matches), skip the progress window entirely and go straight to
853
- // ensureBackend which will confirm readiness.
854
- const existingBin = backendInstaller.findGaiaBin();
855
- if (existingBin) {
856
- const installedVersion = backendInstaller.getInstalledVersion(existingBin);
857
- let expectedVersion = null;
858
- try {
859
- const pkg = JSON.parse(
860
- fs.readFileSync(path.join(__dirname, "package.json"), "utf8")
861
- );
862
- expectedVersion = pkg.version;
863
- } catch {
864
- // ignore
865
- }
866
- if (installedVersion && installedVersion === expectedVersion) {
867
- console.log(
868
- `[main] GAIA backend already at ${installedVersion} — skipping bootstrap UI`
869
- );
870
- // Clean up any stale state file so the state machine reflects reality.
871
- backendInstaller.setState(backendInstaller.STATES.READY, {
872
- version: expectedVersion,
873
- installedVersion,
874
- });
875
- return true;
876
- }
877
- }
878
-
879
- // Slow path: need to install or upgrade. Show the progress window.
880
- let keepTrying = true;
881
- while (keepTrying) {
882
- const progress = installerProgressDialog.createProgressWindow();
883
-
884
- try {
885
- await backendInstaller.ensureBackend({
886
- onProgress: progress.onProgress,
887
- isPackaged: app.isPackaged,
888
- chooseLemonade: (found) => chooseLemonadeInDialog(found, progress.window),
889
- });
890
- progress.close();
891
- console.log("[main] Backend bootstrap complete");
892
- return true;
893
- } catch (err) {
894
- progress.close();
895
- console.error(
896
- `[main] Backend bootstrap failed: ${err && err.message ? err.message : err}`
897
- );
898
-
899
- const errorInfo = {
900
- message: (err && err.message) || "GAIA install failed.",
901
- stage: (err && err.stage) || null,
902
- suggestion: (err && err.suggestion) || null,
903
- };
904
-
905
- const choice = await installerProgressDialog.showFailureDialog(
906
- null,
907
- errorInfo
908
- );
859
+ /** Lets an update fetch its backend before the restart, so the first launch is offline. */
860
+ function updateBackendHooks() {
861
+ return {
862
+ prefetchBackend: (version) => backendInstaller.prefetchBackend(version, { isPackaged: app.isPackaged }),
863
+ isBackendPrefetched: backendInstaller.isBackendPrefetched,
864
+ };
865
+ }
909
866
 
910
- if (choice === "retry") {
911
- continue; // loop
912
- }
913
- if (choice === "manual") {
914
- // The user was directed to the docs in an external browser. Quit so
915
- // they can complete the manual install and restart.
916
- return false;
917
- }
918
- return false; // quit
919
- }
920
- }
921
- return false;
867
+ /** Setup failed after an update: download the version before it and restart into it. */
868
+ async function goBackToPreviousVersion() {
869
+ autoUpdater.init(null, { confirmRestart: async () => true, ...updateBackendHooks() });
870
+ await autoUpdater.revertUpdate();
922
871
  }
923
872
 
924
873
  // ── gaia:// deep-link install bridge (issue #1725) ──────────────────────────
@@ -1153,10 +1102,7 @@ if (process.platform === "win32") {
1153
1102
 
1154
1103
  app.on("second-instance", (_event, argv, _cwd) => {
1155
1104
  // A second launch happened while we were running. Surface our window
1156
- // (the user almost certainly wanted to see it). mainWindow may be null
1157
- // if we're still in the bootstrap phase — in that case the first
1158
- // instance is already showing the install progress dialog and there's
1159
- // nothing else to do.
1105
+ // (the user almost certainly wanted to see it).
1160
1106
  if (typeof mainWindow !== "undefined" && mainWindow && !mainWindow.isDestroyed()) {
1161
1107
  if (mainWindow.isMinimized()) mainWindow.restore();
1162
1108
  if (!mainWindow.isVisible()) mainWindow.show();
@@ -1170,6 +1116,44 @@ app.on("second-instance", (_event, argv, _cwd) => {
1170
1116
  });
1171
1117
 
1172
1118
  app.whenReady().then(async () => {
1119
+ if (_inSafeMode) {
1120
+ await runSafeModePrompt({
1121
+ count: _crashCount,
1122
+ dialogModule: dialog,
1123
+ appModule: app,
1124
+ shellModule: shell,
1125
+ });
1126
+ return;
1127
+ }
1128
+
1129
+ // The window, and the tray that keeps a closed one reachable, come before
1130
+ // anything that can take time, so an install runs in plain sight.
1131
+ titleDownloadSaveDialogs();
1132
+ createWindow();
1133
+ initializeServices();
1134
+ engineInstall = createEngineInstall({
1135
+ installer: backendInstaller,
1136
+ ipcMain,
1137
+ shell,
1138
+ getWindow: () => mainWindow,
1139
+ isPackaged: app.isPackaged,
1140
+ onFailed: showInstallFailure,
1141
+ previousVersion: () => autoUpdater.previousVersion(),
1142
+ revert: goBackToPreviousVersion,
1143
+ });
1144
+
1145
+ app.on("activate", () => {
1146
+ if (BrowserWindow.getAllWindows().length === 0) {
1147
+ reopenWindow();
1148
+ } else if (mainWindow) {
1149
+ mainWindow.show();
1150
+ }
1151
+ });
1152
+
1153
+ // Decided before the window has a page, so a current backend never sees this screen.
1154
+ showingInstallScreen = !backendInstaller.isBackendCurrent();
1155
+ if (showingInstallScreen) await loadApp();
1156
+
1173
1157
  // Phase 0: seed bundled agents BEFORE the Python backend starts, so the
1174
1158
  // agent registry sees them on its first discovery pass. Failures here are
1175
1159
  // non-fatal — the app must still launch even if seeding is blocked (e.g.
@@ -1198,13 +1182,14 @@ app.whenReady().then(async () => {
1198
1182
  // unless that process is gone first (issue #1388).
1199
1183
  await cleanupOrphanedBackend();
1200
1184
 
1201
- // Phase A: ensure the Python backend is installed BEFORE creating the
1202
- // main window. The progress dialog owns the UI during this phase.
1203
- const bootstrapOk = await bootstrapBackend();
1204
- if (!bootstrapOk) {
1205
- console.log("[main] Backend bootstrap aborted — quitting");
1206
- app.quit();
1207
- return;
1185
+ // Phase A: install or upgrade the Python backend behind EngineInstallScreen.
1186
+ if (showingInstallScreen) {
1187
+ const installed = await engineInstall.run();
1188
+ if (!installed) {
1189
+ console.log("[main] Quit while the install waited on Try again");
1190
+ return;
1191
+ }
1192
+ engineInstall.starting();
1208
1193
  }
1209
1194
 
1210
1195
  // Start the Python backend
@@ -1215,21 +1200,16 @@ app.whenReady().then(async () => {
1215
1200
  console.log("[main] Quit during startup — not opening the window");
1216
1201
  return;
1217
1202
  }
1218
-
1219
- // Create the window (hidden until ready-to-show)
1220
- titleDownloadSaveDialogs();
1221
- createWindow();
1222
- isBootstrapping = false; // progress dialog is gone; window-all-closed may now quit
1223
-
1224
- // Initialize services (tray, agent manager, notifications)
1225
- initializeServices();
1203
+ if (trayManager) trayManager.setBackendPort(backendPort);
1204
+ // macOS: the user closed the window during a long install.
1205
+ if (!mainWindow) reopenWindow();
1226
1206
 
1227
1207
  // Phase F: start the auto-updater (non-blocking). First check runs on
1228
1208
  // a 10s delay inside the module so it never competes with app launch.
1229
1209
  // Any failure here is logged and swallowed — the app continues to run
1230
1210
  // even if auto-update is unavailable.
1231
1211
  try {
1232
- autoUpdater.init(mainWindow, { confirmRestart: confirmUpdateRestart });
1212
+ autoUpdater.init(mainWindow, { confirmRestart: confirmUpdateRestart, ...updateBackendHooks() });
1233
1213
  } catch (err) {
1234
1214
  console.error(
1235
1215
  "[main] Failed to init auto-updater:",
@@ -1240,8 +1220,8 @@ app.whenReady().then(async () => {
1240
1220
  // Setup Windows Jump List (T11)
1241
1221
  setupJumpList();
1242
1222
 
1243
- // Show loading state
1244
- await loadApp();
1223
+ // The install screen stays until the backend answers, then gives way to the app.
1224
+ if (!showingInstallScreen) await loadApp();
1245
1225
 
1246
1226
  // Wait for backend API to be reachable. The bundled frontend
1247
1227
  // (loaded from dist/index.html in the asar) auto-detects when the
@@ -1258,6 +1238,19 @@ app.whenReady().then(async () => {
1258
1238
  console.warn("Backend did not respond within timeout.");
1259
1239
  }
1260
1240
  }
1241
+ if (!backendReady) {
1242
+ autoUpdater.reportStartFailure();
1243
+ if (backendProcess) {
1244
+ waitForBackend(STARTUP_TIMEOUT * 10).then((ready) => {
1245
+ if (ready) autoUpdater.clearStartFailure();
1246
+ });
1247
+ }
1248
+ }
1249
+
1250
+ if (showingInstallScreen) {
1251
+ showingInstallScreen = false;
1252
+ if (mainWindow) await loadApp();
1253
+ }
1261
1254
 
1262
1255
  // Act on any gaia:// deep link that arrived during bootstrap or via the
1263
1256
  // cold-start command line (issue #1725) — after the backend is listening,
@@ -1275,14 +1268,6 @@ app.whenReady().then(async () => {
1275
1268
 
1276
1269
  // macOS and Linux: the shutdown is the OS's, so don't ask.
1277
1270
  powerMonitor.on("shutdown", () => quitGuard.allow());
1278
-
1279
- app.on("activate", () => {
1280
- if (BrowserWindow.getAllWindows().length === 0) {
1281
- reopenWindow();
1282
- } else if (mainWindow) {
1283
- mainWindow.show();
1284
- }
1285
- });
1286
1271
  }).catch((err) => {
1287
1272
  // Route explicit rejection through the safety-net so the user gets a
1288
1273
  // GAIA-branded dialog and a stack trace in the log (issue #934).
@@ -1292,12 +1277,6 @@ app.whenReady().then(async () => {
1292
1277
  // ── Window-all-closed (C4 fix) ────────────────────────────────────────────
1293
1278
  // Don't quit when window is hidden — tray keeps app alive
1294
1279
  app.on("window-all-closed", () => {
1295
- // During bootstrap the progress dialog is the only open window. Destroying
1296
- // it (progress.close()) fires this event before the main window exists, which
1297
- // would trigger a premature app.quit() that races with the startup sequence
1298
- // and causes loadURL() to fail with ERR_FAILED (-2) — issue #934.
1299
- if (isBootstrapping) return;
1300
-
1301
1280
  // If minimize-to-tray is active, the window is just hidden, not closed.
1302
1281
  // Only quit on macOS if the user explicitly quit (Cmd+Q).
1303
1282
  const trayActive = trayManager && trayManager.minimizeToTray;
@@ -1331,7 +1310,7 @@ function askAboutRunningWork(action) {
1331
1310
  });
1332
1311
  }
1333
1312
 
1334
- /** The update dialog's "Restart now": the installer starts at once, so ask first. */
1313
+ /** Restarting into an update: the installer starts at once, so ask first. */
1335
1314
  function confirmUpdateRestart() {
1336
1315
  return workCanBeLost() ? askAboutRunningWork(RESTART_FOR_UPDATE) : Promise.resolve(true);
1337
1316
  }
@@ -1363,6 +1342,7 @@ installQuitCleanup({ app, cleanup });
1363
1342
 
1364
1343
  async function cleanup() {
1365
1344
  shutdownStarted = true;
1345
+ if (engineInstall) engineInstall.stop();
1366
1346
 
1367
1347
  // Phase F: tear down auto-updater timers and IPC handlers.
1368
1348
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amd-gaia/agent-ui",
3
- "version": "0.25.4-rc.2",
3
+ "version": "0.25.4-rc.4",
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",
@@ -61,6 +61,7 @@
61
61
  "test:watch": "vitest",
62
62
  "test:visual": "vite build && playwright test -c visual/playwright.config.ts",
63
63
  "test:rtl": "vite build && playwright test -c visual/playwright.config.ts rtl --project=light",
64
+ "test:scroll": "vite build && playwright test -c visual/playwright.config.ts autoscroll --project=light",
64
65
  "gen:api-types": "node scripts/gen-api-types.mjs"
65
66
  },
66
67
  "dependencies": {
package/preload.cjs CHANGED
@@ -11,7 +11,7 @@
11
11
  * agent:* — Agent process management (T2)
12
12
  * tray:* — Tray icon/config (T1)
13
13
  * notification:* — Desktop notifications & permission prompts (T5)
14
- * install:* — First-run backend install progress (Phase A)
14
+ * install:* — Backend install status, retry and log (Phase A)
15
15
  * gaia:update:* — Auto-update status & manual check (Phase F)
16
16
  * system:* — System metrics for the observability dashboard (#2007)
17
17
  * power:* — Keep the PC awake while a chat turn runs (#5226)
@@ -105,19 +105,23 @@ contextBridge.exposeInMainWorld("gaiaWindow", {
105
105
  setColors: (colors) => ipcRenderer.invoke("window:set-titlebar-colors", colors),
106
106
  });
107
107
 
108
- // ── Install progress (Phase A) ──────────────────────────────────────────
109
- // Exposed as a separate global so the progress window can use it without
110
- // pulling in the full gaiaAPI surface (and so it keeps working if an
111
- // install dialog runs before the main window is ready).
108
+ // ── Backend install (Phase A) ───────────────────────────────────────────
109
+ // EngineInstallScreen runs with no backend, so this stands apart from gaiaAPI.
112
110
  contextBridge.exposeInMainWorld("gaiaInstall", {
113
- // Subscribe to progress updates. Returns an unsubscribe function.
111
+ // Subscribe to status updates. Returns an unsubscribe function.
114
112
  onProgress: (cb) => onEvent("install:progress", cb),
115
113
 
116
- // Query the current install state (state machine + log/state paths).
114
+ // The current status: { phase, stage, percent, message, error, logPath }.
117
115
  status: () => ipcRenderer.invoke("install:status"),
118
116
 
119
- // Copy the log file path to the clipboard.
120
- copyLogPath: () => ipcRenderer.invoke("install:copy-log-path"),
117
+ // After a failure, install again. Resolves false when none is waiting.
118
+ retry: () => ipcRenderer.invoke("install:retry"),
119
+
120
+ // Goes on past a failed agent update; the next launch tries it again.
121
+ continueWithoutAgentUpdate: () => ipcRenderer.invoke("install:continue"),
122
+
123
+ // After a failed update, go back to the version before it; the app restarts into it.
124
+ revert: () => ipcRenderer.invoke("install:revert"),
121
125
 
122
126
  // Open the log file in the OS's default viewer.
123
127
  openLogFile: () => ipcRenderer.invoke("install:open-log-file"),
@@ -159,6 +163,9 @@ contextBridge.exposeInMainWorld("gaiaUpdater", {
159
163
  */
160
164
  installVersion: (tag) => ipcRenderer.invoke("gaia:update:install-version", tag),
161
165
 
166
+ /** Go back to the version before the last update, and pin it. */
167
+ revertUpdate: () => ipcRenderer.invoke("gaia:update:revert"),
168
+
162
169
  /**
163
170
  * Resume normal auto-updates — clears the version pin set by installVersion.
164
171
  * Resolves with the updated state.