@amd-gaia/agent-ui 0.25.3 → 0.25.4-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.
Files changed (57) hide show
  1. package/bin/gaia-ui.cjs +2 -23
  2. package/dist/assets/AdvancedSettings-DXf2pogI.js +1 -0
  3. package/dist/assets/ConnectorsPane-CzLYXGMl.css +1 -0
  4. package/dist/assets/ConnectorsPane-R1yvKMNT.js +1 -0
  5. package/dist/assets/DocumentLibrary-CM6o8j2T.css +1 -0
  6. package/dist/assets/DocumentLibrary-DSOiP7Ft.js +4 -0
  7. package/dist/assets/FileBrowser-BlVSPjD2.js +4 -0
  8. package/dist/assets/FileBrowser-Cna3BsT7.css +1 -0
  9. package/dist/assets/MemoryDashboard-D_ZVZbS_.js +1 -0
  10. package/dist/assets/MemoryDashboard-DwrWAXi6.css +1 -0
  11. package/dist/assets/MobileAccessModal-BkVDm3YG.css +1 -0
  12. package/dist/assets/MobileAccessModal-CPjDeIIk.js +2 -0
  13. package/dist/assets/ScheduleManager-CjHJ4NbD.css +1 -0
  14. package/dist/assets/ScheduleManager-D1DOYVQo.js +1 -0
  15. package/dist/assets/api-Co-B9E0t.js +4 -0
  16. package/dist/assets/index-CMVmIBZ8.js +78 -0
  17. package/dist/assets/index-Y93uvqe7.css +1 -0
  18. package/dist/assets/useT-tWXlG-cT.js +3 -0
  19. package/dist/assets/zap-D-WCZ6dX.js +1 -0
  20. package/dist/icons/apple-touch-icon.png +0 -0
  21. package/dist/icons/icon-192.png +0 -0
  22. package/dist/icons/icon-512.png +0 -0
  23. package/dist/icons/icon-maskable-192.png +0 -0
  24. package/dist/icons/icon-maskable-512.png +0 -0
  25. package/dist/index.html +8 -3
  26. package/dist/manifest.json +16 -0
  27. package/dist/sw.js +49 -0
  28. package/main-safety-net.cjs +106 -8
  29. package/main.cjs +145 -162
  30. package/package.json +5 -3
  31. package/preload.cjs +19 -9
  32. package/services/agent-seeder.cjs +22 -13
  33. package/services/auto-updater.cjs +780 -216
  34. package/services/backend-installer.cjs +523 -365
  35. package/services/engine-install.cjs +233 -0
  36. package/services/index-query.cjs +7 -1
  37. package/services/lock-holders.cjs +200 -0
  38. package/services/quit-guard.cjs +36 -3
  39. package/services/tray-manager.cjs +12 -7
  40. package/dist/assets/AdvancedSettings-DERzP9Jc.js +0 -1
  41. package/dist/assets/ConnectorsPane-Dzu2DRTc.css +0 -1
  42. package/dist/assets/ConnectorsPane-s0fFxmus.js +0 -1
  43. package/dist/assets/DocumentLibrary-CIKp6qld.css +0 -1
  44. package/dist/assets/DocumentLibrary-DqapN_Tr.js +0 -4
  45. package/dist/assets/FileBrowser-Dil1lZJ4.css +0 -1
  46. package/dist/assets/FileBrowser-vAJV41RT.js +0 -4
  47. package/dist/assets/MemoryDashboard-CP5wVWkY.js +0 -1
  48. package/dist/assets/MemoryDashboard-Mo721-BI.css +0 -1
  49. package/dist/assets/MobileAccessModal-B31mPs50.js +0 -2
  50. package/dist/assets/MobileAccessModal-DuVy9F1u.css +0 -1
  51. package/dist/assets/ScheduleManager-D5c4gVIt.css +0 -1
  52. package/dist/assets/ScheduleManager-b0gTAf2R.js +0 -2
  53. package/dist/assets/apiBase-CS6zLsJY.js +0 -1
  54. package/dist/assets/index-C-EM_5yl.css +0 -1
  55. package/dist/assets/index-Cae5Fje-.js +0 -78
  56. package/dist/assets/zap-BogJn9oN.js +0 -1
  57. 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");
