@amd-gaia/agent-ui 0.17.0
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/LICENSE +21 -0
- package/README.md +101 -0
- package/app.config.json +37 -0
- package/assets/icon.ico +0 -0
- package/assets/icon.png +0 -0
- package/assets/tray-icon-active.png +0 -0
- package/assets/tray-icon-active@2x.png +0 -0
- package/assets/tray-icon.ico +0 -0
- package/assets/tray-icon.png +0 -0
- package/assets/tray-icon@2x.png +0 -0
- package/assets/tray-iconTemplate.png +0 -0
- package/assets/tray-iconTemplate@2x.png +0 -0
- package/bin/gaia-ui.mjs +572 -0
- package/dist/assets/browser-CTB2jwNe.js +8 -0
- package/dist/assets/dm-sans-latin-Xz1IZZA0.woff2 +0 -0
- package/dist/assets/gaia-robot-NKaQnEIp.png +0 -0
- package/dist/assets/index-C7oO2M6Q.js +432 -0
- package/dist/assets/index-TyWv9Ej0.css +1 -0
- package/dist/assets/jetbrains-mono-latin-6fWv1k7M.woff2 +0 -0
- package/dist/assets/space-mono-400-Co7bH5Hm.woff2 +0 -0
- package/dist/favicon.png +0 -0
- package/dist/index.html +14 -0
- package/main.cjs +511 -0
- package/package.json +83 -0
- package/preload.cjs +61 -0
- package/services/agent-process-manager.cjs +818 -0
- package/services/notification-service.cjs +419 -0
- package/services/tray-manager.cjs +239 -0
package/main.cjs
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
// Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
// GAIA Agent UI - Electron main process
|
|
5
|
+
// Self-contained entry point for the desktop installer.
|
|
6
|
+
//
|
|
7
|
+
// Starts the Python backend (gaia chat --ui), creates the system tray icon,
|
|
8
|
+
// manages OS agent subprocesses, and loads the frontend.
|
|
9
|
+
//
|
|
10
|
+
// Services (co-located per T0 decision):
|
|
11
|
+
// services/tray-manager.js — System tray icon + context menu (T1)
|
|
12
|
+
// services/agent-process-manager.js — OS agent subprocess lifecycle (T2)
|
|
13
|
+
// services/notification-service.js — Desktop notifications + permission prompts (T5)
|
|
14
|
+
// preload.cjs — contextBridge for IPC channels (T0/T1)
|
|
15
|
+
|
|
16
|
+
const { app, BrowserWindow, shell } = require("electron");
|
|
17
|
+
const path = require("path");
|
|
18
|
+
const fs = require("fs");
|
|
19
|
+
const { spawn } = require("child_process");
|
|
20
|
+
|
|
21
|
+
// Services (loaded after app.whenReady)
|
|
22
|
+
const TrayManager = require("./services/tray-manager.cjs");
|
|
23
|
+
const AgentProcessManager = require("./services/agent-process-manager.cjs");
|
|
24
|
+
const NotificationService = require("./services/notification-service.cjs");
|
|
25
|
+
|
|
26
|
+
// ── Configuration ──────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const APP_NAME = "GAIA Agent UI";
|
|
29
|
+
const BACKEND_PORT = 4200;
|
|
30
|
+
const HEALTH_CHECK_URL = `http://localhost:${BACKEND_PORT}/api/health`;
|
|
31
|
+
const STARTUP_TIMEOUT = 30000;
|
|
32
|
+
|
|
33
|
+
// Parse CLI args (T11: --minimized flag for auto-start)
|
|
34
|
+
const startMinimized = process.argv.includes("--minimized");
|
|
35
|
+
|
|
36
|
+
// Load app.config.json if available
|
|
37
|
+
let appConfig = {};
|
|
38
|
+
try {
|
|
39
|
+
const configPath = path.join(__dirname, "app.config.json");
|
|
40
|
+
if (fs.existsSync(configPath)) {
|
|
41
|
+
appConfig = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.warn("Could not load app.config.json:", error.message);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const windowConfig = appConfig.window || {
|
|
48
|
+
width: 1200,
|
|
49
|
+
height: 800,
|
|
50
|
+
minWidth: 800,
|
|
51
|
+
minHeight: 500,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ── State ──────────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
let backendProcess = null;
|
|
57
|
+
let mainWindow = null;
|
|
58
|
+
|
|
59
|
+
/** @type {TrayManager | null} */
|
|
60
|
+
let trayManager = null;
|
|
61
|
+
|
|
62
|
+
/** @type {AgentProcessManager | null} */
|
|
63
|
+
let agentProcessManager = null;
|
|
64
|
+
|
|
65
|
+
/** @type {NotificationService | null} */
|
|
66
|
+
let notificationService = null;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Set to true when the user explicitly quits (via tray "Quit" or Cmd+Q).
|
|
70
|
+
* Prevents minimize-to-tray from intercepting the close event.
|
|
71
|
+
*/
|
|
72
|
+
let isQuitting = false;
|
|
73
|
+
|
|
74
|
+
// ── Backend Process ────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
function findGaiaCommand() {
|
|
77
|
+
const isWindows = process.platform === "win32";
|
|
78
|
+
|
|
79
|
+
// Check common locations
|
|
80
|
+
const candidates = isWindows
|
|
81
|
+
? ["gaia.exe", "gaia", "gaia.cmd"]
|
|
82
|
+
: ["gaia"];
|
|
83
|
+
|
|
84
|
+
for (const cmd of candidates) {
|
|
85
|
+
try {
|
|
86
|
+
const { execSync } = require("child_process");
|
|
87
|
+
const check = isWindows ? `where ${cmd}` : `which ${cmd}`;
|
|
88
|
+
execSync(check, { stdio: "ignore" });
|
|
89
|
+
return cmd;
|
|
90
|
+
} catch {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function startBackend() {
|
|
98
|
+
const gaiaCmd = findGaiaCommand();
|
|
99
|
+
|
|
100
|
+
if (!gaiaCmd) {
|
|
101
|
+
console.warn(
|
|
102
|
+
"Warning: gaia CLI not found. Backend will not start automatically."
|
|
103
|
+
);
|
|
104
|
+
console.warn("Install with: pip install amd-gaia");
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log(`Starting backend: ${gaiaCmd} chat --ui --ui-port ${BACKEND_PORT}`);
|
|
109
|
+
|
|
110
|
+
const child = spawn(
|
|
111
|
+
gaiaCmd,
|
|
112
|
+
["chat", "--ui", "--ui-port", String(BACKEND_PORT)],
|
|
113
|
+
{
|
|
114
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
115
|
+
env: { ...process.env },
|
|
116
|
+
detached: false,
|
|
117
|
+
windowsHide: true, // Prevent console window flash on Windows
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
child.stdout.on("data", (data) => {
|
|
122
|
+
const line = data.toString().trim();
|
|
123
|
+
if (line) console.log(`[backend] ${line}`);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
child.stderr.on("data", (data) => {
|
|
127
|
+
const line = data.toString().trim();
|
|
128
|
+
if (line) console.log(`[backend] ${line}`);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
child.on("error", (err) => {
|
|
132
|
+
console.error("Failed to start backend:", err.message);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
child.on("exit", (code) => {
|
|
136
|
+
if (code !== 0 && code !== null) {
|
|
137
|
+
console.error(`Backend exited with code ${code}`);
|
|
138
|
+
}
|
|
139
|
+
backendProcess = null;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return child;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function waitForBackend(timeoutMs) {
|
|
146
|
+
const start = Date.now();
|
|
147
|
+
const http = require("http");
|
|
148
|
+
|
|
149
|
+
while (Date.now() - start < timeoutMs) {
|
|
150
|
+
try {
|
|
151
|
+
await new Promise((resolve, reject) => {
|
|
152
|
+
const req = http.get(HEALTH_CHECK_URL, (res) => {
|
|
153
|
+
if (res.statusCode === 200) {
|
|
154
|
+
resolve();
|
|
155
|
+
} else {
|
|
156
|
+
reject(new Error(`Status ${res.statusCode}`));
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
req.on("error", reject);
|
|
160
|
+
req.setTimeout(2000, () => {
|
|
161
|
+
req.destroy();
|
|
162
|
+
reject(new Error("timeout"));
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
return true;
|
|
166
|
+
} catch {
|
|
167
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Window ─────────────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
function findDistPath() {
|
|
176
|
+
// Check multiple locations (dev vs packaged)
|
|
177
|
+
const candidates = [
|
|
178
|
+
path.join(__dirname, "dist", "index.html"), // Development
|
|
179
|
+
path.join(process.resourcesPath || "", "dist", "index.html"), // Packaged (extraResource)
|
|
180
|
+
path.join(__dirname, "..", "dist", "index.html"), // Alternative packaged
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
for (const candidate of candidates) {
|
|
184
|
+
if (fs.existsSync(candidate)) {
|
|
185
|
+
return path.dirname(candidate);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function createWindow() {
|
|
192
|
+
mainWindow = new BrowserWindow({
|
|
193
|
+
width: windowConfig.width,
|
|
194
|
+
height: windowConfig.height,
|
|
195
|
+
minWidth: windowConfig.minWidth,
|
|
196
|
+
minHeight: windowConfig.minHeight,
|
|
197
|
+
title: APP_NAME,
|
|
198
|
+
icon: path.join(__dirname, "assets", process.platform === "win32" ? "icon.ico" : "icon.png"),
|
|
199
|
+
show: false, // Don't show until ready (prevents flash)
|
|
200
|
+
webPreferences: {
|
|
201
|
+
nodeIntegration: false,
|
|
202
|
+
contextIsolation: true,
|
|
203
|
+
preload: path.join(__dirname, "preload.cjs"), // C2 fix: expose IPC via contextBridge
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Remove default menu bar
|
|
208
|
+
mainWindow.setMenuBarVisibility(false);
|
|
209
|
+
|
|
210
|
+
// Open external links in the default browser
|
|
211
|
+
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
|
212
|
+
shell.openExternal(url);
|
|
213
|
+
return { action: "deny" };
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ── Minimize-to-tray on close (C4 fix) ──────────────────────────────
|
|
217
|
+
// Intercept window close — hide instead of closing when tray mode is active
|
|
218
|
+
mainWindow.on("close", (event) => {
|
|
219
|
+
if (!isQuitting && trayManager && trayManager.minimizeToTray) {
|
|
220
|
+
event.preventDefault();
|
|
221
|
+
mainWindow.hide();
|
|
222
|
+
console.log("[main] Window hidden to tray");
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
mainWindow.on("closed", () => {
|
|
227
|
+
mainWindow = null;
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Show window when ready (unless --minimized or startMinimized config)
|
|
231
|
+
mainWindow.once("ready-to-show", () => {
|
|
232
|
+
const shouldStartMinimized =
|
|
233
|
+
startMinimized || (trayManager && trayManager.startMinimized);
|
|
234
|
+
|
|
235
|
+
if (!shouldStartMinimized) {
|
|
236
|
+
mainWindow.show();
|
|
237
|
+
} else {
|
|
238
|
+
console.log("[main] Starting minimized to tray");
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return mainWindow;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function loadApp() {
|
|
246
|
+
const distPath = findDistPath();
|
|
247
|
+
|
|
248
|
+
if (distPath) {
|
|
249
|
+
// Load the built frontend directly (for when backend serves it)
|
|
250
|
+
// First try loading from the backend URL
|
|
251
|
+
try {
|
|
252
|
+
await mainWindow.loadURL(`http://localhost:${BACKEND_PORT}`);
|
|
253
|
+
console.log("Loaded app from backend server");
|
|
254
|
+
return;
|
|
255
|
+
} catch {
|
|
256
|
+
// Fall through to loading from file
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Load from built files
|
|
260
|
+
const indexPath = path.join(distPath, "index.html");
|
|
261
|
+
console.log("Loading app from:", indexPath);
|
|
262
|
+
await mainWindow.loadFile(indexPath);
|
|
263
|
+
} else {
|
|
264
|
+
// Show a simple loading/error page
|
|
265
|
+
mainWindow.loadURL(
|
|
266
|
+
`data:text/html,
|
|
267
|
+
<html>
|
|
268
|
+
<head><title>${APP_NAME}</title></head>
|
|
269
|
+
<body style="font-family: -apple-system, BlinkMacSystemFont, sans-serif; display:flex; align-items:center; justify-content:center; height:100vh; margin:0; background:#1a1a2e; color:#eee;">
|
|
270
|
+
<div style="text-align:center;">
|
|
271
|
+
<h1>${APP_NAME}</h1>
|
|
272
|
+
<p>Waiting for backend to start...</p>
|
|
273
|
+
<p style="color:#888; font-size:12px;">Backend: http://localhost:${BACKEND_PORT}</p>
|
|
274
|
+
</div>
|
|
275
|
+
</body>
|
|
276
|
+
</html>`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── Services Setup ─────────────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
function initializeServices() {
|
|
284
|
+
console.log("[main] Initializing services...");
|
|
285
|
+
|
|
286
|
+
// T2: Agent Process Manager (manages OS agent subprocesses)
|
|
287
|
+
agentProcessManager = new AgentProcessManager(mainWindow);
|
|
288
|
+
|
|
289
|
+
// T1: Tray Manager (system tray icon + context menu)
|
|
290
|
+
trayManager = new TrayManager(mainWindow, { backendPort: BACKEND_PORT });
|
|
291
|
+
trayManager.create();
|
|
292
|
+
|
|
293
|
+
// T5: Notification Service (routes agent notifications to OS + renderer)
|
|
294
|
+
notificationService = new NotificationService(
|
|
295
|
+
mainWindow,
|
|
296
|
+
agentProcessManager,
|
|
297
|
+
trayManager
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
console.log("[main] Services initialized");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ── Windows Jump List (T11) ────────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
function setupJumpList() {
|
|
306
|
+
if (process.platform !== "win32") return;
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
app.setJumpList([
|
|
310
|
+
{
|
|
311
|
+
type: "tasks",
|
|
312
|
+
items: [
|
|
313
|
+
{
|
|
314
|
+
type: "task",
|
|
315
|
+
title: "New Task",
|
|
316
|
+
description: "Start a new agent task",
|
|
317
|
+
program: process.execPath,
|
|
318
|
+
args: "",
|
|
319
|
+
iconPath: process.execPath,
|
|
320
|
+
iconIndex: 0,
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
type: "task",
|
|
324
|
+
title: "Agent Manager",
|
|
325
|
+
description: "View and manage OS agents",
|
|
326
|
+
program: process.execPath,
|
|
327
|
+
args: "--show-agents",
|
|
328
|
+
iconPath: process.execPath,
|
|
329
|
+
iconIndex: 0,
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
},
|
|
333
|
+
]);
|
|
334
|
+
console.log("[main] Windows Jump List configured");
|
|
335
|
+
} catch (err) {
|
|
336
|
+
console.warn("[main] Could not set Jump List:", err.message);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── App Lifecycle ──────────────────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
// Handle creating/removing shortcuts on Windows when installing/uninstalling
|
|
343
|
+
try {
|
|
344
|
+
if (require("electron-squirrel-startup")) {
|
|
345
|
+
app.quit();
|
|
346
|
+
}
|
|
347
|
+
} catch {
|
|
348
|
+
// electron-squirrel-startup not available
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
app.whenReady().then(async () => {
|
|
352
|
+
// Start the Python backend
|
|
353
|
+
backendProcess = startBackend();
|
|
354
|
+
|
|
355
|
+
// Create the window (hidden until ready-to-show)
|
|
356
|
+
createWindow();
|
|
357
|
+
|
|
358
|
+
// Initialize services (tray, agent manager, notifications)
|
|
359
|
+
initializeServices();
|
|
360
|
+
|
|
361
|
+
// Setup Windows Jump List (T11)
|
|
362
|
+
setupJumpList();
|
|
363
|
+
|
|
364
|
+
// Show loading state
|
|
365
|
+
await loadApp();
|
|
366
|
+
|
|
367
|
+
// Wait for backend to be ready, then reload
|
|
368
|
+
if (backendProcess) {
|
|
369
|
+
console.log("Waiting for backend to start...");
|
|
370
|
+
const ready = await waitForBackend(STARTUP_TIMEOUT);
|
|
371
|
+
|
|
372
|
+
if (ready && mainWindow && !mainWindow.isDestroyed()) {
|
|
373
|
+
console.log("Backend is ready! Loading app...");
|
|
374
|
+
try {
|
|
375
|
+
await mainWindow.loadURL(`http://localhost:${BACKEND_PORT}`);
|
|
376
|
+
} catch (error) {
|
|
377
|
+
console.error("Failed to load from backend:", error.message);
|
|
378
|
+
}
|
|
379
|
+
} else if (!ready) {
|
|
380
|
+
console.warn("Backend did not respond within timeout.");
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Auto-start enabled agents (T2)
|
|
385
|
+
if (agentProcessManager) {
|
|
386
|
+
try {
|
|
387
|
+
await agentProcessManager.startAllEnabled();
|
|
388
|
+
} catch (err) {
|
|
389
|
+
console.error("Failed to auto-start agents:", err.message);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
app.on("activate", async () => {
|
|
394
|
+
if (BrowserWindow.getAllWindows().length === 0) {
|
|
395
|
+
createWindow();
|
|
396
|
+
// Re-wire existing services to the new window (don't re-create — IPC handlers are already registered)
|
|
397
|
+
if (agentProcessManager) agentProcessManager.mainWindow = mainWindow;
|
|
398
|
+
if (trayManager) trayManager.mainWindow = mainWindow;
|
|
399
|
+
if (notificationService) notificationService.mainWindow = mainWindow;
|
|
400
|
+
try {
|
|
401
|
+
await loadApp();
|
|
402
|
+
} catch (err) {
|
|
403
|
+
console.error("[main] Failed to load app on activate:", err.message);
|
|
404
|
+
}
|
|
405
|
+
} else if (mainWindow) {
|
|
406
|
+
mainWindow.show();
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// ── Window-all-closed (C4 fix) ────────────────────────────────────────────
|
|
412
|
+
// Don't quit when window is hidden — tray keeps app alive
|
|
413
|
+
app.on("window-all-closed", () => {
|
|
414
|
+
// If minimize-to-tray is active, the window is just hidden, not closed.
|
|
415
|
+
// Only quit on macOS if the user explicitly quit (Cmd+Q).
|
|
416
|
+
const trayActive = trayManager && trayManager.minimizeToTray;
|
|
417
|
+
|
|
418
|
+
if (!trayActive && process.platform !== "darwin") {
|
|
419
|
+
// Trigger the will-quit path which handles async cleanup properly
|
|
420
|
+
app.quit();
|
|
421
|
+
}
|
|
422
|
+
// Otherwise: no-op. App stays running via system tray.
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// ── Quit lifecycle ─────────────────────────────────────────────────────────
|
|
426
|
+
// Electron's before-quit does NOT await async handlers.
|
|
427
|
+
// We use will-quit + event.preventDefault() to perform async cleanup, then re-quit.
|
|
428
|
+
|
|
429
|
+
let cleanupDone = false;
|
|
430
|
+
|
|
431
|
+
app.on("before-quit", () => {
|
|
432
|
+
isQuitting = true;
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
app.on("will-quit", (event) => {
|
|
436
|
+
if (cleanupDone) return; // Cleanup already finished, let the app quit
|
|
437
|
+
|
|
438
|
+
event.preventDefault(); // Prevent quit until cleanup is done
|
|
439
|
+
console.log("[main] will-quit: performing async cleanup...");
|
|
440
|
+
|
|
441
|
+
cleanup().then(() => {
|
|
442
|
+
cleanupDone = true;
|
|
443
|
+
console.log("[main] Cleanup complete, quitting...");
|
|
444
|
+
app.quit(); // Re-trigger quit — cleanupDone prevents infinite loop
|
|
445
|
+
}).catch((err) => {
|
|
446
|
+
console.error("[main] Cleanup error:", err.message);
|
|
447
|
+
cleanupDone = true;
|
|
448
|
+
app.quit();
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
async function cleanup() {
|
|
453
|
+
// Clean up notification timers
|
|
454
|
+
if (notificationService) {
|
|
455
|
+
notificationService.destroy();
|
|
456
|
+
notificationService = null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Stop all managed OS agents gracefully
|
|
460
|
+
if (agentProcessManager) {
|
|
461
|
+
console.log("Stopping all managed agents...");
|
|
462
|
+
try {
|
|
463
|
+
await agentProcessManager.stopAll();
|
|
464
|
+
} catch (err) {
|
|
465
|
+
console.error("Error stopping agents:", err.message);
|
|
466
|
+
}
|
|
467
|
+
agentProcessManager = null;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Destroy tray icon
|
|
471
|
+
if (trayManager) {
|
|
472
|
+
trayManager.destroy();
|
|
473
|
+
trayManager = null;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Stop the Python backend
|
|
477
|
+
if (backendProcess) {
|
|
478
|
+
console.log("Stopping backend process...");
|
|
479
|
+
const proc = backendProcess; // Save reference before nulling
|
|
480
|
+
backendProcess = null;
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
proc.kill("SIGTERM");
|
|
484
|
+
} catch {
|
|
485
|
+
// Already dead
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Wait for the process to exit, with a force-kill fallback
|
|
489
|
+
await new Promise((resolve) => {
|
|
490
|
+
// Check if already exited (exitCode is set once the process exits)
|
|
491
|
+
if (proc.exitCode !== null) {
|
|
492
|
+
resolve();
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const forceKillTimer = setTimeout(() => {
|
|
497
|
+
try {
|
|
498
|
+
proc.kill("SIGKILL");
|
|
499
|
+
} catch {
|
|
500
|
+
// Already dead
|
|
501
|
+
}
|
|
502
|
+
resolve();
|
|
503
|
+
}, 3000);
|
|
504
|
+
|
|
505
|
+
proc.once("exit", () => {
|
|
506
|
+
clearTimeout(forceKillTimer);
|
|
507
|
+
resolve();
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@amd-gaia/agent-ui",
|
|
3
|
+
"version": "0.17.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"productName": "GAIA Agent UI",
|
|
6
|
+
"description": "Privacy-first agentic AI interface with document Q&A - runs 100% locally on AMD Ryzen AI",
|
|
7
|
+
"author": "AMD AI Group",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/amd/gaia.git",
|
|
12
|
+
"directory": "src/gaia/apps/webui"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://amd-gaia.ai/guides/agent-ui",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/amd/gaia/issues"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"gaia",
|
|
20
|
+
"amd",
|
|
21
|
+
"ryzen-ai",
|
|
22
|
+
"agent",
|
|
23
|
+
"ai",
|
|
24
|
+
"local-llm",
|
|
25
|
+
"privacy",
|
|
26
|
+
"rag",
|
|
27
|
+
"document-qa",
|
|
28
|
+
"electron"
|
|
29
|
+
],
|
|
30
|
+
"main": "main.cjs",
|
|
31
|
+
"bin": {
|
|
32
|
+
"gaia-ui": "bin/gaia-ui.mjs"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"bin/",
|
|
36
|
+
"dist/",
|
|
37
|
+
"main.cjs",
|
|
38
|
+
"preload.cjs",
|
|
39
|
+
"services/",
|
|
40
|
+
"assets/",
|
|
41
|
+
"app.config.json",
|
|
42
|
+
"README.md",
|
|
43
|
+
"LICENSE"
|
|
44
|
+
],
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"dev": "vite",
|
|
50
|
+
"build": "tsc && vite build",
|
|
51
|
+
"preview": "vite preview",
|
|
52
|
+
"start": "electron .",
|
|
53
|
+
"package": "npm run build && electron-forge package",
|
|
54
|
+
"make": "npm run build && electron-forge make",
|
|
55
|
+
"prepublishOnly": "npm run build"
|
|
56
|
+
},
|
|
57
|
+
"config": {
|
|
58
|
+
"forge": "./forge.config.cjs"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"electron-squirrel-startup": "^1.0.0"
|
|
62
|
+
},
|
|
63
|
+
"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
|
+
"@types/react": "^18.2.48",
|
|
68
|
+
"@types/react-dom": "^18.2.18",
|
|
69
|
+
"@vitejs/plugin-react": "^4.2.1",
|
|
70
|
+
"electron": "^40.6.1",
|
|
71
|
+
"lucide-react": "^0.312.0",
|
|
72
|
+
"qrcode": "^1.5.4",
|
|
73
|
+
"react": "^18.2.0",
|
|
74
|
+
"react-dom": "^18.2.0",
|
|
75
|
+
"react-markdown": "^9.1.0",
|
|
76
|
+
"rehype-raw": "^7.0.0",
|
|
77
|
+
"rehype-sanitize": "^6.0.0",
|
|
78
|
+
"remark-gfm": "^4.0.1",
|
|
79
|
+
"typescript": "^5.3.3",
|
|
80
|
+
"vite": "^5.0.12",
|
|
81
|
+
"zustand": "^4.5.0"
|
|
82
|
+
}
|
|
83
|
+
}
|
package/preload.cjs
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GAIA Agent UI — Preload script (contextBridge)
|
|
6
|
+
*
|
|
7
|
+
* Exposes IPC channels to the renderer process via `window.gaiaAPI`.
|
|
8
|
+
* Required because main.cjs uses `contextIsolation: true`.
|
|
9
|
+
*
|
|
10
|
+
* Channels:
|
|
11
|
+
* agent:* — Agent process management (T2)
|
|
12
|
+
* tray:* — Tray icon/config (T1)
|
|
13
|
+
* notification:* — Desktop notifications & permission prompts (T5)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { contextBridge, ipcRenderer } = require("electron");
|
|
17
|
+
|
|
18
|
+
// Helper: subscribe to an IPC event and return an unsubscribe function
|
|
19
|
+
function onEvent(channel, callback) {
|
|
20
|
+
const handler = (_event, data) => callback(data);
|
|
21
|
+
ipcRenderer.on(channel, handler);
|
|
22
|
+
return () => ipcRenderer.removeListener(channel, handler);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
contextBridge.exposeInMainWorld("gaiaAPI", {
|
|
26
|
+
// ── Agent process management (T2) ─────────────────────────────────────
|
|
27
|
+
agent: {
|
|
28
|
+
start: (id) => ipcRenderer.invoke("agent:start", id),
|
|
29
|
+
stop: (id) => ipcRenderer.invoke("agent:stop", id),
|
|
30
|
+
restart: (id) => ipcRenderer.invoke("agent:restart", id),
|
|
31
|
+
status: (id) => ipcRenderer.invoke("agent:status", id),
|
|
32
|
+
statusAll: () => ipcRenderer.invoke("agent:status-all"),
|
|
33
|
+
sendRpc: (id, method, params) =>
|
|
34
|
+
ipcRenderer.invoke("agent:send-rpc", id, method, params),
|
|
35
|
+
getManifest: () => ipcRenderer.invoke("agent:get-manifest"),
|
|
36
|
+
install: (id) => ipcRenderer.invoke("agent:install", id),
|
|
37
|
+
uninstall: (id) => ipcRenderer.invoke("agent:uninstall", id),
|
|
38
|
+
|
|
39
|
+
// Event streams (return unsubscribe functions)
|
|
40
|
+
onStdout: (cb) => onEvent("agent:stdout", cb),
|
|
41
|
+
onStderr: (cb) => onEvent("agent:stderr", cb),
|
|
42
|
+
onStatusChange: (cb) => onEvent("agent:status-change", cb),
|
|
43
|
+
onCrashed: (cb) => onEvent("agent:crashed", cb),
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// ── Tray configuration (T1) ───────────────────────────────────────────
|
|
47
|
+
tray: {
|
|
48
|
+
getConfig: () => ipcRenderer.invoke("tray:get-config"),
|
|
49
|
+
setConfig: (cfg) => ipcRenderer.invoke("tray:set-config", cfg),
|
|
50
|
+
onNavigate: (cb) => onEvent("tray:navigate", cb),
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// ── Notifications & permission prompts (T5) ───────────────────────────
|
|
54
|
+
notification: {
|
|
55
|
+
onPermissionRequest: (cb) =>
|
|
56
|
+
onEvent("notification:permission-request", cb),
|
|
57
|
+
respondPermission: (id, action, remember) =>
|
|
58
|
+
ipcRenderer.invoke("notification:respond", id, action, remember),
|
|
59
|
+
onNotification: (cb) => onEvent("notification:new", cb),
|
|
60
|
+
},
|
|
61
|
+
});
|