@mindstudio-ai/local-model-tunnel 0.5.21 → 0.5.23

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.
@@ -442,15 +442,6 @@ var DevEventEmitter = class extends EventEmitter {
442
442
  emitConnectionRestored() {
443
443
  this.emit("dev:connection-restored");
444
444
  }
445
- emitImpersonate(event) {
446
- this.emit("dev:impersonate", event);
447
- }
448
- emitScenarioStart(event) {
449
- this.emit("dev:scenario-start", event);
450
- }
451
- emitScenarioComplete(event) {
452
- this.emit("dev:scenario-complete", event);
453
- }
454
445
  onStart(handler) {
455
446
  this.on("dev:start", handler);
456
447
  return () => this.off("dev:start", handler);
@@ -483,18 +474,6 @@ var DevEventEmitter = class extends EventEmitter {
483
474
  this.on("dev:connection-restored", handler);
484
475
  return () => this.off("dev:connection-restored", handler);
485
476
  }
486
- onImpersonate(handler) {
487
- this.on("dev:impersonate", handler);
488
- return () => this.off("dev:impersonate", handler);
489
- }
490
- onScenarioStart(handler) {
491
- this.on("dev:scenario-start", handler);
492
- return () => this.off("dev:scenario-start", handler);
493
- }
494
- onScenarioComplete(handler) {
495
- this.on("dev:scenario-complete", handler);
496
- return () => this.off("dev:scenario-complete", handler);
497
- }
498
477
  };
499
478
  var devRequestEvents = new DevEventEmitter();
500
479
 
@@ -1049,17 +1028,15 @@ var DevRunner = class {
1049
1028
  async setImpersonation(roles) {
1050
1029
  if (!this.session) return;
1051
1030
  log.info("Setting role override", { roles });
1052
- const result = await impersonate(this.appId, this.session.sessionId, roles);
1031
+ await impersonate(this.appId, this.session.sessionId, roles);
1053
1032
  await this.refreshClientContext();
1054
- devRequestEvents.emitImpersonate({ roles: result.roles });
1055
1033
  }
1056
1034
  // Clear role override — revert to session's default roles.
1057
1035
  async clearImpersonation() {
1058
1036
  if (!this.session) return;
1059
1037
  log.info("Clearing role override");
1060
- const result = await impersonate(this.appId, this.session.sessionId, null);
1038
+ await impersonate(this.appId, this.session.sessionId, null);
1061
1039
  await this.refreshClientContext();
1062
- devRequestEvents.emitImpersonate({ roles: result.roles });
1063
1040
  }
1064
1041
  // Fetch fresh clientContext from platform and update the proxy.
1065
1042
  // Called after impersonation changes so the browser gets a new ms_iface token.
@@ -1081,12 +1058,6 @@ var DevRunner = class {
1081
1058
  }
1082
1059
  const requestId = randomBytes2(8).toString("hex");
1083
1060
  const startTime = Date.now();
1084
- devRequestEvents.emitStart({
1085
- id: requestId,
1086
- type: "execute",
1087
- method: opts.methodExport,
1088
- timestamp: startTime
1089
- });
1090
1061
  log.info("Method received (direct)", { requestId, method: opts.methodExport });
1091
1062
  try {
1092
1063
  const authorizationToken = await fetchCallbackToken(this.appId, this.session.sessionId);
@@ -1123,12 +1094,6 @@ var DevRunner = class {
1123
1094
  result,
1124
1095
  duration
1125
1096
  });
1126
- devRequestEvents.emitComplete({
1127
- id: requestId,
1128
- success: result.success,
1129
- duration,
1130
- error: result.error ? formatErrorForDisplay(result.error) : void 0
1131
- });
1132
1097
  return {
1133
1098
  success: result.success,
1134
1099
  output: result.output,
@@ -1151,12 +1116,6 @@ var DevRunner = class {
1151
1116
  result: { success: false, error: { message } },
1152
1117
  duration
1153
1118
  });
1154
- devRequestEvents.emitComplete({
1155
- id: requestId,
1156
- success: false,
1157
- duration,
1158
- error: message
1159
- });
1160
1119
  return { success: false, error: { message }, duration };
1161
1120
  }
1162
1121
  }
@@ -1168,11 +1127,6 @@ var DevRunner = class {
1168
1127
  }
1169
1128
  const startTime = Date.now();
1170
1129
  const scenarioName = scenario.name ?? scenario.export;
