@amd-gaia/agent-ui 0.20.1 → 0.21.1

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/dist/index.html CHANGED
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" type="image/png" href="./favicon.png" />
7
7
  <title>GAIA</title>
8
- <script type="module" crossorigin src="./assets/index-C6QRsg9k.js"></script>
9
- <link rel="stylesheet" crossorigin href="./assets/index-ZwMUX81-.css">
8
+ <script type="module" crossorigin src="./assets/index-Dj-STgr2.js"></script>
9
+ <link rel="stylesheet" crossorigin href="./assets/index-DRnhDSoO.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amd-gaia/agent-ui",
3
- "version": "0.20.1",
3
+ "version": "0.21.1",
4
4
  "type": "module",
5
5
  "productName": "GAIA Agent UI",
6
6
  "description": "Privacy-first agentic AI interface with document Q&A - runs 100% locally on AMD Ryzen AI",
@@ -62,6 +62,11 @@ const NETWORK_CHECK_HOSTS = Object.freeze([
62
62
  ]);
63
63
  const NETWORK_CHECK_TIMEOUT_MS = 5000;
64
64
 
65
+ // Pip-install resilience: the install stage fetches heavy transitive deps from
66
+ // PyPI, so retry a transient network failure a few times before giving up.
67
+ const INSTALL_MAX_ATTEMPTS = 3;
68
+ const INSTALL_RETRY_BACKOFF_MS = 3000;
69
+
65
70
  // ── Bundled `uv` binary ──────────────────────────────────────────────────────
66
71
  //
67
72
  // Issue #782 / T3: the AppImage now ships a pinned `uv` under
@@ -478,6 +483,37 @@ function isFileLockedError(output) {
478
483
  return /os error 32/i.test(output) || /being used by another process/i.test(output);
479
484
  }
480
485
 
486
+ /**
487
+ * Detect a transient network failure in `uv pip install` output so the
488
+ * install stage can retry instead of failing the whole bootstrap. The
489
+ * install stage downloads heavy transitive deps (scipy, numpy, torch) from
490
+ * PyPI; a single mid-stream hiccup ("stream closed because of a broken pipe")
491
+ * otherwise fails the entire backend install — and, in the release pipeline,
492
+ * the whole AppImage smoke test that gates publishing.
493
+ *
494
+ * Matches only network-shaped failures — NOT dependency-resolution errors
495
+ * ("No solution found") or disk-full, which retrying cannot fix.
496
+ */
497
+ function isTransientNetworkError(output) {
498
+ if (!output) return false;
499
+ return (
500
+ /broken pipe/i.test(output) ||
501
+ /stream closed/i.test(output) ||
502
+ /failed to fetch/i.test(output) ||
503
+ /error sending request/i.test(output) ||
504
+ /connection (?:error|reset|closed|refused|aborted)/i.test(output) ||
505
+ /could not connect/i.test(output) ||
506
+ /(?:request|operation|connection)?\s*tim(?:ed\s*out|eout)/i.test(output) ||
507
+ /temporary failure in name resolution/i.test(output) ||
508
+ /(?:could not resolve|failed to lookup|dns error)/i.test(output) ||
509
+ /network is unreachable/i.test(output)
510
+ );
511
+ }
512
+
513
+ function sleep(ms) {
514
+ return new Promise((resolve) => setTimeout(resolve, ms));
515
+ }
516
+
481
517
  // ── Pre-checks ───────────────────────────────────────────────────────────────
482
518
 
