// src/server/browser-stream/viewer-registry.ts
var ViewerRegistry = class {
  /** Map of agentId to set of connected WebSocket contexts */
  viewers = /* @__PURE__ */ new Map();
  /** Map of agentId to active screencast stream */
  screencasts = /* @__PURE__ */ new Map();
  /** Set of viewerKeys currently starting a screencast (to prevent race conditions) */
  startingScreencasts = /* @__PURE__ */ new Set();
  /** Map of agentId to cleanup function for onBrowserReady callback */
  browserReadyCleanups = /* @__PURE__ */ new Map();
  /** Map of agentId to cleanup function for onBrowserClosed callback */
  browserClosedCleanups = /* @__PURE__ */ new Map();
  /** Map of agentId to last known URL (for dedup) */
  lastUrls = /* @__PURE__ */ new Map();
  /** Map of agentId to last known viewport dimensions (for change detection) */
  lastViewports = /* @__PURE__ */ new Map();
  /** Map of viewerKey to last broadcast status (for replay to new viewers) */
  lastStatuses = /* @__PURE__ */ new Map();
  /**
   * Add a viewer for an agent. Starts screencast if this is the first viewer.
   *
   * @param viewerKey - The viewer key (agentId or agentId:threadId for thread-scoped)
   * @param ws - The WebSocket context for this viewer
   * @param getToolset - Function to retrieve the BrowserToolset for this agent
   * @param agentId - The actual agent ID for toolset lookup (optional, defaults to viewerKey)
   * @param threadId - The thread ID for thread-scoped screencasts (optional)
   */
  async addViewer(viewerKey, ws, getToolset, agentId, threadId) {
    let viewerSet = this.viewers.get(viewerKey);
    if (!viewerSet) {
      viewerSet = /* @__PURE__ */ new Set();
      this.viewers.set(viewerKey, viewerSet);
    }
    const wasEmpty = viewerSet.size === 0;
    viewerSet.add(ws);
    if (wasEmpty) {
      await this.startScreencast(viewerKey, getToolset, agentId ?? viewerKey, threadId);
    } else {
      this.sendCurrentState(viewerKey, ws);
    }
  }
  /**
   * Send current state (URL, viewport) to a newly connected viewer.
   */
  sendCurrentState(viewerKey, ws) {
    try {
      const lastUrl = this.lastUrls.get(viewerKey);
      if (lastUrl) {
        ws.send(JSON.stringify({ url: lastUrl }));
      }
      const lastViewport = this.lastViewports.get(viewerKey);
      if (lastViewport) {
        ws.send(JSON.stringify({ viewport: lastViewport }));
      }
      const lastStatus = this.lastStatuses.get(viewerKey);
      if (lastStatus) {
        ws.send(JSON.stringify({ status: lastStatus }));
      }
    } catch (error) {
      console.warn("[ViewerRegistry] Error sending current state to new viewer:", error);
    }
  }
  /**
   * Remove a viewer. Stops screencast if this was the last viewer.
   *
   * @param viewerKey - The viewer key (agentId or agentId:threadId for thread-scoped)
   * @param ws - The WebSocket context to remove
   */
  async removeViewer(viewerKey, ws) {
    const viewerSet = this.viewers.get(viewerKey);
    if (!viewerSet) {
      return;
    }
    viewerSet.delete(ws);
    if (viewerSet.size === 0) {
      this.viewers.delete(viewerKey);
      this.lastUrls.delete(viewerKey);
      this.lastViewports.delete(viewerKey);
      const readyCleanup = this.browserReadyCleanups.get(viewerKey);
      if (readyCleanup) {
        readyCleanup();
        this.browserReadyCleanups.delete(viewerKey);
      }
      const closedCleanup = this.browserClosedCleanups.get(viewerKey);
      if (closedCleanup) {
        closedCleanup();
        this.browserClosedCleanups.delete(viewerKey);
      }
      await this.stopScreencast(viewerKey);
    }
  }
  /**
   * Broadcast a binary frame to all viewers.
   *
   * @param viewerKey - The viewer key (agentId or agentId:threadId for thread-scoped)
   * @param data - The binary frame data (base64 encoded)
   */
  broadcastFrame(viewerKey, data) {
    const viewerSet = this.viewers.get(viewerKey);
    if (!viewerSet) {
      return;
    }
    for (const ws of viewerSet) {
      try {
        ws.send(data);
      } catch (error) {
        console.warn("[ViewerRegistry] Error broadcasting frame:", error);
      }
    }
  }
  /**
   * Broadcast a status message to all viewers.
   *
   * @param viewerKey - The viewer key (agentId or agentId:threadId for thread-scoped)
   * @param status - The status message to send
   */
  broadcastStatus(viewerKey, status) {
    if (status.status) {
      this.lastStatuses.set(viewerKey, status.status);
    }
    const viewerSet = this.viewers.get(viewerKey);
    if (!viewerSet) {
      return;
    }
    const message = JSON.stringify(status);
    for (const ws of viewerSet) {
      try {
        ws.send(message);
      } catch (error) {
        console.warn("[ViewerRegistry] Error broadcasting status:", error);
      }
    }
  }
  /**
   * Broadcast a URL update to all viewers (only if changed).
   */
  broadcastUrlIfChanged(viewerKey, url) {
    if (!url) return;
    if (this.lastUrls.get(viewerKey) === url) return;
    this.lastUrls.set(viewerKey, url);
    const viewerSet = this.viewers.get(viewerKey);
    if (!viewerSet) return;
    const message = JSON.stringify({ url });
    for (const ws of viewerSet) {
      try {
        ws.send(message);
      } catch (error) {
        console.warn("[ViewerRegistry] Error broadcasting URL:", error);
      }
    }
  }
  /**
   * Broadcast viewport metadata to all viewers.
   * Only sends if dimensions have changed from last broadcast.
   * Called on stream start and on each frame to detect dimension changes.
   */
  broadcastViewportIfChanged(viewerKey, viewport) {
    const last = this.lastViewports.get(viewerKey);
    if (last && last.width === viewport.width && last.height === viewport.height) {
      return;
    }
    this.lastViewports.set(viewerKey, { width: viewport.width, height: viewport.height });
    const viewerSet = this.viewers.get(viewerKey);
    if (!viewerSet) return;
    const message = { viewport: { width: viewport.width, height: viewport.height } };
    const json = JSON.stringify(message);
    for (const ws of viewerSet) {
      try {
        ws.send(json);
      } catch (error) {
        console.warn("[ViewerRegistry] Error broadcasting viewport:", error);
      }
    }
  }
  /**
   * Start screencast for a viewer. Only starts if browser is already running.
   * If browser not running, registers a callback to start when browser becomes ready.
   *
   * @param viewerKey - The viewer key (for registry keying)
   * @param getToolset - Function to retrieve the BrowserToolset
   * @param agentId - The actual agent ID for toolset lookup
   * @param threadId - The thread ID for thread-scoped page selection (optional)
   */
  async startScreencast(viewerKey, getToolset, agentId, threadId) {
    const toolset = getToolset(agentId);
    if (!toolset) {
      console.info(`[ViewerRegistry] No toolset for ${viewerKey}, waiting...`);
      return;
    }
    if (!this.browserReadyCleanups.has(viewerKey)) {
      const cleanup = toolset.onBrowserReady(() => {
        if (!this.viewers.has(viewerKey)) {
          return;
        }
        const existingStream = this.screencasts.get(viewerKey);
        if (existingStream) {
          console.info(`[ViewerRegistry] Stopping old screencast for ${viewerKey} before reconnecting...`);
          this.screencasts.delete(viewerKey);
          existingStream.stop().catch(() => {
          });
        }
        console.info(`[ViewerRegistry] Browser ready for ${viewerKey}, starting screencast...`);
        this.doStartScreencast(viewerKey, toolset, threadId).catch((error) => {
          console.error(`[ViewerRegistry] Failed to start screencast on browser ready for ${viewerKey}:`, error);
        });
      }, threadId);
      this.browserReadyCleanups.set(viewerKey, cleanup);
    }
    if (!this.browserClosedCleanups.has(viewerKey)) {
      const cleanup = toolset.onBrowserClosed(() => {
        console.info(`[ViewerRegistry] Browser closed for ${viewerKey}, notifying viewers...`);
        this.screencasts.delete(viewerKey);
        this.broadcastStatus(viewerKey, { status: "browser_closed" });
      }, threadId);
      this.browserClosedCleanups.set(viewerKey, cleanup);
    }
    if (toolset.isBrowserRunning()) {
      await this.doStartScreencast(viewerKey, toolset, threadId);
    } else {
      console.info(`[ViewerRegistry] Browser not running for ${viewerKey}, waiting for browser to start...`);
    }
  }
  /**
   * Internal method to actually start the screencast stream.
   *
   * @param viewerKey - The viewer key (for registry keying and logging)
   * @param toolset - The browser toolset
   * @param threadId - The thread ID for thread-scoped page selection (optional)
   */
  async doStartScreencast(viewerKey, toolset, threadId) {
    if (this.screencasts.has(viewerKey) || this.startingScreencasts.has(viewerKey)) {
      return;
    }
    this.startingScreencasts.add(viewerKey);
    try {
      this.broadcastStatus(viewerKey, { status: "browser_starting" });
      const stream = await toolset.startScreencastIfBrowserActive(threadId ? { threadId } : void 0);
      if (!stream) {
        console.warn(`[ViewerRegistry] No browser session for ${viewerKey}`);
        this.broadcastStatus(viewerKey, { status: "browser_closed" });
        return;
      }
      this.screencasts.set(viewerKey, stream);
      const currentStream = stream;
      stream.on("frame", (frame) => {
        if (this.screencasts.get(viewerKey) !== currentStream) return;
        this.broadcastFrame(viewerKey, frame.data);
        this.broadcastViewportIfChanged(viewerKey, frame.viewport);
      });
      stream.on("url", (url) => {
        if (this.screencasts.get(viewerKey) !== currentStream) return;
        this.broadcastUrlIfChanged(viewerKey, url);
      });
      stream.on("stop", (reason) => {
        if (this.screencasts.get(viewerKey) !== currentStream) {
          console.info(`[ViewerRegistry] Ignoring stop from superseded stream for ${viewerKey}`);
          return;
        }
        console.info(`[ViewerRegistry] Screencast stopped for ${viewerKey}: ${reason}`);
        this.screencasts.delete(viewerKey);
        this.broadcastStatus(viewerKey, { status: "browser_closed" });
      });
      stream.on("error", (error) => {
        if (this.screencasts.get(viewerKey) !== currentStream) return;
        console.error(`[ViewerRegistry] Screencast error for ${viewerKey}:`, error);
        this.screencasts.delete(viewerKey);
        currentStream.stop().catch((stopError) => {
          console.warn(`[ViewerRegistry] Error stopping errored screencast for ${viewerKey}:`, stopError);
        });
        this.broadcastStatus(viewerKey, { status: "browser_closed" });
      });
      this.broadcastStatus(viewerKey, { status: "streaming" });
      const initialUrl = await toolset.getCurrentUrl(threadId);
      this.broadcastUrlIfChanged(viewerKey, initialUrl);
    } catch (error) {
      console.error(`[ViewerRegistry] Failed to start screencast for ${viewerKey}:`, error);
    } finally {
      this.startingScreencasts.delete(viewerKey);
    }
  }
  /**
   * Stop screencast for an agent. Called when last viewer disconnects.
   */
  async stopScreencast(agentId) {
    const stream = this.screencasts.get(agentId);
    if (!stream) {
      return;
    }
    try {
      await stream.stop();
    } catch (error) {
      console.warn(`[ViewerRegistry] Error stopping screencast for ${agentId}:`, error);
    } finally {
      this.screencasts.delete(agentId);
    }
  }
  /**
   * Get the number of viewers for an agent.
   *
   * @param agentId - The agent ID
   * @returns The number of connected viewers
   */
  getViewerCount(agentId) {
    return this.viewers.get(agentId)?.size ?? 0;
  }
  /**
   * Check if an agent has an active screencast.
   *
   * @param agentId - The agent ID
   * @returns True if screencast is active
   */
  hasActiveScreencast(agentId) {
    return this.screencasts.has(agentId);
  }
  /**
   * Close the browser session for an agent.
   * Stops screencast and broadcasts browser_closed status.
   * Call this before calling toolset.close() to ensure UI is notified.
   *
   * @param viewerKey - The viewer key (agentId or agentId:threadId for thread-scoped)
   */
  async closeBrowserSession(viewerKey) {
    this.lastUrls.delete(viewerKey);
    this.lastViewports.delete(viewerKey);
    this.lastStatuses.delete(viewerKey);
    const stream = this.screencasts.get(viewerKey);
    if (stream) {
      try {
        await stream.stop();
      } catch (error) {
        console.warn(`[ViewerRegistry] Error stopping screencast for ${viewerKey}:`, error);
        this.screencasts.delete(viewerKey);
        this.broadcastStatus(viewerKey, { status: "browser_closed" });
      }
    } else {
      this.broadcastStatus(viewerKey, { status: "browser_closed" });
    }
  }
};

