@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,6 +1,6 @@
1
1
  // src/server/index.ts
2
- import * as fs2 from "fs/promises";
3
- import * as path2 from "path";
2
+ import * as fs4 from "fs/promises";
3
+ import * as path4 from "path";
4
4
 
5
5
  // src/server/http-server.ts
6
6
  import * as fs from "fs/promises";
@@ -15,6 +15,16 @@ var MIME_TYPES = {
15
15
  ".png": "image/png",
16
16
  ".ico": "image/x-icon"
17
17
  };
18
+ function injectWsPort(html, wsPort) {
19
+ const tag = `<meta name="wrongstack-ws-port" content="${wsPort}" />`;
20
+ if (html.includes('name="wrongstack-ws-port"')) return html;
21
+ if (html.includes("</head>")) {
22
+ return html.replace("</head>", ` ${tag}
23
+ </head>`);
24
+ }
25
+ return `${tag}
26
+ ${html}`;
27
+ }
18
28
  function buildCspHeader(wsPort) {
19
29
  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'`;
20
30
  }
@@ -55,6 +65,10 @@ function createHttpServer(opts) {
55
65
  if (ext === ".html") {
56
66
  res.setHeader("Cache-Control", "no-cache");
57
67
  res.setHeader("Content-Security-Policy", buildCspHeader(wsPort));
68
+ const html = await fs.readFile(resolvedPath, "utf8");
69
+ res.writeHead(200);
70
+ res.end(injectWsPort(html, wsPort));
71
+ return;
58
72
  }
59
73
  const fileContent = await fs.readFile(resolvedPath);
60
74
  res.writeHead(200);
@@ -62,7 +76,7 @@ function createHttpServer(opts) {
62
76
  } catch (err) {
63
77
  if (err.code === "ENOENT") {
64
78
  try {
65
- const fileContent = await fs.readFile(path.join(distDir, "index.html"));
79
+ const html = await fs.readFile(path.join(distDir, "index.html"), "utf8");
66
80
  res.writeHead(200, {
67
81
  "Content-Type": "text/html",
68
82
  "X-Content-Type-Options": "nosniff",
@@ -70,7 +84,7 @@ function createHttpServer(opts) {
70
84
  "Referrer-Policy": "strict-origin-when-cross-origin",
71
85
  "Content-Security-Policy": buildCspHeader(wsPort)
72
86
  });
73
- res.end(fileContent);
87
+ res.end(injectWsPort(html, wsPort));
74
88
  } catch {
75
89
  res.writeHead(404);
76
90
  res.end("Not found");
@@ -154,7 +168,7 @@ import {
154
168
  ProviderRegistry,
155
169
  TOKENS as TOKENS2,
156
170
  ToolRegistry,
157
- atomicWrite,
171
+ atomicWrite as atomicWrite3,
158
172
  createDefaultPipelines,
159
173
  DEFAULT_CONTEXT_WINDOW_MODE_ID,
160
174
  DEFAULT_TOOLS_CONFIG,
@@ -163,11 +177,9 @@ import {
163
177
  resolveContextWindowPolicy
164
178
  } from "@wrongstack/core";
165
179
  import { ToolExecutor } from "@wrongstack/core/execution";
166
- import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
167
180
  import { buildProviderFactoriesFromRegistry, makeProviderFromConfig } from "@wrongstack/providers";
168
181
  import { builtinToolsPack, forgetTool, rememberTool } from "@wrongstack/tools";
169
- import { WebSocket, WebSocketServer } from "ws";
170
- import { randomBytes } from "crypto";
182
+ import { WebSocketServer } from "ws";
171
183
 
172
184
  // ../runtime/src/container.ts
173
185
  import {
@@ -226,7 +238,8 @@ function createDefaultContainer(opts) {
226
238
  () => new DefaultPermissionPolicy({
227
239
  trustFile: wpaths.projectTrust,
228
240
  yolo: opts.permission?.yolo ?? false,
229
- forceAllYolo: opts.permission?.forceAllYolo ?? false,
241
+ yoloDestructive: opts.permission?.yoloDestructive ?? opts.permission?.forceAllYolo ?? false,
242
+ confirmDestructive: opts.permission?.confirmDestructive ?? false,
230
243
  promptDelegate: opts.permission?.promptDelegate
231
244
  })
232
245
  );
@@ -1446,6 +1459,13 @@ function createShutdown(res) {
1446
1459
  }
1447
1460
  for (const ws of res.clients()) ws.close();
1448
1461
  for (const server of res.servers) server?.close();
1462
+ if (res.onShutdown) {
1463
+ try {
1464
+ await res.onShutdown();
1465
+ } catch (e) {
1466
+ log(`[WebUI] Error during shutdown cleanup: ${e instanceof Error ? e.message : String(e)}`);
1467
+ }
1468
+ }
1449
1469
  exit(0);
1450
1470
  };
1451
1471
  }
@@ -1459,6 +1479,138 @@ function registerShutdownHandlers(res) {
1459
1479
  };
1460
1480
  }
1461
1481
 
1482
+ // src/server/instance-registry.ts
1483
+ import * as os from "os";
1484
+ import * as path2 from "path";
1485
+ import * as fs2 from "fs/promises";
1486
+ import { atomicWrite } from "@wrongstack/core";
1487
+ function defaultBaseDir() {
1488
+ return path2.join(os.homedir(), ".wrongstack");
1489
+ }
1490
+ function registryPath(baseDir = defaultBaseDir()) {
1491
+ return path2.join(baseDir, "webui-instances.json");
1492
+ }
1493
+ function isPidAlive(pid) {
1494
+ if (!Number.isInteger(pid) || pid <= 0) return false;
1495
+ try {
1496
+ process.kill(pid, 0);
1497
+ return true;
1498
+ } catch (err) {
1499
+ return err.code !== "ESRCH";
1500
+ }
1501
+ }
1502
+ async function load(file) {
1503
+ try {
1504
+ const raw = await fs2.readFile(file, "utf8");
1505
+ const parsed = JSON.parse(raw);
1506
+ if (parsed?.version === 1 && Array.isArray(parsed.instances)) {
1507
+ return parsed;
1508
+ }
1509
+ } catch {
1510
+ }
1511
+ return { version: 1, instances: [] };
1512
+ }
1513
+ async function save(file, instances) {
1514
+ await atomicWrite(file, `${JSON.stringify({ version: 1, instances }, null, 2)}
1515
+ `, {
1516
+ mode: 384
1517
+ });
1518
+ }
1519
+ function prune(instances, excludePid) {
1520
+ return instances.filter((i) => i.pid !== excludePid && isPidAlive(i.pid));
1521
+ }
1522
+ async function registerInstance(record, baseDir = defaultBaseDir()) {
1523
+ const file = registryPath(baseDir);
1524
+ const data = await load(file);
1525
+ const instances = prune(data.instances, record.pid);
1526
+ instances.push(record);
1527
+ await save(file, instances);
1528
+ }
1529
+ async function unregisterInstance(pid, baseDir = defaultBaseDir()) {
1530
+ const file = registryPath(baseDir);
1531
+ const data = await load(file);
1532
+ const instances = prune(data.instances, pid);
1533
+ await save(file, instances);
1534
+ }
1535
+ async function listInstances(baseDir = defaultBaseDir()) {
1536
+ const file = registryPath(baseDir);
1537
+ const data = await load(file);
1538
+ const live = prune(data.instances);
1539
+ if (live.length !== data.instances.length) {
1540
+ await save(file, live).catch(() => {
1541
+ });
1542
+ }
1543
+ return live;
1544
+ }
1545
+ function formatInstances(instances) {
1546
+ if (instances.length === 0) {
1547
+ return "No WebUI instances are currently running.";
1548
+ }
1549
+ const lines = [`Running WebUI instances (${instances.length}):`, ""];
1550
+ for (const i of instances) {
1551
+ lines.push(
1552
+ ` \u2022 ${i.url} \xB7 ws:${i.wsPort} \xB7 pid ${i.pid}`,
1553
+ ` project: ${i.projectName} (${i.projectRoot})`,
1554
+ ` since: ${i.startedAt}`
1555
+ );
1556
+ }
1557
+ return lines.join("\n");
1558
+ }
1559
+
1560
+ // src/server/port-utils.ts
1561
+ import * as net from "net";
1562
+ function isPortFree(host, port) {
1563
+ return new Promise((resolve3) => {
1564
+ const srv = net.createServer();
1565
+ srv.once("error", () => resolve3(false));
1566
+ srv.once("listening", () => {
1567
+ srv.close(() => resolve3(true));
1568
+ });
1569
+ try {
1570
+ srv.listen(port, host);
1571
+ } catch {
1572
+ resolve3(false);
1573
+ }
1574
+ });
1575
+ }
1576
+ async function findFreePort(host, startPort, opts = {}) {
1577
+ const exclude = opts.exclude ?? /* @__PURE__ */ new Set();
1578
+ const maxTries = opts.maxTries ?? 200;
1579
+ let port = startPort;
1580
+ for (let i = 0; i < maxTries; i++) {
1581
+ if (port > 65535) port = 1024 + port % 5e4;
1582
+ if (!exclude.has(port) && await isPortFree(host, port)) {
1583
+ return port;
1584
+ }
1585
+ port++;
1586
+ }
1587
+ throw new Error(
1588
+ `No free port found near ${startPort} on ${host} after ${maxTries} attempts.`
1589
+ );
1590
+ }
1591
+
1592
+ // src/server/open-browser.ts
1593
+ import { spawn } from "child_process";
1594
+ function browserOpenCommand(url, platform = process.platform) {
1595
+ if (platform === "win32") {
1596
+ return { command: "cmd", args: ["/c", "start", "", url] };
1597
+ }
1598
+ if (platform === "darwin") {
1599
+ return { command: "open", args: [url] };
1600
+ }
1601
+ return { command: "xdg-open", args: [url] };
1602
+ }
1603
+ function openBrowser(url, platform = process.platform) {
1604
+ try {
1605
+ const { command, args } = browserOpenCommand(url, platform);
1606
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
1607
+ child.on("error", () => {
1608
+ });
1609
+ child.unref();
1610
+ } catch {
1611
+ }
1612
+ }
1613
+
1462
1614
  // src/server/usage-cost.ts
1463
1615
  function getCostRates(model) {
1464
1616
  const cost = model?.cost;
@@ -1567,6 +1719,97 @@ function removeProvider(providers, providerId) {
1567
1719
  return { ok: true, message: `Provider "${providerId}" removed` };
1568
1720
  }
1569
1721
 
1722
+ // src/server/provider-config-io.ts
1723
+ import * as fs3 from "fs/promises";
1724
+ import * as path3 from "path";
1725
+ import { atomicWrite as atomicWrite2 } from "@wrongstack/core";
1726
+ import { decryptConfigSecrets, encryptConfigSecrets } from "@wrongstack/core/security";
1727
+ import { DefaultSecretVault } from "@wrongstack/core";
1728
+ async function loadSavedProviders(configPath, vault) {
1729
+ let raw;
1730
+ try {
1731
+ raw = await fs3.readFile(configPath, "utf8");
1732
+ } catch {
1733
+ return {};
1734
+ }
1735
+ let parsed = {};
1736
+ try {
1737
+ parsed = JSON.parse(raw);
1738
+ } catch {
1739
+ return {};
1740
+ }
1741
+ if (!parsed.providers) return {};
1742
+ return decryptConfigSecrets(parsed.providers, vault);
1743
+ }
1744
+ async function saveProviders(configPath, vault, providers) {
1745
+ let raw;
1746
+ let fileExists = true;
1747
+ try {
1748
+ raw = await fs3.readFile(configPath, "utf8");
1749
+ } catch (err) {
1750
+ if (err.code !== "ENOENT") {
1751
+ throw new Error(
1752
+ `Refusing to mutate ${configPath}: ${err.message}`,
1753
+ { cause: err }
1754
+ );
1755
+ }
1756
+ fileExists = false;
1757
+ raw = "{}";
1758
+ }
1759
+ let parsed;
1760
+ try {
1761
+ parsed = JSON.parse(raw);
1762
+ } catch (err) {
1763
+ if (fileExists) {
1764
+ throw new Error(
1765
+ `Refusing to overwrite corrupt config at ${configPath} (${err.message}). Fix or move the file aside before retrying.`,
1766
+ { cause: err }
1767
+ );
1768
+ }
1769
+ parsed = {};
1770
+ }
1771
+ parsed.providers = providers;
1772
+ const encrypted = encryptConfigSecrets(parsed, vault);
1773
+ await atomicWrite2(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
1774
+ }
1775
+ function createProviderConfigIO(configPath) {
1776
+ const keyFile = path3.join(path3.dirname(configPath), ".key");
1777
+ const vault = new DefaultSecretVault({ keyFile });
1778
+ return {
1779
+ load: () => loadSavedProviders(configPath, vault),
1780
+ save: (providers) => saveProviders(configPath, vault, providers)
1781
+ };
1782
+ }
1783
+
1784
+ // src/server/ws-utils.ts
1785
+ import { randomBytes } from "crypto";
1786
+ import { WebSocket } from "ws";
1787
+ function send(ws, msg) {
1788
+ if (ws.readyState === WebSocket.OPEN) {
1789
+ ws.send(JSON.stringify(msg));
1790
+ }
1791
+ }
1792
+ function broadcast(clients, msg) {
1793
+ const data = JSON.stringify(msg);
1794
+ for (const [ws] of clients) {
1795
+ if (ws.readyState === WebSocket.OPEN) {
1796
+ try {
1797
+ ws.send(data);
1798
+ } catch {
1799
+ }
1800
+ }
1801
+ }
1802
+ }
1803
+ function sendResult(ws, success, message) {
1804
+ send(ws, { type: "key.operation_result", payload: { success, message } });
1805
+ }
1806
+ function errMessage(err) {
1807
+ return err instanceof Error ? err.message : String(err);
1808
+ }
1809
+ function generateAuthToken() {
1810
+ return randomBytes(16).toString("hex");
1811
+ }
1812
+
1570
1813
  // src/server/token-estimator.ts
1571
1814
  function estimateTokens(s) {
1572
1815
  return Math.ceil(s.length / 4);
@@ -1625,12 +1868,23 @@ function estimateContextBreakdown(input) {
1625
1868
  }
1626
1869
 
1627
1870
  // src/server/index.ts
1628
- function errMessage(err) {
1629
- return err instanceof Error ? err.message : String(err);
1630
- }
1631
1871
  async function startWebUI(opts = {}) {
1632
- const wsPort = opts.wsPort ?? 3457;
1872
+ const requestedWsPort = opts.wsPort ?? 3457;
1633
1873
  const wsHost = opts.wsHost ?? "127.0.0.1";
1874
+ const requestedHttpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
1875
+ const strictPort = process.env["WEBUI_STRICT_PORT"] === "1" || process.env["WEBUI_STRICT_PORT"] === "true";
1876
+ let wsPort = requestedWsPort;
1877
+ let httpPort = requestedHttpPort;
1878
+ if (!strictPort) {
1879
+ httpPort = await findFreePort(wsHost, requestedHttpPort);
1880
+ wsPort = await findFreePort(wsHost, requestedWsPort, { exclude: /* @__PURE__ */ new Set([httpPort]) });
1881
+ if (httpPort !== requestedHttpPort) {
1882
+ console.warn(`[WebUI] HTTP port ${requestedHttpPort} in use \u2192 using ${httpPort}`);
1883
+ }
1884
+ if (wsPort !== requestedWsPort) {
1885
+ console.warn(`[WebUI] WS port ${requestedWsPort} in use \u2192 using ${wsPort}`);
1886
+ }
1887
+ }
1634
1888
  console.log("[WebUI] Starting backend services...");
1635
1889
  const boot = await bootConfig();
1636
1890
  const { config: baseConfig, vault, globalConfigPath, projectRoot, wpaths, logger } = boot;
@@ -1780,7 +2034,15 @@ async function startWebUI(opts = {}) {
1780
2034
  });
1781
2035
  let autoCompactor;
1782
2036
  if (config.context?.autoCompact !== false) {
1783
- const effectiveMaxContext = config.context?.effectiveMaxContext ?? provider.capabilities.maxContext;
2037
+ let effectiveMaxContext = config.context?.effectiveMaxContext ?? 0;
2038
+ if (!effectiveMaxContext) {
2039
+ try {
2040
+ const m = await modelsRegistry.getModel(provider.id, context.model);
2041
+ effectiveMaxContext = m?.capabilities?.maxContext ?? 0;
2042
+ } catch {
2043
+ }
2044
+ }
2045
+ if (!effectiveMaxContext) effectiveMaxContext = provider.capabilities.maxContext;
1784
2046
  autoCompactor = new AutoCompactionMiddleware(
1785
2047
  compactor,
1786
2048
  effectiveMaxContext,
@@ -1876,14 +2138,14 @@ async function startWebUI(opts = {}) {
1876
2138
  inputCost,
1877
2139
  outputCost,
1878
2140
  cacheReadCost,
1879
- projectName: path2.basename(projectRoot) || projectRoot,
2141
+ projectName: path4.basename(projectRoot) || projectRoot,
1880
2142
  cwd: projectRoot,
1881
2143
  mode: modeId,
1882
2144
  contextMode: String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID),
1883
2145
  wsToken
1884
2146
  };
1885
2147
  }
1886
- const wsToken = randomBytes(16).toString("hex");
2148
+ const wsToken = generateAuthToken();
1887
2149
  console.log(`[WebUI] WS auth token: ${wsToken.slice(0, 4)}\u2026${wsToken.slice(-4)} (masked)`);
1888
2150
  const verifyClient2 = (info) => verifyClient({
1889
2151
  origin: info.origin,
@@ -1907,10 +2169,11 @@ async function startWebUI(opts = {}) {
1907
2169
  maxPayload: WS_MAX_PAYLOAD
1908
2170
  }) : null;
1909
2171
  const clients = /* @__PURE__ */ new Map();
1910
- const RATE_LIMIT_MESSAGES = 60;
2172
+ const RATE_LIMIT_MESSAGES = Number.parseInt(process.env["WEBUI_RATE_LIMIT"] ?? "0", 10);
1911
2173
  const RATE_LIMIT_WINDOW_MS = 6e4;
1912
2174
  const rateLimits = /* @__PURE__ */ new Map();
1913
2175
  function checkRateLimit(ws, client) {
2176
+ if (RATE_LIMIT_MESSAGES <= 0) return true;
1914
2177
  const now = Date.now();
1915
2178
  const key = client.sessionId ?? String(ws);
1916
2179
  const limit = rateLimits.get(key);
@@ -1929,25 +2192,25 @@ async function startWebUI(opts = {}) {
1929
2192
  const pendingConfirms = /* @__PURE__ */ new Map();
1930
2193
  function setupEvents() {
1931
2194
  events.on("iteration.started", (e) => {
1932
- broadcast({
2195
+ broadcast(clients, {
1933
2196
  type: "iteration.started",
1934
2197
  payload: { index: e.index, maxIterations: config.tools?.maxIterations ?? 100 }
1935
2198
  });
1936
2199
  });
1937
2200
  events.on("provider.text_delta", (e) => {
1938
- broadcast({ type: "provider.text_delta", payload: { text: e.text, messageId: "current" } });
2201
+ broadcast(clients, { type: "provider.text_delta", payload: { text: e.text, messageId: "current" } });
1939
2202
  });
1940
2203
  events.on("provider.thinking_delta", (e) => {
1941
- broadcast({ type: "provider.thinking_delta", payload: { text: e.text } });
2204
+ broadcast(clients, { type: "provider.thinking_delta", payload: { text: e.text } });
1942
2205
  });
1943
2206
  events.on("tool.started", (e) => {
1944
- broadcast({
2207
+ broadcast(clients, {
1945
2208
  type: "tool.started",
1946
2209
  payload: { id: e.id, name: e.name, input: e.input, messageId: `tool_${e.id}` }
1947
2210
  });
1948
2211
  });
1949
2212
  events.on("tool.progress", (e) => {
1950
- broadcast({
2213
+ broadcast(clients, {
1951
2214
  type: "tool.progress",
1952
2215
  payload: {
1953
2216
  id: e.id,
@@ -1958,7 +2221,7 @@ async function startWebUI(opts = {}) {
1958
2221
  });
1959
2222
  });
1960
2223
  events.on("tool.executed", (e) => {
1961
- broadcast({
2224
+ broadcast(clients, {
1962
2225
  type: "tool.executed",
1963
2226
  payload: {
1964
2227
  // Forward the tool_use id so frontend can correlate with the
@@ -1973,13 +2236,13 @@ async function startWebUI(opts = {}) {
1973
2236
  output: e.output
1974
2237
  }
1975
2238
  });
1976
- broadcast({
2239
+ broadcast(clients, {
1977
2240
  type: "todos.updated",
1978
2241
  payload: { todos: [...context.todos] }
1979
2242
  });
1980
2243
  });
1981
2244
  events.on("provider.response", (e) => {
1982
- broadcast({
2245
+ broadcast(clients, {
1983
2246
  type: "provider.response",
1984
2247
  payload: {
1985
2248
  usage: e.usage,
@@ -1989,7 +2252,7 @@ async function startWebUI(opts = {}) {
1989
2252
  });
1990
2253
  });
1991
2254
  events.on("context.repaired", (e) => {
1992
- broadcast({
2255
+ broadcast(clients, {
1993
2256
  type: "context.repaired",
1994
2257
  payload: {
1995
2258
  removedToolUses: e.removedToolUses,
@@ -2001,7 +2264,7 @@ async function startWebUI(opts = {}) {
2001
2264
  events.on("tool.confirm_needed", (e) => {
2002
2265
  const id = e.toolUseId ?? `confirm_${Date.now()}`;
2003
2266
  pendingConfirms.set(id, e.resolve);
2004
- broadcast({
2267
+ broadcast(clients, {
2005
2268
  type: "tool.confirm_needed",
2006
2269
  payload: {
2007
2270
  id,
@@ -2012,7 +2275,7 @@ async function startWebUI(opts = {}) {
2012
2275
  });
2013
2276
  });
2014
2277
  events.on("error", (e) => {
2015
- broadcast({
2278
+ broadcast(clients, {
2016
2279
  type: "error",
2017
2280
  payload: {
2018
2281
  phase: e.phase,
@@ -2020,19 +2283,71 @@ async function startWebUI(opts = {}) {
2020
2283
  }
2021
2284
  });
2022
2285
  });
2023
- }
2024
- function send(ws, msg) {
2025
- if (ws.readyState === WebSocket.OPEN) {
2026
- ws.send(JSON.stringify(msg));
2027
- }
2028
- }
2029
- function broadcast(msg) {
2030
- const data = JSON.stringify(msg);
2031
- for (const [ws] of clients) {
2032
- if (ws.readyState === WebSocket.OPEN) {
2033
- ws.send(data);
2034
- }
2035
- }
2286
+ const forwardSubagent = (kind, payload) => broadcast(clients, { type: "subagent.event", payload: { kind, ...payload } });
2287
+ events.on(
2288
+ "subagent.spawned",
2289
+ (e) => forwardSubagent("spawned", {
2290
+ subagentId: e.subagentId,
2291
+ taskId: e.taskId,
2292
+ name: e.name,
2293
+ provider: e.provider,
2294
+ model: e.model,
2295
+ description: e.description
2296
+ })
2297
+ );
2298
+ events.on(
2299
+ "subagent.task_started",
2300
+ (e) => forwardSubagent("task_started", {
2301
+ subagentId: e.subagentId,
2302
+ taskId: e.taskId,
2303
+ description: e.description
2304
+ })
2305
+ );
2306
+ events.on(
2307
+ "subagent.tool_executed",
2308
+ (e) => forwardSubagent("tool_executed", {
2309
+ subagentId: e.subagentId,
2310
+ toolName: e.name,
2311
+ durationMs: e.durationMs,
2312
+ ok: e.ok
2313
+ })
2314
+ );
2315
+ events.on(
2316
+ "subagent.iteration_summary",
2317
+ (e) => forwardSubagent("iteration_summary", {
2318
+ subagentId: e.subagentId,
2319
+ iteration: e.iteration,
2320
+ toolCalls: e.toolCalls,
2321
+ costUsd: e.costUsd,
2322
+ currentTool: e.currentTool
2323
+ })
2324
+ );
2325
+ events.on(
2326
+ "subagent.budget_extended",
2327
+ (e) => forwardSubagent("budget_extended", {
2328
+ subagentId: e.subagentId,
2329
+ totalExtensions: e.totalExtensions
2330
+ })
2331
+ );
2332
+ events.on(
2333
+ "subagent.ctx_pct",
2334
+ (e) => forwardSubagent("ctx_pct", {
2335
+ subagentId: e.subagentId,
2336
+ load: e.load,
2337
+ tokens: e.tokens,
2338
+ maxContext: e.maxContext
2339
+ })
2340
+ );
2341
+ events.on(
2342
+ "subagent.task_completed",
2343
+ (e) => forwardSubagent("task_completed", {
2344
+ subagentId: e.subagentId,
2345
+ status: e.status,
2346
+ iterations: e.iterations,
2347
+ toolCalls: e.toolCalls,
2348
+ error: e.error ? { kind: e.error.kind, message: e.error.message } : void 0
2349
+ })
2350
+ );
2036
2351
  }
2037
2352
  const handleConnection = (ws) => {
2038
2353
  const client = { ws, sessionId: session.id, connectedAt: Date.now() };
@@ -2176,7 +2491,7 @@ async function startWebUI(opts = {}) {
2176
2491
  }
2177
2492
  case "abort":
2178
2493
  runLock?.abort();
2179
- broadcast({ type: "error", payload: { phase: "abort", message: "User aborted" } });
2494
+ broadcast(clients, { type: "error", payload: { phase: "abort", message: "User aborted" } });
2180
2495
  break;
2181
2496
  case "ping":
2182
2497
  send(ws, { type: "pong", payload: {} });
@@ -2195,7 +2510,7 @@ async function startWebUI(opts = {}) {
2195
2510
  context.fileMtimes.clear();
2196
2511
  tokenCounter.reset();
2197
2512
  sessionStartedAt = Date.now();
2198
- broadcast({ type: "session.start", payload: await sessionStartPayload() });
2513
+ broadcast(clients, { type: "session.start", payload: await sessionStartPayload() });
2199
2514
  break;
2200
2515
  }
2201
2516
  case "context.clear": {
@@ -2205,7 +2520,7 @@ async function startWebUI(opts = {}) {
2205
2520
  context.fileMtimes.clear();
2206
2521
  tokenCounter.reset();
2207
2522
  sendResult(ws, true, "Context cleared");
2208
- broadcast({
2523
+ broadcast(clients, {
2209
2524
  type: "session.start",
2210
2525
  payload: { ...await sessionStartPayload(), reset: true }
2211
2526
  });
@@ -2264,7 +2579,7 @@ async function startWebUI(opts = {}) {
2264
2579
  beforeMessages,
2265
2580
  afterMessages: context.messages.length
2266
2581
  };
2267
- broadcast({ type: "context.repaired", payload });
2582
+ broadcast(clients, { type: "context.repaired", payload });
2268
2583
  const removed = payload.removedToolUses.length + payload.removedToolResults.length + payload.removedMessages;
2269
2584
  sendResult(
2270
2585
  ws,
@@ -2302,7 +2617,7 @@ async function startWebUI(opts = {}) {
2302
2617
  context.meta["contextWindowMode"] = policy.id;
2303
2618
  context.meta["contextWindowPolicy"] = policy;
2304
2619
  sendResult(ws, true, `Context mode switched to ${policy.id}`);
2305
- broadcast({
2620
+ broadcast(clients, {
2306
2621
  type: "context.mode.changed",
2307
2622
  payload: { id: policy.id, name: policy.name, policy }
2308
2623
  });
@@ -2328,7 +2643,7 @@ async function startWebUI(opts = {}) {
2328
2643
  break;
2329
2644
  }
2330
2645
  case "providers.saved": {
2331
- const saved = await loadSavedProviders();
2646
+ const saved = await loadConfigProviders();
2332
2647
  send(ws, {
2333
2648
  type: "providers.saved",
2334
2649
  payload: {
@@ -2387,11 +2702,11 @@ async function startWebUI(opts = {}) {
2387
2702
  updateAutoCompactionMaxContext?.(newProv);
2388
2703
  try {
2389
2704
  configWriteLock = configWriteLock.then(async () => {
2390
- const raw = await fs2.readFile(globalConfigPath, "utf8");
2705
+ const raw = await fs4.readFile(globalConfigPath, "utf8");
2391
2706
  const parsed = JSON.parse(raw);
2392
2707
  parsed.provider = newProvider;
2393
2708
  parsed.model = newModel;
2394
- await atomicWrite(globalConfigPath, JSON.stringify(parsed, null, 2));
2709
+ await atomicWrite3(globalConfigPath, JSON.stringify(parsed, null, 2));
2395
2710
  });
2396
2711
  await configWriteLock;
2397
2712
  } catch (err) {
@@ -2411,7 +2726,7 @@ async function startWebUI(opts = {}) {
2411
2726
  });
2412
2727
  break;
2413
2728
  }
2414
- broadcast({ type: "session.start", payload: await sessionStartPayload() });
2729
+ broadcast(clients, { type: "session.start", payload: await sessionStartPayload() });
2415
2730
  break;
2416
2731
  }
2417
2732
  case "key.add":
@@ -2500,7 +2815,7 @@ async function startWebUI(opts = {}) {
2500
2815
  tokenCounter.reset();
2501
2816
  tokenCounter.account(resumed.data.usage, config.model);
2502
2817
  sessionStartedAt = Date.now();
2503
- broadcast({
2818
+ broadcast(clients, {
2504
2819
  type: "session.start",
2505
2820
  payload: {
2506
2821
  ...await sessionStartPayload(),
@@ -2640,7 +2955,7 @@ async function startWebUI(opts = {}) {
2640
2955
  case "todos.clear": {
2641
2956
  context.state.replaceTodos([]);
2642
2957
  sendResult(ws, true, "Todos cleared");
2643
- broadcast({ type: "todos.updated", payload: { todos: [] } });
2958
+ broadcast(clients, { type: "todos.updated", payload: { todos: [] } });
2644
2959
  break;
2645
2960
  }
2646
2961
  case "plan.get": {
@@ -2687,7 +3002,7 @@ async function startWebUI(opts = {}) {
2687
3002
  }
2688
3003
  await savePlan(planPath, plan);
2689
3004
  sendResult(ws, true, `Applied template "${tpl.name}" \u2014 ${tpl.items.length} items added.`);
2690
- broadcast({
3005
+ broadcast(clients, {
2691
3006
  type: "plan.updated",
2692
3007
  payload: { plan }
2693
3008
  });
@@ -2704,7 +3019,7 @@ async function startWebUI(opts = {}) {
2704
3019
  if (depth > 8 || results.length >= 600) return;
2705
3020
  let entries = [];
2706
3021
  try {
2707
- entries = await fs2.readdir(dir, { withFileTypes: true });
3022
+ entries = await fs4.readdir(dir, { withFileTypes: true });
2708
3023
  } catch {
2709
3024
  return;
2710
3025
  }
@@ -2714,7 +3029,7 @@ async function startWebUI(opts = {}) {
2714
3029
  const childRel = rel ? `${rel}/${e.name}` : e.name;
2715
3030
  if (e.isDirectory()) {
2716
3031
  if (SKIP_DIRS.has(e.name)) continue;
2717
- await walk(path2.join(dir, e.name), childRel, depth + 1);
3032
+ await walk(path4.join(dir, e.name), childRel, depth + 1);
2718
3033
  } else if (e.isFile()) {
2719
3034
  results.push(childRel);
2720
3035
  }
@@ -2783,7 +3098,7 @@ async function startWebUI(opts = {}) {
2783
3098
  model: config.model
2784
3099
  });
2785
3100
  sendResult(ws, true, `Switched to mode "${id}"`);
2786
- broadcast({
3101
+ broadcast(clients, {
2787
3102
  type: "session.start",
2788
3103
  payload: { ...await sessionStartPayload() }
2789
3104
  });
@@ -2822,39 +3137,20 @@ async function startWebUI(opts = {}) {
2822
3137
  }
2823
3138
  }
2824
3139
  }
2825
- async function loadSavedProviders() {
2826
- try {
2827
- const raw = await fs2.readFile(globalConfigPath, "utf8");
2828
- const parsed = JSON.parse(raw);
2829
- if (!parsed.providers) return {};
2830
- return decryptConfigSecrets(parsed.providers, vault);
2831
- } catch {
2832
- return {};
2833
- }
3140
+ async function loadConfigProviders() {
3141
+ return loadSavedProviders(globalConfigPath, vault);
2834
3142
  }
2835
- async function saveProviders(providers) {
2836
- configWriteLock = configWriteLock.then(async () => {
2837
- let parsed;
2838
- try {
2839
- const raw = await fs2.readFile(globalConfigPath, "utf8");
2840
- parsed = JSON.parse(raw);
2841
- } catch {
2842
- parsed = {};
2843
- }
2844
- parsed["providers"] = providers;
2845
- const encrypted = encryptConfigSecrets(parsed, vault);
2846
- await atomicWrite(globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
2847
- });
3143
+ async function saveConfigProviders(providers) {
3144
+ configWriteLock = configWriteLock.then(
3145
+ () => saveProviders(globalConfigPath, vault, providers)
3146
+ );
2848
3147
  await configWriteLock;
2849
3148
  }
2850
- function sendResult(ws, success, message) {
2851
- send(ws, { type: "key.operation_result", payload: { success, message } });
2852
- }
2853
3149
  async function handleKeyUpsert(ws, providerId, label, apiKey) {
2854
3150
  try {
2855
- const providers = await loadSavedProviders();
3151
+ const providers = await loadConfigProviders();
2856
3152
  const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
2857
- if (result.ok) await saveProviders(providers);
3153
+ if (result.ok) await saveConfigProviders(providers);
2858
3154
  sendResult(ws, result.ok, result.message);
2859
3155
  } catch (err) {
2860
3156
  sendResult(ws, false, errMessage(err));
@@ -2862,9 +3158,9 @@ async function startWebUI(opts = {}) {
2862
3158
  }
2863
3159
  async function handleKeyDelete(ws, providerId, label) {
2864
3160
  try {
2865
- const providers = await loadSavedProviders();
3161
+ const providers = await loadConfigProviders();
2866
3162
  const result = deleteKey(providers, providerId, label);
2867
- if (result.ok) await saveProviders(providers);
3163
+ if (result.ok) await saveConfigProviders(providers);
2868
3164
  sendResult(ws, result.ok, result.message);
2869
3165
  } catch (err) {
2870
3166
  sendResult(ws, false, errMessage(err));
@@ -2872,9 +3168,9 @@ async function startWebUI(opts = {}) {
2872
3168
  }
2873
3169
  async function handleKeySetActive(ws, providerId, label) {
2874
3170
  try {
2875
- const providers = await loadSavedProviders();
3171
+ const providers = await loadConfigProviders();
2876
3172
  const result = setActiveKey(providers, providerId, label);
2877
- if (result.ok) await saveProviders(providers);
3173
+ if (result.ok) await saveConfigProviders(providers);
2878
3174
  sendResult(ws, result.ok, result.message);
2879
3175
  } catch (err) {
2880
3176
  sendResult(ws, false, errMessage(err));
@@ -2882,9 +3178,9 @@ async function startWebUI(opts = {}) {
2882
3178
  }
2883
3179
  async function handleProviderAdd(ws, payload) {
2884
3180
  try {
2885
- const providers = await loadSavedProviders();
3181
+ const providers = await loadConfigProviders();
2886
3182
  const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
2887
- if (result.ok) await saveProviders(providers);
3183
+ if (result.ok) await saveConfigProviders(providers);
2888
3184
  sendResult(ws, result.ok, result.message);
2889
3185
  } catch (err) {
2890
3186
  sendResult(ws, false, errMessage(err));
@@ -2892,9 +3188,9 @@ async function startWebUI(opts = {}) {
2892
3188
  }
2893
3189
  async function handleProviderRemove(ws, providerId) {
2894
3190
  try {
2895
- const providers = await loadSavedProviders();
3191
+ const providers = await loadConfigProviders();
2896
3192
  const result = removeProvider(providers, providerId);
2897
- if (result.ok) await saveProviders(providers);
3193
+ if (result.ok) await saveConfigProviders(providers);
2898
3194
  sendResult(ws, result.ok, result.message);
2899
3195
  } catch (err) {
2900
3196
  sendResult(ws, false, errMessage(err));
@@ -2902,12 +3198,27 @@ async function startWebUI(opts = {}) {
2902
3198
  }
2903
3199
  const httpServer = createHttpServer({
2904
3200
  host: wsHost,
2905
- distDir: path2.resolve(import.meta.dirname, "../../dist"),
3201
+ distDir: path4.resolve(import.meta.dirname, "../../dist"),
2906
3202
  wsPort
2907
3203
  });
2908
- const httpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
3204
+ const registryBaseDir = path4.dirname(globalConfigPath);
2909
3205
  httpServer.listen(httpPort, wsHost, () => {
2910
- console.log(`[WebUI] HTTP server running on http://${wsHost}:${httpPort}`);
3206
+ const openUrl = `http://${wsHost}:${httpPort}`;
3207
+ console.log(`[WebUI] HTTP server running on ${openUrl}`);
3208
+ if (opts.open) openBrowser(openUrl);
3209
+ void registerInstance(
3210
+ {
3211
+ pid: process.pid,
3212
+ httpPort,
3213
+ wsPort,
3214
+ host: wsHost,
3215
+ projectRoot,
3216
+ projectName: path4.basename(projectRoot) || projectRoot,
3217
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3218
+ url: `http://${wsHost}:${httpPort}`
3219
+ },
3220
+ registryBaseDir
3221
+ ).catch((err) => console.warn("[WebUI] Could not record instance:", errMessage(err)));
2911
3222
  });
2912
3223
  registerShutdownHandlers({
2913
3224
  flushSession: async () => {
@@ -2919,10 +3230,48 @@ async function startWebUI(opts = {}) {
2919
3230
  await session.close();
2920
3231
  },
2921
3232
  clients: () => clients.keys(),
2922
- servers: [httpServer, wssPrimary, wssSecondary]
3233
+ servers: [httpServer, wssPrimary, wssSecondary],
3234
+ // Drop this instance from the registry on a clean exit so the file reflects
3235
+ // reality. Crash exits are healed by the next register()/list() prune pass.
3236
+ onShutdown: () => unregisterInstance(process.pid, registryBaseDir)
2923
3237
  });
2924
3238
  }
2925
3239
  export {
2926
- startWebUI
3240
+ addProvider,
3241
+ broadcast,
3242
+ browserOpenCommand,
3243
+ buildCspHeader,
3244
+ createHttpServer,
3245
+ createProviderConfigIO,
3246
+ defaultBaseDir,
3247
+ deleteKey,
3248
+ errMessage,
3249
+ extractToken,
3250
+ findFreePort,
3251
+ formatInstances,
3252
+ generateAuthToken,
3253
+ hostHeaderOk,
3254
+ injectWsPort,
3255
+ isLoopbackBind,
3256
+ isLoopbackHostname,
3257
+ isPortFree,
3258
+ listInstances,
3259
+ loadSavedProviders,
3260
+ maskedKey,
3261
+ normalizeKeys,
3262
+ openBrowser,
3263
+ registerInstance,
3264
+ registryPath,
3265
+ removeProvider,
3266
+ saveProviders,
3267
+ send,
3268
+ sendResult,
3269
+ setActiveKey,
3270
+ startWebUI,
3271
+ tokenMatches,
3272
+ unregisterInstance,
3273
+ upsertKey,
3274
+ verifyClient,
3275
+ writeKeysBack
2927
3276
  };
2928
3277
  //# sourceMappingURL=index.js.map