@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.
Files changed (57) hide show
  1. package/bin/gaia-ui.cjs +2 -23
  2. package/dist/assets/AdvancedSettings-DXf2pogI.js +1 -0
  3. package/dist/assets/ConnectorsPane-CzLYXGMl.css +1 -0
  4. package/dist/assets/ConnectorsPane-R1yvKMNT.js +1 -0
  5. package/dist/assets/DocumentLibrary-CM6o8j2T.css +1 -0
  6. package/dist/assets/DocumentLibrary-DSOiP7Ft.js +4 -0
  7. package/dist/assets/FileBrowser-BlVSPjD2.js +4 -0
  8. package/dist/assets/FileBrowser-Cna3BsT7.css +1 -0
  9. package/dist/assets/MemoryDashboard-D_ZVZbS_.js +1 -0
  10. package/dist/assets/MemoryDashboard-DwrWAXi6.css +1 -0
  11. package/dist/assets/MobileAccessModal-BkVDm3YG.css +1 -0
  12. package/dist/assets/MobileAccessModal-CPjDeIIk.js +2 -0
  13. package/dist/assets/ScheduleManager-CjHJ4NbD.css +1 -0
  14. package/dist/assets/ScheduleManager-D1DOYVQo.js +1 -0
  15. package/dist/assets/api-Co-B9E0t.js +4 -0
  16. package/dist/assets/index-CMVmIBZ8.js +78 -0
  17. package/dist/assets/index-Y93uvqe7.css +1 -0
  18. package/dist/assets/useT-tWXlG-cT.js +3 -0
  19. package/dist/assets/zap-D-WCZ6dX.js +1 -0
  20. package/dist/icons/apple-touch-icon.png +0 -0
  21. package/dist/icons/icon-192.png +0 -0
  22. package/dist/icons/icon-512.png +0 -0
  23. package/dist/icons/icon-maskable-192.png +0 -0
  24. package/dist/icons/icon-maskable-512.png +0 -0
  25. package/dist/index.html +8 -3
  26. package/dist/manifest.json +16 -0
  27. package/dist/sw.js +49 -0
  28. package/main-safety-net.cjs +106 -8
  29. package/main.cjs +145 -162
  30. package/package.json +5 -3
  31. package/preload.cjs +19 -9
  32. package/services/agent-seeder.cjs +22 -13
  33. package/services/auto-updater.cjs +780 -216
  34. package/services/backend-installer.cjs +523 -365
  35. package/services/engine-install.cjs +233 -0
  36. package/services/index-query.cjs +7 -1
  37. package/services/lock-holders.cjs +200 -0
  38. package/services/quit-guard.cjs +36 -3
  39. package/services/tray-manager.cjs +12 -7
  40. package/dist/assets/AdvancedSettings-DERzP9Jc.js +0 -1
  41. package/dist/assets/ConnectorsPane-Dzu2DRTc.css +0 -1
  42. package/dist/assets/ConnectorsPane-s0fFxmus.js +0 -1
  43. package/dist/assets/DocumentLibrary-CIKp6qld.css +0 -1
  44. package/dist/assets/DocumentLibrary-DqapN_Tr.js +0 -4
  45. package/dist/assets/FileBrowser-Dil1lZJ4.css +0 -1
  46. package/dist/assets/FileBrowser-vAJV41RT.js +0 -4
  47. package/dist/assets/MemoryDashboard-CP5wVWkY.js +0 -1
  48. package/dist/assets/MemoryDashboard-Mo721-BI.css +0 -1
  49. package/dist/assets/MobileAccessModal-B31mPs50.js +0 -2
  50. package/dist/assets/MobileAccessModal-DuVy9F1u.css +0 -1
  51. package/dist/assets/ScheduleManager-D5c4gVIt.css +0 -1
  52. package/dist/assets/ScheduleManager-b0gTAf2R.js +0 -2
  53. package/dist/assets/apiBase-CS6zLsJY.js +0 -1
  54. package/dist/assets/index-C-EM_5yl.css +0 -1
  55. package/dist/assets/index-Cae5Fje-.js +0 -78
  56. package/dist/assets/zap-BogJn9oN.js +0 -1
  57. package/services/backend-installer-progress-dialog.cjs +0 -510
