@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.
- package/bin/gaia-ui.cjs +2 -23
- package/dist/assets/AdvancedSettings-DoIDd09m.js +1 -0
- package/dist/assets/ConnectorsPane-CK3h7C7E.js +1 -0
- package/dist/assets/ConnectorsPane-CzLYXGMl.css +1 -0
- package/dist/assets/DocumentLibrary-BWvkvxTu.js +4 -0
- package/dist/assets/FileBrowser-DhqWGirJ.js +4 -0
- package/dist/assets/MemoryDashboard-BF-o8284.js +1 -0
- package/dist/assets/MobileAccessModal-51t2kQaJ.js +2 -0
- package/dist/assets/MobileAccessModal-BkVDm3YG.css +1 -0
- package/dist/assets/ScheduleManager-2xjZsnLK.js +2 -0
- package/dist/assets/api-BzlFpP4T.js +4 -0
- package/dist/assets/apiBase-BMkGbdXt.js +1 -0
- package/dist/assets/index-C_Cx5IlD.css +1 -0
- package/dist/assets/index-hAcJijAU.js +78 -0
- package/dist/assets/{zap-BdhDcjhB.js → zap-D7aLu_f0.js} +1 -1
- package/dist/icons/apple-touch-icon.png +0 -0
- package/dist/icons/icon-192.png +0 -0
- package/dist/icons/icon-512.png +0 -0
- package/dist/icons/icon-maskable-192.png +0 -0
- package/dist/icons/icon-maskable-512.png +0 -0
- package/dist/index.html +8 -3
- package/dist/manifest.json +16 -0
- package/dist/sw.js +49 -0
- package/main-safety-net.cjs +106 -8
- package/main.cjs +139 -159
- package/package.json +2 -1
- package/preload.cjs +16 -9
- package/services/auto-updater.cjs +780 -216
- package/services/backend-installer.cjs +233 -344
- package/services/engine-install.cjs +142 -0
- package/services/index-query.cjs +7 -1
- package/services/quit-guard.cjs +10 -3
- package/services/tray-manager.cjs +12 -7
- package/dist/assets/AdvancedSettings-B4MY5XPH.js +0 -1
- package/dist/assets/ConnectorsPane-B71QL-Kl.js +0 -1
- package/dist/assets/ConnectorsPane-Dzu2DRTc.css +0 -1
- package/dist/assets/DocumentLibrary-c78Y6arz.js +0 -4
- package/dist/assets/FileBrowser-DwEPODjV.js +0 -4
- package/dist/assets/MemoryDashboard-BQhg3t2B.js +0 -1
- package/dist/assets/MobileAccessModal-DuVy9F1u.css +0 -1
- package/dist/assets/MobileAccessModal-RI4HtcYe.js +0 -2
- package/dist/assets/ScheduleManager-tcyjbGY9.js +0 -2
- package/dist/assets/apiBase-DRtaTd8u.js +0 -1
- package/dist/assets/index-C9zLn8zo.css +0 -1
- package/dist/assets/index-CKOG_8jk.js +0 -78
- 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
|
-
({
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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 (`
|
|
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
|
-
* `
|
|
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
|
-
|
|
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:
|
|
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, "
|
|
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
|
-
/**
|
|
812
|
-
|
|
813
|
-
//
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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
|
|
825
|
-
|
|
826
|
-
|
|
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
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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).
|
|
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:
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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
|
-
//
|
|
1220
|
-
|
|
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
|
-
//
|
|
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
|
-
/**
|
|
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.
|
|
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:* —
|
|
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
|
-
// ──
|
|
109
|
-
//
|
|
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
|
|
111
|
+
// Subscribe to status updates. Returns an unsubscribe function.
|
|
114
112
|
onProgress: (cb) => onEvent("install:progress", cb),
|
|
115
113
|
|
|
116
|
-
//
|
|
114
|
+
// The current status: { phase, stage, percent, message, error, logPath }.
|
|
117
115
|
status: () => ipcRenderer.invoke("install:status"),
|
|
118
116
|
|
|
119
|
-
//
|
|
120
|
-
|
|
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.
|