@wrongstack/webui 0.54.1 → 0.66.13

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 (53) hide show
  1. package/dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  2. package/dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  3. package/dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  4. package/dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  5. package/dist/assets/ibm-plex-mono-cyrillic-600-normal-CTOM6hUh.woff2 +0 -0
  6. package/dist/assets/ibm-plex-mono-cyrillic-600-normal-fLZuRloM.woff +0 -0
  7. package/dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  8. package/dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  9. package/dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  10. package/dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  11. package/dist/assets/ibm-plex-mono-cyrillic-ext-600-normal-9HEixskS.woff +0 -0
  12. package/dist/assets/ibm-plex-mono-cyrillic-ext-600-normal-V-xxqcpd.woff2 +0 -0
  13. package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  14. package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  15. package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  16. package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  17. package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  18. package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
  19. package/dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  20. package/dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  21. package/dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  22. package/dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  23. package/dist/assets/ibm-plex-mono-latin-ext-600-normal-D38SheWl.woff2 +0 -0
  24. package/dist/assets/ibm-plex-mono-latin-ext-600-normal-DmB0ttJJ.woff +0 -0
  25. package/dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  26. package/dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  27. package/dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  28. package/dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  29. package/dist/assets/ibm-plex-mono-vietnamese-600-normal-D2EvbN8M.woff2 +0 -0
  30. package/dist/assets/ibm-plex-mono-vietnamese-600-normal-iLQfcSjf.woff +0 -0
  31. package/dist/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
  32. package/dist/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
  33. package/dist/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
  34. package/dist/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
  35. package/dist/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
  36. package/dist/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
  37. package/dist/assets/index-CnURnGh-.css +1 -0
  38. package/dist/assets/index-aAReViZF.js +94 -0
  39. package/dist/assets/vendor-DW1jimNH.css +1 -0
  40. package/dist/index.css +333 -200
  41. package/dist/index.css.map +1 -1
  42. package/dist/index.html +4 -3
  43. package/dist/index.js +984 -641
  44. package/dist/index.js.map +1 -1
  45. package/dist/server/entry.js +444 -126
  46. package/dist/server/entry.js.map +1 -1
  47. package/dist/server/index.d.ts +298 -2
  48. package/dist/server/index.js +445 -96
  49. package/dist/server/index.js.map +1 -1
  50. package/package.json +9 -6
  51. package/dist/assets/index-5ECutVTP.css +0 -1
  52. package/dist/assets/index-BRHGqfHg.js +0 -94
  53. /package/dist/assets/{vendor-oYD55Pw4.js → vendor-wUxgMlp-.js} +0 -0
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // src/server/index.ts
3
- import * as fs2 from "fs/promises";
4
- import * as path2 from "path";
3
+ import * as fs4 from "fs/promises";
4
+ import * as path4 from "path";
5
5
 
6
6
  // src/server/http-server.ts
7
7
  import * as fs from "fs/promises";
@@ -16,8 +16,18 @@ var MIME_TYPES = {
16
16
  ".png": "image/png",
17
17
  ".ico": "image/x-icon"
18
18
  };