@@ -77,6 +92,7 @@ const {
77
92
  fetchWorkInProgress,
78
93
  installQuitGuard,
79
94
  QUIT,
95
+ quitOnTermSignal,
80
96
  RESTART_FOR_UPDATE,
81
97
  } = require("./services/quit-guard.cjs");
82
98
  const AgentProcessManager = require("./services/agent-process-manager.cjs");
@@ -85,9 +101,9 @@ const KeepAwake = require("./services/keep-awake.cjs");
85
101
  const PortManager = require("./services/port-manager.cjs");
86
102
  const { isGaiaBackendProcess } = require("./services/backend-orphan.cjs");
87
103
  const { showBackendCrashDialog } = require("./services/backend-crash-dialog.cjs");
88
- const { buildIndexQuery } = require("./services/index-query.cjs");
104
+ const { buildIndexQuery, INSTALL_SCREEN_QUERY } = require("./services/index-query.cjs");
89
105
  const backendInstaller = require("./services/backend-installer.cjs");
90
- const installerProgressDialog = require("./services/backend-installer-progress-dialog.cjs");
106
+ const { createEngineInstall } = require("./services/engine-install.cjs");
91
107
  const autoUpdater = require("./services/auto-updater.cjs");
92
108
  const agentSeeder = require("./services/agent-seeder.cjs");
93
109
  const systemMetrics = require("./services/system-metrics.cjs");
