@wrongstack/webui 0.63.4 → 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 +434 -124
  46. package/dist/server/entry.js.map +1 -1
  47. package/dist/server/index.d.ts +298 -2
  48. package/dist/server/index.js +435 -94
  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 {
@@ -227,6 +239,7 @@ function createDefaultContainer(opts) {
227
239
  trustFile: wpaths.projectTrust,
228
240
  yolo: opts.permission?.yolo ?? false,
229
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;
@@ -1884,14 +2138,14 @@ async function startWebUI(opts = {}) {
1884
2138
  inputCost,
1885
2139
  outputCost,
1886
2140
  cacheReadCost,
1887
- projectName: path2.basename(projectRoot) || projectRoot,
2141
+ projectName: path4.basename(projectRoot) || projectRoot,
1888
2142
  cwd: projectRoot,
1889
2143
  mode: modeId,
1890
2144
  contextMode: String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID),
1891
2145
  wsToken
1892
2146
  };
1893
2147
  }
1894
- const wsToken = randomBytes(16).toString("hex");
2148
+ const wsToken = generateAuthToken();
1895
2149
  console.log(`[WebUI] WS auth token: ${wsToken.slice(0, 4)}\u2026${wsToken.slice(-4)} (masked)`);
1896
2150
  const verifyClient2 = (info) => verifyClient({
1897
2151
  origin: info.origin,
@@ -1915,10 +2169,11 @@ async function startWebUI(opts = {}) {
1915
2169
  maxPayload: WS_MAX_PAYLOAD
1916
2170
  }) : null;
1917
2171
  const clients = /* @__PURE__ */ new Map();
1918
- const RATE_LIMIT_MESSAGES = 60;
2172
+ const RATE_LIMIT_MESSAGES = Number.parseInt(process.env["WEBUI_RATE_LIMIT"] ?? "0", 10);
1919
2173
  const RATE_LIMIT_WINDOW_MS = 6e4;
1920
2174
  const rateLimits = /* @__PURE__ */ new Map();
1921
2175
  function checkRateLimit(ws, client) {
2176
+ if (RATE_LIMIT_MESSAGES <= 0) return true;
1922
2177
  const now = Date.now();
1923
2178
  const key = client.sessionId ?? String(ws);
1924
2179
  const limit = rateLimits.get(key);
@@ -1937,25 +2192,25 @@ async function startWebUI(opts = {}) {
1937
2192
  const pendingConfirms = /* @__PURE__ */ new Map();
1938
2193
  function setupEvents() {
1939
2194
  events.on("iteration.started", (e) => {
1940
- broadcast({
2195
+ broadcast(clients, {
1941
2196
  type: "iteration.started",
1942
2197
  payload: { index: e.index, maxIterations: config.tools?.maxIterations ?? 100 }
1943
2198
  });
1944
2199
  });
1945
2200
  events.on("provider.text_delta", (e) => {
1946
- broadcast({ type: "provider.text_delta", payload: { text: e.text, messageId: "current" } });
2201
+ broadcast(clients, { type: "provider.text_delta", payload: { text: e.text, messageId: "current" } });
1947
2202
  });
1948
2203
  events.on("provider.thinking_delta", (e) => {
1949
- broadcast({ type: "provider.thinking_delta", payload: { text: e.text } });
2204
+ broadcast(clients, { type: "provider.thinking_delta", payload: { text: e.text } });
1950
2205
  });
1951
2206
  events.on("tool.started", (e) => {
1952
- broadcast({
2207
+ broadcast(clients, {
1953
2208
  type: "tool.started",
1954
2209
  payload: { id: e.id, name: e.name, input: e.input, messageId: `tool_${e.id}` }
1955
2210
  });
1956
2211
  });
1957
2212
  events.on("tool.progress", (e) => {
1958
- broadcast({
2213
+ broadcast(clients, {
1959
2214
  type: "tool.progress",
1960
2215
  payload: {
1961
2216
  id: e.id,
@@ -1966,7 +2221,7 @@ async function startWebUI(opts = {}) {
1966
2221
  });
1967
2222
  });
1968
2223
  events.on("tool.executed", (e) => {
1969
- broadcast({
2224
+ broadcast(clients, {
1970
2225
  type: "tool.executed",
1971
2226
  payload: {
1972
2227
  // Forward the tool_use id so frontend can correlate with the
@@ -1981,13 +2236,13 @@ async function startWebUI(opts = {}) {
1981
2236
  output: e.output
1982
2237
  }
1983
2238
  });
1984
- broadcast({
2239
+ broadcast(clients, {
1985
2240
  type: "todos.updated",
1986
2241
  payload: { todos: [...context.todos] }
1987
2242
  });
1988
2243
  });
1989
2244
  events.on("provider.response", (e) => {
1990
- broadcast({
2245
+ broadcast(clients, {
1991
2246
  type: "provider.response",
1992
2247
  payload: {
1993
2248
  usage: e.usage,
@@ -1997,7 +2252,7 @@ async function startWebUI(opts = {}) {
1997
2252
  });
1998
2253
  });
1999
2254
  events.on("context.repaired", (e) => {
2000
- broadcast({
2255
+ broadcast(clients, {
2001
2256
  type: "context.repaired",
2002
2257
  payload: {
2003
2258
  removedToolUses: e.removedToolUses,
@@ -2009,7 +2264,7 @@ async function startWebUI(opts = {}) {
2009
2264
  events.on("tool.confirm_needed", (e) => {
2010
2265
  const id = e.toolUseId ?? `confirm_${Date.now()}`;
2011
2266
  pendingConfirms.set(id, e.resolve);
2012
- broadcast({
2267
+ broadcast(clients, {
2013
2268
  type: "tool.confirm_needed",
2014
2269
  payload: {
2015
2270
  id,
@@ -2020,7 +2275,7 @@ async function startWebUI(opts = {}) {
2020
2275
  });
2021
2276
  });
2022
2277
  events.on("error", (e) => {
2023
- broadcast({
2278
+ broadcast(clients, {
2024
2279
  type: "error",
2025
2280
  payload: {
2026
2281
  phase: e.phase,
@@ -2028,19 +2283,71 @@ async function startWebUI(opts = {}) {
2028
2283
  }
2029
2284
  });
2030
2285
  });
2031
- }
2032
- function send(ws, msg) {
2033
- if (ws.readyState === WebSocket.OPEN) {
2034
- ws.send(JSON.stringify(msg));
2035
- }
2036
- }
2037
- function broadcast(msg) {
2038
- const data = JSON.stringify(msg);
2039
- for (const [ws] of clients) {
2040
- if (ws.readyState === WebSocket.OPEN) {
2041
- ws.send(data);
2042
- }
2043
- }
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
+ );
2044
2351
  }
2045
2352
  const handleConnection = (ws) => {
2046
2353
  const client = { ws, sessionId: session.id, connectedAt: Date.now() };
@@ -2184,7 +2491,7 @@ async function startWebUI(opts = {}) {
2184
2491
  }
2185
2492
  case "abort":
2186
2493
  runLock?.abort();
2187
- broadcast({ type: "error", payload: { phase: "abort", message: "User aborted" } });
2494
+ broadcast(clients, { type: "error", payload: { phase: "abort", message: "User aborted" } });
2188
2495
  break;
2189
2496
  case "ping":
2190
2497
  send(ws, { type: "pong", payload: {} });
@@ -2203,7 +2510,7 @@ async function startWebUI(opts = {}) {
2203
2510
  context.fileMtimes.clear();
2204
2511
  tokenCounter.reset();
2205
2512
  sessionStartedAt = Date.now();
2206
- broadcast({ type: "session.start", payload: await sessionStartPayload() });
2513
+ broadcast(clients, { type: "session.start", payload: await sessionStartPayload() });
2207
2514
  break;
2208
2515
  }
2209
2516
  case "context.clear": {
@@ -2213,7 +2520,7 @@ async function startWebUI(opts = {}) {
2213
2520
  context.fileMtimes.clear();
2214
2521
  tokenCounter.reset();
2215
2522
  sendResult(ws, true, "Context cleared");
2216
- broadcast({
2523
+ broadcast(clients, {
2217
2524
  type: "session.start",
2218
2525
  payload: { ...await sessionStartPayload(), reset: true }
2219
2526
  });
@@ -2272,7 +2579,7 @@ async function startWebUI(opts = {}) {
2272
2579
  beforeMessages,
2273
2580
  afterMessages: context.messages.length
2274
2581
  };
2275
- broadcast({ type: "context.repaired", payload });
2582
+ broadcast(clients, { type: "context.repaired", payload });
2276
2583
  const removed = payload.removedToolUses.length + payload.removedToolResults.length + payload.removedMessages;
2277
2584
  sendResult(
2278
2585
  ws,
@@ -2310,7 +2617,7 @@ async function startWebUI(opts = {}) {
2310
2617
  context.meta["contextWindowMode"] = policy.id;
2311
2618
  context.meta["contextWindowPolicy"] = policy;
2312
2619
  sendResult(ws, true, `Context mode switched to ${policy.id}`);
2313
- broadcast({
2620
+ broadcast(clients, {
2314
2621
  type: "context.mode.changed",
2315
2622
  payload: { id: policy.id, name: policy.name, policy }
2316
2623
  });
@@ -2336,7 +2643,7 @@ async function startWebUI(opts = {}) {
2336
2643
  break;
2337
2644
  }
2338
2645
  case "providers.saved": {
2339
- const saved = await loadSavedProviders();
2646
+ const saved = await loadConfigProviders();
2340
2647
  send(ws, {
2341
2648
  type: "providers.saved",
2342
2649
  payload: {
@@ -2395,11 +2702,11 @@ async function startWebUI(opts = {}) {
2395
2702
  updateAutoCompactionMaxContext?.(newProv);
2396
2703
  try {
2397
2704
  configWriteLock = configWriteLock.then(async () => {
2398
- const raw = await fs2.readFile(globalConfigPath, "utf8");
2705
+ const raw = await fs4.readFile(globalConfigPath, "utf8");
2399
2706
  const parsed = JSON.parse(raw);
2400
2707
  parsed.provider = newProvider;
2401
2708
  parsed.model = newModel;
2402
- await atomicWrite(globalConfigPath, JSON.stringify(parsed, null, 2));
2709
+ await atomicWrite3(globalConfigPath, JSON.stringify(parsed, null, 2));
2403
2710
  });
2404
2711
  await configWriteLock;
2405
2712
  } catch (err) {
@@ -2419,7 +2726,7 @@ async function startWebUI(opts = {}) {
2419
2726
  });
2420
2727
  break;
2421
2728
  }
2422
- broadcast({ type: "session.start", payload: await sessionStartPayload() });
2729
+ broadcast(clients, { type: "session.start", payload: await sessionStartPayload() });
2423
2730
  break;
2424
2731
  }
2425
2732
  case "key.add":
@@ -2508,7 +2815,7 @@ async function startWebUI(opts = {}) {
2508
2815
  tokenCounter.reset();
2509
2816
  tokenCounter.account(resumed.data.usage, config.model);
2510
2817
  sessionStartedAt = Date.now();
2511
- broadcast({
2818
+ broadcast(clients, {
2512
2819
  type: "session.start",
2513
2820
  payload: {
2514
2821
  ...await sessionStartPayload(),
@@ -2648,7 +2955,7 @@ async function startWebUI(opts = {}) {
2648
2955
  case "todos.clear": {
2649
2956
  context.state.replaceTodos([]);
2650
2957
  sendResult(ws, true, "Todos cleared");
2651
- broadcast({ type: "todos.updated", payload: { todos: [] } });
2958
+ broadcast(clients, { type: "todos.updated", payload: { todos: [] } });
2652
2959
  break;
2653
2960
  }
2654
2961
  case "plan.get": {
@@ -2695,7 +3002,7 @@ async function startWebUI(opts = {}) {
2695
3002
  }
2696
3003
  await savePlan(planPath, plan);
2697
3004
  sendResult(ws, true, `Applied template "${tpl.name}" \u2014 ${tpl.items.length} items added.`);
2698
- broadcast({
3005
+ broadcast(clients, {
2699
3006
  type: "plan.updated",
2700
3007
  payload: { plan }
2701
3008
  });
@@ -2712,7 +3019,7 @@ async function startWebUI(opts = {}) {
2712
3019
  if (depth > 8 || results.length >= 600) return;
2713
3020
  let entries = [];
2714
3021
  try {
2715
- entries = await fs2.readdir(dir, { withFileTypes: true });
3022
+ entries = await fs4.readdir(dir, { withFileTypes: true });
2716
3023
  } catch {
2717
3024
  return;
2718
3025
  }
@@ -2722,7 +3029,7 @@ async function startWebUI(opts = {}) {
2722
3029
  const childRel = rel ? `${rel}/${e.name}` : e.name;
2723
3030
  if (e.isDirectory()) {
2724
3031
  if (SKIP_DIRS.has(e.name)) continue;
2725
- await walk(path2.join(dir, e.name), childRel, depth + 1);
3032
+ await walk(path4.join(dir, e.name), childRel, depth + 1);
2726
3033
  } else if (e.isFile()) {
2727
3034
  results.push(childRel);
2728
3035
  }
@@ -2791,7 +3098,7 @@ async function startWebUI(opts = {}) {
2791
3098
  model: config.model
2792
3099
  });
2793
3100
  sendResult(ws, true, `Switched to mode "${id}"`);
2794
- broadcast({
3101
+ broadcast(clients, {
2795
3102
  type: "session.start",
2796
3103
  payload: { ...await sessionStartPayload() }
2797
3104
  });
@@ -2830,39 +3137,20 @@ async function startWebUI(opts = {}) {
2830
3137
  }
2831
3138
  }
2832
3139
  }
2833
- async function loadSavedProviders() {
2834
- try {
2835
- const raw = await fs2.readFile(globalConfigPath, "utf8");
2836
- const parsed = JSON.parse(raw);
2837
- if (!parsed.providers) return {};
2838
- return decryptConfigSecrets(parsed.providers, vault);
2839
- } catch {
2840
- return {};
2841
- }
3140
+ async function loadConfigProviders() {
3141
+ return loadSavedProviders(globalConfigPath, vault);
2842
3142
  }
2843
- async function saveProviders(providers) {
2844
- configWriteLock = configWriteLock.then(async () => {
2845
- let parsed;
2846
- try {
2847
- const raw = await fs2.readFile(globalConfigPath, "utf8");
2848
- parsed = JSON.parse(raw);
2849
- } catch {
2850
- parsed = {};
2851
- }
2852
- parsed["providers"] = providers;
2853
- const encrypted = encryptConfigSecrets(parsed, vault);
2854
- await atomicWrite(globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
2855
- });
3143
+ async function saveConfigProviders(providers) {
3144
+ configWriteLock = configWriteLock.then(
3145
+ () => saveProviders(globalConfigPath, vault, providers)
3146
+ );
2856
3147
  await configWriteLock;
2857
3148
  }
2858
- function sendResult(ws, success, message) {
2859
- send(ws, { type: "key.operation_result", payload: { success, message } });
2860
- }
2861
3149
  async function handleKeyUpsert(ws, providerId, label, apiKey) {
2862
3150
  try {
2863
- const providers = await loadSavedProviders();
3151
+ const providers = await loadConfigProviders();
2864
3152
  const result = upsertKey(providers, providerId, label, apiKey, (/* @__PURE__ */ new Date()).toISOString());
2865
- if (result.ok) await saveProviders(providers);
3153
+ if (result.ok) await saveConfigProviders(providers);
2866
3154
  sendResult(ws, result.ok, result.message);
2867
3155
  } catch (err) {
2868
3156
  sendResult(ws, false, errMessage(err));
@@ -2870,9 +3158,9 @@ async function startWebUI(opts = {}) {
2870
3158
  }
2871
3159
  async function handleKeyDelete(ws, providerId, label) {
2872
3160
  try {
2873
- const providers = await loadSavedProviders();
3161
+ const providers = await loadConfigProviders();
2874
3162
  const result = deleteKey(providers, providerId, label);
2875
- if (result.ok) await saveProviders(providers);
3163
+ if (result.ok) await saveConfigProviders(providers);
2876
3164
  sendResult(ws, result.ok, result.message);
2877
3165
  } catch (err) {
2878
3166
  sendResult(ws, false, errMessage(err));
@@ -2880,9 +3168,9 @@ async function startWebUI(opts = {}) {
2880
3168
  }
2881
3169
  async function handleKeySetActive(ws, providerId, label) {
2882
3170
  try {
2883
- const providers = await loadSavedProviders();
3171
+ const providers = await loadConfigProviders();
2884
3172
  const result = setActiveKey(providers, providerId, label);
2885
- if (result.ok) await saveProviders(providers);
3173
+ if (result.ok) await saveConfigProviders(providers);
2886
3174
  sendResult(ws, result.ok, result.message);
2887
3175
  } catch (err) {
2888
3176
  sendResult(ws, false, errMessage(err));
@@ -2890,9 +3178,9 @@ async function startWebUI(opts = {}) {
2890
3178
  }
2891
3179
  async function handleProviderAdd(ws, payload) {
2892
3180
  try {
2893
- const providers = await loadSavedProviders();
3181
+ const providers = await loadConfigProviders();
2894
3182
  const result = addProvider(providers, payload, (/* @__PURE__ */ new Date()).toISOString());
2895
- if (result.ok) await saveProviders(providers);
3183
+ if (result.ok) await saveConfigProviders(providers);
2896
3184
  sendResult(ws, result.ok, result.message);
2897
3185
  } catch (err) {
2898
3186
  sendResult(ws, false, errMessage(err));
@@ -2900,9 +3188,9 @@ async function startWebUI(opts = {}) {
2900
3188
  }
2901
3189
  async function handleProviderRemove(ws, providerId) {
2902
3190
  try {
2903
- const providers = await loadSavedProviders();
3191
+ const providers = await loadConfigProviders();
2904
3192
  const result = removeProvider(providers, providerId);
2905
- if (result.ok) await saveProviders(providers);
3193
+ if (result.ok) await saveConfigProviders(providers);
2906
3194
  sendResult(ws, result.ok, result.message);
2907
3195
  } catch (err) {
2908
3196
  sendResult(ws, false, errMessage(err));
@@ -2910,12 +3198,27 @@ async function startWebUI(opts = {}) {
2910
3198
  }
2911
3199
  const httpServer = createHttpServer({
2912
3200
  host: wsHost,
2913
- distDir: path2.resolve(import.meta.dirname, "../../dist"),
3201
+ distDir: path4.resolve(import.meta.dirname, "../../dist"),
2914
3202
  wsPort
2915
3203
  });
2916
- const httpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
3204
+ const registryBaseDir = path4.dirname(globalConfigPath);
2917
3205
  httpServer.listen(httpPort, wsHost, () => {
2918
- 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)));
2919
3222
  });
2920
3223
  registerShutdownHandlers({
2921
3224
  flushSession: async () => {
@@ -2927,10 +3230,48 @@ async function startWebUI(opts = {}) {
2927
3230
  await session.close();
2928
3231
  },
2929
3232
  clients: () => clients.keys(),
2930
- 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)
2931
3237
  });
2932
3238
  }
2933
3239
  export {
2934
- 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
2935
3276
  };
2936
3277
  //# sourceMappingURL=index.js.map