@amd-gaia/agent-ui 0.25.0 → 0.25.1-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/assets/tray-icon.png +0 -0
- package/assets/tray-icon@2x.png +0 -0
- package/dist/assets/AdvancedSettings-CtYKNNWc.js +1 -0
- package/dist/assets/AdvancedSettings-D5VKpkPY.css +1 -0
- package/dist/assets/ConnectorsPane-Ctfp9d5s.js +1 -0
- package/dist/assets/ConnectorsPane-Dzu2DRTc.css +1 -0
- package/dist/assets/DocumentLibrary-CIKp6qld.css +1 -0
- package/dist/assets/DocumentLibrary-Fp70O1-8.js +4 -0
- package/dist/assets/FileBrowser-3H3DoJIm.js +4 -0
- package/dist/assets/FileBrowser-D04I8Pvv.css +1 -0
- package/dist/assets/MemoryDashboard-D5HzmtDi.js +1 -0
- package/dist/assets/MemoryDashboard-Mo721-BI.css +1 -0
- package/dist/assets/MobileAccessModal-B9peVPY4.js +2 -0
- package/dist/assets/MobileAccessModal-DuVy9F1u.css +1 -0
- package/dist/assets/ScheduleManager-DotJl5JW.css +1 -0
- package/dist/assets/ScheduleManager-HcGB9P6m.js +2 -0
- package/dist/assets/{jsx-runtime-DVeaULmT.js → apiBase-y00JgRBP.js} +1 -1
- package/dist/assets/eye-B6FejHNG.js +1 -0
- package/dist/assets/fraunces-latin-BwwriW9Y.woff2 +0 -0
- package/dist/assets/index-D2UMLtwY.js +74 -0
- package/dist/assets/index-DF5qku9L.css +1 -0
- package/dist/assets/inter-latin-C2S99t-D.woff2 +0 -0
- package/dist/assets/inter-latin-ext-CFHvXkgd.woff2 +0 -0
- package/dist/assets/message-square-Dwm7y3C0.js +1 -0
- package/dist/fonts-OFL.txt +101 -0
- package/dist/index.html +6 -6
- package/main-safety-net.cjs +54 -7
- package/main.cjs +276 -120
- package/package.json +3 -1
- package/preload.cjs +24 -0
- package/services/agent-process-manager.cjs +116 -13
- package/services/app-icon.cjs +29 -0
- package/services/auto-updater.cjs +26 -2
- package/services/backend-crash-dialog.cjs +102 -0
- package/services/backend-installer-progress-dialog.cjs +71 -43
- package/services/backend-installer.cjs +233 -25
- package/services/chromium-switches.cjs +29 -0
- package/services/keep-awake.cjs +52 -0
- package/services/notification-service.cjs +62 -0
- package/services/port-manager.cjs +19 -70
- package/services/process-tree.cjs +328 -0
- package/services/quit-cleanup.cjs +86 -0
- package/services/quit-guard.cjs +242 -0
- package/services/tray-icons.cjs +174 -0
- package/services/tray-manager.cjs +582 -83
- package/services/tray-state.cjs +231 -0
- package/services/window-keys.cjs +43 -0
- package/assets/tray-icon.ico +0 -0
- package/dist/assets/AdvancedSettings-5cX0pGZc.js +0 -1
- package/dist/assets/AdvancedSettings-Ce6EvcAe.css +0 -1
- package/dist/assets/ConnectorsPane-CG7atLb2.css +0 -1
- package/dist/assets/ConnectorsPane-CuWxzcBY.js +0 -1
- package/dist/assets/DocumentLibrary-BdAKDe8H.js +0 -1
- package/dist/assets/DocumentLibrary-DrIP_032.css +0 -1
- package/dist/assets/FileBrowser-B8s6hKC5.js +0 -4
- package/dist/assets/FileBrowser-Bj6aB6Ro.css +0 -1
- package/dist/assets/MemoryDashboard-DQxtAW90.css +0 -1
- package/dist/assets/MemoryDashboard-DtolA-HZ.js +0 -1
- package/dist/assets/MobileAccessModal-C3nWkpWA.js +0 -2
- package/dist/assets/MobileAccessModal-C_t8DG0n.css +0 -1
- package/dist/assets/ScheduleManager-CIescbX6.css +0 -1
- package/dist/assets/ScheduleManager-HHdD6sko.js +0 -2
- package/dist/assets/dm-sans-latin-Xz1IZZA0.woff2 +0 -0
- package/dist/assets/index-CFVpPQal.css +0 -1
- package/dist/assets/index-DEPbenxC.js +0 -60
- package/dist/assets/message-square-Ccmbk96j.js +0 -1
- package/dist/assets/space-mono-400-Co7bH5Hm.woff2 +0 -0
- package/dist/assets/useEscapeToClose-B8fw5mn-.js +0 -1
|
@@ -55,8 +55,8 @@ const GAIA_PYTHON_BIN = IS_WINDOWS
|
|
|
55
55
|
const STATE_FILE = path.join(GAIA_HOME, "electron-install-state.json");
|
|
56
56
|
const LOG_FILE = path.join(GAIA_HOME, "electron-install.log");
|
|
57
57
|
|
|
58
|
-
//
|
|
59
|
-
//
|
|
58
|
+
// Floor for uv, Python and the GAIA package only. The models, often 30 GB,
|
|
59
|
+
// are checked by `gaia init` against Lemonade's model store, which it sizes.
|
|
60
60
|
const MIN_DISK_SPACE_BYTES = 5 * 1024 * 1024 * 1024; // 5 GB
|
|
61
61
|
const NETWORK_CHECK_HOSTS = Object.freeze([
|
|
62
62
|
"https://pypi.org/simple/",
|
|
@@ -135,6 +135,12 @@ const STAGES = Object.freeze({
|
|
|
135
135
|
VERIFY: "verify",
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
+
const GAIA_INIT_MESSAGE =
|
|
139
|
+
"Setting up Lemonade Server and downloading models (this can take several minutes)";
|
|
140
|
+
const WAITING_FOR_LEMONADE_CHOICE_MESSAGE =
|
|
141
|
+
"Waiting for your answer: GAIA found a Lemonade Server already running " +
|
|
142
|
+
"and is asking whether to use it. Setup continues once you choose.";
|
|
143
|
+
|
|
138
144
|
// Weight each stage contributes to the overall 0-100 progress.
|
|
139
145
|
// Sum must equal 100.
|
|
140
146
|
const STAGE_WEIGHTS = {
|
|
@@ -399,6 +405,148 @@ function makeProgressReporter(onProgress) {
|
|
|
399
405
|
};
|
|
400
406
|
}
|
|
401
407
|
|
|
408
|
+
// ── gaia init progress (`gaia init --progress-json`) ─────────────────────────
|
|
409
|
+
|
|
410
|
+
// Where the plan and the end of the downloads land in the GAIA_INIT slice; the
|
|
411
|
+
// rest of the slice is Lemonade setup before and verification after.
|
|
412
|
+
const INIT_PLAN_PERCENT = 5;
|
|
413
|
+
const INIT_DOWNLOADED_PERCENT = 95;
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Read one line of `gaia init --progress-json` output. Returns `{ event }` for
|
|
417
|
+
* a progress event, `{ error }` for a line that looks like one but is not, and
|
|
418
|
+
* null for human output.
|
|
419
|
+
*/
|
|
420
|
+
function parseInitEventLine(line) {
|
|
421
|
+
const text = String(line || "").trim();
|
|
422
|
+
if (!text.startsWith("{")) return null;
|
|
423
|
+
let value;
|
|
424
|
+
try {
|
|
425
|
+
value = JSON.parse(text);
|
|
426
|
+
} catch (err) {
|
|
427
|
+
return { error: err.message };
|
|
428
|
+
}
|
|
429
|
+
if (!value || typeof value !== "object" || typeof value.event !== "string") {
|
|
430
|
+
return { error: 'no "event" field' };
|
|
431
|
+
}
|
|
432
|
+
return { event: value };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function formatGB(bytes) {
|
|
436
|
+
return `${(Math.max(0, bytes || 0) / 1e9).toFixed(1)} GB`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function formatSpeed(bytesPerSecond) {
|
|
440
|
+
return `${(bytesPerSecond / 1e6).toFixed(1)} MB/s`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function formatTimeLeft(seconds) {
|
|
444
|
+
if (seconds < 60) return "less than a minute left";
|
|
445
|
+
const minutes = Math.round(seconds / 60);
|
|
446
|
+
if (minutes < 60) return `about ${minutes} min left`;
|
|
447
|
+
return `about ${Math.floor(minutes / 60)} h ${minutes % 60} min left`;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Map `gaia init --progress-json` events onto the GAIA_INIT slice of the bar.
|
|
452
|
+
* `report(withinStagePercent, message)` receives a percent that never goes
|
|
453
|
+
* back. `onLine` returns true for the lines too frequent to log.
|
|
454
|
+
*/
|
|
455
|
+
function createInitProgress(report, logFn = log) {
|
|
456
|
+
const models = new Map();
|
|
457
|
+
let percent = 0;
|
|
458
|
+
|
|
459
|
+
const advance = (within, message) => {
|
|
460
|
+
percent = Math.max(percent, Math.round(within));
|
|
461
|
+
report(percent, message);
|
|
462
|
+
};
|
|
463
|
+
const totals = () => {
|
|
464
|
+
let done = 0;
|
|
465
|
+
let total = 0;
|
|
466
|
+
for (const m of models.values()) {
|
|
467
|
+
total += m.total;
|
|
468
|
+
done += Math.min(m.done, m.total);
|
|
469
|
+
}
|
|
470
|
+
return { done, total };
|
|
471
|
+
};
|
|
472
|
+
const downloadPercent = () => {
|
|
473
|
+
const { done, total } = totals();
|
|
474
|
+
const share = total > 0 ? done / total : 0;
|
|
475
|
+
return INIT_PLAN_PERCENT + (INIT_DOWNLOADED_PERCENT - INIT_PLAN_PERCENT) * share;
|
|
476
|
+
};
|
|
477
|
+
const model = (id) => {
|
|
478
|
+
if (!models.has(id)) models.set(id, { done: 0, total: 0 });
|
|
479
|
+
return models.get(id);
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
function handle(event) {
|
|
483
|
+
switch (event.event) {
|
|
484
|
+
case "step":
|
|
485
|
+
advance(percent, `Step ${event.step} of ${event.total}: ${event.message}`);
|
|
486
|
+
return true;
|
|
487
|
+
case "plan":
|
|
488
|
+
for (const m of event.models || []) {
|
|
489
|
+
if (!m.downloaded) model(m.id).total = m.bytes_total || 0;
|
|
490
|
+
}
|
|
491
|
+
advance(
|
|
492
|
+
INIT_PLAN_PERCENT,
|
|
493
|
+
event.bytes_total > 0
|
|
494
|
+
? `Downloading ${formatGB(event.bytes_total)} of models`
|
|
495
|
+
: "Models already downloaded"
|
|
496
|
+
);
|
|
497
|
+
return true;
|
|
498
|
+
case "progress": {
|
|
499
|
+
const m = model(event.model);
|
|
500
|
+
m.done = event.bytes_done || 0;
|
|
501
|
+
m.total = Math.max(event.bytes_total || 0, m.done);
|
|
502
|
+
const parts = [
|
|
503
|
+
`Downloading ${event.model} (${event.index} of ${event.count}): ` +
|
|
504
|
+
`${formatGB(m.done)} of ${formatGB(m.total)}`,
|
|
505
|
+
];
|
|
506
|
+
if (event.speed_bps > 0) {
|
|
507
|
+
const { done, total } = totals();
|
|
508
|
+
parts.push(
|
|
509
|
+
`at ${formatSpeed(event.speed_bps)}, ` +
|
|
510
|
+
formatTimeLeft((total - done) / event.speed_bps)
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
advance(downloadPercent(), parts.join(" "));
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
case "model_done": {
|
|
517
|
+
const m = model(event.model);
|
|
518
|
+
m.done = m.total;
|
|
519
|
+
advance(downloadPercent(), `${event.model} ready`);
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
case "done":
|
|
523
|
+
advance(
|
|
524
|
+
event.ok ? INIT_DOWNLOADED_PERCENT : percent,
|
|
525
|
+
event.ok ? "Models downloaded, finishing setup" : "Model download did not finish"
|
|
526
|
+
);
|
|
527
|
+
return true;
|
|
528
|
+
case "error":
|
|
529
|
+
advance(percent, String(event.message || "gaia init reported an error").trim());
|
|
530
|
+
return true;
|
|
531
|
+
default:
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function onLine(line) {
|
|
537
|
+
const parsed = parseInitEventLine(line);
|
|
538
|
+
if (!parsed) return false;
|
|
539
|
+
if (parsed.error) {
|
|
540
|
+
logFn(`Malformed gaia init progress line (${parsed.error}): ${line}`);
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
handle(parsed.event);
|
|
544
|
+
return parsed.event.event === "progress";
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return { onLine, handle };
|
|
548
|
+
}
|
|
549
|
+
|
|
402
550
|
// ── Command helpers ──────────────────────────────────────────────────────────
|
|
403
551
|
|
|
404
552
|
/**
|
|
@@ -430,8 +578,11 @@ function findGaiaBin() {
|
|
|
430
578
|
/**
|
|
431
579
|
* Run a child process and stream output to the log file in real time.
|
|
432
580
|
* Returns a Promise that resolves with { code, stdout, stderr }.
|
|
581
|
+
*
|
|
582
|
+
* `onLine(line)` sees each whole stdout line as it arrives; a line it returns
|
|
583
|
+
* true for is left out of the log.
|
|
433
584
|
*/
|
|
434
|
-
function runCommand(cmd, args, { env, stageLabel } = {}) {
|
|
585
|
+
function runCommand(cmd, args, { env, stageLabel, onLine } = {}) {
|
|
435
586
|
return new Promise((resolve) => {
|
|
436
587
|
log(`$ ${cmd} ${args.join(" ")}`);
|
|
437
588
|
let proc;
|
|
@@ -451,14 +602,42 @@ function runCommand(cmd, args, { env, stageLabel } = {}) {
|
|
|
451
602
|
|
|
452
603
|
let stdout = "";
|
|
453
604
|
let stderr = "";
|
|
605
|
+
let partial = "";
|
|
606
|
+
const logLine = (line) => {
|
|
607
|
+
if (line) log(` ${stageLabel ? `[${stageLabel}] ` : ""}${line}`);
|
|
608
|
+
};
|
|
609
|
+
// An in-place progress bar redraws with \r, so each redraw is its own segment.
|
|
610
|
+
const takeLine = (line) => {
|
|
611
|
+
let quiet = true;
|
|
612
|
+
for (const segment of line.split("\r")) {
|
|
613
|
+
if (!segment) continue;
|
|
614
|
+
try {
|
|
615
|
+
quiet = onLine(segment) === true && quiet;
|
|
616
|
+
} catch (err) {
|
|
617
|
+
logError(`onLine callback threw: ${err.message}`);
|
|
618
|
+
quiet = false;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (!quiet) logLine(line);
|
|
622
|
+
};
|
|
454
623
|
|
|
455
624
|
proc.stdout.on("data", (data) => {
|
|
456
625
|
const chunk = data.toString();
|
|
457
626
|
stdout += chunk;
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
627
|
+
if (!onLine) {
|
|
628
|
+
chunk.split(/\r?\n/).forEach(logLine);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const lines = (partial + chunk).split(/\r?\n/);
|
|
632
|
+
partial = lines.pop();
|
|
633
|
+
lines.forEach(takeLine);
|
|
461
634
|
});
|
|
635
|
+
if (onLine) {
|
|
636
|
+
proc.stdout.on("end", () => {
|
|
637
|
+
if (partial) takeLine(partial);
|
|
638
|
+
partial = "";
|
|
639
|
+
});
|
|
640
|
+
}
|
|
462
641
|
|
|
463
642
|
proc.stderr.on("data", (data) => {
|
|
464
643
|
const chunk = data.toString();
|
|
@@ -1387,37 +1566,52 @@ function shouldSkipGaiaInit(opts) {
|
|
|
1387
1566
|
* `chooseLemonade` is asked about a Lemonade Server already running here; see
|
|
1388
1567
|
* lemonadeInitArgs.
|
|
1389
1568
|
*
|
|
1390
|
-
* Returns "done", "failed" or "skipped"
|
|
1391
|
-
*
|
|
1569
|
+
* Returns `{ init }`, "done", "failed" or "skipped", plus `initError` when it
|
|
1570
|
+
* failed. The state file records both: a backend whose setup never finished is
|
|
1571
|
+
* not installed, so the next launch runs it, and the app shows the error.
|
|
1392
1572
|
*/
|
|
1393
1573
|
async function runGaiaInit(gaiaBin, report, version, skipGaiaInit, chooseLemonade) {
|
|
1394
1574
|
if (skipGaiaInit) {
|
|
1395
1575
|
log("Skipping gaia init (skipGaiaInit=true or GAIA_SKIP_GAIA_INIT set)");
|
|
1396
|
-
return "skipped";
|
|
1576
|
+
return { init: "skipped" };
|
|
1397
1577
|
}
|
|
1398
1578
|
setState(STATES.INSTALLING, { stage: STAGES.GAIA_INIT, version });
|
|
1399
|
-
report(
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1579
|
+
report(STAGES.GAIA_INIT, 0, GAIA_INIT_MESSAGE);
|
|
1580
|
+
const lemonadeArgs = await lemonadeInitArgs(gaiaBin, chooseLemonade, runCommand, report);
|
|
1581
|
+
const progress = createInitProgress((within, message) =>
|
|
1582
|
+
report(STAGES.GAIA_INIT, within, message)
|
|
1403
1583
|
);
|
|
1404
|
-
const lemonadeArgs = await lemonadeInitArgs(gaiaBin, chooseLemonade);
|
|
1405
1584
|
const initResult = await runCommand(
|
|
1406
1585
|
gaiaBin,
|
|
1407
|
-
["init", "--profile", "gaia", "--yes", "--skip-webui-build", ...lemonadeArgs],
|
|
1408
|
-
|
|
1586
|
+
["init", "--profile", "gaia", "--yes", "--skip-webui-build", "--progress-json", ...lemonadeArgs],
|
|
1587
|
+
// Unwrapped, so an error line reaches initFailureReason whole.
|
|
1588
|
+
{ stageLabel: "gaia-init", env: { ...process.env, COLUMNS: "1000" }, onLine: progress.onLine }
|
|
1409
1589
|
);
|
|
1410
1590
|
if (initResult.code !== 0) {
|
|
1411
|
-
// Non-fatal: the app opens and its
|
|
1412
|
-
// next launch, because the state file records
|
|
1591
|
+
// Non-fatal: the app opens and its start screen shows initError, and the
|
|
1592
|
+
// next launch runs setup again, because the state file records the failure.
|
|
1413
1593
|
log(
|
|
1414
1594
|
`Warning: gaia init exited with code ${initResult.code}. Continuing; setup runs again next launch.`
|
|
1415
1595
|
);
|
|
1416
1596
|
report(STAGES.GAIA_INIT, 100, "Lemonade Server setup did not finish");
|
|
1417
|
-
return "failed";
|
|
1597
|
+
return { init: "failed", initError: initFailureReason(initResult) };
|
|
1418
1598
|
}
|
|
1419
1599
|
report(STAGES.GAIA_INIT, 100, "Lemonade Server setup complete");
|
|
1420
|
-
return "done";
|
|
1600
|
+
return { init: "done" };
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
/** Why `gaia init` failed, in its own words: its last ❌ line, else its last line. */
|
|
1604
|
+
function initFailureReason({ code, stdout, stderr }) {
|
|
1605
|
+
const lines = `${stdout || ""}\n${stderr || ""}`
|
|
1606
|
+
.split(/\r?\n/)
|
|
1607
|
+
.map((line) => line.trim())
|
|
1608
|
+
// --progress-json event lines are for the progress bar, not the user.
|
|
1609
|
+
.filter((line) => line && !line.startsWith('{"event"'));
|
|
1610
|
+
const marked = lines.filter((line) => line.startsWith("❌"));
|
|
1611
|
+
const reason = marked.length
|
|
1612
|
+
? marked[marked.length - 1].replace(/^❌\s*/, "")
|
|
1613
|
+
: lines[lines.length - 1];
|
|
1614
|
+
return reason || `gaia init exited with code ${code} and printed nothing.`;
|
|
1421
1615
|
}
|
|
1422
1616
|
|
|
1423
1617
|
/**
|
|
@@ -1448,8 +1642,11 @@ async function readGaiaJson(run, gaiaBin, args) {
|
|
|
1448
1642
|
* `--lemonade-url <url>`, "gaia" adds `--gaia-lemonade`, null adds nothing:
|
|
1449
1643
|
* init then sets up GAIA's own and prints how to switch, as it always has.
|
|
1450
1644
|
* A server that needs a key or is too old is left to the app's setup screen.
|
|
1645
|
+
*
|
|
1646
|
+
* `report` is the progress reporter: while the question is open the progress
|
|
1647
|
+
* window says setup is waiting on the user, then goes back to GAIA_INIT_MESSAGE.
|
|
1451
1648
|
*/
|
|
1452
|
-
async function lemonadeInitArgs(gaiaBin, chooseLemonade, run = runCommand) {
|
|
1649
|
+
async function lemonadeInitArgs(gaiaBin, chooseLemonade, run = runCommand, report = () => {}) {
|
|
1453
1650
|
if (typeof chooseLemonade !== "function") return [];
|
|
1454
1651
|
const unchanged = "gaia init will set up GAIA's own Lemonade Server; to use yours later run `gaia lemonade connect <url>`.";
|
|
1455
1652
|
const status = await readGaiaJson(run, gaiaBin, ["lemonade", "status", "--json"]);
|
|
@@ -1469,11 +1666,14 @@ async function lemonadeInitArgs(gaiaBin, chooseLemonade, run = runCommand) {
|
|
|
1469
1666
|
return [];
|
|
1470
1667
|
}
|
|
1471
1668
|
let answer;
|
|
1669
|
+
report(STAGES.GAIA_INIT, 0, WAITING_FOR_LEMONADE_CHOICE_MESSAGE);
|
|
1472
1670
|
try {
|
|
1473
1671
|
answer = await chooseLemonade(found);
|
|
1474
1672
|
} catch (err) {
|
|
1475
1673
|
log(`Warning: could not ask whether to use the Lemonade Server at ${found.url}: ${err.message}; ${unchanged}`);
|
|
1476
1674
|
return [];
|
|
1675
|
+
} finally {
|
|
1676
|
+
report(STAGES.GAIA_INIT, 0, GAIA_INIT_MESSAGE);
|
|
1477
1677
|
}
|
|
1478
1678
|
log(`Lemonade Server at ${found.url}: user chose ${answer || "not to answer"}`);
|
|
1479
1679
|
if (answer === "external") return ["--lemonade-url", found.url];
|
|
@@ -1708,7 +1908,7 @@ async function installBackend(opts = {}) {
|
|
|
1708
1908
|
report(STAGES.INSTALL_PACKAGE, 100, "GAIA package installed");
|
|
1709
1909
|
|
|
1710
1910
|
// Stage 4: gaia init
|
|
1711
|
-
const
|
|
1911
|
+
const setup = await runGaiaInit(
|
|
1712
1912
|
GAIA_BIN,
|
|
1713
1913
|
report,
|
|
1714
1914
|
version,
|
|
@@ -1774,7 +1974,7 @@ async function installBackend(opts = {}) {
|
|
|
1774
1974
|
log(`Shim creation check failed: ${err.message}`);
|
|
1775
1975
|
}
|
|
1776
1976
|
|
|
1777
|
-
setState(STATES.READY, { stage: null, version, installedVersion,
|
|
1977
|
+
setState(STATES.READY, { stage: null, version, installedVersion, ...setup });
|
|
1778
1978
|
log("Backend install complete");
|
|
1779
1979
|
}
|
|
1780
1980
|
|
|
@@ -1914,14 +2114,14 @@ async function ensureBackend(opts = {}) {
|
|
|
1914
2114
|
`(state=${prev.state}, stage=${prev.stage || "-"}) — running gaia init`
|
|
1915
2115
|
);
|
|
1916
2116
|
const report = makeProgressReporter(opts.onProgress);
|
|
1917
|
-
const
|
|
2117
|
+
const setup = await runGaiaInit(
|
|
1918
2118
|
existingBin,
|
|
1919
2119
|
report,
|
|
1920
2120
|
expectedVersion,
|
|
1921
2121
|
shouldSkipGaiaInit(opts),
|
|
1922
2122
|
opts.chooseLemonade
|
|
1923
2123
|
);
|
|
1924
|
-
setState(STATES.READY, { version: expectedVersion, installedVersion,
|
|
2124
|
+
setState(STATES.READY, { version: expectedVersion, installedVersion, ...setup });
|
|
1925
2125
|
report(STAGES.VERIFY, 100, `GAIA ${installedVersion} ready`);
|
|
1926
2126
|
return existingBin;
|
|
1927
2127
|
}
|
|
@@ -1997,6 +2197,7 @@ module.exports = {
|
|
|
1997
2197
|
findGaiaBin,
|
|
1998
2198
|
toPep440,
|
|
1999
2199
|
lemonadeInitArgs,
|
|
2200
|
+
initFailureReason,
|
|
2000
2201
|
|
|
2001
2202
|
// Pre-checks
|
|
2002
2203
|
runPreChecks,
|
|
@@ -2015,6 +2216,11 @@ module.exports = {
|
|
|
2015
2216
|
setState,
|
|
2016
2217
|
clearState,
|
|
2017
2218
|
|
|
2219
|
+
// gaia init progress
|
|
2220
|
+
parseInitEventLine,
|
|
2221
|
+
createInitProgress,
|
|
2222
|
+
computeOverallPercent,
|
|
2223
|
+
|
|
2018
2224
|
// Logging
|
|
2019
2225
|
openLog,
|
|
2020
2226
|
closeLog,
|
|
@@ -2026,6 +2232,8 @@ module.exports = {
|
|
|
2026
2232
|
// Constants
|
|
2027
2233
|
STATES,
|
|
2028
2234
|
STAGES,
|
|
2235
|
+
GAIA_INIT_MESSAGE,
|
|
2236
|
+
WAITING_FOR_LEMONADE_CHOICE_MESSAGE,
|
|
2029
2237
|
GAIA_HOME,
|
|
2030
2238
|
GAIA_VENV,
|
|
2031
2239
|
GAIA_BIN,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Copyright(C) 2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Chromium switches main.cjs appends before app.whenReady(). Pure so the
|
|
6
|
+
* per-platform and per-env choices are testable without launching Electron.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const TRUTHY = new Set(["1", "true", "yes"]);
|
|
10
|
+
|
|
11
|
+
function gpuDisabled(env) {
|
|
12
|
+
return TRUTHY.has(String(env.GAIA_DISABLE_GPU || "").trim().toLowerCase());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** @returns {Array<[string, string?]>} switch name and optional value, in append order */
|
|
16
|
+
function chromiumSwitches({ platform, env }) {
|
|
17
|
+
const switches = [];
|
|
18
|
+
if (platform === "linux") {
|
|
19
|
+
// chrome-sandbox is stripped by after-pack.cjs, and direct ./AppImage runs skip the .desktop flag (#782).
|
|
20
|
+
switches.push(["no-sandbox"]);
|
|
21
|
+
// Partial wp_color_manager support in some Wayland compositors SIGTRAPs Chromium.
|
|
22
|
+
switches.push(["disable-features", "WaylandColorManagement"]);
|
|
23
|
+
}
|
|
24
|
+
// Opt-in escape hatch for GPU stacks whose GPU process aborts at startup (#999).
|
|
25
|
+
if (gpuDisabled(env)) switches.push(["disable-gpu"]);
|
|
26
|
+
return switches;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = { chromiumSwitches, gpuDisabled };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Keep the PC from idle-sleeping while GAIA works (#5226).
|
|
6
|
+
*
|
|
7
|
+
* The renderer decides when (a turn is running, the setting is on, and the
|
|
8
|
+
* battery policy allows it) and calls `power:keep-awake`; this service owns
|
|
9
|
+
* the one powerSaveBlocker. The display may still turn off — only system
|
|
10
|
+
* sleep is held off.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { ipcMain, powerSaveBlocker } = require("electron");
|
|
14
|
+
|
|
15
|
+
class KeepAwake {
|
|
16
|
+
constructor() {
|
|
17
|
+
/** @type {number | null} */
|
|
18
|
+
this._blockerId = null;
|
|
19
|
+
ipcMain.handle("power:keep-awake", (_event, on) => this.set(on));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {boolean} on
|
|
24
|
+
* @returns {boolean} whether sleep is now being held off.
|
|
25
|
+
*/
|
|
26
|
+
set(on) {
|
|
27
|
+
if (typeof on !== "boolean") {
|
|
28
|
+
throw new TypeError(`power:keep-awake expects a boolean, got ${typeof on}`);
|
|
29
|
+
}
|
|
30
|
+
if (on && this._blockerId === null) {
|
|
31
|
+
this._blockerId = powerSaveBlocker.start("prevent-app-suspension");
|
|
32
|
+
console.log("[power] Holding off idle sleep while GAIA works");
|
|
33
|
+
} else if (!on && this._blockerId !== null) {
|
|
34
|
+
powerSaveBlocker.stop(this._blockerId);
|
|
35
|
+
this._blockerId = null;
|
|
36
|
+
console.log("[power] Idle sleep allowed again");
|
|
37
|
+
}
|
|
38
|
+
return this.active;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @returns {boolean} */
|
|
42
|
+
get active() {
|
|
43
|
+
return this._blockerId !== null && powerSaveBlocker.isStarted(this._blockerId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
destroy() {
|
|
47
|
+
this.set(false);
|
|
48
|
+
ipcMain.removeHandler("power:keep-awake");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = KeepAwake;
|
|
@@ -314,6 +314,55 @@ class NotificationService extends EventEmitter {
|
|
|
314
314
|
this._sendToRenderer("tray:navigate", `notification:${notif.id}`);
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
+
// ── Chat alerts ──────────────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Show an OS banner for a chat event. Clicking it focuses the window and
|
|
321
|
+
* opens that chat. Silent: the renderer plays the user's chosen sound.
|
|
322
|
+
*
|
|
323
|
+
* @param {{title: string, body: string, sessionId: string}} payload
|
|
324
|
+
* @returns {boolean} false when the OS has no notification support.
|
|
325
|
+
*/
|
|
326
|
+
showChatBanner(payload) {
|
|
327
|
+
if (!payload || typeof payload.title !== "string" || typeof payload.sessionId !== "string") {
|
|
328
|
+
throw new TypeError("notification:show expects {title, body, sessionId}");
|
|
329
|
+
}
|
|
330
|
+
if (!Notification.isSupported()) {
|
|
331
|
+
console.warn("[notif] OS notifications not supported on this platform");
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
const osNotif = new Notification({
|
|
335
|
+
title: payload.title,
|
|
336
|
+
body: typeof payload.body === "string" ? payload.body : "",
|
|
337
|
+
icon: path.join(__dirname, "..", "assets", "icon.png"),
|
|
338
|
+
silent: true,
|
|
339
|
+
});
|
|
340
|
+
osNotif.on("click", () => {
|
|
341
|
+
if (!this.mainWindow || this.mainWindow.isDestroyed()) return;
|
|
342
|
+
if (this.mainWindow.isMinimized()) this.mainWindow.restore();
|
|
343
|
+
this.mainWindow.show();
|
|
344
|
+
this.mainWindow.focus();
|
|
345
|
+
this._sendToRenderer("tray:navigate", `session:${payload.sessionId}`);
|
|
346
|
+
});
|
|
347
|
+
osNotif.show();
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Flash the taskbar button until the window is focused. A no-op while
|
|
353
|
+
* it already has focus.
|
|
354
|
+
* @returns {boolean} whether it flashed.
|
|
355
|
+
*/
|
|
356
|
+
flashWindow() {
|
|
357
|
+
const win = this.mainWindow;
|
|
358
|
+
if (!win || win.isDestroyed() || win.isFocused()) return false;
|
|
359
|
+
win.flashFrame(true);
|
|
360
|
+
win.once("focus", () => {
|
|
361
|
+
if (!win.isDestroyed()) win.flashFrame(false);
|
|
362
|
+
});
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
|
|
317
366
|
// ── Private: Tray badge ──────────────────────────────────────────────
|
|
318
367
|
|
|
319
368
|
_updateTrayBadge() {
|
|
@@ -423,6 +472,19 @@ class NotificationService extends EventEmitter {
|
|
|
423
472
|
this._respondToPermission(notifId, action, remember);
|
|
424
473
|
}
|
|
425
474
|
);
|
|
475
|
+
|
|
476
|
+
// Chat alerts (#5226): the renderer decides what fires per the user's
|
|
477
|
+
// settings; main owns the OS surfaces it can't reach.
|
|
478
|
+
ipcMain.handle("notification:show", (_event, payload) =>
|
|
479
|
+
this.showChatBanner(payload)
|
|
480
|
+
);
|
|
481
|
+
ipcMain.handle("notification:flash", () => this.flashWindow());
|
|
482
|
+
ipcMain.handle("notification:attention", (_event, count) => {
|
|
483
|
+
if (!this.trayManager) {
|
|
484
|
+
throw new Error("No tray to badge: the tray manager was not created");
|
|
485
|
+
}
|
|
486
|
+
this.trayManager.setAttentionCount(count);
|
|
487
|
+
});
|
|
426
488
|
}
|
|
427
489
|
|
|
428
490
|
// ── Private: Helpers ─────────────────────────────────────────────────
|
|
@@ -22,6 +22,10 @@ const fs = require("fs");
|
|
|
22
22
|
const os = require("os");
|
|
23
23
|
const path = require("path");
|
|
24
24
|
const { execFileSync } = require("child_process");
|
|
25
|
+
const { killProcessTree } = require("./process-tree.cjs");
|
|
26
|
+
|
|
27
|
+
const BACKEND_KILL_GRACE_MS = 3000;
|
|
28
|
+
const BACKEND_FORCE_WAIT_MS = 1000;
|
|
25
29
|
|
|
26
30
|
/**
|
|
27
31
|
* Resolve the GAIA home directory lazily so tests can point it elsewhere
|
|
@@ -54,86 +58,29 @@ function findFreePort() {
|
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
/**
|
|
57
|
-
* Terminate a spawned backend
|
|
61
|
+
* Terminate a spawned backend and everything it started: SIGTERM the tree,
|
|
62
|
+
* wait up to 3 s, then SIGKILL what is left. Without the tree walk, killing
|
|
63
|
+
* the uv `gaia.exe` launcher on Windows left its Python running.
|
|
58
64
|
* Accepts either a ChildProcess reference (preferred — lets us
|
|
59
65
|
* short-circuit via `exitCode` so PID reuse can never hit us) or a raw
|
|
60
|
-
* pid (
|
|
66
|
+
* pid (the orphan cleanup and tests). Safe to call with nullish.
|
|
61
67
|
*/
|
|
62
68
|
async function killBackend(procOrPid, logger) {
|
|
63
69
|
const log = (logger && logger.log) || console.log;
|
|
64
70
|
const logError = (logger && logger.error) || console.error;
|
|
65
|
-
|
|
66
71
|
if (!procOrPid) return;
|
|
67
|
-
|
|
68
|
-
// Distinguish ChildProcess vs raw pid. A ChildProcess has both .pid
|
|
69
|
-
// and .exitCode/.signalCode; typeof === "object" is enough.
|
|
70
|
-
const isChildProcess = typeof procOrPid === "object";
|
|
71
|
-
const proc = isChildProcess ? procOrPid : null;
|
|
72
|
-
const pid = isChildProcess ? procOrPid.pid : procOrPid;
|
|
73
|
-
|
|
74
|
-
if (!pid) return;
|
|
75
|
-
|
|
76
|
-
// If we have the ChildProcess handle, trust it — the exitCode is set
|
|
77
|
-
// synchronously by Node's child_process before any pid reuse could
|
|
78
|
-
// happen, so this closes the PID-reuse TOCTOU that a pid-only API has.
|
|
72
|
+
const proc = typeof procOrPid === "object" ? procOrPid : null;
|
|
79
73
|
if (proc && (proc.exitCode !== null || proc.signalCode !== null)) {
|
|
80
|
-
log(`[port-manager] child pid ${pid} already exited (exitCode=${proc.exitCode} signal=${proc.signalCode})`);
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Is the process still alive? kill(pid, 0) throws ESRCH if not.
|
|
85
|
-
// Only relied on when the caller passed a raw pid (tests); callers
|
|
86
|
-
// with a ChildProcess were already short-circuited above.
|
|
87
|
-
const alive = (p) => {
|
|
88
|
-
try {
|
|
89
|
-
process.kill(p, 0);
|
|
90
|
-
return true;
|
|
91
|
-
} catch {
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
if (!alive(pid)) {
|
|
97
|
-
log(`[port-manager] pid ${pid} already gone`);
|
|
74
|
+
log(`[port-manager] child pid ${proc.pid} already exited (exitCode=${proc.exitCode} signal=${proc.signalCode})`);
|
|
98
75
|
return;
|
|
99
76
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
log
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const deadline = Date.now() + 3000;
|
|
110
|
-
while (Date.now() < deadline) {
|
|
111
|
-
if (!alive(pid)) {
|
|
112
|
-
log(`[port-manager] pid ${pid} exited after SIGTERM`);
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
try {
|
|
119
|
-
process.kill(pid, "SIGKILL");
|
|
120
|
-
log(`[port-manager] SIGKILL sent to pid ${pid} (did not exit on SIGTERM)`);
|
|
121
|
-
} catch (err) {
|
|
122
|
-
logError(`[port-manager] SIGKILL failed for pid ${pid}: ${err.message}`);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Poll until the kernel reaps the process so callers can safely rebind
|
|
127
|
-
// the port immediately after we return. 1 s cap at 50 ms intervals —
|
|
128
|
-
// SIGKILL is synchronous in the kernel but reaping the zombie can lag.
|
|
129
|
-
const killDeadline = Date.now() + 1000;
|
|
130
|
-
while (Date.now() < killDeadline) {
|
|
131
|
-
if (!alive(pid)) return;
|
|
132
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
133
|
-
}
|
|
134
|
-
logError(
|
|
135
|
-
`[port-manager] pid ${pid} still alive 1s after SIGKILL — orphan risk`,
|
|
136
|
-
);
|
|
77
|
+
await killProcessTree(procOrPid, {
|
|
78
|
+
graceMs: BACKEND_KILL_GRACE_MS,
|
|
79
|
+
forceWaitMs: BACKEND_FORCE_WAIT_MS,
|
|
80
|
+
log,
|
|
81
|
+
logError,
|
|
82
|
+
label: "backend",
|
|
83
|
+
});
|
|
137
84
|
}
|
|
138
85
|
|
|
139
86
|
/**
|
|
@@ -231,4 +178,6 @@ module.exports = PortManager;
|
|
|
231
178
|
module.exports.PortManager = PortManager;
|
|
232
179
|
module.exports.findFreePort = findFreePort;
|
|
233
180
|
module.exports.killBackend = killBackend;
|
|
181
|
+
module.exports.BACKEND_KILL_GRACE_MS = BACKEND_KILL_GRACE_MS;
|
|
182
|
+
module.exports.BACKEND_FORCE_WAIT_MS = BACKEND_FORCE_WAIT_MS;
|
|
234
183
|
module.exports.writeDiagnosticsBundle = writeDiagnosticsBundle;
|