@@ -0,0 +1,233 @@
1
+ // Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ /**
5
+ * engine-install.cjs — runs the backend install behind the app window.
6
+ *
7
+ * Holds one status object, sends it to the window's EngineInstallScreen on each
8
+ * change, and waits on that screen's Try again after a failure. The install
9
+ * itself is backend-installer.cjs.
10
+ *
11
+ * Status: { phase: 'running'|'failed', stage, percent, message, error }
12
+ * percent is null when the stage has none to report. error, when failed:
13
+ * { stage, reason, message, logTail, previousVersion, lockedBy }; reason is
14
+ * one of FAILURE_REASONS or null, previousVersion is the version an update can
15
+ * go back to, or null, and lockedBy names the programs holding GAIA's files
16
+ * after a 'locked' failure on Windows (lock-holders.cjs), or is null.
17
+ */
18
+
19
+ "use strict";
20
+
21
+ const defaultLockHolders = require("./lock-holders.cjs");
22
+
23
+ // Keep these in sync with preload.cjs.
24
+ const IPC_PROGRESS_EVENT = "install:progress";
25
+ const IPC_STATUS_REQUEST = "install:status";
26
+ const IPC_RETRY_REQUEST = "install:retry";
27
+ const IPC_CONTINUE_REQUEST = "install:continue";
28
+ const IPC_OPEN_LOG_FILE = "install:open-log-file";
29
+ const IPC_REVERT_REQUEST = "install:revert";
30
+ const IPC_CLOSE_LOCKERS_REQUEST = "install:close-lockers";
31
+
32
+ /** Not an installer stage: the backend is installed and starting. */
33
+ const STARTING_STAGE = "start";
34
+ const AGENT_REFRESH_STAGE = "agent-refresh";
35
+ const CONTINUE = "continue";
36
+
37
+ /**
38
+ * @param {object} deps
39
+ * @param {object} deps.installer - backend-installer.cjs
40
+ * @param {object} deps.ipcMain
41
+ * @param {object} deps.shell
42
+ * @param {() => Electron.BrowserWindow|null} deps.getWindow
43
+ * @param {boolean} deps.isPackaged
44
+ * @param {(err: Error) => void} deps.onFailed - Runs once the failure is on screen.
45
+ * @param {() => string|null} deps.previousVersion - The version an update can go back to.
46
+ * @param {() => Promise<void>} deps.revert - Installs that version; the app then restarts into it.
47
+ * @param {object} [deps.lockHolders] - lock-holders.cjs
48
+ * @param {number} [deps.ownPid] - This app's pid; its own process tree is never offered for closing.
49
+ * @param {string} [deps.platform]
50
+ */
51
+ function createEngineInstall({
52
+ installer,
53
+ ipcMain,
54
+ shell,
55
+ getWindow,
56
+ isPackaged,
57
+ onFailed,
58
+ previousVersion,
59
+ revert,
60
+ lockHolders = defaultLockHolders,
61
+ ownPid = process.pid,
62
+ platform = process.platform,
63
+ }) {
64
+ let status = { phase: "running", stage: null, percent: null, message: "", error: null };
65
+ let wakeForRetry = null;
66
+ let lockedFiles = null;
67
+ let closing = false;
68
+ let quitting = null;
69
+ let installing = null;
70
+
71
+ /** Who holds `files` open, summarized for the screen; null when Windows can't say. */
72
+ async function findHolders(files) {
73
+ try {
74
+ const holders = lockHolders.summarizeHolders(await lockHolders.queryLockHolders(files), ownPid);
75
+ console.log(
76
+ `[main] Holding ${files.join(", ")}: ` +
77
+ (holders.map((h) => `${h.name} pid ${h.pid} (${h.path || "path unknown"}, ${h.isGaia ? "GAIA" : "not GAIA"})`).join("; ") || "nothing now")
78
+ );
79
+ return holders;
80
+ } catch (err) {
81
+ console.error(`[main] Could not find which programs hold ${files.join(", ")}: ${err.message}`);
82
+ return null;
83
+ }
84
+ }
85
+
86
+ function publish(next) {
87
+ status = { ...status, ...next };
88
+ const win = getWindow();
89
+ if (win && !win.isDestroyed()) win.webContents.send(IPC_PROGRESS_EVENT, status);
90
+ }
91
+
92
+ ipcMain.handle(IPC_STATUS_REQUEST, () => ({ ...status, logPath: installer.getLogPath() }));
93
+
94
+ // Resolves false when no failure is waiting, so a double click can't start a second install.
95
+ ipcMain.handle(IPC_RETRY_REQUEST, () => {
96
+ if (!wakeForRetry) return false;
97
+ wakeForRetry(true);
98
+ return true;
99
+ });
100
+
101
+ // Only a failed agent update can be passed: the app works without it, and it
102
+ // stays recorded, so the next launch tries again. A failed install can't.
103
+ ipcMain.handle(IPC_CONTINUE_REQUEST, () => {
104
+ if (!wakeForRetry || status.error?.stage !== AGENT_REFRESH_STAGE) return false;
105
+ wakeForRetry(CONTINUE);
106
+ return true;
107
+ });
108
+
109
+ // After a failed update: go back to the version before it. Its errors reach the screen.
110
+ ipcMain.handle(IPC_REVERT_REQUEST, async () => {
111
+ if (!wakeForRetry || !status.error?.previousVersion) return false;
112
+ await revert();
113
+ return true;
114
+ });
115
+
116
+ // Ends the GAIA processes the screen named, then installs again. Takes no
117
+ // pids from the renderer, and ends only a process that is still the one
118
+ // named, so a pid reused since cannot be hit.
119
+ ipcMain.handle(IPC_CLOSE_LOCKERS_REQUEST, async () => {
120
+ const shown = status.error?.lockedBy;
121
+ if (!wakeForRetry || closing || !shown?.length || !shown.every((h) => h.isGaia)) return false;
122
+ closing = true;
123
+ try {
124
+ const current = await lockHolders.queryLockHolders(lockedFiles);
125
+ const fresh = lockHolders.summarizeHolders(current, ownPid);
126
+ const pids = shown
127
+ .filter((h) => fresh.some((f) => f.isGaia && f.pid === h.pid && f.path === h.path))
128
+ .map((h) => h.pid);
129
+ console.log(`[main] Ending GAIA processes holding the install: ${pids.join(", ") || "none still running"}`);
130
+ await lockHolders.endProcessTrees(pids);
131
+ } finally {
132
+ closing = false;
133
+ }
134
+ wakeForRetry(true);
135
+ return true;
136
+ });
137
+
138
+ // Takes no path: the renderer only ever opens the installer's own log.
139
+ ipcMain.handle(IPC_OPEN_LOG_FILE, async () => {
140
+ const problem = await shell.openPath(installer.getLogPath());
141
+ return { ok: !problem, message: problem || null };
142
+ });
143
+
144
+ /**
145
+ * Install, and after a failure wait for Try again. Resolves true once the
146
+ * backend is installed, false when stop() ended the install or the wait.
147
+ */
148
+ async function run() {
149
+ quitting = new AbortController();
150
+ for (;;) {
151
+ publish({ phase: "running", stage: null, percent: null, message: "", error: null });
152
+ try {
153
+ installing = installer.ensureBackend({
154
+ isPackaged,
155
+ onProgress: (stage, percent, message) => publish({ stage, percent, message }),
156
+ signal: quitting.signal,
157
+ });
158
+ await installing;
159
+ return true;
160
+ } catch (err) {
161
+ if (quitting.signal.aborted) {
162
+ console.log(`[main] Backend install stopped for the quit: ${err.message}`);
163
+ return false;
164
+ }
165
+ console.error(`[main] Backend install failed: ${err.stack || err}`);
166
+ lockedFiles = err.reason === "locked" && err.lockedFiles?.length ? err.lockedFiles : null;
167
+ const lockedBy = lockedFiles && platform === "win32" ? await findHolders(lockedFiles) : null;
168
+ const retry = new Promise((resolve) => {
169
+ wakeForRetry = resolve;
170
+ });
171
+ publish({
172
+ phase: "failed",
173
+ error: {
174
+ stage: err.stage || null,
175
+ reason: err.reason || null,
176
+ // The rest is the tool's output, which the log tail carries.
177
+ message: String(err.message).split("\n")[0],
178
+ logTail: logTailOrWhy(),
179
+ previousVersion: previousVersion(),
180
+ lockedBy,
181
+ },
182
+ });
183
+ onFailed(err);
184
+ const retrying = await retry;
185
+ wakeForRetry = null;
186
+ if (retrying === CONTINUE) return true;
187
+ if (!retrying) return false;
188
+ }
189
+ }
190
+ }
191
+
192
+ /** The log's last lines; a log that can't be read says why instead of crashing the failure screen. */
193
+ function logTailOrWhy() {
194
+ try {
195
+ return installer.readLogTail();
196
+ } catch (err) {
197
+ console.error(`[main] Could not read the install log: ${err.stack || err}`);
198
+ return [`The install log could not be read: ${err.message}`];
199
+ }
200
+ }
201
+
202
+ return {
203
+ run,
204
+ /** The install is done and the backend process is coming up. */
205
+ starting: () => publish({ stage: STARTING_STAGE, percent: null, message: "" }),
206
+ /**
207
+ * Quitting: stop a running install, with every process it started, or
208
+ * release a run() waiting on Try again. Resolves once the install has ended.
209
+ */
210
+ stop: async () => {
211
+ if (wakeForRetry) wakeForRetry(false);
212
+ if (!quitting) return;
213
+ quitting.abort();
214
+ // run() reports the outcome; this only waits for the processes to be gone.
215
+ await installing?.then(
216
+ () => {},
217
+ () => {}
218
+ );
219
+ },
220
+ };
221
+ }
222
+
223
+ module.exports = {
224
+ createEngineInstall,
225
+ STARTING_STAGE,
226
+ IPC_PROGRESS_EVENT,
227
+ IPC_STATUS_REQUEST,
228
+ IPC_RETRY_REQUEST,
229
+ IPC_CONTINUE_REQUEST,
230
+ IPC_OPEN_LOG_FILE,
231
+ IPC_REVERT_REQUEST,
232
+ IPC_CLOSE_LOCKERS_REQUEST,
233
+ };
@@ -46,4 +46,10 @@ function buildIndexQuery(port) {
46
46
  return { api: `http://127.0.0.1:${port}${API_PATH}` };
47
47
  }
48
48
 
