@amd-gaia/agent-ui 0.17.1 → 0.17.3
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 +370 -0
- package/dist/assets/index-B4Qzv7Ys.js +443 -0
- package/dist/assets/index-eQemgF08.css +1 -0
- package/dist/index.html +3 -3
- package/main.cjs +209 -54
- package/package.json +8 -11
- package/preload.cjs +42 -0
- package/services/agent-seeder.cjs +301 -0
- package/services/auto-updater.cjs +437 -0
- package/services/backend-installer-progress-dialog.cjs +429 -0
- package/services/backend-installer.cjs +1082 -0
- package/services/tray-manager.cjs +1 -1
- package/bin/gaia-ui.mjs +0 -571
- package/dist/assets/index-DFaWywBV.js +0 -432
- package/dist/assets/index-TyWv9Ej0.css +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amd-gaia/agent-ui",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.3",
|
|
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",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
],
|
|
30
30
|
"main": "main.cjs",
|
|
31
31
|
"bin": {
|
|
32
|
-
"gaia-ui": "bin/gaia-ui.
|
|
32
|
+
"gaia-ui": "bin/gaia-ui.cjs"
|
|
33
33
|
},
|
|
34
34
|
"files": [
|
|
35
35
|
"bin/",
|
|
@@ -50,24 +50,21 @@
|
|
|
50
50
|
"build": "tsc && vite build",
|
|
51
51
|
"preview": "vite preview",
|
|
52
52
|
"start": "electron .",
|
|
53
|
-
"package": "npm run build && electron-
|
|
54
|
-
"
|
|
53
|
+
"package": "npm run build && electron-builder --config electron-builder.yml",
|
|
54
|
+
"package:win": "npm run build && electron-builder --win --config electron-builder.yml",
|
|
55
|
+
"package:mac": "npm run build && electron-builder --mac --config electron-builder.yml",
|
|
56
|
+
"package:linux": "npm run build && electron-builder --linux --config electron-builder.yml",
|
|
55
57
|
"prepublishOnly": "npm run build"
|
|
56
58
|
},
|
|
57
|
-
"config": {
|
|
58
|
-
"forge": "./forge.config.cjs"
|
|
59
|
-
},
|
|
60
59
|
"dependencies": {
|
|
61
|
-
"electron-
|
|
60
|
+
"electron-updater": "^6.8.3"
|
|
62
61
|
},
|
|
63
62
|
"devDependencies": {
|
|
64
|
-
"@electron-forge/cli": "^7.2.0",
|
|
65
|
-
"@electron-forge/maker-deb": "^7.2.0",
|
|
66
|
-
"@electron-forge/maker-squirrel": "^7.2.0",
|
|
67
63
|
"@types/react": "^18.2.48",
|
|
68
64
|
"@types/react-dom": "^18.2.18",
|
|
69
65
|
"@vitejs/plugin-react": "^4.2.1",
|
|
70
66
|
"electron": "^40.6.1",
|
|
67
|
+
"electron-builder": "^26.8.1",
|
|
71
68
|
"lucide-react": "^0.312.0",
|
|
72
69
|
"qrcode": "^1.5.4",
|
|
73
70
|
"react": "^18.2.0",
|
package/preload.cjs
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* agent:* — Agent process management (T2)
|
|
12
12
|
* tray:* — Tray icon/config (T1)
|
|
13
13
|
* notification:* — Desktop notifications & permission prompts (T5)
|
|
14
|
+
* install:* — First-run backend install progress (Phase A)
|
|
15
|
+
* gaia:update:* — Auto-update status & manual check (Phase F)
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
18
|
const { contextBridge, ipcRenderer } = require("electron");
|
|
@@ -59,3 +61,43 @@ contextBridge.exposeInMainWorld("gaiaAPI", {
|
|
|
59
61
|
onNotification: (cb) => onEvent("notification:new", cb),
|
|
60
62
|
},
|
|
61
63
|
});
|
|
64
|
+
|
|
65
|
+
// ── Install progress (Phase A) ──────────────────────────────────────────
|
|
66
|
+
// Exposed as a separate global so the progress window can use it without
|
|
67
|
+
// pulling in the full gaiaAPI surface (and so it keeps working if an
|
|
68
|
+
// install dialog runs before the main window is ready).
|
|
69
|
+
contextBridge.exposeInMainWorld("gaiaInstall", {
|
|
70
|
+
// Subscribe to progress updates. Returns an unsubscribe function.
|
|
71
|
+
onProgress: (cb) => onEvent("install:progress", cb),
|
|
72
|
+
|
|
73
|
+
// Query the current install state (state machine + log/state paths).
|
|
74
|
+
status: () => ipcRenderer.invoke("install:status"),
|
|
75
|
+
|
|
76
|
+
// Copy the log file path to the clipboard.
|
|
77
|
+
copyLogPath: () => ipcRenderer.invoke("install:copy-log-path"),
|
|
78
|
+
|
|
79
|
+
// Open the log file in the OS's default viewer.
|
|
80
|
+
openLogFile: () => ipcRenderer.invoke("install:open-log-file"),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ── Auto-update (Phase F) ───────────────────────────────────────────────
|
|
84
|
+
// Exposed as a separate global so the renderer can show a small "update
|
|
85
|
+
// available" chip without pulling in the full gaiaAPI surface. Mirrors
|
|
86
|
+
// the naming convention (gaiaInstall, gaiaUpdater, gaiaAPI).
|
|
87
|
+
contextBridge.exposeInMainWorld("gaiaUpdater", {
|
|
88
|
+
/** Get the current update state (status, version, progress, error). */
|
|
89
|
+
getStatus: () => ipcRenderer.invoke("gaia:update:get-status"),
|
|
90
|
+
|
|
91
|
+
/** Manually trigger a check. Resolves with the post-check state. */
|
|
92
|
+
check: () => ipcRenderer.invoke("gaia:update:check"),
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Subscribe to status changes. The callback is invoked with the full
|
|
96
|
+
* state object on every state transition. Returns an unsubscribe fn.
|
|
97
|
+
*/
|
|
98
|
+
onStatusChange: (callback) => {
|
|
99
|
+
const handler = (_event, state) => callback(state);
|
|
100
|
+
ipcRenderer.on("gaia:update:status", handler);
|
|
101
|
+
return () => ipcRenderer.removeListener("gaia:update:status", handler);
|
|
102
|
+
},
|
|
103
|
+
});
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* agent-seeder.cjs — First-launch bundled-agent seeder.
|
|
6
|
+
*
|
|
7
|
+
* Copies agents bundled with the installer (placed at
|
|
8
|
+
* `<resourcesPath>/agents/` by electron-builder's extraResources rule) into
|
|
9
|
+
* the user's per-agent home directory at `~/.gaia/agents/<agent-id>/`. A
|
|
10
|
+
* `.seeded` sentinel file is written after a successful copy so subsequent
|
|
11
|
+
* launches skip the agent.
|
|
12
|
+
*
|
|
13
|
+
* Design invariants (see .claude/plans/bundle-path-contract.md):
|
|
14
|
+
* - Source: path.join(process.resourcesPath, "agents", "<id>")
|
|
15
|
+
* - Windows: <install>\resources\agents\<id>\
|
|
16
|
+
* - macOS: <Bundle>.app/Contents/Resources/agents/<id>/
|
|
17
|
+
* - Linux: /opt/<AppName>/resources/agents/<id>/
|
|
18
|
+
* - Target: path.join(os.homedir(), ".gaia", "agents", "<id>")
|
|
19
|
+
* - Sentinel: <target>/.seeded (exists → already seeded → skip)
|
|
20
|
+
*
|
|
21
|
+
* Write protocol (atomic-ish, crash-safe):
|
|
22
|
+
* 1. Remove any stale `<id>.partial/` sibling from a prior failed run.
|
|
23
|
+
* 2. Copy source → `<id>.partial/`.
|
|
24
|
+
* 3. `fs.renameSync(<id>.partial, <id>)` — atomic on the same filesystem.
|
|
25
|
+
* 4. Write `<id>/.seeded` last, so a partial seed never looks complete.
|
|
26
|
+
*
|
|
27
|
+
* Behaviour:
|
|
28
|
+
* - Target `<id>/` exists WITH `.seeded` → already seeded, skip.
|
|
29
|
+
* - Target `<id>/` exists WITHOUT `.seeded` → treat as user-owned data,
|
|
30
|
+
* log a warning, and skip (never clobber a hand-authored agent).
|
|
31
|
+
* - `process.resourcesPath` unset (dev / Jest) or source dir missing →
|
|
32
|
+
* empty result, no error.
|
|
33
|
+
* - Per-agent failures are isolated: they go into `errors[]` but do not
|
|
34
|
+
* stop the next agent from being seeded.
|
|
35
|
+
*
|
|
36
|
+
* Pure CommonJS. Only Node stdlib (fs / path / os). No Electron imports so
|
|
37
|
+
* the module is testable without spinning up Electron.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
"use strict";
|
|
41
|
+
|
|
42
|
+
const fs = require("fs");
|
|
43
|
+
const path = require("path");
|
|
44
|
+
const os = require("os");
|
|
45
|
+
|
|
46
|
+
// ── Path helpers ─────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
function gaiaHome() {
|
|
49
|
+
return path.join(os.homedir(), ".gaia");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function agentsTargetRoot() {
|
|
53
|
+
return path.join(gaiaHome(), "agents");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function logsDir() {
|
|
57
|
+
return path.join(gaiaHome(), "logs");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function logFilePath() {
|
|
61
|
+
return path.join(logsDir(), "seeder.log");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Logging ──────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
function log(level, message) {
|
|
67
|
+
const line = `${new Date().toISOString()} [${level}] ${message}\n`;
|
|
68
|
+
try {
|
|
69
|
+
fs.mkdirSync(logsDir(), { recursive: true });
|
|
70
|
+
fs.appendFileSync(logFilePath(), line, { encoding: "utf8" });
|
|
71
|
+
} catch {
|
|
72
|
+
// If we cannot write the log, fall back to console so the message
|
|
73
|
+
// isn't lost entirely. We never let logging failure propagate.
|
|
74
|
+
}
|
|
75
|
+
// Also mirror to console so `electron .` tail-of-stdout users see it.
|
|
76
|
+
// eslint-disable-next-line no-console
|
|
77
|
+
const writer =
|
|
78
|
+
level === "ERROR" ? console.error : level === "WARN" ? console.warn : console.log;
|
|
79
|
+
writer(`[agent-seeder] ${message}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Filesystem helpers ───────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Recursive copy using fs.cpSync when available (Node 16.7+), falling back
|
|
86
|
+
* to a hand-rolled recursive copy for older runtimes. Electron 40 ships
|
|
87
|
+
* Node 20, so cpSync is always present in production — but we keep the
|
|
88
|
+
* fallback for test environments that might mock cpSync.
|
|
89
|
+
*/
|
|
90
|
+
function copyDirRecursive(src, dest) {
|
|
91
|
+
if (typeof fs.cpSync === "function") {
|
|
92
|
+
// dereference: true flattens symlinks into their targets rather than
|
|
93
|
+
// copying the symlink itself. This prevents a malicious or accidentally
|
|
94
|
+
// symlinked installer bundle from planting out-of-tree references in
|
|
95
|
+
// ~/.gaia/agents/<id>/.
|
|
96
|
+
fs.cpSync(src, dest, { recursive: true, errorOnExist: false, force: true, dereference: true });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Fallback path (shouldn't normally hit on Electron 40 / Node 20).
|
|
100
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
101
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
102
|
+
const s = path.join(src, entry.name);
|
|
103
|
+
const d = path.join(dest, entry.name);
|
|
104
|
+
if (entry.isDirectory()) {
|
|
105
|
+
copyDirRecursive(s, d);
|
|
106
|
+
} else if (entry.isSymbolicLink()) {
|
|
107
|
+
// Skip symlinks in the fallback path for the same reason as dereference:true above.
|
|
108
|
+
log("WARN", `Skipping symlink in installer bundle: ${s}`);
|
|
109
|
+
} else {
|
|
110
|
+
fs.copyFileSync(s, d);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function rmDirRecursive(target) {
|
|
116
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isDirectory(p) {
|
|
120
|
+
try {
|
|
121
|
+
return fs.statSync(p).isDirectory();
|
|
122
|
+
} catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Seeding core ─────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Seed a single agent directory. Returns a category string:
|
|
131
|
+
* "seeded" — copied successfully, sentinel written.
|
|
132
|
+
* "skipped" — already seeded or user-owned; left untouched.
|
|
133
|
+
* "error" — copy failed; partial data cleaned up (best effort).
|
|
134
|
+
*
|
|
135
|
+
* Throws only on programmer error. All IO errors are caught and logged.
|
|
136
|
+
*/
|
|
137
|
+
function seedOneAgent(sourceDir, targetRoot, id) {
|
|
138
|
+
const src = path.join(sourceDir, id);
|
|
139
|
+
const target = path.join(targetRoot, id);
|
|
140
|
+
const partial = path.join(targetRoot, `${id}.partial`);
|
|
141
|
+
const sentinel = path.join(target, ".seeded");
|
|
142
|
+
|
|
143
|
+
// Already seeded?
|
|
144
|
+
if (fs.existsSync(sentinel)) {
|
|
145
|
+
log("INFO", `Skipping "${id}" — already seeded (sentinel present)`);
|
|
146
|
+
return { status: "skipped" };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Target exists but no sentinel → user-owned data. Do not touch.
|
|
150
|
+
if (fs.existsSync(target)) {
|
|
151
|
+
log(
|
|
152
|
+
"WARN",
|
|
153
|
+
`Skipping "${id}" — target exists without .seeded sentinel ` +
|
|
154
|
+
`(treating as user-owned data): ${target}`
|
|
155
|
+
);
|
|
156
|
+
return { status: "skipped" };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Verify the source is actually a directory before doing anything.
|
|
160
|
+
if (!isDirectory(src)) {
|
|
161
|
+
log("WARN", `Skipping "${id}" — source is not a directory: ${src}`);
|
|
162
|
+
return { status: "skipped" };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
// Clean up any leftover from a prior failed run.
|
|
167
|
+
if (fs.existsSync(partial)) {
|
|
168
|
+
log("INFO", `Removing stale partial directory for "${id}": ${partial}`);
|
|
169
|
+
rmDirRecursive(partial);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Ensure the parent exists.
|
|
173
|
+
fs.mkdirSync(targetRoot, { recursive: true });
|
|
174
|
+
|
|
175
|
+
// Copy into sibling, then atomically rename.
|
|
176
|
+
copyDirRecursive(src, partial);
|
|
177
|
+
fs.renameSync(partial, target);
|
|
178
|
+
|
|
179
|
+
// Write sentinel LAST — its presence means "copy completed".
|
|
180
|
+
fs.writeFileSync(
|
|
181
|
+
sentinel,
|
|
182
|
+
JSON.stringify(
|
|
183
|
+
{
|
|
184
|
+
seededAt: new Date().toISOString(),
|
|
185
|
+
source: src,
|
|
186
|
+
},
|
|
187
|
+
null,
|
|
188
|
+
2
|
|
189
|
+
),
|
|
190
|
+
{ encoding: "utf8" }
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
log("INFO", `Seeded "${id}" from ${src} to ${target}`);
|
|
194
|
+
return { status: "seeded" };
|
|
195
|
+
} catch (err) {
|
|
196
|
+
// Best-effort cleanup. If the rename already happened (partial no longer
|
|
197
|
+
// exists but target does and has no sentinel), remove target so the next
|
|
198
|
+
// launch retries cleanly instead of treating it as user-owned data.
|
|
199
|
+
try {
|
|
200
|
+
if (fs.existsSync(partial)) {
|
|
201
|
+
rmDirRecursive(partial);
|
|
202
|
+
} else if (fs.existsSync(target) && !fs.existsSync(sentinel)) {
|
|
203
|
+
rmDirRecursive(target);
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
// ignore — original error is more important
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
log(
|
|
210
|
+
"ERROR",
|
|
211
|
+
`Failed to seed "${id}": ${err && err.message ? err.message : err}`
|
|
212
|
+
);
|
|
213
|
+
return { status: "error", error: err };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Seed all bundled agents found under `<resourcesPath>/agents/`.
|
|
219
|
+
*
|
|
220
|
+
* Idempotent — safe to call on every app launch.
|
|
221
|
+
*
|
|
222
|
+
* @returns {Promise<{seeded: string[], skipped: string[], errors: {id: string, error: Error}[]}>}
|
|
223
|
+
*/
|
|
224
|
+
async function seedBundledAgents() {
|
|
225
|
+
const result = { seeded: [], skipped: [], errors: [] };
|
|
226
|
+
|
|
227
|
+
// Guard against dev / test environments where resourcesPath is unset.
|
|
228
|
+
if (!process.resourcesPath) {
|
|
229
|
+
log(
|
|
230
|
+
"INFO",
|
|
231
|
+
"process.resourcesPath is undefined — skipping bundled-agent seeding"
|
|
232
|
+
);
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const sourceDir = path.join(process.resourcesPath, "agents");
|
|
237
|
+
|
|
238
|
+
if (!fs.existsSync(sourceDir) || !isDirectory(sourceDir)) {
|
|
239
|
+
// Not an error — a build might simply ship without bundled agents.
|
|
240
|
+
// In a packaged Electron app the directory is expected to exist, so raise
|
|
241
|
+
// to WARN; in dev/test contexts leave it at INFO.
|
|
242
|
+
let isPackaged = false;
|
|
243
|
+
try {
|
|
244
|
+
isPackaged = require("electron").app?.isPackaged === true;
|
|
245
|
+
} catch (_) {
|
|
246
|
+
// not in an Electron context (tests, CLI)
|
|
247
|
+
}
|
|
248
|
+
log(
|
|
249
|
+
isPackaged ? "WARN" : "INFO",
|
|
250
|
+
`No bundled agents directory at ${sourceDir} — nothing to seed`
|
|
251
|
+
);
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let entries;
|
|
256
|
+
try {
|
|
257
|
+
entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
|
258
|
+
} catch (err) {
|
|
259
|
+
log(
|
|
260
|
+
"ERROR",
|
|
261
|
+
`Failed to read bundled agents directory ${sourceDir}: ${
|
|
262
|
+
err && err.message ? err.message : err
|
|
263
|
+
}`
|
|
264
|
+
);
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const targetRoot = agentsTargetRoot();
|
|
269
|
+
|
|
270
|
+
for (const entry of entries) {
|
|
271
|
+
if (!entry.isDirectory()) continue;
|
|
272
|
+
const id = entry.name;
|
|
273
|
+
|
|
274
|
+
const outcome = seedOneAgent(sourceDir, targetRoot, id);
|
|
275
|
+
if (outcome.status === "seeded") {
|
|
276
|
+
result.seeded.push(id);
|
|
277
|
+
} else if (outcome.status === "skipped") {
|
|
278
|
+
result.skipped.push(id);
|
|
279
|
+
} else {
|
|
280
|
+
result.errors.push({ id, error: outcome.error });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
log(
|
|
285
|
+
"INFO",
|
|
286
|
+
`Seeding complete — seeded=${result.seeded.length} ` +
|
|
287
|
+
`skipped=${result.skipped.length} errors=${result.errors.length}`
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = {
|
|
294
|
+
seedBundledAgents,
|
|
295
|
+
// Exposed for tests — do not rely on these from production code.
|
|
296
|
+
_internals: {
|
|
297
|
+
seedOneAgent,
|
|
298
|
+
agentsTargetRoot,
|
|
299
|
+
logFilePath,
|
|
300
|
+
},
|
|
301
|
+
};
|