483
519
  /**
@@ -1156,7 +1192,33 @@ async function installBackend(opts = {}) {
1156
1192
  pipArgs.push("--extra-index-url", "https://download.pytorch.org/whl/cpu");
1157
1193
  }
1158
1194
 
1159
- const installResult = await runCommand("uv", pipArgs, { stageLabel: "pip" });
1195
+ // Retry the install on transient PyPI/network failures. The heavy
1196
+ // transitive deps (scipy, numpy, torch) are fetched live from PyPI even
1197
+ // when the gaia wheel itself is local, so a single broken-pipe mid-download
1198
+ // would otherwise fail the whole bootstrap (and block a release). File-lock
1199
+ // failures (Windows os-error-32) are NOT retried — they need user action.
1200
+ let installResult;
1201
+ for (let attempt = 1; attempt <= INSTALL_MAX_ATTEMPTS; attempt++) {
1202
+ installResult = await runCommand("uv", pipArgs, { stageLabel: "pip" });
1203
+ if (installResult.code === 0) break;
1204
+ const attemptOutput = `${installResult.stdout || ""}\n${installResult.stderr || ""}`;
1205
+ const canRetry =
1206
+ attempt < INSTALL_MAX_ATTEMPTS &&
1207
+ isTransientNetworkError(attemptOutput) &&
1208
+ !isFileLockedError(attemptOutput);
1209
+ if (!canRetry) break;
1210
+ const backoffMs = INSTALL_RETRY_BACKOFF_MS * attempt;
1211
+ log(
1212
+ `Transient network error during install (attempt ${attempt}/${INSTALL_MAX_ATTEMPTS}). ` +
1213
+ `Retrying in ${Math.round(backoffMs / 1000)}s…`
1214
+ );
1215
+ report(
1216
+ STAGES.INSTALL_PACKAGE,
1217
+ 0,
1218
+ `Network hiccup — retrying install (attempt ${attempt + 1}/${INSTALL_MAX_ATTEMPTS})`
1219
+ );
1220
+ await sleep(backoffMs);
1221
+ }
1160
1222
  if (installResult.code !== 0) {
1161
1223
  // Windows: an upgrade can't replace gaia.exe while a previous GAIA
1162
1224
  // process still holds it open (issue #1388). The orphan cleanup in
@@ -1458,6 +1520,7 @@ module.exports = {
1458
1520
  checkDiskSpace,
1459
1521
  checkNetwork,
1460
1522
  isFileLockedError,
1523
+ isTransientNetworkError,
1461
1524
 
1462
1525
  // State machine
1463
1526
  getState,
@@ -57,8 +57,7 @@ class TrayManager {
57
57
  this.config = this._loadConfig();
58
58
 
59
59
  /** @type {Electron.NativeImage} */
60
- const trayIconFile = process.platform === "win32" ? "icon.ico" : "icon.png";
61
- this._icon = this._loadIcon(trayIconFile);
60
+ this._icon = this._loadTrayIcon();
62
61
 
63
62
  this._registerIpcHandlers();
64
63
  }
@@ -153,18 +152,33 @@ class TrayManager {
153
152
 
154
153
  // ── Private: Icon loading ────────────────────────────────────────────
155
154
 
155
+ /**
156
+ * Pick the platform-appropriate tray icon. The menu bar is ~22pt tall, so
157
+ * each platform gets a purpose-built small asset — never the 4K app icon.
158
+ * @returns {Electron.NativeImage}
159
+ */
160
+ _loadTrayIcon() {
161
+ if (process.platform === "win32") {
162
+ return this._loadIcon("tray-icon.ico");
163
+ }
164
+ if (process.platform === "darwin") {
165
+ // macOS menu-bar extras must be template images: the OS tints the
166
+ // alpha mask for light/dark mode. Electron auto-discovers the @2x
167
+ // retina variant by name.
168
+ const img = this._loadIcon("tray-iconTemplate.png");
169
+ img.setTemplateImage(true);
170
+ return img;
171
+ }
172
+ return this._loadIcon("tray-icon.png"); // Linux: full-colour PNG
173
+ }
174
+
156
175
  _loadIcon(filename) {
157
176
  // __dirname is services/, assets/ is one level up alongside main.cjs
158
177
  const iconPath = path.join(__dirname, "..", "assets", filename);
159
- try {
160
- if (fs.existsSync(iconPath)) {
161
- return nativeImage.createFromPath(iconPath);
162
- }
163
- } catch (err) {
164
- console.warn(`[tray] Could not load icon ${filename}:`, err.message);
178
+ if (!fs.existsSync(iconPath)) {
179
+ throw new Error(`[tray] icon asset missing: ${iconPath}`);
165
180
  }
166
- // Return empty image as fallback (Electron will show a default)
167
- return nativeImage.createEmpty();
181
+ return nativeImage.createFromPath(iconPath);
168
182
  }
169
183
 
170
184
  // ── Private: Config persistence ──────────────────────────────────────