1171
- devRequestEvents.emitScenarioStart({
1172
- id: scenario.id,
1173
- name: scenarioName,
1174
- timestamp: startTime
1175
- });
1176
1130
  log.info("Scenario starting", { id: scenario.id, name: scenarioName });
1177
1131
  try {
1178
1132
  log.info("Resetting database for scenario");
@@ -1202,13 +1156,6 @@ var DevRunner = class {
1202
1156
  result,
1203
1157
  duration: Date.now() - startTime
1204
1158
  });
1205
- devRequestEvents.emitScenarioComplete({
1206
- id: scenario.id,
1207
- success: false,
1208
- duration: Date.now() - startTime,
1209
- roles: scenario.roles,
1210
- error
1211
- });
1212
1159
  return { success: false, databases, error };
1213
1160
  }
1214
1161
  if (scenario.roles.length > 0) {
@@ -1225,12 +1172,6 @@ var DevRunner = class {
1225
1172
  result,
1226
1173
  duration
1227
1174
  });
1228
- devRequestEvents.emitScenarioComplete({
1229
- id: scenario.id,
1230
- success: true,
1231
- duration,
1232
- roles: scenario.roles
1233
- });
1234
1175
  return { success: true, databases };
1235
1176
  } catch (err) {
1236
1177
  const error = err instanceof Error ? err.message : "Unknown error";
@@ -1243,13 +1184,6 @@ var DevRunner = class {
1243
1184
  infrastructureError: error,
1244
1185
  duration: Date.now() - startTime
1245
1186
  });
1246
- devRequestEvents.emitScenarioComplete({
1247
- id: scenario.id,
1248
- success: false,
1249
- duration: Date.now() - startTime,
1250
- roles: scenario.roles,
1251
- error
1252
- });
1253
1187
  return { success: false, databases: this.session.databases, error };
1254
1188
  }
1255
1189
  }
