@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.
- package/bin/gaia-ui.cjs +2 -23
- package/dist/assets/AdvancedSettings-DXf2pogI.js +1 -0
- package/dist/assets/ConnectorsPane-CzLYXGMl.css +1 -0
- package/dist/assets/ConnectorsPane-R1yvKMNT.js +1 -0
- package/dist/assets/DocumentLibrary-CM6o8j2T.css +1 -0
- package/dist/assets/DocumentLibrary-DSOiP7Ft.js +4 -0
- package/dist/assets/FileBrowser-BlVSPjD2.js +4 -0
- package/dist/assets/FileBrowser-Cna3BsT7.css +1 -0
- package/dist/assets/MemoryDashboard-D_ZVZbS_.js +1 -0
- package/dist/assets/MemoryDashboard-DwrWAXi6.css +1 -0
- package/dist/assets/MobileAccessModal-BkVDm3YG.css +1 -0
- package/dist/assets/MobileAccessModal-CPjDeIIk.js +2 -0
- package/dist/assets/ScheduleManager-CjHJ4NbD.css +1 -0
- package/dist/assets/ScheduleManager-D1DOYVQo.js +1 -0
- package/dist/assets/api-Co-B9E0t.js +4 -0
- package/dist/assets/index-CMVmIBZ8.js +78 -0
- package/dist/assets/index-Y93uvqe7.css +1 -0
- package/dist/assets/useT-tWXlG-cT.js +3 -0
- package/dist/assets/zap-D-WCZ6dX.js +1 -0
- 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 +145 -162
- package/package.json +5 -3
- package/preload.cjs +19 -9
- package/services/agent-seeder.cjs +22 -13
- package/services/auto-updater.cjs +780 -216
- package/services/backend-installer.cjs +523 -365
- package/services/engine-install.cjs +233 -0
- package/services/index-query.cjs +7 -1
- package/services/lock-holders.cjs +200 -0
- package/services/quit-guard.cjs +36 -3
- package/services/tray-manager.cjs +12 -7
- package/dist/assets/AdvancedSettings-DERzP9Jc.js +0 -1
- package/dist/assets/ConnectorsPane-Dzu2DRTc.css +0 -1
- package/dist/assets/ConnectorsPane-s0fFxmus.js +0 -1
- package/dist/assets/DocumentLibrary-CIKp6qld.css +0 -1
- package/dist/assets/DocumentLibrary-DqapN_Tr.js +0 -4
- package/dist/assets/FileBrowser-Dil1lZJ4.css +0 -1
- package/dist/assets/FileBrowser-vAJV41RT.js +0 -4
- package/dist/assets/MemoryDashboard-CP5wVWkY.js +0 -1
- package/dist/assets/MemoryDashboard-Mo721-BI.css +0 -1
- package/dist/assets/MobileAccessModal-B31mPs50.js +0 -2
- package/dist/assets/MobileAccessModal-DuVy9F1u.css +0 -1
- package/dist/assets/ScheduleManager-D5c4gVIt.css +0 -1
- package/dist/assets/ScheduleManager-b0gTAf2R.js +0 -2
- package/dist/assets/apiBase-CS6zLsJY.js +0 -1
- package/dist/assets/index-C-EM_5yl.css +0 -1
- package/dist/assets/index-Cae5Fje-.js +0 -78
- package/dist/assets/zap-BogJn9oN.js +0 -1
- 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");
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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 (`
|
|
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
|
-
* `
|
|
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
|
-
|
|
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:
|
|
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, "
|
|
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
|
-
/**
|
|
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);
|
|
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
|
|
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";
|
|
855
|
+
const win = mainWindow && !mainWindow.isDestroyed() ? mainWindow : reopenWindow();
|
|
856
|
+
if (win.isMinimized()) win.restore();
|
|
857
|
+
win.show();
|
|
839
858
|
}
|
|
840
859
|
|
|
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
|
-
);
|
|
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
|
-
|
|
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;
|
|
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).
|
|
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:
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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
|
-
//
|
|
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();
|
|
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
|
-
//
|
|
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
|
-
/**
|
|
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
|
|
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
|
+
"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:* —
|
|
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
|
-
// ──
|
|
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"),
|
|
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`
|
|
45
|
-
* ~/.gaia/agents/ when
|
|
46
|
-
* present, no file newer than its seededAt (+5s slack), never
|
|
47
|
-
* symlink
|
|
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 (
|
|
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.
|
|
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
|