// src/server/browser-stream/input-handler.ts
var VALID_MOUSE_EVENTS = /* @__PURE__ */ new Set(["mousePressed", "mouseReleased", "mouseMoved", "mouseWheel"]);
var VALID_KEYBOARD_EVENTS = /* @__PURE__ */ new Set(["keyDown", "keyUp", "char"]);
var inputQueues = /* @__PURE__ */ new Map();
function enqueueInput(key, fn) {
  const current = inputQueues.get(key) ?? Promise.resolve();
  const next = current.then(fn).catch(() => {
  });
  inputQueues.set(key, next);
}
var VIRTUAL_KEY_CODES = {
  // Control keys
  Backspace: 8,
  Tab: 9,
  Enter: 13,
  Shift: 16,
  Control: 17,
  Alt: 18,
  Pause: 19,
  CapsLock: 20,
  Escape: 27,
  Space: 32,
  " ": 32,
  PageUp: 33,
  PageDown: 34,
  End: 35,
  Home: 36,
  // Arrow keys
  ArrowLeft: 37,
  ArrowUp: 38,
  ArrowRight: 39,
  ArrowDown: 40,
  // Editing keys
  Insert: 45,
  Delete: 46,
  // Function keys
  F1: 112,
  F2: 113,
  F3: 114,
  F4: 115,
  F5: 116,
  F6: 117,
  F7: 118,
  F8: 119,
  F9: 120,
  F10: 121,
  F11: 122,
  F12: 123
};
function getVirtualKeyCode(key) {
  if (!key) return void 0;
  if (VIRTUAL_KEY_CODES[key] !== void 0) {
    return VIRTUAL_KEY_CODES[key];
  }
  if (key.length === 1) {
    return key.toUpperCase().charCodeAt(0);
  }
  return void 0;
}
function handleInputMessage(data, getToolset, agentId, threadId) {
  let message;
  try {
    message = JSON.parse(data);
  } catch {
    return;
  }
  if (!isValidInputMessage(message)) {
    return;
  }
  const toolset = getToolset(agentId);
  if (!toolset) {
    return;
  }
  const queueKey = `${agentId}:${threadId ?? "default"}`;
  switch (message.type) {
    case "mouse":
      enqueueInput(queueKey, async () => {
        try {
          await injectMouse(toolset, message, threadId);
        } catch (err) {
          if (isDisconnectionError(err)) {
            notifyBrowserClosed(toolset, threadId);
          } else if (!isExpectedInjectionError(err)) {
            console.warn("[InputHandler] Mouse injection error:", err);
          }
        }
      });
      break;
    case "keyboard":
      enqueueInput(queueKey, async () => {
        try {
          await injectKeyboard(toolset, message, threadId);
        } catch (err) {
          if (isDisconnectionError(err)) {
            notifyBrowserClosed(toolset, threadId);
          } else if (!isExpectedInjectionError(err)) {
            console.warn("[InputHandler] Keyboard injection error:", err);
          }
        }
      });
      break;
  }
}
function isDisconnectionError(err) {
  if (!(err instanceof Error)) return false;
  const msg = err.message.toLowerCase();
  return msg.includes("target closed") || msg.includes("browser has been closed") || msg.includes("page has been closed") || msg.includes("session closed") || msg.includes("browser has disconnected");
}
function isExpectedInjectionError(err) {
  if (!(err instanceof Error)) return false;
  const msg = err.message.toLowerCase();
  return msg.includes("no cdp session") || msg.includes("browser not launched") || msg.includes("not connected to browser") || msg.includes("no active target") || msg.includes("target") && msg.includes("not attached");
}
function notifyBrowserClosed(toolset, threadId) {
  const browser = toolset;
  if (threadId && typeof browser.closeThreadSession === "function") {
    void browser.closeThreadSession(threadId).catch(() => {
      browser.handleBrowserDisconnected?.();
    });
    return;
  }
  browser.handleBrowserDisconnected?.();
}
async function injectMouse(toolset, msg, threadId) {
  await toolset.injectMouseEvent(
    {
      type: msg.eventType,
      x: msg.x,
      y: msg.y,
      button: msg.button,
      clickCount: msg.clickCount,
      deltaX: msg.deltaX,
      deltaY: msg.deltaY,
      modifiers: msg.modifiers
    },
    threadId
  );
}
async function injectKeyboard(toolset, msg, threadId) {
  const windowsVirtualKeyCode = getVirtualKeyCode(msg.key);
  await toolset.injectKeyboardEvent(
    {
      type: msg.eventType,
      key: msg.key,
      code: msg.code,
      text: msg.text,
      modifiers: msg.modifiers,
      windowsVirtualKeyCode
    },
    threadId
  );
}
function isValidInputMessage(msg) {
  if (typeof msg !== "object" || msg === null) {
    return false;
  }
  const typed = msg;
  if (typed.type === "mouse") {
    if (typeof typed.eventType !== "string" || !VALID_MOUSE_EVENTS.has(typed.eventType)) {
      return false;
    }
    if (typeof typed.x !== "number" || typeof typed.y !== "number") {
      return false;
    }
    if (typed.button !== void 0 && typeof typed.button !== "string") {
      return false;
    }
    if (typed.deltaX !== void 0 && typeof typed.deltaX !== "number") {
      return false;
    }
    if (typed.deltaY !== void 0 && typeof typed.deltaY !== "number") {
      return false;
    }
    return true;
  }
  if (typed.type === "keyboard") {
    if (typeof typed.eventType !== "string" || !VALID_KEYBOARD_EVENTS.has(typed.eventType)) {
      return false;
    }
    const hasKey = typeof typed.key === "string";
    const hasCode = typeof typed.code === "string";
    const hasText = typeof typed.text === "string";
    if (!hasKey && !hasCode && !hasText) {
      return false;
    }
    if (typed.modifiers !== void 0 && typeof typed.modifiers !== "number") {
      return false;
    }
    return true;
  }
  return false;
}

export { ViewerRegistry, handleInputMessage };
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map