19
- function buildCspHeader(wsPort2) {
20
- return `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws://127.0.0.1:${wsPort2} wss://127.0.0.1:${wsPort2} ws://[::1]:${wsPort2} wss://[::1]:${wsPort2}; img-src 'self' data:; font-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'`;
19
+ function injectWsPort(html, wsPort) {
20
+ const tag = `<meta name="wrongstack-ws-port" content="${wsPort}" />`;
21
+ if (html.includes('name="wrongstack-ws-port"')) return html;
22
+ if (html.includes("</head>")) {
23
+ return html.replace("</head>", ` ${tag}
24
+ </head>`);
25
+ }
26
+ return `${tag}
27
+ ${html}`;
28
+ }
29
+ function buildCspHeader(wsPort) {
30
+ return `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws://127.0.0.1:${wsPort} wss://127.0.0.1:${wsPort} ws://[::1]:${wsPort} wss://[::1]:${wsPort}; img-src 'self' data:; font-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'`;
21
31
  }
22
32
  function isInsideDist(candidate, distDir) {
23
33
  const root = path.resolve(distDir);
@@ -27,7 +37,7 @@ function isInsideDist(candidate, distDir) {
27
37
  function createHttpServer(opts) {
28
38
  const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
29
39
  const distDir = path.resolve(opts.distDir);
30
- const wsPort2 = opts.wsPort;
40
+ const wsPort = opts.wsPort;
31
41
  return http.createServer(async (req, res) => {
32
42
  try {
33
43
  const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
@@ -55,7 +65,11 @@ function createHttpServer(opts) {
55
65
  res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
56
66
  if (ext === ".html") {
57
67
  res.setHeader("Cache-Control", "no-cache");
58
- res.setHeader("Content-Security-Policy", buildCspHeader(wsPort2));
68
+ res.setHeader("Content-Security-Policy", buildCspHeader(wsPort));
69
+ const html = await fs.readFile(resolvedPath, "utf8");
70
+ res.writeHead(200);
71
+ res.end(injectWsPort(html, wsPort));
72
+ return;
59
73
  }
60
74
  const fileContent = await fs.readFile(resolvedPath);
61
75
  res.writeHead(200);
@@ -63,15 +77,15 @@ function createHttpServer(opts) {
63
77
  } catch (err) {
64
78
  if (err.code === "ENOENT") {
65
79
  try {
66
- const fileContent = await fs.readFile(path.join(distDir, "index.html"));
80
+ const html = await fs.readFile(path.join(distDir, "index.html"), "utf8");
67
81
  res.writeHead(200, {
68
82
  "Content-Type": "text/html",
69
83
  "X-Content-Type-Options": "nosniff",
70
84
  "X-Frame-Options": "DENY",
71
85
  "Referrer-Policy": "strict-origin-when-cross-origin",
72
- "Content-Security-Policy": buildCspHeader(wsPort2)
86
+ "Content-Security-Policy": buildCspHeader(wsPort)
73
87
  });
74
- res.end(fileContent);
88
+ res.end(injectWsPort(html, wsPort));
75
89
  } catch {
76
90
  res.writeHead(404);
77
91
  res.end("Not found");
@@ -155,7 +169,7 @@ import {
155
169
  ProviderRegistry,
156
170
  TOKENS as TOKENS2,
157
171
  ToolRegistry,
158
- atomicWrite,
172
+ atomicWrite as atomicWrite3,
159
173
  createDefaultPipelines,
160
174
  DEFAULT_CONTEXT_WINDOW_MODE_ID,
161
175
  DEFAULT_TOOLS_CONFIG,
@@ -164,11 +178,9 @@ import {
164
178
  resolveContextWindowPolicy
165
179
  } from "@wrongstack/core";
166
180
  import { ToolExecutor } from "@wrongstack/core/execution";
167
- import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
168
181
  import { buildProviderFactoriesFromRegistry, makeProviderFromConfig } from "@wrongstack/providers";
169
182
  import { builtinToolsPack, forgetTool, rememberTool } from "@wrongstack/tools";
170
- import { WebSocket, WebSocketServer } from "ws";
171
- import { randomBytes } from "crypto";
183
+ import { WebSocketServer } from "ws";
172
184
 
173
185
  // ../runtime/src/container.ts
174
186
  import {
@@ -227,7 +239,8 @@ function createDefaultContainer(opts) {
227
239
  () => new DefaultPermissionPolicy({
228
240
  trustFile: wpaths.projectTrust,
229
241
  yolo: opts.permission?.yolo ?? false,
230
- forceAllYolo: opts.permission?.forceAllYolo ?? false,
242
+ yoloDestructive: opts.permission?.yoloDestructive ?? opts.permission?.forceAllYolo ?? false,
243
+ confirmDestructive: opts.permission?.confirmDestructive ?? false,
231
244
  promptDelegate: opts.permission?.promptDelegate
232
245
  })
233
246
  );
@@ -1386,8 +1399,8 @@ import { timingSafeEqual } from "crypto";
1386
1399
  function isLoopbackHostname(hostname) {
1387
1400
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
1388
1401
  }
1389
- function isLoopbackBind(wsHost2) {
1390
- return wsHost2 === "127.0.0.1" || wsHost2 === "::1" || wsHost2 === "localhost";
1402
+ function isLoopbackBind(wsHost) {
1403
+ return wsHost === "127.0.0.1" || wsHost === "::1" || wsHost === "localhost";
1391
1404
  }
1392
1405
  function tokenMatches(provided, expected) {
1393
1406
  if (!provided) return false;
@@ -1413,14 +1426,14 @@ function hostHeaderOk(input) {
1413
1426
  return isLoopbackHostname(hostname);
1414
1427
  }
1415
1428
  function verifyClient(input) {
1416
- const { origin, url, hostHeader, remoteAddress, wsHost: wsHost2, expectedToken } = input;
1429
+ const { origin, url, hostHeader, remoteAddress, wsHost, expectedToken } = input;
1417
1430
  const tokenOk = tokenMatches(extractToken(url ?? ""), expectedToken);
1418
- if (!hostHeaderOk({ hostHeader, wsHost: wsHost2 })) return false;
1431
+ if (!hostHeaderOk({ hostHeader, wsHost })) return false;
1419
1432
  if (!origin) {
1420
1433
  const remoteIp = remoteAddress ?? "";
1421
1434
  const isRemoteLoopback = remoteIp === "127.0.0.1" || remoteIp === "::1";
1422
- if (!isRemoteLoopback && wsHost2 === "0.0.0.0") return false;
1423
- return tokenOk || isLoopbackBind(wsHost2);
1435
+ if (!isRemoteLoopback && wsHost === "0.0.0.0") return false;
1436
+ return tokenOk || isLoopbackBind(wsHost);
1424
1437
  }
1425
1438
  try {
1426
1439
  const { hostname } = new URL(origin);
@@ -1447,6 +1460,13 @@ function createShutdown(res) {
1447
1460
  }
1448
1461
  for (const ws of res.clients()) ws.close();
1449
1462
  for (const server of res.servers) server?.close();
1463
+ if (res.onShutdown) {
1464
+ try {
1465
+ await res.onShutdown();
1466
+ } catch (e) {
1467
+ log(`[WebUI] Error during shutdown cleanup: ${e instanceof Error ? e.message : String(e)}`);
1468
+ }
1469
+ }
1450
1470
  exit(0);
1451
1471
  };
1452
1472
  }
@@ -1460,6 +1480,138 @@ function registerShutdownHandlers(res) {
1460
1480
  };
1461
1481
  }
1462
1482
 
1483
+ // src/server/instance-registry.ts
1484
+ import * as os from "os";
1485
+ import * as path2 from "path";
1486
+ import * as fs2 from "fs/promises";
1487
+ import { atomicWrite } from "@wrongstack/core";
1488
+ function defaultBaseDir() {
1489
+ return path2.join(os.homedir(), ".wrongstack");
1490
+ }
1491
+ function registryPath(baseDir = defaultBaseDir()) {
1492
+ return path2.join(baseDir, "webui-instances.json");
1493
+ }
1494
+ function isPidAlive(pid) {
1495
+ if (!Number.isInteger(pid) || pid <= 0) return false;
1496
+ try {
1497
+ process.kill(pid, 0);
1498
+ return true;
1499
+ } catch (err) {
1500
+ return err.code !== "ESRCH";
1501
+ }
1502
+ }
1503
+ async function load(file) {
1504
+ try {
1505
+ const raw = await fs2.readFile(file, "utf8");
1506
+ const parsed = JSON.parse(raw);
1507
+ if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
1508
+ return parsed;
1509
+ }
1510
+ } catch {
1511
+ }
1512
+ return { version: 1, instances: [] };
1513
+ }
1514
+ async function save(file, instances) {
1515
+ await atomicWrite(file, `${JSON.stringify({ version: 1, instances }, null, 2)}
1516
+ `, {
1517
+ mode: 384
1518
+ });
1519
+ }
1520
+ function prune(instances, excludePid) {
1521
+ return instances.filter((i) => i.pid !== excludePid && isPidAlive(i.pid));
1522
+ }
1523
+ async function registerInstance(record, baseDir = defaultBaseDir()) {
1524
+ const file = registryPath(baseDir);
1525
+ const data = await load(file);
1526
+ const instances = prune(data.instances, record.pid);
1527
+ instances.push(record);
1528
+ await save(file, instances);
1529
+ }
1530
+ async function unregisterInstance(pid, baseDir = defaultBaseDir()) {
1531
+ const file = registryPath(baseDir);
1532
+ const data = await load(file);
1533
+ const instances = prune(data.instances, pid);
1534
+ await save(file, instances);
1535
+ }
1536
+ async function listInstances(baseDir = defaultBaseDir()) {
1537
+ const file = registryPath(baseDir);
1538
+ const data = await load(file);
1539
+ const live = prune(data.instances);
1540
+ if (live.length !== data.instances.length) {
1541
+ await save(file, live).catch(() => {
1542
+ });
1543
+ }
1544
+ return live;
1545
+ }
1546
+ function formatInstances(instances) {
1547
+ if (instances.length === 0) {
1548
+ return "No WebUI instances are currently running.";
1549
+ }
1550
+ const lines = [`Running WebUI instances (${instances.length}):`, ""];
1551
+ for (const i of instances) {
1552
+ lines.push(
1553
+ ` \u2022 ${i.url} \xB7 ws:${i.wsPort} \xB7 pid ${i.pid}`,
1554
+ ` project: ${i.projectName} (${i.projectRoot})`,
1555
+ ` since: ${i.startedAt}`
1556
+ );
1557
+ }
1558
+ return lines.join("\n");
1559
+ }
1560
+
1561
+ // src/server/port-utils.ts
1562
+ import * as net from "net";
1563
+ function isPortFree(host, port) {
1564
+ return new Promise((resolve3) => {
1565
+ const srv = net.createServer();
1566
+ srv.once("error", () => resolve3(false));
1567
+ srv.once("listening", () => {
1568
+ srv.close(() => resolve3(true));
1569
+ });
1570
+ try {
1571
+ srv.listen(port, host);
1572
+ } catch {
1573
+ resolve3(false);
1574
+ }
1575
+ });
1576
+ }
1577
+ async function findFreePort(host, startPort, opts = {}) {
1578
+ const exclude = opts.exclude ?? /* @__PURE__ */ new Set();
1579
+ const maxTries = opts.maxTries ?? 200;
1580
+ let port = startPort;
1581
+ for (let i = 0; i < maxTries; i++) {
1582
+ if (port > 65535) port = 1024 + port % 5e4;
1583
+ if (!exclude.has(port) && await isPortFree(host, port)) {
1584
+ return port;
1585
+ }
1586
+ port++;
1587
+ }
1588
+ throw new Error(
1589
+ `No free port found near ${startPort} on ${host} after ${maxTries} attempts.`
1590
+ );
1591
+ }
1592
+
1593
+ // src/server/open-browser.ts
1594
+ import { spawn } from "child_process";
1595
+ function browserOpenCommand(url, platform = process.platform) {
1596
+ if (platform === "win32") {
1597
+ return { command: "cmd", args: ["/c", "start", "", url] };
1598
+ }
1599
+ if (platform === "darwin") {
1600
+ return { command: "open", args: [url] };
1601
+ }
1602
+ return { command: "xdg-open", args: [url] };
1603
+ }
1604
+ function openBrowser(url, platform = process.platform) {
1605
+ try {
1606
+ const { command, args } = browserOpenCommand(url, platform);
1607
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
1608
+ child.on("error", () => {
1609
+ });
1610
+ child.unref();
1611
+ } catch {
1612
+ }
1613
+ }
1614
+
1463
1615
  // src/server/usage-cost.ts
1464
1616
  function getCostRates(model) {
1465
1617
  const cost = model?.cost;
@@ -1568,6 +1720,89 @@ function removeProvider(providers, providerId) {
1568
1720
  return { ok: true, message: `Provider "${providerId}" removed` };
1569
1721
  }
1570
1722
 
1723
+ // src/server/provider-config-io.ts
1724
+ import * as fs3 from "fs/promises";
1725
+ import * as path3 from "path";
1726
+ import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
1727
+ import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
1728
+ import { DefaultSecretVault } from "@wrongstack/core";
1729
+ async function loadSavedProviders(configPath, vault) {
1730
+ let raw;
1731
+ try {
1732
+ raw = await fs3.readFile(configPath, "utf8");
1733
+ } catch {
1734
+ return {};
1735
+ }
1736
+ let parsed = {};
1737
+ try {
1738
+ parsed = JSON.parse(raw);
1739
+ } catch {
1740
+ return {};
1741
+ }
1742
+ if (!parsed.providers) return {};
1743
+ return decryptConfigSecrets(parsed.providers, vault);
1744
+ }
1745
+ async function saveProviders(configPath, vault, providers) {
1746
+ let raw;
1747
+ let fileExists = true;
1748
+ try {
1749
+ raw = await fs3.readFile(configPath, "utf8");
1750
+ } catch (err) {
1751
+ if (err.code !== "ENOENT") {
1752
+ throw new Error(
1753
+ `Refusing to mutate ${configPath}: ${err.message}`,
1754
+ { cause: err }
1755
+ );
1756
+ }
1757
+ fileExists = false;
1758
+ raw = "{}";
1759
+ }
1760
+ let parsed;
1761
+ try {
1762
+ parsed = JSON.parse(raw);
1763
+ } catch (err) {
1764
+ if (fileExists) {
1765
+ throw new Error(
1766
+ `Refusing to overwrite corrupt config at ${configPath} (${err.message}). Fix or move the file aside before retrying.`,
1767
+ { cause: err }
1768
+ );
1769
+ }
1770
+ parsed = {};
1771
+ }
1772
+ parsed.providers = providers;
1773
+ const encrypted = encryptConfigSecrets(parsed, vault);
1774
+ await atomicWrite2(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
1775
+ }
1776
+
1777
+ // src/server/ws-utils.ts
1778
+ import { randomBytes } from "crypto";
1779
+ import { WebSocket } from "ws";
1780
+ function send(ws, msg) {
1781
+ if (ws.readyState === WebSocket.OPEN) {
1782
+ ws.send(JSON.stringify(msg));
1783
+ }
1784
+ }
1785
+ function broadcast(clients, msg) {
1786
+ const data = JSON.stringify(msg);
1787
+ for (const [ws] of clients) {
1788
+ if (ws.readyState === WebSocket.OPEN) {
1789
+ try {
1790
+ ws.send(data);
1791
+ } catch {
1792
+ }
1793
+ }
1794
+ }
1795
+ }
1796
+ function sendResult(ws, success, message) {
1797
+ send(ws, { type: "key.operation_result", payload: { success, message } });
1798
+ }
1799
+ function errMessage(err) {
1800
+ return err instanceof Error ? err.message : String(err);
1801
+ }
1802
+ function generateAuthToken() {
1803
+ return randomBytes(16).toString("hex");
1804
+ }
1805
+
1571
1806
  // src/server/token-estimator.ts
1572
1807
  function estimateTokens(s) {
1573
1808
  return Math.ceil(s.length / 4);
@@ -1626,12 +1861,23 @@ function estimateContextBreakdown(input) {
1626
1861
  }
1627
1862
 
1628
1863
  // src/server/index.ts
1629
- function errMessage(err) {
1630
- return err instanceof Error ? err.message : String(err);
1631
- }
1632
1864
  async function startWebUI(opts = {}) {
1633
- const wsPort2 = opts.wsPort ?? 3457;
1634
- const wsHost2 = opts.wsHost ?? "127.0.0.1";
1865
+ const requestedWsPort = opts.wsPort ?? 3457;
1866
+ const wsHost = opts.wsHost ?? "127.0.0.1";
1867
+ const requestedHttpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
1868
+ const strictPort = process.env["WEBUI_STRICT_PORT"] === "1" || process.env["WEBUI_STRICT_PORT"] === "true";
1869
+ let wsPort = requestedWsPort;
1870
+ let httpPort = requestedHttpPort;
1871
+ if (!strictPort) {
1872
+ httpPort = await findFreePort(wsHost, requestedHttpPort);
1873
+ wsPort = await findFreePort(wsHost, requestedWsPort, { exclude: /* @__PURE__ */ new Set([httpPort]) });
1874
+ if (httpPort !== requestedHttpPort) {
1875
+ console.warn(`[WebUI] HTTP port ${requestedHttpPort} in use \u2192 using ${httpPort}`);
1876
+ }
1877
+ if (wsPort !== requestedWsPort) {
1878
+ console.warn(`[WebUI] WS port ${requestedWsPort} in use \u2192 using ${wsPort}`);
1879
+ }
1880
+ }
1635
1881
  console.log("[WebUI] Starting backend services...");
1636
1882
  const boot = await bootConfig();
1637
1883
  const { config: baseConfig, vault, globalConfigPath, projectRoot, wpaths, logger } = boot;
@@ -1781,7 +2027,15 @@ async function startWebUI(opts = {}) {
1781
2027
  });
1782
2028
  let autoCompactor;
1783
2029
  if (config.context?.autoCompact !== false) {
1784
- const effectiveMaxContext = config.context?.effectiveMaxContext ?? provider.capabilities.maxContext;
2030
+ let effectiveMaxContext = config.context?.effectiveMaxContext ?? 0;
2031
+ if (!effectiveMaxContext) {
2032
+ try {
2033
+ const m = await modelsRegistry.getModel(provider.id, context.model);
2034
+ effectiveMaxContext = m?.capabilities?.maxContext ?? 0;
2035
+ } catch {
2036
+ }
2037
+ }
2038
+ if (!effectiveMaxContext) effectiveMaxContext = provider.capabilities.maxContext;
1785
2039
  autoCompactor = new AutoCompactionMiddleware(
1786
2040
  compactor,
1787
2041
  effectiveMaxContext,
@@ -1877,41 +2131,42 @@ async function startWebUI(opts = {}) {
1877
2131
  inputCost,
1878
2132
  outputCost,
1879
2133
  cacheReadCost,
1880
- projectName: path2.basename(projectRoot) || projectRoot,
2134
+ projectName: path4.basename(projectRoot) || projectRoot,
1881
2135
  cwd: projectRoot,
1882
2136
  mode: modeId,
1883
2137
  contextMode: String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID),
1884
2138
  wsToken
1885
2139
  };
1886
2140
  }
1887
- const wsToken = randomBytes(16).toString("hex");
2141
+ const wsToken = generateAuthToken();
1888
2142
  console.log(`[WebUI] WS auth token: ${wsToken.slice(0, 4)}\u2026${wsToken.slice(-4)} (masked)`);
1889
2143
  const verifyClient2 = (info) => verifyClient({
1890
2144
  origin: info.origin,
1891
2145
  url: info.req.url ?? "",
1892
2146
  hostHeader: info.req.headers.host,
1893
2147
  remoteAddress: info.req.socket.remoteAddress,
1894
- wsHost: wsHost2,
2148
+ wsHost,
1895
2149
  expectedToken: wsToken
1896
2150
  });
1897
2151
  const WS_MAX_PAYLOAD = 8 * 1024 * 1024;
1898
2152
  const wssPrimary = new WebSocketServer({
1899
- port: wsPort2,
1900
- host: wsHost2,
2153
+ port: wsPort,
2154
+ host: wsHost,
1901
2155
  verifyClient: verifyClient2,
1902
2156
  maxPayload: WS_MAX_PAYLOAD
1903
2157
  });
1904
- const wssSecondary = wsHost2 === "127.0.0.1" ? new WebSocketServer({
1905
- port: wsPort2,
2158
+ const wssSecondary = wsHost === "127.0.0.1" ? new WebSocketServer({
2159
+ port: wsPort,
1906
2160
  host: "::1",
1907
2161
  verifyClient: verifyClient2,
1908
2162
  maxPayload: WS_MAX_PAYLOAD
1909
2163
  }) : null;
1910
2164
  const clients = /* @__PURE__ */ new Map();
1911
- const RATE_LIMIT_MESSAGES = 60;
2165
+ const RATE_LIMIT_MESSAGES = Number.parseInt(process.env["WEBUI_RATE_LIMIT"] ?? "0", 10);
1912
2166
  const RATE_LIMIT_WINDOW_MS = 6e4;
1913
2167
  const rateLimits = /* @__PURE__ */ new Map();
1914
2168
  function checkRateLimit(ws, client) {
2169
+ if (RATE_LIMIT_MESSAGES <= 0) return true;
1915
2170
  const now = Date.now();
1916
2171
  const key = client.sessionId ?? String(ws);
1917
2172
  const limit = rateLimits.get(key);
@@ -1925,30 +2180,30 @@ async function startWebUI(opts = {}) {
1925
2180
  }
1926
2181
  let runLock = null;
1927
2182
  console.log(
1928
- `[WebUI] WebSocket server running on ws://${wsHost2}:${wsPort2}` + (wssSecondary ? ` (and ws://[::1]:${wsPort2})` : "")
2183
+ `[WebUI] WebSocket server running on ws://${wsHost}:${wsPort}` + (wssSecondary ? ` (and ws://[::1]:${wsPort})` : "")
1929
2184
  );
1930
2185
  const pendingConfirms = /* @__PURE__ */ new Map();
1931
2186
  function setupEvents() {
1932
2187
  events.on("iteration.started", (e) => {
1933
- broadcast({
2188
+ broadcast(clients, {
1934
2189
  type: "iteration.started",
1935
2190
  payload: { index: e.index, maxIterations: config.tools?.maxIterations ?? 100 }
1936
2191
  });
1937
2192
  });
1938
2193
  events.on("provider.text_delta", (e) => {
1939
- broadcast({ type: "provider.text_delta", payload: { text: e.text, messageId: "current" } });
2194
+ broadcast(clients, { type: "provider.text_delta", payload: { text: e.text, messageId: "current" } });
1940
2195
  });
1941
2196
  events.on("provider.thinking_delta", (e) => {
1942
- broadcast({ type: "provider.thinking_delta", payload: { text: e.text } });
2197
+ broadcast(clients, { type: "provider.thinking_delta", payload: { text: e.text } });
1943
2198
  });
1944
2199
  events.on("tool.started", (e) => {
1945
- broadcast({
2200
+ broadcast(clients, {
1946
2201
  type: "tool.started",
1947
2202
  payload: { id: e.id, name: e.name, input: e.input, messageId: `tool_${e.id}` }
1948
2203
  });
1949
2204
  });
1950
2205
  events.on("tool.progress", (e) => {
1951
- broadcast({
2206
+ broadcast(clients, {
1952
2207
  type: "tool.progress",
1953
2208
  payload: {
1954
2209
  id: e.id,
@@ -1959,7 +2214,7 @@ async function startWebUI(opts = {}) {
1959
2214
  });
1960
2215
  });
1961
2216
  events.on("tool.executed", (e) => {
1962
- broadcast({
2217
+ broadcast(clients, {
1963
2218
  type: "tool.executed",
1964
2219
  payload: {
1965
2220
  // Forward the tool_use id so frontend can correlate with the
@@ -1974,13 +2229,13 @@ async function startWebUI(opts = {}) {
1974
2229
  output: e.output
1975
2230
  }
1976
2231
  });
1977
- broadcast({
2232
+ broadcast(clients, {
1978
2233
  type: "todos.updated",
1979
2234
  payload: { todos: [...context.todos] }
1980
2235
  });
1981
2236
  });
1982
2237
  events.on("provider.response", (e) => {
1983
- broadcast({
2238
+ broadcast(clients, {
1984
2239
  type: "provider.response",
1985
2240
  payload: {
1986
2241
  usage: e.usage,
@@ -1990,7 +2245,7 @@ async function startWebUI(opts = {}) {
1990
2245
  });
1991
2246
  });
1992
2247
  events.on("context.repaired", (e) => {
1993
- broadcast({
2248
+ broadcast(clients, {
1994
2249
  type: "context.repaired",
1995
2250
  payload: {
1996
2251
  removedToolUses: e.removedToolUses,
@@ -2002,7 +2257,7 @@ async function startWebUI(opts = {}) {
2002
2257
  events.on("tool.confirm_needed", (e) => {
2003
2258
  const id = e.toolUseId ?? `confirm_${Date.now()}`;
2004
2259
  pendingConfirms.set(id, e.resolve);
2005
- broadcast({
2260
+ broadcast(clients, {
2006
2261
  type: "tool.confirm_needed",
2007
2262
  payload: {
2008
2263
  id,
@@ -2013,7 +2268,7 @@ async function startWebUI(opts = {}) {
2013
2268
  });
2014
2269
  });
2015
2270
  events.on("error", (e) => {
2016
- broadcast({
2271
+ broadcast(clients, {
2017
2272
  type: "error",
2018
2273
  payload: {
2019
2274
  phase: e.phase,
@@ -2021,19 +2276,71 @@ async function startWebUI(opts = {}) {
2021
2276
  }
2022
2277
  });
2023
2278
  });
2024
- }
2025
- function send(ws, msg) {
2026
- if (ws.readyState === WebSocket.OPEN) {
2027
- ws.send(JSON.stringify(msg));
2028
- }
2029
- }
2030
- function broadcast(msg) {
2031
- const data = JSON.stringify(msg);
2032
- for (const [ws] of clients) {
2033
- if (ws.readyState === WebSocket.OPEN) {
2034
- ws.send(data);
2035
- }
2036
- }
2279
+ const forwardSubagent = (kind, payload) => broadcast(clients, { type: "subagent.event", payload: { kind, ...payload } });
2280
+ events.on(
2281
+ "subagent.spawned",
2282
+ (e) => forwardSubagent("spawned", {
2283
+ subagentId: e.subagentId,
2284
+ taskId: e.taskId,
2285
+ name: e.name,
2286
+ provider: e.provider,
2287
+ model: e.model,
2288
+ description: e.description
2289
+ })
2290
+ );
2291
+ events.on(
2292
+ "subagent.task_started",
2293
+ (e) => forwardSubagent("task_started", {
2294
+ subagentId: e.subagentId,
2295
+ taskId: e.taskId,
2296
+ description: e.description
2297
+ })
2298
+ );
2299
+ events.on(
2300
+ "subagent.tool_executed",
2301
+ (e) => forwardSubagent("tool_executed", {
2302
+ subagentId: e.subagentId,
2303
+ toolName: e.name,
2304
+ durationMs: e.durationMs,
2305
+ ok: e.ok
2306
+ })
2307
+ );
2308
+ events.on(
2309
+ "subagent.iteration_summary",
2310
+ (e) => forwardSubagent("iteration_summary", {
2311
+ subagentId: e.subagentId,
2312
+ iteration: e.iteration,
2313
+ toolCalls: e.toolCalls,
2314
+ costUsd: e.costUsd,
2315
+ currentTool: e.currentTool
2316
+ })
2317
+ );
2318
+ events.on(
2319
+ "subagent.budget_extended",
2320
+ (e) => forwardSubagent("budget_extended", {
2321
+ subagentId: e.subagentId,
2322
+ totalExtensions: e.totalExtensions
2323
+ })
2324
+ );
2325
+ events.on(
2326
+ "subagent.ctx_pct",
2327
+ (e) => forwardSubagent("ctx_pct", {
2328
+ subagentId: e.subagentId,
2329
+ load: e.load,
2330
+ tokens: e.tokens,
2331
+ maxContext: e.maxContext
2332
+ })
2333
+ );
2334
+ events.on(
2335
+ "subagent.task_completed",
2336
+ (e) => forwardSubagent("task_completed", {
2337
+ subagentId: e.subagentId,
2338
+ status: e.status,
2339
+ iterations: e.iterations,
2340
+ toolCalls: e.toolCalls,
2341
+ error: e.error ? { kind: e.error.kind, message: e.error.message } : void 0
2342
+ })
2343
+ );
2037
2344
  }
2038
2345
  const handleConnection = (ws) => {
2039
2346
  const client = { ws, sessionId: session.id, connectedAt: Date.now() };
@@ -2094,13 +2401,13 @@ async function startWebUI(opts = {}) {
2094
2401
  console.log(`[WebUI] Backend ready (${label})`);
2095
2402
  setupEvents();
2096
2403
  };
2097
- wssPrimary.on("listening", () => armOnce(`${wsHost2}:${wsPort2}`));
2404
+ wssPrimary.on("listening", () => armOnce(`${wsHost}:${wsPort}`));
2098
2405
  wssPrimary.on("connection", handleConnection);
2099
2406
  wssPrimary.on("error", (err) => {
2100
- console.error(`[WebUI] Primary WS server error (${wsHost2}):`, err);
2407
+ console.error(`[WebUI] Primary WS server error (${wsHost}):`, err);
2101
2408
  });
2102
2409
  if (wssSecondary) {
2103
- wssSecondary.on("listening", () => armOnce(`::1:${wsPort2}`));
2410
+ wssSecondary.on("listening", () => armOnce(`::1:${wsPort}`));
2104
2411
  wssSecondary.on("connection", handleConnection);
2105
2412
  wssSecondary.on("error", (err) => {
2106
2413
  if (err.code === "EAFNOSUPPORT" || err.code === "EADDRNOTAVAIL") {
@@ -2177,7 +2484,7 @@ async function startWebUI(opts = {}) {
2177
2484
  }
2178
2485
  case "abort":
2179
2486
  runLock?.abort();
2180
- broadcast({ type: "error", payload: { phase: "abort", message: "User aborted" } });
2487
+ broadcast(clients, { type: "error", payload: { phase: "abort", message: "User aborted" } });
2181
2488
  break;
2182
2489
  case "ping":
2183
2490
  send(ws, { type: "pong", payload: {} });
@@ -2196,7 +2503,7 @@ async function startWebUI(opts = {}) {
2196
2503
  context.fileMtimes.clear();
2197
2504
  tokenCounter.reset();
2198
2505
  sessionStartedAt = Date.now();
2199
- broadcast({ type: "session.start", payload: await sessionStartPayload() });
2506
+ broadcast(clients, { type: "session.start", payload: await sessionStartPayload() });
2200
2507
  break;
2201
2508
  }
2202
2509
  case "context.clear": {
@@ -2206,7 +2513,7 @@ async function startWebUI(opts = {}) {
2206
2513
  context.fileMtimes.clear();
2207
2514
  tokenCounter.reset();
2208
2515
  sendResult(ws, true, "Context cleared");
2209
- broadcast({
2516
+ broadcast(clients, {
2210
2517
  type: "session.start",
2211
2518
  payload: { ...await sessionStartPayload(), reset: true }
2212
2519
  });
@@ -2265,7 +2572,7 @@ async function startWebUI(opts = {}) {
2265
2572
  beforeMessages,
2266
2573
  afterMessages: context.messages.length
2267
2574
  };
2268
- broadcast({ type: "context.repaired", payload });
2575
+ broadcast(clients, { type: "context.repaired", payload });
2269
2576
  const removed = payload.removedToolUses.length + payload.removedToolResults.length + payload.removedMessages;
2270
2577
  sendResult(
2271
2578
  ws,
@@ -2303,7 +2610,7 @@ async function startWebUI(opts = {}) {
2303
2610
  context.meta["contextWindowMode"] = policy.id;
2304
2611
  context.meta["contextWindowPolicy"] = policy;
2305
2612
  sendResult(ws, true, `Context mode switched to ${policy.id}`);
2306
- broadcast({
2613
+ broadcast(clients, {
2307
2614
  type: "context.mode.changed",
2308
2615
  payload: { id: policy.id, name: policy.name, policy }
2309
2616
  });
@@ -2329,7 +2636,7 @@ async function startWebUI(opts = {}) {
2329
2636
  break;
2330
2637
  }
2331
2638
  case "providers.saved": {
2332
- const saved = await loadSavedProviders();
2639
+ const saved = await loadConfigProviders();
2333
2640
  send(ws, {
2334
2641
  type: "providers.saved",
2335
2642
  payload: {
@@ -2388,11 +2695,11 @@ async function startWebUI(opts = {}) {
2388
2695
  updateAutoCompactionMaxContext?.(newProv);
2389
2696
  try {
2390
2697
  configWriteLock = configWriteLock.then(async () => {
2391
- const raw = await fs2.readFile(globalConfigPath, "utf8");
2698
+ const raw = await fs4.readFile(globalConfigPath, "utf8");
2392
2699
  const parsed = JSON.parse(raw);
2393
2700
  parsed.provider = newProvider;
2394
2701
  parsed.model = newModel;
2395
- await atomicWrite(globalConfigPath, JSON.stringify(parsed, null, 2));
2702
+ await atomicWrite3(globalConfigPath, JSON.stringify(parsed, null, 2));
2396
2703
  });
2397
2704
  await configWriteLock;
2398
2705
  } catch (err) {
@@ -2412,7 +2719,7 @@ async function startWebUI(opts = {}) {
2412
2719
  });
2413
2720
  break;
2414
2721
  }
2415
- broadcast({ type: "session.start", payload: await sessionStartPayload() });
2722
+ broadcast(clients, { type: "session.start", payload: await sessionStartPayload() });
2416
2723
  break;
2417
2724
  }
2418
2725
  case "key.add":
@@ -2501,7 +2808,7 @@ async function startWebUI(opts = {}) {
2501
2808
  tokenCounter.reset();
2502
2809
  tokenCounter.account(resumed.data.usage, config.model);
2503
2810
  sessionStartedAt = Date.now();
2504
- broadcast({
2811
+ broadcast(clients, {
2505
2812
  type: "session.start",
2506
2813
  payload: {
2507
2814
  ...await sessionStartPayload(),
@@ -2641,7 +2948,7 @@ async function startWebUI(opts = {}) {
2641
2948
  case "todos.clear": {
2642
2949
  context.state.replaceTodos([]);
2643
2950
  sendResult(ws, true, "Todos cleared");
2644
- broadcast({ type: "todos.updated", payload: { todos: [] } });
2951
+ broadcast(clients, { type: "todos.updated", payload: { todos: [] } });
2645
2952
  break;
2646
2953
  }
2647
2954
  case "plan.get": {
@@ -2688,7 +2995,7 @@ async function startWebUI(opts = {}) {
2688
2995
  }
2689
2996
  await savePlan(planPath, plan);
2690
2997
  sendResult(ws, true, `Applied template "${tpl.name}" \u2014 ${tpl.items.length} items added.`);
2691
- broadcast({
2998
+ broadcast(clients, {
2692
2999
  type: "plan.updated",
2693
3000
  payload: { plan }
2694
3001
  });
@@ -2705,7 +3012,7 @@ async function startWebUI(opts = {}) {
2705
3012
  if (depth > 8 || results.length >= 600) return;
2706
3013
  let entries = [];
2707
3014
  try {
2708
- entries = await fs2.readdir(dir, { withFileTypes: true });
3015
+ entries = await fs4.readdir(dir, { withFileTypes: true });
2709
3016
  } catch {
2710
3017
  return;
2711
3018
  }
@@ -2715,7 +3022,7 @@ async function startWebUI(opts = {}) {
2715
3022
  const childRel = rel ? `${rel}/${e.name}` : e.name;
2716
3023
  if (e.isDirectory()) {
2717
3024
  if (SKIP_DIRS.has(e.name)) continue;
2718
- await walk(path2.join(dir, e.name), childRel, depth + 1);
3025
+ await walk(path4.join(dir, e.name), childRel, depth + 1);
2719
3026
  } else if (e.isFile()) {
2720
3027
  results.push(childRel);
2721
3028
  }
@@ -2784,7 +3091,7 @@ async function startWebUI(opts = {}) {
2784
3091
  model: config.model
2785
3092
  });
2786
3093
  sendResult(ws, true, `Switched to mode "${id}"`);
2787
- broadcast({
3094
+ broadcast(clients, {
2788
3095
  type: "session.start",
2789
3096
  payload: { ...await sessionStartPayload() }
2790
3097
  });
@@ -2823,39 +3130,20 @@ async function startWebUI(opts = {}) {
2823
3130
  }
2824
3131
  }
2825
3132
  }
2826
- async function loadSavedProviders() {
2827
- try {
2828
- const raw = await fs2.readFile(globalConfigPath, "utf8");
2829
- const parsed = JSON.parse(raw);
2830
- if (!parsed.providers) return {};
2831
- return decryptConfigSecrets(parsed.providers, vault);
2832
- } catch {
2833
- return {};
2834
- }
3133
+ async function loadConfigProviders() {
3134
+ return loadSavedProviders(globalConfigPath, vault);
2835
3135
  }
2836
- async function saveProviders(providers) {
2837
- configWriteLock = configWriteLock.then(async () => {
2838
- let parsed;
2839
- try {
2840
- const raw = await fs2.readFile(globalConfigPath, "utf8");
2841
- parsed = JSON.parse(raw);
2842
- } catch {
2843
- parsed = {};
2844
- }
2845
- parsed["providers"] = providers;
2846
- const encrypted = encryptConfigSecrets(parsed, vault);
2847
- await atomicWrite(globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
2848
- });
3136
+ async function saveConfigProviders(providers) {
3137
+ configWriteLock = configWriteLock.then(
3138
+ () => saveProviders(globalConfigPath, vault, providers)
3139
+ );
2849
3140
  await configWriteLock;
2850
3141
  }
2851
- function sendResult(ws, success, message) {
2852
- send(ws, { type: "key.operation_result", payload: { success, message } });
2853
- }
2854
3142
  async function handleKeyUpsert(ws, providerId, label, apiKey) {
2855
3143
  try {
2856
- const providers = await loadSavedProviders();
3144
+ const providers = await loadConfigProviders();
2857
3145
  const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
2858
- if (result.ok) await saveProviders(providers);
3146
+ if (result.ok) await saveConfigProviders(providers);
2859
3147
  sendResult(ws, result.ok, result.message);
2860
3148
  } catch (err) {
2861
3149
  sendResult(ws, false, errMessage(err));
@@ -2863,9 +3151,9 @@ async function startWebUI(opts = {}) {
2863
3151
  }
2864
3152
  async function handleKeyDelete(ws, providerId, label) {
2865
3153
  try {
2866
- const providers = await loadSavedProviders();
3154
+ const providers = await loadConfigProviders();
2867
3155
  const result = deleteKey(providers, providerId, label);
2868
- if (result.ok) await saveProviders(providers);
3156
+ if (result.ok) await saveConfigProviders(providers);
2869
3157
  sendResult(ws, result.ok, result.message);
2870
3158
  } catch (err) {
2871
3159
  sendResult(ws, false, errMessage(err));
@@ -2873,9 +3161,9 @@ async function startWebUI(opts = {}) {
2873
3161
  }
2874
3162
  async function handleKeySetActive(ws, providerId, label) {
2875
3163
  try {
2876
- const providers = await loadSavedProviders();
3164
+ const providers = await loadConfigProviders();
2877
3165
  const result = setActiveKey(providers, providerId, label);
2878
- if (result.ok) await saveProviders(providers);
3166
+ if (result.ok) await saveConfigProviders(providers);
2879
3167
  sendResult(ws, result.ok, result.message);
2880
3168
  } catch (err) {
2881
3169
  sendResult(ws, false, errMessage(err));
@@ -2883,9 +3171,9 @@ async function startWebUI(opts = {}) {
2883
3171
  }
2884
3172
  async function handleProviderAdd(ws, payload) {
2885
3173
  try {
2886
- const providers = await loadSavedProviders();
3174
+ const providers = await loadConfigProviders();
2887
3175
  const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
2888
- if (result.ok) await saveProviders(providers);
3176
+ if (result.ok) await saveConfigProviders(providers);
2889
3177
  sendResult(ws, result.ok, result.message);
2890
3178
  } catch (err) {
2891
3179
  sendResult(ws, false, errMessage(err));
@@ -2893,22 +3181,37 @@ async function startWebUI(opts = {}) {
2893
3181
  }
2894
3182
  async function handleProviderRemove(ws, providerId) {
2895
3183
  try {
2896
- const providers = await loadSavedProviders();
3184
+ const providers = await loadConfigProviders();
2897
3185
  const result = removeProvider(providers, providerId);
2898
- if (result.ok) await saveProviders(providers);
3186
+ if (result.ok) await saveConfigProviders(providers);
2899
3187
  sendResult(ws, result.ok, result.message);
2900
3188
  } catch (err) {
2901
3189
  sendResult(ws, false, errMessage(err));
2902
3190
  }
2903
3191
  }
2904
3192
  const httpServer = createHttpServer({
2905
- host: wsHost2,
2906
- distDir: path2.resolve(import.meta.dirname, "../../dist"),
2907
- wsPort: wsPort2
3193
+ host: wsHost,
3194
+ distDir: path4.resolve(import.meta.dirname, "../../dist"),
3195
+ wsPort
2908
3196
  });
2909
- const httpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
2910
- httpServer.listen(httpPort, wsHost2, () => {
2911
- console.log(`[WebUI] HTTP server running on http://${wsHost2}:${httpPort}`);
3197
+ const registryBaseDir = path4.dirname(globalConfigPath);
3198
+ httpServer.listen(httpPort, wsHost, () => {
3199
+ const openUrl = `http://${wsHost}:${httpPort}`;
3200
+ console.log(`[WebUI] HTTP server running on ${openUrl}`);
3201
+ if (opts.open) openBrowser(openUrl);
3202
+ void registerInstance(
3203
+ {
3204
+ pid: process.pid,
3205
+ httpPort,
3206
+ wsPort,
3207
+ host: wsHost,
3208
+ projectRoot,
3209
+ projectName: path4.basename(projectRoot) || projectRoot,
3210
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3211
+ url: `http://${wsHost}:${httpPort}`
3212
+ },
3213
+ registryBaseDir
3214
+ ).catch((err) => console.warn("[WebUI] Could not record instance:", errMessage(err)));
2912
3215
  });
2913
3216
  registerShutdownHandlers({
2914
3217
  flushSession: async () => {
@@ -2920,16 +3223,31 @@ async function startWebUI(opts = {}) {
2920
3223
  await session.close();
2921
3224
  },
2922
3225
  clients: () => clients.keys(),
2923
- servers: [httpServer, wssPrimary, wssSecondary]
3226
+ servers: [httpServer, wssPrimary, wssSecondary],
3227
+ // Drop this instance from the registry on a clean exit so the file reflects
3228
+ // reality. Crash exits are healed by the next register()/list() prune pass.
3229
+ onShutdown: () => unregisterInstance(process.pid, registryBaseDir)
2924
3230
  });
2925
3231
  }
2926
3232
 
2927
3233
  // src/server/entry.ts
2928
- var wsPort = Number.parseInt(process.env["WS_PORT"] ?? "3457", 10);
2929
- var wsHost = process.env["WS_HOST"] ?? "127.0.0.1";
2930
- console.log(`[WebUI] Starting standalone server on ${wsHost}:${wsPort}...`);
2931
- startWebUI({ wsPort, wsHost }).catch((err) => {
2932
- console.error("[WebUI] Fatal error:", err);
2933
- process.exit(1);
2934
- });
3234
+ var argv = process.argv.slice(2);
3235
+ if (argv.includes("--list") || argv.includes("-l") || argv[0] === "ls") {
3236
+ listInstances().then((instances) => {
3237
+ console.log(formatInstances(instances));
3238
+ process.exit(0);
3239
+ }).catch((err) => {
3240
+ console.error("[WebUI] Could not read instance registry:", err);
3241
+ process.exit(1);
3242
+ });
3243
+ } else {
3244
+ const wsPort = Number.parseInt(process.env["WS_PORT"] ?? "3457", 10);
3245
+ const wsHost = process.env["WS_HOST"] ?? "127.0.0.1";
3246
+ const open = argv.includes("--open") || argv.includes("-o") || process.env["WEBUI_OPEN"] === "1";
3247
+ console.log(`[WebUI] Starting standalone server on ${wsHost}:${wsPort}...`);
3248
+ startWebUI({ wsPort, wsHost, open }).catch((err) => {
3249
+ console.error("[WebUI] Fatal error:", err);
3250
+ process.exit(1);
3251
+ });
3252
+ }
2935
3253
  //# sourceMappingURL=entry.js.map