@@ -1481,7 +1415,99 @@ function closeBrowserLog() {
1481
1415
 
1482
1416
  // src/dev/proxy.ts
1483
1417
  import http from "http";
1418
+ import { randomBytes as randomBytes4 } from "crypto";
1419
+ import { WebSocketServer } from "ws";
1420
+
1421
+ // src/dev/ws-clients.ts
1484
1422
  import { randomBytes as randomBytes3 } from "crypto";
1423
+ var ClientRegistry = class {
1424
+ clients = /* @__PURE__ */ new Map();
1425
+ add(ws, hello) {
1426
+ const id = randomBytes3(4).toString("hex");
1427
+ this.clients.set(id, {
1428
+ id,
1429
+ ws,
1430
+ mode: hello.mode,
1431
+ url: hello.url,
1432
+ viewport: hello.viewport,
1433
+ connectedAt: Date.now(),
1434
+ alive: true,
1435
+ activeCommandId: null
1436
+ });
1437
+ log.info("Browser client connected", { clientId: id, mode: hello.mode, url: hello.url });
1438
+ return id;
1439
+ }
1440
+ remove(id) {
1441
+ const client = this.clients.get(id);
1442
+ if (client) {
1443
+ this.clients.delete(id);
1444
+ log.info("Browser client disconnected", { clientId: id, mode: client.mode });
1445
+ }
1446
+ return client;
1447
+ }
1448
+ get(id) {
1449
+ return this.clients.get(id);
1450
+ }
1451
+ /**
1452
+ * Get the preferred client for C&C commands.
1453
+ * Prefers idle iframe clients, falls back to any idle client.
1454
+ * Skips clients that are already executing a command.
1455
+ */
1456
+ getCommandTarget() {
1457
+ let fallback = null;
1458
+ for (const client of this.clients.values()) {
1459
+ if (client.activeCommandId) continue;
1460
+ if (client.mode === "iframe") return client;
1461
+ if (!fallback) fallback = client;
1462
+ }
1463
+ return fallback;
1464
+ }
1465
+ getAll() {
1466
+ return [...this.clients.values()];
1467
+ }
1468
+ hasConnected() {
1469
+ return this.clients.size > 0;
1470
+ }
1471
+ count() {
1472
+ return this.clients.size;
1473
+ }
1474
+ /** Find the client executing a given command. */
1475
+ findByCommandId(commandId) {
1476
+ for (const client of this.clients.values()) {
1477
+ if (client.activeCommandId === commandId) return client;
1478
+ }
1479
+ return void 0;
1480
+ }
1481
+ markAlive(id) {
1482
+ const client = this.clients.get(id);
1483
+ if (client) client.alive = true;
1484
+ }
1485
+ /** Mark all clients as not-alive, then ping. Clients that respond set alive=true. */
1486
+ pingAll() {
1487
+ for (const client of this.clients.values()) {
1488
+ client.alive = false;
1489
+ try {
1490
+ client.ws.ping();
1491
+ } catch {
1492
+ }
1493
+ }
1494
+ }
1495
+ /** Remove clients that didn't respond to the last ping. Returns active command IDs that need rejection. */
1496
+ sweepDead() {
1497
+ const removed = [];
1498
+ for (const client of this.clients.values()) {
1499
+ if (!client.alive) {
1500
+ log.warn("Browser client timed out (no pong)", { clientId: client.id, activeCommandId: client.activeCommandId });
1501
+ removed.push({ clientId: client.id, activeCommandId: client.activeCommandId });
1502
+ this.clients.delete(client.id);
1503
+ client.ws.terminate();
1504
+ }
1505
+ }
1506
+ return removed;
1507
+ }
1508
+ };
1509
+
1510
+ // src/dev/proxy.ts
1485
1511
  var DevProxy = class _DevProxy {
1486
1512
  constructor(upstreamPort, clientContext, bindAddress = "127.0.0.1", browserAgentUrl) {
1487
1513
  this.upstreamPort = upstreamPort;
@@ -1491,58 +1517,104 @@ var DevProxy = class _DevProxy {
1491
1517
  }
1492
1518
  server = null;
1493
1519
  proxyPort = null;
1494
- commandQueue = [];
1520
+ wss = null;
1521
+ clients = new ClientRegistry();
1495
1522
  pendingResults = /* @__PURE__ */ new Map();
1496
- lastBrowserPoll = 0;
1497
- /** Long-poll waiters — browser agents waiting for the next command. */
1498
- commandWaiters = [];
1523
+ commandQueue = [];
1499
1524
  /** Upstream dev server health tracking. */
1500
1525
  upstreamUp = true;
1501
1526
  healthCheckTimer = null;
1527
+ pingTimer = null;
1502
1528
  static HEALTH_CHECK_INTERVAL = 3e3;
1503
1529
  static HEALTH_CHECK_INTERVAL_DOWN = 1e3;
1504
1530
  static HEALTH_CHECK_TIMEOUT = 2e3;
1531
+ static PING_INTERVAL = 3e4;
1532
+ static HELLO_TIMEOUT = 5e3;
1505
1533
  updateClientContext(context) {
1506
1534
  this.clientContext = context;
1507
1535
  log.info("Dev proxy context updated after role change");
1508
1536
  }
1509
1537
  /**
1510
- * Whether a browser agent is actively connected.
1511
- * True if there's a long-poll waiter or we've seen activity recently.
1538
+ * Whether any browser agent is actively connected via WebSocket.
1512
1539
  */
1513
1540
  isBrowserConnected() {
1514
- return this.commandWaiters.length > 0 || Date.now() - this.lastBrowserPoll < 500;
1541
+ return this.clients.hasConnected();
1515
1542
  }
1516
1543
  /**
1517
- * Dispatch a browser command and wait for the result.
1518
- * The command is queued for the browser agent to pick up via polling.
1519
- * Returns a promise that resolves when the browser posts the result back.
1544
+ * Dispatch a command to the preferred browser client and wait for the result.
1545
+ * Commands are queued and executed one at a time per client (FIFO).
1520
1546
  */
1521
1547
  dispatchBrowserCommand(steps, timeoutMs = 3e4) {
1522
- if (!this.isBrowserConnected()) {
1548
+ if (!this.clients.hasConnected()) {
1523
1549
  return Promise.reject(
1524
1550
  new Error("No browser connected, please refresh the MindStudio preview")
1525
1551
  );
1526
1552
  }
1527
- const id = randomBytes3(4).toString("hex");
1528
- log.info("Browser command queued", { id, stepCount: steps.length, commands: steps.map((s) => s.command) });
1553
+ const id = randomBytes4(4).toString("hex");
1529
1554
  return new Promise((resolve2, reject) => {
1555
+ this.commandQueue.push({ id, steps, timeoutMs, resolve: resolve2, reject, queuedAt: Date.now() });
1556
+ log.info("Browser command queued", { id, queueLength: this.commandQueue.length, commands: steps.map((s) => s.command) });
1557
+ this.drainCommandQueue();
1558
+ });
1559
+ }
1560
+ /**
1561
+ * Try to send the next queued command to an available client.
1562
+ */
1563
+ drainCommandQueue() {
1564
+ while (this.commandQueue.length > 0) {
1565
+ const target = this.clients.getCommandTarget();
1566
+ if (!target) break;
1567
+ const queued = this.commandQueue.shift();
1568
+ const { id, steps, timeoutMs, resolve: resolve2, reject } = queued;
1569
+ log.info("Browser command sent", { id, clientId: target.id, mode: target.mode, stepCount: steps.length, commands: steps.map((s) => s.command) });
1530
1570
  const timeout = setTimeout(() => {
1531
1571
  this.pendingResults.delete(id);
1532
- log.warn("Browser command timed out", { id, pendingCount: this.pendingResults.size, queueLength: this.commandQueue.length });
1572
+ const client = this.clients.findByCommandId(id);
1573
+ if (client) client.activeCommandId = null;
1574
+ log.warn("Browser command timed out", { id, pendingCount: this.pendingResults.size });
1533
1575
  reject(new Error("Browser command timed out"));
1576
+ this.drainCommandQueue();
1534
1577
  }, timeoutMs);
1535
- this.pendingResults.set(id, { resolve: resolve2, timeout });
1536
- this.commandQueue.push({ id, steps });
1537
- this.flushCommandToWaiter();
1538
- });
1578
+ this.pendingResults.set(id, { resolve: resolve2, timeout, clientId: target.id });
1579
+ target.activeCommandId = id;
1580
+ try {
1581
+ target.ws.send(JSON.stringify({ type: "command", id, steps }));
1582
+ } catch {
1583
+ this.pendingResults.delete(id);
1584
+ clearTimeout(timeout);
1585
+ target.activeCommandId = null;
1586
+ reject(new Error("Failed to send command to browser"));
1587
+ }
1588
+ }
1589
+ }
1590
+ /**
1591
+ * Send a broadcast message to all connected browser clients.
1592
+ */
1593
+ broadcastToClients(action, payload) {
1594
+ const msg = JSON.stringify({ type: "broadcast", action, payload });
1595
+ const clients = this.clients.getAll();
1596
+ log.info("Broadcasting to browser clients", { action, clientCount: clients.length });
1597
+ for (const client of clients) {
1598
+ try {
1599
+ client.ws.send(msg);
1600
+ } catch {
1601
+ }
1602
+ }
1539
1603
  }
1540
1604
  async start(preferredPort) {
1541
1605
  const server = http.createServer((req, res) => {
1542
1606
  this.handleRequest(req, res);
1543
1607
  });
1608
+ this.wss = new WebSocketServer({ noServer: true });
1609
+ this.wss.on("connection", (ws) => this.handleWsConnection(ws));
1544
1610
  server.on("upgrade", (req, socket, head) => {
1545
- this.handleUpgrade(req, socket, head);
1611
+ if (req.url === "/__mindstudio_dev__/ws") {
1612
+ this.wss.handleUpgrade(req, socket, head, (ws) => {
1613
+ this.wss.emit("connection", ws, req);
1614
+ });
1615
+ } else {
1616
+ this.handleUpstreamUpgrade(req, socket, head);
1617
+ }
1546
1618
  });
1547
1619
  const portsToTry = preferredPort ? [preferredPort, 0] : [0];
1548
1620
  for (const port of portsToTry) {
@@ -1551,6 +1623,7 @@ var DevProxy = class _DevProxy {
1551
1623
  this.server = server;
1552
1624
  this.proxyPort = assignedPort;
1553
1625
  this.startHealthCheck();
1626
+ this.startPingTimer();
1554
1627
  log.info("Dev proxy started", { port: assignedPort, bind: this.bindAddress });
1555
1628
  return assignedPort;
1556
1629
  } catch {
@@ -1579,6 +1652,18 @@ var DevProxy = class _DevProxy {
1579
1652
  }
1580
1653
  stop() {
1581
1654
  this.stopHealthCheck();
1655
+ this.stopPingTimer();
1656
+ for (const client of this.clients.getAll()) {
1657
+ try {
1658
+ client.ws.close(1001, "Proxy stopping");
1659
+ } catch {
1660
+ client.ws.terminate();
1661
+ }
1662
+ }
1663
+ if (this.wss) {
1664
+ this.wss.close();
1665
+ this.wss = null;
1666
+ }
1582
1667
  if (this.server) {
1583
1668
  log.info("Dev proxy stopping");
1584
1669
  this.server.close();
@@ -1590,19 +1675,124 @@ var DevProxy = class _DevProxy {
1590
1675
  pending2.resolve({ id, steps: [], error: "Proxy stopped" });
1591
1676
  }
1592
1677
  this.pendingResults.clear();
1593
- this.commandQueue.length = 0;
1594
- for (const waiter of this.commandWaiters) {
1595
- clearTimeout(waiter.timer);
1596
- if (!waiter.res.writableEnded) {
1597
- waiter.res.writeHead(204).end();
1598
- }
1678
+ for (const queued of this.commandQueue) {
1679
+ queued.reject(new Error("Proxy stopped"));
1599
1680
  }
1600
- this.commandWaiters.length = 0;
1681
+ this.commandQueue.length = 0;
1601
1682
  }
1602
1683
  getPort() {
1603
1684
  return this.proxyPort;
1604
1685
  }
1605
1686
  // ---------------------------------------------------------------------------
1687
+ // WebSocket connection handler
1688
+ // ---------------------------------------------------------------------------
1689
+ handleWsConnection(ws) {
1690
+ let clientId = null;
1691
+ const helloTimeout = setTimeout(() => {
1692
+ if (!clientId) {
1693
+ log.warn("Browser WS client did not send hello in time, closing");
1694
+ ws.close(4e3, "Hello timeout");
1695
+ }
1696
+ }, _DevProxy.HELLO_TIMEOUT);
1697
+ ws.on("message", (data) => {
1698
+ let msg;
1699
+ try {
1700
+ msg = JSON.parse(data.toString());
1701
+ } catch {
1702
+ return;
1703
+ }
1704
+ if (!clientId) {
1705
+ if (msg.type !== "hello") {
1706
+ ws.close(4001, "Expected hello");
1707
+ return;
1708
+ }
1709
+ clearTimeout(helloTimeout);
1710
+ const mode = msg.mode === "iframe" ? "iframe" : "standalone";
1711
+ const viewport = msg.viewport || { w: 0, h: 0 };
1712
+ clientId = this.clients.add(ws, {
1713
+ mode,
1714
+ url: String(msg.url || ""),
1715
+ viewport
1716
+ });
1717
+ ws.send(JSON.stringify({ type: "ack", clientId }));
1718
+ return;
1719
+ }
1720
+ switch (msg.type) {
1721
+ case "result":
1722
+ this.handleCommandResult(msg);
1723
+ break;
1724
+ case "log":
1725
+ if (Array.isArray(msg.entries)) {
1726
+ appendBrowserLogEntries(msg.entries);
1727
+ }
1728
+ break;
1729
+ }
1730
+ });
1731
+ ws.on("pong", () => {
1732
+ if (clientId) this.clients.markAlive(clientId);
1733
+ });
1734
+ ws.on("close", () => {
1735
+ clearTimeout(helloTimeout);
1736
+ if (clientId) {
1737
+ const client = this.clients.remove(clientId);
1738
+ if (client?.activeCommandId) {
1739
+ this.rejectPendingCommand(client.activeCommandId, "Browser disconnected during command execution");
1740
+ }
1741
+ }
1742
+ });
1743
+ ws.on("error", () => {
1744
+ });
1745
+ }
1746
+ handleCommandResult(msg) {
1747
+ const id = msg.id;
1748
+ if (!id) {
1749
+ log.warn("Browser command result received with no id");
1750
+ return;
1751
+ }
1752
+ const pending2 = this.pendingResults.get(id);
1753
+ if (pending2) {
1754
+ log.info("Browser command result received", { id, stepCount: msg.steps?.length, duration: msg.duration });
1755
+ clearTimeout(pending2.timeout);
1756
+ this.pendingResults.delete(id);
1757
+ const client = this.clients.findByCommandId(id);
1758
+ if (client) client.activeCommandId = null;
1759
+ pending2.resolve(msg);
1760
+ this.drainCommandQueue();
1761
+ } else {
1762
+ log.warn("Browser command result received but no pending command found", { id, pendingIds: [...this.pendingResults.keys()] });
1763
+ }
1764
+ }
1765
+ rejectPendingCommand(commandId, reason) {
1766
+ const pending2 = this.pendingResults.get(commandId);
1767
+ if (pending2) {
1768
+ clearTimeout(pending2.timeout);
1769
+ this.pendingResults.delete(commandId);
1770
+ pending2.resolve({ id: commandId, steps: [], error: reason });
1771
+ log.warn("Pending command rejected", { id: commandId, reason });
1772
+ this.drainCommandQueue();
1773
+ }
1774
+ }
1775
+ // ---------------------------------------------------------------------------
1776
+ // Ping/pong liveness
1777
+ // ---------------------------------------------------------------------------
1778
+ startPingTimer() {
1779
+ this.pingTimer = setInterval(() => {
1780
+ const removed = this.clients.sweepDead();
1781
+ for (const { activeCommandId } of removed) {
1782
+ if (activeCommandId) {
1783
+ this.rejectPendingCommand(activeCommandId, "Browser client timed out");
1784
+ }
1785
+ }
1786
+ this.clients.pingAll();
1787
+ }, _DevProxy.PING_INTERVAL);
1788
+ }
1789
+ stopPingTimer() {
1790
+ if (this.pingTimer) {
1791
+ clearInterval(this.pingTimer);
1792
+ this.pingTimer = null;
1793
+ }
1794
+ }
1795
+ // ---------------------------------------------------------------------------
1606
1796
  // Upstream health check
1607
1797
  // ---------------------------------------------------------------------------
1608
1798
  /**
@@ -1644,8 +1834,7 @@ var DevProxy = class _DevProxy {
1644
1834
  log.warn("Upstream dev server is down");
1645
1835
  } else if (!wasUp && this.upstreamUp) {
1646
1836
  log.info("Upstream dev server is back up, reloading browser");
1647
- this.dispatchBrowserCommand([{ command: "reload" }]).catch(() => {
1648
- });
1837
+ this.broadcastToClients("reload");
1649
1838
  }
1650
1839
  const interval = this.upstreamUp ? _DevProxy.HEALTH_CHECK_INTERVAL : _DevProxy.HEALTH_CHECK_INTERVAL_DOWN;
1651
1840
  this.scheduleHealthCheck(interval);
@@ -1670,14 +1859,6 @@ var DevProxy = class _DevProxy {
1670
1859
  this.handleBrowserLogs(clientReq, clientRes);
1671
1860
  return;
1672
1861
  }
1673
- if (clientReq.url === "/__mindstudio_dev__/commands" && clientReq.method === "GET") {
1674
- this.handleGetCommand(clientReq, clientRes);
1675
- return;
1676
- }
1677
- if (clientReq.url === "/__mindstudio_dev__/results" && clientReq.method === "POST") {
1678
- this.handlePostResult(clientReq, clientRes);
1679
- return;
1680
- }
1681
1862
  if (clientReq.url?.startsWith("/__mindstudio_dev__/font-proxy?") && clientReq.method === "GET") {
1682
1863
  this.handleFontProxy(clientReq, clientRes);
1683
1864
  return;
@@ -1748,8 +1929,9 @@ var DevProxy = class _DevProxy {
1748
1929
  clientReq.pipe(upstreamReq);
1749
1930
  }
1750
1931
  // ---------------------------------------------------------------------------
1751
- // Browser agent endpoints
1932
+ // Browser agent HTTP endpoints (fallbacks)
1752
1933
  // ---------------------------------------------------------------------------
1934
+ /** Accept log entries via HTTP POST — used by sendBeacon on page unload. */
1753
1935
  handleBrowserLogs(clientReq, clientRes) {
1754
1936
  const chunks = [];
1755
1937
  clientReq.on("data", (chunk) => chunks.push(chunk));
@@ -1810,86 +1992,6 @@ var DevProxy = class _DevProxy {
1810
1992
  clientRes.end(`Font proxy error: ${err instanceof Error ? err.message : String(err)}`);
1811
1993
  }
1812
1994
  }
1813
- handleGetCommand(clientReq, clientRes) {
1814
- this.lastBrowserPoll = Date.now();
1815
- const command = this.commandQueue.shift();
1816
- if (command) {
1817
- log.info("Browser command dispatched to agent", { id: command.id, commands: command.steps.map((s) => s.command) });
1818
- clientRes.writeHead(200, {
1819
- ...this.corsHeaders(clientReq),
1820
- "content-type": "application/json",
1821
- "cache-control": "no-store"
1822
- });
1823
- clientRes.end(JSON.stringify(command));
1824
- return;
1825
- }
1826
- const timer = setTimeout(() => {
1827
- this.removeCommandWaiter(clientRes);
1828
- clientRes.writeHead(204, {
1829
- ...this.corsHeaders(clientReq),
1830
- "cache-control": "no-store"
1831
- });
1832
- clientRes.end();
1833
- }, 25e3);
1834
- this.commandWaiters.push({ req: clientReq, res: clientRes, timer });
1835
- clientReq.on("close", () => {
1836
- this.removeCommandWaiter(clientRes);
1837
- });
1838
- }
1839
- /**
1840
- * Flush a queued command to a waiting long-poll connection, if any.
1841
- */
1842
- flushCommandToWaiter() {
1843
- while (this.commandWaiters.length > 0 && this.commandQueue.length > 0) {
1844
- const waiter = this.commandWaiters.shift();
1845
- clearTimeout(waiter.timer);
1846
- if (waiter.res.writableEnded) continue;
1847
- const command = this.commandQueue.shift();
1848
- this.lastBrowserPoll = Date.now();
1849
- log.info("Browser command dispatched to agent", { id: command.id, commands: command.steps.map((s) => s.command) });
1850
- waiter.res.writeHead(200, {
1851
- ...this.corsHeaders(waiter.req),
1852
- "content-type": "application/json",
1853
- "cache-control": "no-store"
1854
- });
1855
- waiter.res.end(JSON.stringify(command));
1856
- return;
1857
- }
1858
- }
1859
- removeCommandWaiter(res) {
1860
- const idx = this.commandWaiters.findIndex((w) => w.res === res);
1861
- if (idx !== -1) {
1862
- clearTimeout(this.commandWaiters[idx].timer);
1863
- this.commandWaiters.splice(idx, 1);
1864
- }
1865
- }
1866
- handlePostResult(clientReq, clientRes) {
1867
- const chunks = [];
1868
- clientReq.on("data", (chunk) => chunks.push(chunk));
1869
- clientReq.on("end", () => {
1870
- try {
1871
- const body = Buffer.concat(chunks).toString("utf-8");
1872
- const result = JSON.parse(body);
1873
- if (result?.id) {
1874
- const pending2 = this.pendingResults.get(result.id);
1875
- if (pending2) {
1876
- log.info("Browser command result received", { id: result.id, stepCount: result.steps?.length, duration: result.duration });
1877
- clearTimeout(pending2.timeout);
1878
- this.pendingResults.delete(result.id);
1879
- pending2.resolve(result);
1880
- } else {
1881
- log.warn("Browser command result received but no pending command found", { id: result.id, pendingIds: [...this.pendingResults.keys()] });
1882
- }
1883
- } else {
1884
- log.warn("Browser command result received with no id", { bodyLength: body.length });
1885
- }
1886
- } catch (err) {
1887
- log.warn("Browser command result parse error", { error: err instanceof Error ? err.message : String(err) });
1888
- }
1889
- clientRes.writeHead(204, this.corsHeaders(clientReq));
1890
- clientRes.end();
1891
- });
1892
- }
1893
1995
  /**
1894
1996
  * Inject window.__MINDSTUDIO__ context and browser agent script tag into HTML.
1895
1997
  */
@@ -1905,8 +2007,11 @@ ${agentScript}`;
1905
2007
  }
1906
2008
  return injection + "\n" + html;
1907
2009
  }
1908
- handleUpgrade(clientReq, clientSocket, head) {
1909
- log.debug("Dev proxy WebSocket upgrade", { path: clientReq.url });
2010
+ // ---------------------------------------------------------------------------
2011
+ // Upstream WebSocket forwarding (HMR etc.)
2012
+ // ---------------------------------------------------------------------------
2013
+ handleUpstreamUpgrade(clientReq, clientSocket, head) {
2014
+ log.debug("Dev proxy WebSocket upgrade (upstream)", { path: clientReq.url });
1910
2015
  const options = {
1911
2016
  hostname: "127.0.0.1",
1912
2017
  port: this.upstreamPort,
@@ -2189,4 +2294,4 @@ export {
2189
2294
  watchTableFiles,
2190
2295
  watchConfigFile
2191
2296
  };
2192
- //# sourceMappingURL=chunk-VJTZSOKC.js.map
2297
+ //# sourceMappingURL=chunk-HZZVPY7J.js.map