49
- module.exports = { buildIndexQuery, API_PATH };
49
+ /**
50
+ * Query for the page shown while the backend installs: there is no port yet.
51
+ * Must match isInstallScreen in src/gaia/apps/webui/src/utils/engineInstall.ts.
52
+ */
53
+ const INSTALL_SCREEN_QUERY = Object.freeze({ engine: "install" });
54
+
55
+ module.exports = { buildIndexQuery, API_PATH, INSTALL_SCREEN_QUERY };
@@ -0,0 +1,200 @@
1
+ // Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ /**
5
+ * lock-holders.cjs — which processes hold a file open on Windows (#6570).
6
+ *
7
+ * Asks Windows Restart Manager, the API installers use for "files in use",
8
+ * through a PowerShell script, then sorts the holders into GAIA processes the
9
+ * user can let the app close and other programs it must only name.
10
+ */
11
+
12
+ "use strict";
13
+
14
+ const childProcess = require("child_process");
15
+
16
+ const QUERY_TIMEOUT_MS = 30000;
17
+ const KILL_TIMEOUT_MS = 10000;
18
+ // taskkill's exit code when the process has already exited.
19
+ const TASKKILL_NOT_FOUND = 128;
20
+
21
+ const RM_SCRIPT = String.raw`
22
+ $ErrorActionPreference = 'Stop'
23
+ $files = [string[]](ConvertFrom-Json $env:GAIA_LOCKED_FILES)
24
+ Add-Type -TypeDefinition @'
25
+ using System;
26
+ using System.Runtime.InteropServices;
27
+ public static class GaiaRestartManager {
28
+ [StructLayout(LayoutKind.Sequential)]
29
+ struct RM_UNIQUE_PROCESS { public int dwProcessId; public System.Runtime.InteropServices.ComTypes.FILETIME ProcessStartTime; }
30
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
31
+ struct RM_PROCESS_INFO {
32
+ public RM_UNIQUE_PROCESS Process;
33
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string strAppName;
34
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] public string strServiceShortName;
35
+ public int ApplicationType; public uint AppStatus; public uint TSSessionId;
36
+ [MarshalAs(UnmanagedType.Bool)] public bool bRestartable;
37
+ }
38
+ [DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)] static extern int RmStartSession(out uint h, int flags, string key);
39
+ [DllImport("rstrtmgr.dll")] static extern int RmEndSession(uint h);
40
+ [DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)] static extern int RmRegisterResources(uint h, uint nFiles, string[] files, uint nApps, RM_UNIQUE_PROCESS[] apps, uint nServices, string[] services);
41
+ [DllImport("rstrtmgr.dll")] static extern int RmGetList(uint h, out uint needed, ref uint count, [In, Out] RM_PROCESS_INFO[] info, ref uint reasons);
42
+ public static int[] Holders(string[] files) {
43
+ uint h;
44
+ int rc = RmStartSession(out h, 0, Guid.NewGuid().ToString());
45
+ if (rc != 0) throw new Exception("RmStartSession failed with " + rc);
46
+ try {
47
+ rc = RmRegisterResources(h, (uint)files.Length, files, 0, null, 0, null);
48
+ if (rc != 0) throw new Exception("RmRegisterResources failed with " + rc);
49
+ uint needed = 0, count = 0, reasons = 0;
50
+ rc = RmGetList(h, out needed, ref count, null, ref reasons);
51
+ if (rc == 0) return new int[0];
52
+ if (rc != 234) throw new Exception("RmGetList failed with " + rc);
53
+ RM_PROCESS_INFO[] info = new RM_PROCESS_INFO[needed];
54
+ count = needed;
55
+ rc = RmGetList(h, out needed, ref count, info, ref reasons);
56
+ if (rc != 0) throw new Exception("RmGetList failed with " + rc);
57
+ int[] pids = new int[count];
58
+ for (int i = 0; i < count; i++) pids[i] = info[i].Process.dwProcessId;
59
+ return pids;
60
+ } finally { RmEndSession(h); }
61
+ }
62
+ }
63
+ '@
64
+ function Describe($p) {
65
+ [pscustomobject]@{ pid = [int]$p.ProcessId; name = $p.Name; path = $p.ExecutablePath; commandLine = $p.CommandLine }
66
+ }
67
+ $holderIds = [GaiaRestartManager]::Holders($files)
68
+ $byId = @{}
69
+ if ($holderIds.Count -gt 0) {
70
+ foreach ($proc in Get-CimInstance Win32_Process) { $byId[[int]$proc.ProcessId] = $proc }
71
+ }
72
+ $out = @()
73
+ foreach ($holderId in $holderIds) {
74
+ $p = $byId[[int]$holderId]
75
+ if (-not $p) { continue }
76
+ $ancestors = @()
77
+ $child = $p
78
+ while ($ancestors.Count -lt 16) {
79
+ $a = $byId[[int]$child.ParentProcessId]
80
+ # A parent that started after its child is an unrelated process that reused the pid.
81
+ if (-not $a -or $a.ProcessId -eq $child.ProcessId -or $a.CreationDate -gt $child.CreationDate) { break }
82
+ $ancestors += Describe $a
83
+ $child = $a
84
+ }
85
+ $entry = Describe $p
86
+ $entry | Add-Member -NotePropertyName ancestors -NotePropertyValue $ancestors
87
+ $out += $entry
88
+ }
89
+ ConvertTo-Json -InputObject @($out) -Depth 4 -Compress
90
+ `;
91
+
92
+ function encodedCommand(script) {
93
+ return Buffer.from(script, "utf16le").toString("base64");
94
+ }
95
+
96
+ /**
97
+ * The processes holding any of `files` open, each with its ancestors (parent
98
+ * first): [{ pid, name, path, commandLine, ancestors: [...] }]. Rejects with
99
+ * the script's stderr when Restart Manager or PowerShell fails.
100
+ */
101
+ function queryLockHolders(files, { execFile = childProcess.execFile, timeoutMs = QUERY_TIMEOUT_MS } = {}) {
102
+ return new Promise((resolve, reject) => {
103
+ execFile(
104
+ "powershell.exe",
105
+ ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encodedCommand(RM_SCRIPT)],
106
+ {
107
+ env: { ...process.env, GAIA_LOCKED_FILES: JSON.stringify(files) },
108
+ timeout: timeoutMs,
109
+ windowsHide: true,
110
+ maxBuffer: 4 * 1024 * 1024,
111
+ },
112
+ (err, stdout, stderr) => {
113
+ if (err) {
114
+ reject(new Error(`Restart Manager lookup for ${files.join(", ")} failed: ${String(stderr || err.message).trim()}`));
115
+ return;
116
+ }
117
+ try {
118
+ resolve(JSON.parse(String(stdout).trim() || "[]"));
119
+ } catch (parseErr) {
120
+ reject(new Error(`Restart Manager lookup printed something other than JSON: ${String(stdout).slice(0, 200)}`));
121
+ }
122
+ }
123
+ );
124
+ });
125
+ }
126
+
127
+ function winPath(p) {
128
+ return String(p || "").replace(/\//g, "\\");
129
+ }
130
+
131
+ /** A GAIA install's own executable: under a .gaia venv, agents or bin folder, or GAIA's desktop or agent binary. */
132
+ function isGaiaExecutable(exePath) {
133
+ const p = winPath(exePath).toLowerCase();
134
+ if (!p) return false;
135
+ if (/\\\.gaia\\(?:venv\\scripts|agents|bin)\\/.test(p)) return true;
136
+ const base = p.split("\\").pop();
137
+ return base === "gaia-desktop.exe" || base === "gaia-agent.exe";
138
+ }
139
+
140
+ /** Where a holder came from, for people: the folder holding its `.gaia`, else its own folder. */
141
+ function startedFrom(exePath) {
142
+ const p = winPath(exePath);
143
+ if (!p) return null;
144
+ const at = p.toLowerCase().indexOf("\\.gaia\\");
145
+ if (at > 0) return p.slice(0, at);
146
+ const slash = p.lastIndexOf("\\");
147
+ return slash > 0 ? p.slice(0, slash) : null;
148
+ }
149
+
150
+ /**
151
+ * One entry per program to name: a GAIA holder reports the topmost GAIA
152
+ * process above it (so gaia.exe stands for its python child), anything else
153
+ * reports itself. Holders inside this app's own process tree are never GAIA
154
+ * to close. Returns [{ pid, name, path, folder, isGaia }].
155
+ */
156
+ function summarizeHolders(raw, ownPid) {
157
+ const byPid = new Map();
158
+ for (const holder of raw || []) {
159
+ const chain = [holder, ...(holder.ancestors || [])];
160
+ const ownAt = chain.findIndex((p) => p.pid === ownPid);
161
+ const outside = ownAt === -1 ? chain : chain.slice(0, ownAt);
162
+ let root = null;
163
+ for (const p of outside) if (isGaiaExecutable(p.path || p.name)) root = p;
164
+ const shown = root || holder;
165
+ if (byPid.has(shown.pid)) continue;
166
+ byPid.set(shown.pid, {
167
+ pid: shown.pid,
168
+ name: shown.name || null,
169
+ path: shown.path || null,
170
+ folder: startedFrom(shown.path),
171
+ isGaia: Boolean(root),
172
+ });
173
+ }
174
+ return [...byPid.values()];
175
+ }
176
+
177
+ /** End each pid and its children; a pid that has already exited is not an error. */
178
+ async function endProcessTrees(pids, { execFile = childProcess.execFile } = {}) {
179
+ for (const pid of pids) {
180
+ await new Promise((resolve, reject) => {
181
+ execFile(
182
+ "taskkill",
183
+ ["/PID", String(pid), "/T", "/F"],
184
+ { timeout: KILL_TIMEOUT_MS, windowsHide: true },
185
+ (err, stdout, stderr) => {
186
+ if (!err || err.code === TASKKILL_NOT_FOUND) resolve();
187
+ else reject(new Error(`Could not end process ${pid}: ${String(stderr || err.message).trim()}`));
188
+ }
189
+ );
190
+ });
191
+ }
192
+ }
193
+
194
+ module.exports = {
195
+ queryLockHolders,
196
+ summarizeHolders,
197
+ endProcessTrees,
198
+ isGaiaExecutable,
199
+ startedFrom,
200
+ };
@@ -70,15 +70,18 @@ const RESTART_FOR_UPDATE = Object.freeze({
70
70
  /**
71
71
  * The warning for this work, or null when nothing would be lost.
72
72
  *
73
- * @param {{answers: Array<{title: string|null}>, indexing: Array<{filename: string|null}>}} work
73
+ * @param {{answers: Array<{title: string|null}>, indexing: Array<{filename: string|null}>, setup?: boolean}} work
74
74
  * @param {string} [verb] "Quitting" or "Restarting".
75
75
  */
76
76
  function describeWork(work, verb = QUIT.verb) {
77
77
  const answers = work.answers.map((a) => a.title || "a chat");
78
78
  const files = work.indexing.map((d) => d.filename || "a document");
79
- if (answers.length === 0 && files.length === 0) return null;
79
+ if (answers.length === 0 && files.length === 0 && !work.setup) return null;
80
80
 
81
81
  const lines = [];
82
+ if (work.setup) {
83
+ lines.push(`${verb} pauses setup. What has downloaded is kept, and setup offers to resume next time.`);
84
+ }
82
85
  if (answers.length === 1) {
83
86
  lines.push(`${verb} stops the answer in ${quoteList(answers)}, and that answer will be lost.`);
84
87
  } else if (answers.length > 1) {
@@ -88,7 +91,11 @@ function describeWork(work, verb = QUIT.verb) {
88
91
  lines.push(`Indexing ${quoteList(files)} stops too; add ${files.length === 1 ? "it" : "them"} again later.`);
89
92
  }
90
93
  let message;
91
- if (files.length === 0) {
94
+ if (work.setup && answers.length === 0 && files.length === 0) {
95
+ message = "GAIA is still setting up.";
96
+ } else if (work.setup) {
97
+ message = "GAIA is still working.";
98
+ } else if (files.length === 0) {
92
99
  message = answers.length === 1 ? "GAIA is still writing an answer." : `GAIA is still writing ${answers.length} answers.`;
93
100
  } else if (answers.length === 0) {
94
101
  message = "GAIA is still indexing documents.";
@@ -228,9 +235,35 @@ function attachWindowGuards(win, { app, guard, getTray, isQuitting, platform, re
228
235
  win.on("session-end", () => guard.allow());
229
236
  }
230
237
 
238
+ /**
239
+ * SIGTERM (logout, shutdown, `kill`) quits without the question: nobody is
240
+ * there to answer it, so the dialog would hold the app until it is SIGKILLed.
241
+ * Electron's own SIGTERM handling quits through the guard, which asks.
242
+ *
243
+ * @param {{proc: NodeJS.Process, app: Electron.App,
244
+ * guard: ReturnType<typeof installQuitGuard>, logger?: {log: Function}}} deps
245
+ * @returns {Promise<void>} Settles once the handler is back on top of Electron's.
246
+ */
247
+ function quitOnTermSignal({ proc, app, guard, logger = console }) {
248
+ proc.on("SIGTERM", () => {
249
+ logger.log("[main] SIGTERM: quitting without asking about running work");
250
+ guard.allow();
251
+ app.quit();
252
+ });
253
+ // Electron installs its own SIGTERM handler after the main script runs, and
254
+ // it replaces Node's, so the listener above never fires. Re-adding every
255
+ // listener once ready makes Node install its handler again, over Electron's.
256
+ return app.whenReady().then(() => {
257
+ const listeners = proc.listeners("SIGTERM");
258
+ proc.removeAllListeners("SIGTERM");
259
+ for (const listener of listeners) proc.on("SIGTERM", listener);
260
+ });
261
+ }
262
+
231
263
  module.exports = {
232
264
  QUIT_ANYWAY_MS,
233
265
  attachWindowGuards,
266
+ quitOnTermSignal,
234
267
  backendJson,
235
268
  fetchWorkInProgress,
236
269
  describeWork,
@@ -42,8 +42,8 @@ function describeBusy(work) {
42
42
  const GAIA_DIR = path.join(os.homedir(), ".gaia");
43
43
  const CONFIG_PATH = path.join(GAIA_DIR, "tray-config.json");
44
44
  const LINUX_AUTOSTART_FILE = "gaia-agent-ui.desktop";
45
- // The Run value the Windows installer writes (installer/nsis/installer.nsh,
46
- // ${PRODUCT_NAME}); the switch owns that same entry, command line included.
45
+ // The Run value this switch owns; the installer removes it on uninstall
46
+ // (installer/nsis/installer.nsh, ${PRODUCT_NAME}) and earlier ones wrote it.
47
47
  const WIN_LOGIN_ITEM_NAME = "GAIA";
48
48
  const WIN_LOGIN_ITEM_ARGS = ["--minimized"];
49
49
 
@@ -778,11 +778,16 @@ class TrayManager {
778
778
  });
779
779
  }
780
780
 
781
+ /** The tray exists before the backend does; its port is known once it starts. */
782
+ setBackendPort(port) {
783
+ this._backendPort = port;
784
+ }
785
+
781
786
  /**
782
787
  * Line the sign-in switch up with what the OS will actually do. Call once
783
- * at startup. Until the user sets the switch, the OS decides it (the
784
- * Windows installer turns sign-in start on). Once they turn it off, an
785
- * entry that came back (a reinstall or update rewrites it) is removed again.
788
+ * at startup. Until the user sets the switch, the OS decides it. Once they
789
+ * turn it off, an entry that came back (an earlier installer wrote it) is
790
+ * removed again.
786
791
  */
787
792
  syncLoginItem() {
788
793
  // A dev build's exe is electron itself; never touch the installed app's entry.
@@ -824,8 +829,8 @@ class TrayManager {
824
829
  }
825
830
 
826
831
  /**
827
- * Register or remove GAIA as a login item. On Windows that is the same Run
828
- * value the installer writes, launched into the tray; elsewhere "Start in
832
+ * Register or remove GAIA as a login item. On Windows that is the Run value
833
+ * the uninstaller removes, launched into the tray; elsewhere "Start in
829
834
  * the tray" decides whether that launch shows the window.
830
835
  * @throws {Error} When the OS refused; the caller's save is skipped.
831
836
  */
@@ -1 +0,0 @@
1
- import{r as e}from"./rolldown-runtime-XKyaHtl9.js";import{n as t,o as n,r}from"./apiBase-CS6zLsJY.js";import{Ft as i,G as a,J as o,Yt as s,a as c,ct as l,dn as u,et as d,gt as f,ht as p,it as m,mt as h,nt as g,o as _,pn as v,r as y,rt as b,tt as x}from"./index-Cae5Fje-.js";var S=e(n(),1),C=t();function w(){let[e,t]=(0,S.useState)(null),[n,r]=(0,S.useState)(null),[i,a]=(0,S.useState)(``),[o,l]=(0,S.useState)(``),[u,f]=(0,S.useState)(!1),[p,m]=(0,S.useState)(null),[g,_]=(0,S.useState)(!1);(0,S.useEffect)(()=>{d().then(e=>{t(e),a(e.saved_base_url??e.base_url??``)}).catch(e=>r(e instanceof Error?e.message:String(e)))},[]);let y=(0,S.useCallback)(async e=>{f(!0),m(null);try{let n=await h(e);t(n),l(``),_(!1)}catch(e){_(c(e)),m(e instanceof Error?e.message:String(e))}finally{f(!1)}},[]),b=()=>void y({mode:`external`,base_url:i.trim(),api_key:o.trim()||void 0});if(n)return(0,C.jsxs)(`section`,{className:`settings-section`,children:[(0,C.jsx)(`h4`,{children:`Model server`}),(0,C.jsxs)(`div`,{className:`model-warning`,role:`alert`,children:[(0,C.jsx)(v,{size:14}),(0,C.jsxs)(`div`,{className:`model-warning-content`,children:[(0,C.jsx)(`strong`,{children:`Could not read which model server GAIA uses`}),(0,C.jsx)(`p`,{children:n})]})]})]});if(!e)return null;let x=e.mode===`external`;return(0,C.jsxs)(`section`,{className:`settings-section`,"aria-label":`Model server`,children:[(0,C.jsx)(`h4`,{children:`Model server`}),(0,C.jsx)(`p`,{className:`model-override-desc`,role:`status`,children:e.source===`env`?(0,C.jsxs)(C.Fragment,{children:[`GAIA uses the Lemonade Server at `,(0,C.jsx)(`code`,{children:e.base_url}),`, set by `,(0,C.jsx)(`code`,{children:`LEMONADE_BASE_URL`}),`.`]}):x?(0,C.jsxs)(C.Fragment,{children:[`GAIA uses your Lemonade Server at `,(0,C.jsx)(`code`,{children:e.base_url}),`.`]}):(0,C.jsx)(C.Fragment,{children:`GAIA uses its own Lemonade Server.`})}),e.source===`env`?(0,C.jsxs)(`p`,{className:`model-status-hint`,children:[`Unset `,(0,C.jsx)(`code`,{children:`LEMONADE_BASE_URL`}),` and restart GAIA to change this here.`]}):(0,C.jsxs)(C.Fragment,{children:[(0,C.jsxs)(`p`,{className:`model-status-hint`,children:[`Already run Lemonade? Use it, so this PC keeps one server and one copy of each model.`,` `,e.what_gaia_does]}),(0,C.jsxs)(`form`,{className:`model-input-row`,onSubmit:e=>{e.preventDefault(),b()},children:[(0,C.jsx)(`input`,{type:`text`,className:`model-input`,placeholder:`http://localhost:13305`,value:i,onChange:e=>{a(e.target.value),m(null)},disabled:u,spellCheck:!1,autoCapitalize:`off`,autoCorrect:`off`,"aria-label":`Lemonade Server address`}),(0,C.jsx)(`input`,{type:`password`,className:`model-input`,autoComplete:`off`,placeholder:e.api_key_stored?`API key (blank keeps the saved one)`:`API key, if it needs one`,value:o,onChange:e=>l(e.target.value),disabled:u,"aria-label":`Lemonade API key`}),(0,C.jsx)(`div`,{className:`model-btn-group`,children:(0,C.jsx)(`button`,{type:`submit`,className:`btn-model-save`,disabled:u||!i.trim(),children:u?(0,C.jsxs)(C.Fragment,{children:[(0,C.jsx)(s,{size:13,className:`btn-spinner`}),` Checking…`]}):`Use this server`})})]}),x&&(0,C.jsx)(`div`,{className:`setting-actions`,children:(0,C.jsx)(`button`,{type:`button`,className:`btn-secondary`,disabled:u,onClick:()=>void y({mode:`gaia`}),children:`Use GAIA’s own`})}),p&&(0,C.jsxs)(`div`,{className:`model-warning`,role:`alert`,children:[(0,C.jsx)(v,{size:14}),(0,C.jsxs)(`div`,{className:`model-warning-content`,children:[(0,C.jsx)(`strong`,{children:g?`Needs the API key`:`Not switched`}),(0,C.jsx)(`p`,{children:p})]})]})]})]})}var T={vulkan:`Vulkan`,rocm:`ROCm`,metal:`Metal`,cpu:`CPU`,npu:`NPU`,remote:`Remote (cloud)`},E={gaia:`GAIA's own Lemonade`,user:`Your Lemonade`,remote:`Lemonade on another machine`};function D(e){return e?T[e]??e:`unknown`}function O(e){if(!e)return`unknown`;let t=D(e.backend);return e.backend_source===`lemonade_default`?`${t} (Lemonade's default)`:t}function k(e){return e?`${Math.round(e/1024)}K tokens`:`unknown`}function A({label:e,value:t,title:n}){return(0,C.jsxs)(`div`,{className:`status-row`,children:[(0,C.jsx)(`span`,{className:`status-label`,children:e}),(0,C.jsx)(`span`,{className:`status-value model-server-value`,title:n??t,children:t})]})}function j(){let e=i(e=>e.openSettings),[t,n]=(0,S.useState)(null),[r,a]=(0,S.useState)(null),[c,u]=(0,S.useState)(null),[d,f]=(0,S.useState)(null),[m,h]=(0,S.useState)(!1);(0,S.useEffect)(()=>{g().then(n).catch(e=>a(e instanceof Error?e.message:String(e)))},[]);let _=(0,S.useCallback)(async e=>{u(e),f(null);try{let t=await p(e);n(t),f({ok:!0,text:`Saved. It applies the next time the chat model loads.`})}catch(e){f({ok:!1,text:e instanceof Error?e.message:String(e)})}finally{u(null)}},[]),v=t?.chat_model??null,y=(0,S.useCallback)(async()=>{if(v?.name){h(!0);try{if(!await l(v.name,v.ctx_size??void 0))return;f({ok:!0,text:`Reloading ${v.name}. Reopen this panel in a moment to see its backend.`})}catch(e){f({ok:!1,text:e instanceof Error?e.message:String(e)})}finally{h(!1)}}},[v]),b=(0,C.jsx)(`button`,{type:`button`,className:`btn-secondary`,onClick:()=>e(`model`),children:`Change model`});if(r)return(0,C.jsxs)(`section`,{className:`settings-section`,"aria-label":`Model server`,children:[(0,C.jsx)(`h4`,{children:`Model server`}),(0,C.jsx)(`p`,{className:`settings-error`,role:`alert`,children:r})]});if(!t)return(0,C.jsxs)(`section`,{className:`settings-section`,"aria-label":`Model server`,children:[(0,C.jsx)(`h4`,{children:`Model server`}),(0,C.jsx)(`p`,{className:`loading-text`,children:`Checking the model server…`})]});if(!t.reachable)return(0,C.jsxs)(`section`,{className:`settings-section`,"aria-label":`Model server`,children:[(0,C.jsx)(`h4`,{children:`Model server`}),(0,C.jsx)(`p`,{className:`settings-error`,role:`alert`,children:t.error}),b]});let x=t.available_backends??[],w=t.preferred_backend??null,T=[`auto`,...x.map(e=>e.name)];w&&!T.includes(w)&&T.push(w);let j=w??`auto`,M=x.filter(e=>e.state===`installable`).map(e=>D(e.name)),N=!!v?.name&&v.recipe===`llamacpp`;return(0,C.jsxs)(`section`,{className:`settings-section`,"aria-label":`Model server`,children:[(0,C.jsx)(`h4`,{children:`Model server`}),(0,C.jsxs)(`div`,{className:`status-grid`,children:[(0,C.jsx)(A,{label:`Server`,value:`${E[t.owner]??t.owner}${t.version?` v${t.version}`:``}`}),(0,C.jsx)(A,{label:`Address`,value:t.base_url}),(0,C.jsx)(A,{label:`Chat model`,value:v?.name?o(v.name):`None loaded`,title:v?.name??void 0}),v&&(0,C.jsx)(A,{label:`Runs on`,value:O(v)}),v&&v.backend!==`remote`&&(0,C.jsx)(A,{label:`Context window`,value:k(v.ctx_size)}),(0,C.jsx)(A,{label:`Embedding model`,value:t.embedding_model?.name?`${t.embedding_model.name} · ${O(t.embedding_model)}`:`None loaded`})]}),t.embedding_note&&(0,C.jsx)(`p`,{className:`settings-hint`,children:t.embedding_note}),t.system_info_error&&(0,C.jsx)(`p`,{className:`settings-hint`,children:t.system_info_error}),(0,C.jsxs)(`div`,{className:`settings-field model-server-backend`,children:[(0,C.jsx)(`span`,{className:`settings-label`,children:`llama.cpp backend for chat models`}),x.length===0&&!w?(0,C.jsx)(`span`,{className:`settings-hint`,children:`This Lemonade does not report which backends this PC can run, so there is nothing to choose. Update Lemonade to pick one here.`}):(0,C.jsxs)(C.Fragment,{children:[(0,C.jsx)(`div`,{className:`segmented`,role:`radiogroup`,"aria-label":`llama.cpp backend`,children:T.map(e=>(0,C.jsxs)(`button`,{type:`button`,role:`radio`,"aria-checked":j===e,className:j===e?`is-active`:``,disabled:c!==null,onClick:()=>{e!==j&&_(e)},children:[c===e?(0,C.jsx)(s,{size:12,className:`spin`}):null,e===`auto`?`Auto${t.default_backend?` (${D(t.default_backend)})`:``}`:D(e)]},e))}),(0,C.jsxs)(`span`,{className:`settings-hint`,children:[`Applies the next time a chat model loads. The embedding model keeps its own backend.`,M.length>0&&` ${M.join(`, `)} downloads on first use.`]})]}),d&&(0,C.jsx)(`p`,{className:d.ok?`settings-ok`:`settings-error`,role:d.ok?`status`:`alert`,children:d.text})]}),(0,C.jsxs)(`div`,{className:`settings-row`,children:[b,N&&(0,C.jsxs)(`button`,{type:`button`,className:`btn-secondary`,onClick:()=>void y(),disabled:m,children:[m?(0,C.jsx)(s,{size:13,className:`spin`}):null,`Reload chat model now`]})]})]})}function M(){let[e,t]=(0,S.useState)(null),[n,i]=(0,S.useState)(!0),[o,c]=(0,S.useState)([]),[l,d]=(0,S.useState)(``),[p,h]=(0,S.useState)(``),[g,T]=(0,S.useState)(!1),[E,D]=(0,S.useState)(!1),[O,k]=(0,S.useState)(null),[A,M]=(0,S.useState)(!1),P=(0,S.useRef)(null),[F,I]=(0,S.useState)(!1),[L,R]=(0,S.useState)(!1),[z,B]=(0,S.useState)(!1),[V,H]=(0,S.useState)(null);(0,S.useEffect)(()=>{r.system.info(`Checking system status...`);let e=r.system.time();m().then(n=>{t(n),r.system.timed(`System status received`,e,{lemonade:n.lemonade_running?`running`:`stopped`,model:n.model_loaded||`none`,embedding:n.embedding_model_loaded?`yes`:`no`,disk:`${n.disk_space_gb}GB free`,memory:n.memory_available_gb==null?`unknown`:`${n.memory_available_gb}GB available`}),n.lemonade_running||r.system.warn(`Lemonade Server is NOT running.`)}).catch(e=>{r.system.error(`Failed to get system status`,e),t(null)}).finally(()=>i(!1)),x().then(e=>c(e.servers)).catch(()=>{}),b().then(e=>{let t=e.custom_model??``;d(t),h(t),I(e.dynamic_tools),R(e.dynamic_tools_locked)}).catch(e=>{r.system.error(`Failed to load settings`,e)}).finally(()=>T(!0))},[]),(0,S.useEffect)(()=>()=>{P.current&&clearTimeout(P.current)},[]);let U=(0,S.useCallback)(async()=>{let e=l.trim(),t=e.length>0?e:``;D(!0),k(null),M(!1);try{r.system.info(`Saving custom_model override`,{custom_model:t});let e=(await f({custom_model:t})).custom_model??``;d(e),h(e),M(!0),P.current&&clearTimeout(P.current),P.current=setTimeout(()=>M(!1),2200)}catch(e){let t=e instanceof Error?e.message:String(e);r.system.error(`Failed to save custom_model`,e),k(t)}finally{D(!1)}},[l]),W=(0,S.useCallback)(async()=>{if(L||z)return;let e=F,t=!e;I(t),B(!0),H(null);try{r.system.info(`Saving dynamic_tools setting`,{dynamic_tools:t});let e=await f({dynamic_tools:t});I(e.dynamic_tools),R(e.dynamic_tools_locked)}catch(t){let n=t instanceof Error?t.message:String(t);r.system.error(`Failed to save dynamic_tools`,t),I(e),H(n)}finally{B(!1)}},[F,L,z]),G=l.trim()!==p.trim(),K=e?e.default_model_name:a,{isLoadingModel:q,isDownloadingModel:J,loadModel:Y,downloadModel:X}=y(K??void 0),Z=!!e?.default_model_is_cloud,Q=!Z&&!!(e?.lemonade_running&&e.model_loaded&&e.expected_model_loaded===!1),$=!Z&&!!(e?.lemonade_running&&e.model_loaded&&e.context_size_sufficient===!1),ee=!Z&&!(!e?.lemonade_running||e.model_loaded||e.model_downloaded!==!1),te=(Q||$)&&K!==null;return(0,C.jsxs)(`div`,{className:`settings-pane`,children:[(0,C.jsx)(`h2`,{className:`settings-pane-title`,children:`Advanced`}),(0,C.jsx)(j,{}),(0,C.jsxs)(`section`,{className:`settings-section`,children:[(0,C.jsx)(`h4`,{children:`Local model server`}),n?(0,C.jsx)(`p`,{className:`loading-text`,children:`Checking system...`}):e?(0,C.jsxs)(C.Fragment,{children:[(0,C.jsxs)(`div`,{className:`status-grid`,children:[(0,C.jsx)(N,{label:`Model server`,value:e.lemonade_running?`Running${e.lemonade_version?` v${e.lemonade_version}`:``}`:e.lemonade_error?`Status unavailable`:`Not Running`,ok:e.lemonade_running,hint:e.lemonade_running?void 0:e.lemonade_error?`Could not query it: ${e.lemonade_error}`:e.start_command?`Run: ${e.start_command}`:e.start_instruction??`Not running.`}),(0,C.jsx)(N,{label:`Model`,value:e.model_loaded||`None loaded`,ok:!!e.model_loaded&&(Z||e.expected_model_loaded!==!1),hint:e.model_loaded?!Z&&e.expected_model_loaded===!1?`Expected: ${K}`:void 0:`None loaded yet — it loads with the first message.`}),e.model_size_gb!=null&&(0,C.jsx)(N,{label:`Model Size`,value:`${e.model_size_gb} GB`,ok:!0}),e.model_device&&!Z&&(0,C.jsx)(N,{label:`Device`,value:e.model_device.toUpperCase(),ok:e.model_device!==`cpu`}),e.model_context_size!=null&&(0,C.jsx)(N,{label:`Context Window`,value:`${(e.model_context_size/1024).toFixed(0)}K tokens`,ok:e.context_size_sufficient}),e.model_labels&&e.model_labels.length>0&&(0,C.jsx)(N,{label:`Capabilities`,value:e.model_labels.join(`, `),ok:!0}),(0,C.jsx)(N,{label:`Embedding Model`,value:e.embedding_model_loaded?`Available`:`Not loaded`,ok:e.embedding_model_loaded}),e.gpu_name&&(0,C.jsx)(N,{label:`GPU`,value:`${e.gpu_name}${e.gpu_vram_gb?` (${_(e.gpu_vram_gb)})`:``}`,ok:!0}),(0,C.jsx)(N,{label:`Disk Space`,value:`${e.disk_space_gb} GB free`,ok:e.disk_space_gb>5,hint:!e.model_loaded&&e.disk_space_gb<30?`Local models need about 6 GB — only ${e.disk_space_gb} GB free`:void 0}),(0,C.jsx)(N,{label:`Memory`,value:e.memory_available_gb==null?`unknown`:`${e.memory_available_gb} GB available`,ok:e.memory_available_gb!=null&&e.memory_available_gb>2}),e.processor_name&&(0,C.jsx)(N,{label:`Processor`,value:e.processor_name,ok:!0})]}),ee&&(0,C.jsxs)(`div`,{className:`model-action-row model-action-row--download`,children:[(0,C.jsxs)(`div`,{className:`model-action-info`,children:[(0,C.jsx)(`span`,{className:`model-action-label`,children:`Model not downloaded.`}),(0,C.jsxs)(`span`,{className:`model-action-desc`,children:[(0,C.jsx)(`strong`,{children:K}),` is GAIA’s default local model (about 6 GB).`]})]}),(0,C.jsx)(`button`,{className:`btn-model-action btn-model-action--download`,onClick:()=>X(!1),disabled:J,children:J?(0,C.jsxs)(C.Fragment,{children:[(0,C.jsx)(s,{size:13,className:`btn-spinner`}),` Downloading…`]}):`Download`})]}),te&&(0,C.jsxs)(`div`,{className:`model-action-row model-action-row--load`,children:[(0,C.jsxs)(`div`,{className:`model-action-info`,children:[(0,C.jsx)(`span`,{className:`model-action-label`,children:Q?`Wrong model loaded.`:`Context window too small.`}),(0,C.jsxs)(`span`,{className:`model-action-desc`,children:[`Load `,(0,C.jsx)(`strong`,{children:K}),` with `,32 .toFixed(0),`K token context.`]})]}),(0,C.jsx)(`button`,{className:`btn-model-action btn-model-action--load`,onClick:()=>Y(),disabled:q,children:q?(0,C.jsxs)(C.Fragment,{children:[(0,C.jsx)(s,{size:13,className:`btn-spinner`}),` Loading…`]}):`Load Model`})]}),e.lemonade_running&&K!==null&&!Z&&(0,C.jsxs)(`div`,{className:`force-redownload-row`,children:[(0,C.jsx)(`span`,{className:`force-redownload-label`,children:`If the model file is corrupted:`}),(0,C.jsx)(`button`,{className:`btn-force-redownload`,onClick:()=>X(!0),disabled:J,children:J?(0,C.jsxs)(C.Fragment,{children:[(0,C.jsx)(s,{size:12,className:`btn-spinner`}),` Downloading…`]}):`Force Re-download`})]})]}):(0,C.jsx)(`div`,{className:`status-error`,children:(0,C.jsx)(`p`,{children:`Could not reach GAIA. Check that it is still running.`})})]}),(0,C.jsx)(w,{}),(0,C.jsxs)(`section`,{className:`settings-section`,children:[(0,C.jsx)(`h4`,{children:`Custom model ID`}),(0,C.jsx)(`p`,{className:`model-override-desc`,children:`Run any model the local server knows by its ID. Leave empty to use the model chosen under Model.`}),(0,C.jsxs)(`div`,{className:`model-input-row`,children:[(0,C.jsx)(`input`,{type:`text`,className:`model-input${p?` has-override`:``}`,placeholder:`Use the model chosen under Model`,value:l,onChange:e=>{d(e.target.value),k(null),M(!1)},onKeyDown:e=>{e.key===`Enter`&&!E&&G&&g&&(e.preventDefault(),U())},disabled:!g||E,spellCheck:!1,autoCapitalize:`off`,autoCorrect:`off`,"aria-label":`Custom model override`}),(0,C.jsx)(`div`,{className:`model-btn-group`,children:(0,C.jsx)(`button`,{className:`btn-model-save${A?` saved`:``}`,onClick:()=>{U()},disabled:!g||E||!G&&!A,"aria-label":`Save custom model`,children:E?(0,C.jsxs)(C.Fragment,{children:[(0,C.jsx)(s,{size:13,className:`btn-spinner`}),` Saving…`]}):A?(0,C.jsxs)(C.Fragment,{children:[(0,C.jsx)(u,{size:13}),` Saved`]}):`Save`})})]}),(0,C.jsxs)(`p`,{className:`model-status-hint`,children:[`Accepts a Lemonade model ID (e.g. `,(0,C.jsx)(`code`,{children:`Qwen3-4B-Instruct-2507-GGUF`}),`)`,` `,`or a HuggingFace ID (e.g. `,(0,C.jsx)(`code`,{children:`unsloth/Qwen3-4B-GGUF`}),`).`]}),O&&(0,C.jsxs)(`div`,{className:`model-warning`,role:`alert`,children:[(0,C.jsx)(v,{size:14}),(0,C.jsxs)(`div`,{className:`model-warning-content`,children:[(0,C.jsx)(`strong`,{children:`Could not save`}),(0,C.jsx)(`p`,{children:O})]})]})]}),(0,C.jsxs)(`section`,{className:`settings-section`,children:[(0,C.jsxs)(`h4`,{children:[`Dynamic Tools `,(0,C.jsx)(`span`,{className:`beta-badge`,children:`BETA`})]}),(0,C.jsx)(`p`,{className:`model-override-desc`,children:`Trim each turn’s tool list to a semantically-matched subset to speed up the first response. It does not apply to the GAIA agent yet.`}),(0,C.jsxs)(`label`,{className:`setting-row`,children:[(0,C.jsx)(`span`,{children:`Enable dynamic tool loading`}),(0,C.jsxs)(`span`,{className:`toggle-switch`,children:[(0,C.jsx)(`input`,{type:`checkbox`,checked:F,onChange:()=>void W(),disabled:!g||z||L,"aria-label":F?`Disable dynamic tools`:`Enable dynamic tools`}),(0,C.jsx)(`span`,{className:`toggle-track`})]})]}),L&&(0,C.jsxs)(`p`,{className:`model-status-hint`,children:[`Controlled by `,(0,C.jsx)(`code`,{children:`GAIA_DYNAMIC_TOOLS`}),` — unset that environment variable to change this here.`]}),V&&(0,C.jsxs)(`div`,{className:`model-warning`,role:`alert`,children:[(0,C.jsx)(v,{size:14}),(0,C.jsxs)(`div`,{className:`model-warning-content`,children:[(0,C.jsx)(`strong`,{children:`Could not save`}),(0,C.jsx)(`p`,{children:V})]})]})]}),o.length>0&&(0,C.jsxs)(`section`,{className:`settings-section`,children:[(0,C.jsx)(`h4`,{children:`MCP Servers`}),(0,C.jsx)(`div`,{className:`status-grid`,children:o.map(e=>(0,C.jsxs)(`div`,{className:`status-row`,children:[(0,C.jsx)(`span`,{className:`status-label`,children:e.name}),(0,C.jsx)(`div`,{className:`status-value-wrap`,children:e.connected?(0,C.jsxs)(`span`,{className:`status-value ok mcp-status-connected`,children:[(0,C.jsx)(u,{size:12}),e.tool_count,` tool`,e.tool_count===1?``:`s`]}):(0,C.jsxs)(`span`,{className:`status-value warn mcp-status-failed`,title:e.error??void 0,children:[(0,C.jsx)(v,{size:12}),`Failed`]})})]},e.name))})]}),(0,C.jsxs)(`section`,{className:`settings-section`,children:[(0,C.jsx)(`h4`,{children:`About`}),(0,C.jsxs)(`div`,{className:`about-info`,children:[(0,C.jsxs)(`p`,{children:[`GAIA v`,`0.25.3`,` `,(0,C.jsx)(`span`,{className:`beta-badge`,children:`BETA`})]}),(0,C.jsx)(`p`,{className:`about-sub`,children:`Privacy-first AI chat for AMD Ryzen AI PCs.`})]})]})]})}function N({label:e,value:t,ok:n,hint:r}){return(0,C.jsxs)(`div`,{className:`status-row${r?` status-row--has-hint`:``}`,children:[(0,C.jsx)(`span`,{className:`status-label`,children:e}),(0,C.jsxs)(`div`,{className:`status-value-wrap`,children:[(0,C.jsx)(`span`,{className:`status-value ${n?`ok`:`warn`}`,children:t}),r&&(0,C.jsx)(`span`,{className:`status-hint`,children:(0,C.jsx)(`code`,{children:r})})]})]})}export{M as AdvancedSettings};
@@ -1 +0,0 @@
1
- .connectors-section .settings-help{color:var(--text-secondary);font-size:13px;line-height:1.5;font-family:var(--font-sans);margin-bottom:12px}.connectors-loading{color:var(--text-muted);justify-content:center;padding:20px;display:flex}.connectors-list{flex-direction:column;gap:6px;display:flex}.connector-tile{border:1px solid var(--border-light);border-radius:var(--radius-md);background:var(--bg-secondary);transition:border-color var(--duration) var(--ease);overflow:hidden}.connector-tile:hover,.connector-tile--open{border-color:var(--border)}.connector-tile-header{cursor:pointer;text-align:left;width:100%;font-family:var(--font-sans);color:var(--text-primary);background:0 0;border:none;align-items:center;gap:10px;padding:12px 16px;display:flex}.connector-tile-header:hover{background:var(--bg-hover)}[data-theme=dark] .connector-tile-header:hover{background:color-mix(in srgb, var(--text-primary) 4%, transparent)}.connector-tile-name{font-size:13px;font-weight:600}.connector-tile-type{font-size:var(--fs-caption);font-family:var(--font-sans);color:var(--text-muted);background:var(--bg-tertiary);border:1px solid var(--border-light);border-radius:999px;flex-shrink:0;padding:1px 6px}.connector-status{font-size:12px;font-family:var(--font-sans);align-items:center;gap:4px;display:inline-flex}.connector-status.ok{color:var(--accent-green)}.connector-status.idle{color:var(--text-muted)}.connector-status.disabled{color:var(--text-muted);background:var(--bg-tertiary);border:1px solid var(--border-light);border-radius:999px;padding:1px 8px;font-size:11px}.connector-tile-chevron{color:var(--text-muted);flex-shrink:0;margin-left:auto}.connector-detail{border-top:1px solid var(--border-light);padding:16px 18px}.connector-desc{color:var(--text-secondary);font-size:12px;line-height:1.5;font-family:var(--font-sans);margin-bottom:12px}.configure-error{color:var(--danger);background:color-mix(in srgb, var(--danger) 8%, transparent);border:1px solid color-mix(in srgb, var(--danger) 20%, transparent);border-radius:var(--radius-sm);font-size:12px;font-family:var(--font-sans);align-items:center;gap:6px;margin-bottom:10px;padding:6px 10px;display:flex}.configure-info{color:var(--text-secondary);background:var(--bg-tertiary);border:1px solid var(--border-light);border-radius:var(--radius-sm);font-size:12px;font-family:var(--font-sans);align-items:flex-start;gap:6px;margin-bottom:10px;padding:6px 10px;line-height:1.4;display:flex}.configure-info svg{color:var(--text-muted);flex-shrink:0;margin-top:2px}.connector-setup-required{font-size:12px;font-family:var(--font-sans);color:var(--text-muted);background:var(--bg-tertiary);border:1px solid var(--border-light);cursor:default;border-radius:999px;align-items:center;gap:4px;padding:3px 10px;display:inline-flex}.configure-actions{align-items:center;gap:8px;margin-top:10px;display:flex}.oauth-setup-form{background:var(--bg-tertiary);border:1px solid var(--border-light);border-radius:6px;flex-direction:column;gap:12px;margin:8px 0 4px;padding:12px;display:flex}.oauth-setup-field{font-size:12px;font-family:var(--font-sans);flex-direction:column;gap:4px;display:flex}.oauth-setup-label{color:var(--text-primary);font-weight:500}.oauth-setup-input{border:1px solid var(--border-light);background:var(--bg-primary);color:var(--text-primary);font-family:var(--font-mono);border-radius:4px;padding:6px 8px;font-size:12px}.oauth-setup-input:focus{border-color:var(--accent);outline:none}.oauth-setup-help{color:var(--text-secondary);font-size:11px;line-height:1.4}.oauth-grant-onconnect{background:var(--bg-tertiary);border:1px solid var(--border-light);border-radius:6px;flex-direction:column;gap:6px;margin:8px 0 4px;padding:10px 12px;display:flex}.oauth-grant-onconnect-label{color:var(--text-primary);margin:0;font-size:12px;font-weight:500}.oauth-grant-onconnect-item{color:var(--text-primary);cursor:pointer;align-items:center;gap:8px;font-size:12px;display:flex}.oauth-grant-onconnect-help{color:var(--text-secondary);margin:2px 0 0;font-size:11px;line-height:1.4}.connector-product-link{color:var(--text-secondary);font-size:12px;font-family:var(--font-sans);align-items:center;gap:3px;margin-left:auto;text-decoration:none;display:inline-flex}.connector-product-link:hover{color:var(--accent);text-decoration:underline}.mcp-key-row{flex-direction:column;gap:4px;margin-bottom:10px;display:flex}.mcp-key-label{font-size:var(--fs-label);font-family:var(--font-sans);color:var(--text-secondary)}.mcp-key-input{font-size:13px;font-family:var(--font-mono);background:var(--bg-primary);border:1px solid var(--border-light);border-radius:var(--radius-md);color:var(--text-primary);transition:border-color var(--duration) var(--ease);outline:none;padding:7px 10px}.mcp-key-input:focus{border-color:var(--accent)}.connection-grants{border-top:1px dashed var(--border-light);margin-top:12px;padding-top:12px}.grants-header{font-size:var(--fs-label);font-weight:600;font-family:var(--font-sans);color:var(--text-muted);margin-bottom:8px}.grants-empty{color:var(--text-muted);font-size:12px;font-family:var(--font-sans);font-style:italic}.grant-agent-card{border:1px solid var(--border-light);border-radius:var(--radius-sm);background:var(--bg-secondary);margin-top:6px;padding:10px 14px}.grant-agent-card-name{font-size:12px;font-weight:600;font-family:var(--font-sans);color:var(--text-primary);margin-bottom:10px}.grant-scope-list{border-top:1px solid var(--border-light);flex-direction:column;gap:2px;padding-top:8px;display:flex}.grant-scope-item{cursor:pointer;-webkit-user-select:none;user-select:none;justify-content:space-between;align-items:center;gap:12px;padding:5px 0;display:flex}.grant-scope-label{font-size:12px;font-family:var(--font-sans);color:var(--text-secondary);flex:1;line-height:1.3}.grant-scope-warning{font-size:11px;font-family:var(--font-sans);color:var(--text-muted);align-items:center;gap:5px;margin-top:7px;display:flex}.grant-scope-warning--error{color:var(--danger)}.error-banner{background:color-mix(in srgb, var(--danger) 8%, transparent);color:var(--danger);border-radius:var(--radius-md);border:1px solid color-mix(in srgb, var(--danger) 20%, transparent);font-size:12px;font-family:var(--font-sans);align-items:center;gap:6px;margin-bottom:10px;padding:8px 12px;display:flex}.spin{animation:spin var(--duration-spin) var(--ease-linear) infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.connector-toggle-row{background:var(--bg-secondary);border:1px solid var(--border-light);border-radius:6px;align-items:center;gap:12px;margin-bottom:10px;padding:10px 14px;display:flex}.connector-toggle-label{font-size:12px;font-family:var(--font-sans);color:var(--text-primary);flex:1}.connector-toggle-help{color:var(--text-muted);margin:-4px 0 10px 14px;font-size:11px;font-style:italic}.connector-tile-menu{flex-shrink:0;align-items:center;display:inline-flex;position:relative}.connector-tile-menu-trigger{color:var(--text-muted);cursor:pointer;background:0 0;border:none;border-radius:4px;padding:4px;line-height:0}.connector-tile-menu-trigger:hover{background:var(--bg-tertiary);color:var(--text-primary)}.connector-tile-menu-trigger:focus-visible{outline:2px solid var(--accent-blue);outline-offset:1px}.connector-tile-menu-trigger:disabled{opacity:.4;cursor:not-allowed}.connector-tile-menu-popup{z-index:1000;background:var(--bg-primary);border:1px solid var(--border);text-align:left;border-radius:4px;flex-direction:column;min-width:160px;padding:4px;display:flex;box-shadow:0 4px 12px #0000002e}.connector-tile-menu-item{font-size:12px;font-family:var(--font-sans);color:var(--text-primary);text-align:left;cursor:pointer;background:0 0;border:none;border-radius:3px;padding:6px 10px;line-height:1.4}.connector-tile-menu-item:hover{background:var(--bg-secondary)}.connector-tile-menu-item:disabled{opacity:.4;cursor:not-allowed}.connector-tile-menu-item--danger{color:var(--danger)}.connector-tile-menu-divider{background:var(--border-light);height:1px;margin:4px 0}.agent-mcps-section{margin-top:20px}.agent-mcps-section-header{font-size:var(--fs-label);font-weight:600;font-family:var(--font-sans);color:var(--text-muted);margin-bottom:8px}.agent-mcp-tile{border:1px solid var(--border-light);border-radius:var(--radius-md);background:var(--bg-secondary);transition:border-color var(--duration) var(--ease);overflow:hidden}.agent-mcp-tile:hover,.agent-mcp-tile--open{border-color:var(--border)}.agent-mcp-tile-header{cursor:pointer;text-align:left;width:100%;font-family:var(--font-sans);color:var(--text-primary);background:0 0;border:none;align-items:center;gap:10px;padding:10px 14px;display:flex}.agent-mcp-tile-header:hover{background:var(--bg-hover)}[data-theme=dark] .agent-mcp-tile-header:hover{background:color-mix(in srgb, var(--text-primary) 4%, transparent)}.agent-mcp-server-name{font-size:13px;font-weight:600}.agent-mcp-agent-label{font-size:11px;font-family:var(--font-sans);color:var(--text-muted);flex:1}.agent-mcp-status{font-size:11px;font-family:var(--font-sans);align-items:center;gap:4px;display:inline-flex}.agent-mcp-status.enabled{color:var(--accent-green)}.agent-mcp-status.disabled{color:var(--text-muted);background:var(--bg-tertiary);border:1px solid var(--border-light);border-radius:999px;padding:1px 8px}.agent-mcp-readonly-badge{font-size:var(--fs-caption);font-family:var(--font-sans);color:var(--text-muted);background:var(--bg-tertiary);border:1px solid var(--border-light);border-radius:999px;flex-shrink:0;padding:1px 6px}.agent-mcp-chevron{color:var(--text-muted);flex-shrink:0}.agent-mcp-detail{border-top:1px solid var(--border-light);padding:12px 16px}.agent-mcp-command-row{font-size:12px;font-family:var(--font-mono);color:var(--text-secondary);background:var(--bg-tertiary);border:1px solid var(--border-light);border-radius:var(--radius-sm);word-break:break-all;margin-bottom:8px;padding:6px 10px}.agent-mcp-config-path{font-size:11px;font-family:var(--font-mono);color:var(--text-muted);word-break:break-all;margin-bottom:4px}.agent-mcp-config-hint{font-size:11px;font-family:var(--font-sans);color:var(--text-muted);font-style:italic}.connector-tile-menu-error{color:var(--danger);border-top:1px solid var(--border-light);margin-top:4px;padding:6px 10px;font-size:11px}.oauth-client-editor{border:1px solid var(--border-light);border-radius:var(--radius-sm);margin:6px 0 8px}.oauth-client-editor__toggle{width:100%;color:var(--text-primary);cursor:pointer;text-align:left;background:0 0;border:none;align-items:center;gap:6px;padding:6px 8px;font-size:12px;display:flex}.oauth-client-editor__status{color:var(--text-muted);text-overflow:ellipsis;white-space:nowrap;max-width:60%;margin-left:auto;font-size:11px;overflow:hidden}.oauth-client-editor .oauth-setup-form{padding:0 8px 8px}.device-code-block{border:1px solid var(--border);background:var(--bg-secondary);border-radius:8px;margin-top:10px;padding:12px}.device-code-instructions{color:var(--text-primary);margin:0 0 8px;font-size:12px}.device-code-value{align-items:center;gap:8px;margin-bottom:8px;display:flex}.device-code-value code{letter-spacing:2px;background:var(--bg-tertiary);color:var(--text-primary);-webkit-user-select:all;user-select:all;border-radius:6px;padding:4px 10px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:18px;font-weight:600}.device-code-waiting{color:var(--text-muted);align-items:center;gap:6px;margin:0 0 8px;font-size:12px;display:flex}