@@ -119,7 +135,7 @@ for (const [name, value] of chromiumSwitches({ platform: process.platform, env:
119
135
  // Users often launch AppImages by double-click, not from a terminal, so
120
136
  // console output vanishes. Mirror console.log/error to a file so the
121
137
  // diagnostics bundler has something to attach.
122
- (function installMainLogTee() {
138
+ function installMainLogTee() {
123
139
  try {
124
140
  try { fs.mkdirSync(_GAIA_DIR, { recursive: true }); } catch { /* ignore */ }
125
141
  const logPath = _MAIN_LOG_PATH;
@@ -186,7 +202,8 @@ for (const [name, value] of chromiumSwitches({ platform: process.platform, env:
186
202
  // not refuse to launch over a log-tee failure.
187
203
  process.stderr.write(`[main] log tee failed: ${err.message}\n`);
188
204
  }
189
- })();
205
+ }
206
+ if (!_inSafeMode) installMainLogTee();
190
207
 
191
208
  // ── Configuration ──────────────────────────────────────────────────────────
192
209
 
@@ -232,10 +249,11 @@ let backendStderrTail = [];
232
249
  let isIntentionalKill = false;
233
250
  let mainWindow = null;
234
251
 
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;
252
+ // The window shows EngineInstallScreen, not the app, until the backend answers.
253
+ let showingInstallScreen = false;
254
+
255
+ /** @type {ReturnType<typeof createEngineInstall> | null} */
256
+ let engineInstall = null;
239
257
 
240
258
  /** @type {TrayManager | null} */
241
259
  let trayManager = null;
@@ -269,7 +287,7 @@ let isQuitting = false;
269
287
  * orphaned `gaia chat --ui` process. On Windows that process keeps an open
270
288
  * handle on `gaia.exe`, so the next upgrade's `uv pip install --refresh` fails
271
289
  * 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,
290
+ * installer (`engineInstall.run()`), not just before spawning the new backend,
273
291
  * so the executable can be replaced.
274
292
  *
275
293
  * The PID is read from ~/.gaia/backend.pid and verified to be a GAIA backend
@@ -316,7 +334,7 @@ async function cleanupOrphanedBackend() {
316
334
  /**
317
335
  * Start the GAIA Python backend. Expects the backend installer to have
318
336
  * already ensured the venv is populated — callers should await
319
- * `bootstrapBackend()` first.
337
+ * `engineInstall.run()` first.
320
338
  *
321
339
  * Returns the ChildProcess, or null if the gaia binary cannot be found
322
340
  * (shouldn't happen post-ensureBackend, but we guard just in case).
@@ -363,7 +381,15 @@ async function startBackend() {
363
381
  stdio: ["ignore", "pipe", "pipe"],
364
382
  // GAIA_APP_PID: GAIA's daemon unloads its models once this app exits
365
383
  // and no other GAIA client is using them.
366
- env: { ...process.env, GAIA_APP_PID: String(process.pid) },
384
+ // GAIA_BUNDLED_LEMONADE_DIR: where EmbeddedLemonade.install() finds the
385
+ // archive this app ships (electron-builder.yml) instead of downloading it.
386
+ env: {
387
+ ...process.env,
388
+ GAIA_APP_PID: String(process.pid),
389
+ ...(app.isPackaged && {
390
+ GAIA_BUNDLED_LEMONADE_DIR: path.join(process.resourcesPath, "vendor", "lemonade"),
391
+ }),
392
+ },
367
393
  detached: false,
368
394
  windowsHide: true, // Prevent console window flash on Windows
369
395
  }
@@ -440,6 +466,7 @@ async function startBackend() {
440
466
  * Quit. Invoked from child.on("exit") when the kill was NOT initiated by us.
441
467
  */
442
468
  async function handleBackendCrash(code, signal) {
469
+ autoUpdater.reportStartFailure();
443
470
  try {
444
471
  const choice = await showBackendCrashDialog({
445
472
  dialog,
@@ -548,11 +575,21 @@ function titleDownloadSaveDialogs() {
548
575
  session.defaultSession.on("will-download", (_event, item) => {
549
576
  item.setSaveDialogOptions({
550
577
  title: "Save from GAIA",
551
- defaultPath: path.join(app.getPath("downloads"), item.getFilename()),
578
+ defaultPath: saveFolderFor(item.getFilename()),
552
579
  });
553
580
  });
554
581
  }
555
582
 
583
+ // Windows throws when the user's Downloads folder is missing or redirected away; the dialog still lets them pick one.
584
+ function saveFolderFor(filename) {
585
+ try {
586
+ return path.join(app.getPath("downloads"), filename);
587
+ } catch (err) {
588
+ console.warn(`[GAIA] No Downloads folder to suggest for ${filename}: ${err.message}`);
589
+ return filename;
590
+ }
591
+ }
592
+
556
593
  function createWindow() {
557
594
  mainWindow = new BrowserWindow({
558
595
  ...titleBarOptions(process.platform),
@@ -656,8 +693,8 @@ async function loadApp() {
656
693
  // renderer (apiBase.ts) validates this value against an allowlist
657
694
  // before using it — keep buildIndexQuery in sync with TRUSTED_API_RE.
658
695
  const indexPath = path.join(distPath, "index.html");
659
- const indexQuery = buildIndexQuery(backendPort);
660
- console.log("Loading app from:", indexPath, "api:", indexQuery.api);
696
+ const indexQuery = showingInstallScreen ? INSTALL_SCREEN_QUERY : buildIndexQuery(backendPort);
697
+ console.log("Loading app from:", indexPath, "query:", indexQuery);
661
698
  // Use pathToFileURL so the file:// URL always has forward slashes on
662
699
  // Windows — Chromium 130+ (Electron 40) rejects backslash file URLs
663
700
  // that Node's url.format() (used by loadFile) produces on Windows.
@@ -808,117 +845,30 @@ function setupJumpList() {
808
845
 
809
846
  // ── Backend Bootstrap (Phase A) ───────────────────────────────────────────
810
847
 
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);
848
+ /** A failed install is on screen, even for a launch that started in the tray. */
849
+ function showInstallFailure(err) {
850
+ // No frontend build to show it in: a source checkout that never ran `npm run build`.
851
+ if (!findDistPath()) {
852
+ _fatalHandler(err);
853
+ return;
823
854
  }
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";
855
+ const win = mainWindow && !mainWindow.isDestroyed() ? mainWindow : reopenWindow();
856
+ if (win.isMinimized()) win.restore();
857
+ win.show();
839
858
  }
840
859
 
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
- );
860
+ /** Lets an update fetch its backend before the restart, so the first launch is offline. */
861
+ function updateBackendHooks() {
862
+ return {
863
+ prefetchBackend: (version) => backendInstaller.prefetchBackend(version, { isPackaged: app.isPackaged }),
864
+ isBackendPrefetched: backendInstaller.isBackendPrefetched,
865
+ };
866
+ }
909
867
 
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;
868
+ /** Setup failed after an update: download the version before it and restart into it. */
869
+ async function goBackToPreviousVersion() {
870
+ autoUpdater.init(null, { confirmRestart: async () => true, ...updateBackendHooks() });
871
+ await autoUpdater.revertUpdate();
922
872
  }
923
873
 
924
874
  // ── gaia:// deep-link install bridge (issue #1725) ──────────────────────────
@@ -1153,10 +1103,7 @@ if (process.platform === "win32") {
1153
1103
 
1154
1104
  app.on("second-instance", (_event, argv, _cwd) => {
1155
1105
  // 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.
1106
+ // (the user almost certainly wanted to see it).
1160
1107
  if (typeof mainWindow !== "undefined" && mainWindow && !mainWindow.isDestroyed()) {
1161
1108
  if (mainWindow.isMinimized()) mainWindow.restore();
1162
1109
  if (!mainWindow.isVisible()) mainWindow.show();
@@ -1170,6 +1117,44 @@ app.on("second-instance", (_event, argv, _cwd) => {
1170
1117
  });
1171
1118
 
1172
1119
  app.whenReady().then(async () => {
1120
+ if (_inSafeMode) {
1121
+ await runSafeModePrompt({
1122
+ count: _crashCount,
1123
+ dialogModule: dialog,
1124
+ appModule: app,
1125
+ shellModule: shell,
1126
+ });
1127
+ return;
1128
+ }
1129
+
1130
+ // The window, and the tray that keeps a closed one reachable, come before
1131
+ // anything that can take time, so an install runs in plain sight.
1132
+ titleDownloadSaveDialogs();
1133
+ createWindow();
1134
+ initializeServices();
1135
+ engineInstall = createEngineInstall({
1136
+ installer: backendInstaller,
1137
+ ipcMain,
1138
+ shell,
1139
+ getWindow: () => mainWindow,
1140
+ isPackaged: app.isPackaged,
1141
+ onFailed: showInstallFailure,
1142
+ previousVersion: () => autoUpdater.previousVersion(),
1143
+ revert: goBackToPreviousVersion,
1144
+ });
1145
+
1146
+ app.on("activate", () => {
1147
+ if (BrowserWindow.getAllWindows().length === 0) {
1148
+ reopenWindow();
1149
+ } else if (mainWindow) {
1150
+ mainWindow.show();
1151
+ }
1152
+ });
1153
+
1154
+ // Decided before the window has a page, so a current backend never sees this screen.
1155
+ showingInstallScreen = !backendInstaller.isBackendCurrent();
1156
+ if (showingInstallScreen) await loadApp();
1157
+
1173
1158
  // Phase 0: seed bundled agents BEFORE the Python backend starts, so the
1174
1159
  // agent registry sees them on its first discovery pass. Failures here are
1175
1160
  // non-fatal — the app must still launch even if seeding is blocked (e.g.
@@ -1198,13 +1183,14 @@ app.whenReady().then(async () => {
1198
1183
  // unless that process is gone first (issue #1388).
1199
1184
  await cleanupOrphanedBackend();
1200
1185
 
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;
1186
+ // Phase A: install or upgrade the Python backend behind EngineInstallScreen.
1187
+ if (showingInstallScreen) {
1188
+ const installed = await engineInstall.run();
1189
+ if (!installed) {
1190
+ console.log("[main] Quit during the backend install");
1191
+ return;
1192
+ }
1193
+ engineInstall.starting();
1208
1194
  }
1209
1195
 
1210
1196
  // Start the Python backend
@@ -1215,21 +1201,16 @@ app.whenReady().then(async () => {
1215
1201
  console.log("[main] Quit during startup — not opening the window");
1216
1202
  return;
1217
1203
  }
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();
1204
+ if (trayManager) trayManager.setBackendPort(backendPort);
1205
+ // macOS: the user closed the window during a long install.
1206
+ if (!mainWindow) reopenWindow();
1226
1207
 
1227
1208
  // Phase F: start the auto-updater (non-blocking). First check runs on
1228
1209
  // a 10s delay inside the module so it never competes with app launch.
1229
1210
  // Any failure here is logged and swallowed — the app continues to run
1230
1211
  // even if auto-update is unavailable.
1231
1212
  try {
1232
- autoUpdater.init(mainWindow, { confirmRestart: confirmUpdateRestart });
1213
+ autoUpdater.init(mainWindow, { confirmRestart: confirmUpdateRestart, ...updateBackendHooks() });
1233
1214
  } catch (err) {
1234
1215
  console.error(
1235
1216
  "[main] Failed to init auto-updater:",
@@ -1240,8 +1221,8 @@ app.whenReady().then(async () => {
1240
1221
  // Setup Windows Jump List (T11)
1241
1222
  setupJumpList();
1242
1223
 
1243
- // Show loading state
1244
- await loadApp();
1224
+ // The install screen stays until the backend answers, then gives way to the app.
1225
+ if (!showingInstallScreen) await loadApp();
1245
1226
 
1246
1227
  // Wait for backend API to be reachable. The bundled frontend
1247
1228
  // (loaded from dist/index.html in the asar) auto-detects when the
@@ -1258,6 +1239,19 @@ app.whenReady().then(async () => {
1258
1239
  console.warn("Backend did not respond within timeout.");
1259
1240
  }
1260
1241
  }
1242
+ if (!backendReady) {
1243
+ autoUpdater.reportStartFailure();
1244
+ if (backendProcess) {
1245
+ waitForBackend(STARTUP_TIMEOUT * 10).then((ready) => {
1246
+ if (ready) autoUpdater.clearStartFailure();
1247
+ });
1248
+ }
1249
+ }
1250
+
1251
+ if (showingInstallScreen) {
1252
+ showingInstallScreen = false;
1253
+ if (mainWindow) await loadApp();
1254
+ }
1261
1255
 
1262
1256
  // Act on any gaia:// deep link that arrived during bootstrap or via the
1263
1257
  // cold-start command line (issue #1725) — after the backend is listening,
@@ -1275,14 +1269,6 @@ app.whenReady().then(async () => {
1275
1269
 
1276
1270
  // macOS and Linux: the shutdown is the OS's, so don't ask.
1277
1271
  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
1272
  }).catch((err) => {
1287
1273
  // Route explicit rejection through the safety-net so the user gets a
1288
1274
  // GAIA-branded dialog and a stack trace in the log (issue #934).
@@ -1292,12 +1278,6 @@ app.whenReady().then(async () => {
1292
1278
  // ── Window-all-closed (C4 fix) ────────────────────────────────────────────
1293
1279
  // Don't quit when window is hidden — tray keeps app alive
1294
1280
  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
1281
  // If minimize-to-tray is active, the window is just hidden, not closed.
1302
1282
  // Only quit on macOS if the user explicitly quit (Cmd+Q).
1303
1283
  const trayActive = trayManager && trayManager.minimizeToTray;
@@ -1331,7 +1311,7 @@ function askAboutRunningWork(action) {
1331
1311
  });
1332
1312
  }
1333
1313
 
1334
- /** The update dialog's "Restart now": the installer starts at once, so ask first. */
1314
+ /** Restarting into an update: the installer starts at once, so ask first. */
1335
1315
  function confirmUpdateRestart() {
1336
1316
  return workCanBeLost() ? askAboutRunningWork(RESTART_FOR_UPDATE) : Promise.resolve(true);
1337
1317
  }
@@ -1359,10 +1339,13 @@ const quitGuard = installQuitGuard({
1359
1339
  },
1360
1340
  });
1361
1341
 
1342
+ quitOnTermSignal({ proc: process, app, guard: quitGuard });
1343
+
1362
1344
  installQuitCleanup({ app, cleanup });
1363
1345
 
1364
1346
  async function cleanup() {
1365
1347
  shutdownStarted = true;
1348
+ const installStopped = engineInstall ? engineInstall.stop() : null;
1366
1349
 
1367
1350
  // Phase F: tear down auto-updater timers and IPC handlers.
1368
1351
  try {
@@ -1385,9 +1368,9 @@ async function cleanup() {
1385
1368
  keepAwake = null;
1386
1369
  }
1387
1370
 
1388
- // The agents and the backend stop side by side: each waits out its own
1389
- // grace period, and in series they could overrun the quit's time limit.
1390
- await Promise.all([stopAgentsForQuit(), stopBackendForQuit()]);
1371
+ // The install, the agents and the backend stop side by side: each waits out
1372
+ // its own grace period, and in series they could overrun the quit's time limit.
1373
+ await Promise.all([installStopped, stopAgentsForQuit(), stopBackendForQuit()]);
1391
1374
 
1392
1375
  // Destroy tray icon
1393
1376
  if (trayManager) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amd-gaia/agent-ui",
3
- "version": "0.25.3",
3
+ "version": "0.25.4-rc.10",
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",
@@ -51,15 +51,17 @@
51
51
  "build": "tsc && vite build",
52
52
  "preview": "vite preview",
53
53
  "start": "electron .",
54
- "package": "npm run build && electron-builder --config electron-builder.yml",
54
+ "package": "npm run build && node scripts/strip-appimage-copyleft.cjs && electron-builder --config electron-builder.yml",
55
55
  "make": "npm run package",
56
56
  "package:win": "npm run build && electron-builder --win --config electron-builder.yml",
57
57
  "package:mac": "npm run build && electron-builder --mac --config electron-builder.yml",
58
- "package:linux": "npm run build && electron-builder --linux --config electron-builder.yml",
58
+ "package:linux": "npm run build && node scripts/strip-appimage-copyleft.cjs && electron-builder --linux --config electron-builder.yml",
59
59
  "prepublishOnly": "npm run build",
60
60
  "test": "vitest run",
61
61
  "test:watch": "vitest",
62
62
  "test:visual": "vite build && playwright test -c visual/playwright.config.ts",
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",
63
65
  "gen:api-types": "node scripts/gen-api-types.mjs"
64
66
  },
65
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,26 @@ 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"),
125
+
126
+ // After a 'locked' failure held only by GAIA processes: ends those and installs again.
127
+ closeLockingPrograms: () => ipcRenderer.invoke("install:close-lockers"),
121
128
 
122
129
  // Open the log file in the OS's default viewer.
123
130
  openLogFile: () => ipcRenderer.invoke("install:open-log-file"),
@@ -159,6 +166,9 @@ contextBridge.exposeInMainWorld("gaiaUpdater", {
159
166
  */
160
167
  installVersion: (tag) => ipcRenderer.invoke("gaia:update:install-version", tag),
161
168
 
169
+ /** Go back to the version before the last update, and pin it. */
170
+ revertUpdate: () => ipcRenderer.invoke("gaia:update:revert"),
171
+
162
172
  /**
163
173
  * Resume normal auto-updates — clears the version pin set by installVersion.
164
174
  * Resolves with the updated state.
@@ -41,10 +41,11 @@
41
41
  * Recovery: to get a deleted bundled agent back, remove
42
42
  * `~/.gaia/seeder/<id>.seeded` and relaunch.
43
43
  *
44
- * Legacy cleanup: the retired `zoo-agent` demo is removed from
45
- * ~/.gaia/agents/ when it is provably an unmodified seeded copy — sentinel
46
- * present, no file newer than its seededAt (+5s slack), never through a
47
- * symlink. Removed ids are reported via the additive `cleaned` array.
44
+ * Legacy cleanup: the retired `zoo-agent` and `example-agent` demos are
45
+ * removed from ~/.gaia/agents/ when provably an unmodified seeded copy —
46
+ * sentinel present, no file newer than its seededAt (+5s slack), never
47
+ * through a symlink, and never while this build still bundles that id.
48
+ * Removed ids are reported via the additive `cleaned` array.
48
49
  *
49
50
  * Behaviour:
50
51
  * - `process.resourcesPath` unset (dev / Jest) → empty result, no error,
@@ -310,7 +311,7 @@ function seedOneAgent(sourceDir, targetRoot, id) {
310
311
  // ── Legacy cleanup ───────────────────────────────────────────────────────
311
312
 
312
313
  // Retired bundled ids removed from user machines when provably unmodified.
313
- const LEGACY_CLEANUP_IDS = ["zoo-agent"];
314
+ const LEGACY_CLEANUP_IDS = ["zoo-agent", "example-agent"];
314
315
 
315
316
  // Filesystem mtime granularity + copy latency headroom for the
316
317
  // modified-since-seed check.
@@ -334,20 +335,28 @@ function listFilesRecursive(dir) {
334
335
  }
335
336
 
336
337
  /**
337
- * Remove retired seeded demo agents (currently `zoo-agent`) from
338
+ * Remove retired seeded demo agents (LEGACY_CLEANUP_IDS) from
338
339
  * ~/.gaia/agents/. Deletes ONLY when the dir is provably an unmodified
339
- * seeded copy; anything ambiguous is left in place. Failures are logged
340
+ * seeded copy; anything ambiguous is left in place. An id the current
341
+ * installer still bundles is skipped: deleting it would leave a marker
342
+ * that blocks the re-seed, so the bundled agent would vanish for good. Failures are logged
340
343
  * and retried on the next launch (the sentinel keys the retry — no marker
341
344
  * is involved, since a retired id can never be re-seeded anyway).
342
345
  *
346
+ * @param {string} sourceDir `<resourcesPath>/agents` of this build.
343
347
  * @returns {string[]} ids actually removed (verified gone from disk).
344
348
  */
345
- function cleanupLegacyAgents() {
349
+ function cleanupLegacyAgents(sourceDir) {
346
350
  const cleaned = [];
347
351
 
348
352
  for (const id of LEGACY_CLEANUP_IDS) {
349
353
  const target = path.join(agentsTargetRoot(), id);
350
354
 
355
+ if (sourceDir && isDirectory(path.join(sourceDir, id))) {
356
+ log("INFO", `Legacy "${id}" is still bundled by this build — not removing it`);
357
+ continue;
358
+ }
359
+
351
360
  let st;
352
361
  try {
353
362
  st = fs.lstatSync(target);
@@ -467,13 +476,13 @@ async function seedBundledAgents() {
467
476
  return result;
468
477
  }
469
478
 
470
- // Legacy cleanup runs before the missing-source guard: an upgraded
471
- // install must shed the retired zoo-agent even if this build ships no
472
- // bundled agents.
473
- result.cleaned = cleanupLegacyAgents();
474
-
475
479
  const sourceDir = path.join(process.resourcesPath, "agents");
476
480
 
481
+ // Legacy cleanup runs before the missing-source guard: an upgraded
482
+ // install must shed retired demos even if this build ships no bundled
483
+ // agents.
484
+ result.cleaned = cleanupLegacyAgents(sourceDir);
485
+
477
486
  if (!fs.existsSync(sourceDir) || !isDirectory(sourceDir)) {
478
487
  // Not an error — a build might simply ship without bundled agents.
479
488
  // In a packaged Electron app the directory is expected to exist, so raise