"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  // If the importer is in node compatibility mode or this is not an ESM
  // file that has been converted to a CommonJS file using a Babel-
  // compatible transform (i.e. "__esModule" has not been set), then set
  // "default" to the CommonJS "module.exports" for node compatibility.
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var traceRecordingBackend_exports = {};
__export(traceRecordingBackend_exports, {
  TraceRecordingBackend: () => TraceRecordingBackend
});
module.exports = __toCommonJS(traceRecordingBackend_exports);
var import_fs = __toESM(require("fs"));
var import_os = __toESM(require("os"));
var import_path = __toESM(require("path"));
var import_utilsBundle = require("playwright-core/lib/utilsBundle");
var import_browserServerBackend = require("../browser/browserServerBackend");
var import_config = require("../browser/config");
var import_browserContextFactory = require("../browser/browserContextFactory");
var import_response = require("../browser/response");
var import_log = require("../log");
var import_skyRampExport = require("../test/skyRampExport");
var import_exportTool = require("./exportTool");
var import_assertTool = require("./assertTool");
var import_types = require("./types");
const traceDebug = (0, import_utilsBundle.debug)("pw:mcp:trace");
class TraceRecordingBackend {
  // true while page.reload() is in progress — suppresses spurious popup tracking
  constructor(options) {
    this._trackedActions = [];
    this._initialized = false;
    this._pageCount = 0;
    // number of popup pages seen
    this._currentPageAlias = "page";
    // alias of the currently active page
    this._pendingPopupAlias = null;
    // popup alias to stamp on the NEXT tracked click
    this._reloading = false;
    this._options = options || {};
    this._outputDir = options?.outputDir || process.cwd();
    this._tempDir = import_fs.default.mkdtempSync(import_path.default.join(import_os.default.tmpdir(), "skyramp-trace-"));
    this._harPath = import_path.default.join(this._tempDir, "skyramp_network.har");
  }
  get trackedActions() {
    return this._trackedActions;
  }
  async initialize(clientInfo) {
    const userDataDir = this._options.userDataDir || process.env.PLAYWRIGHT_USER_DATA_DIR || void 0;
    const loadExtension = this._options.loadExtension ?? splitExtensionPaths(process.env.PLAYWRIGHT_LOAD_EXTENSION);
    const persistent = !!userDataDir || !!loadExtension?.length;
    const extensionArgs = loadExtension?.length ? [
      `--disable-extensions-except=${loadExtension.join(",")}`,
      `--load-extension=${loadExtension.join(",")}`
    ] : void 0;
    const headless = loadExtension?.length ? false : this._options.headless ?? (!!process.env.CI || import_os.default.platform() === "linux" && !process.env.DISPLAY);
    const config = await (0, import_config.resolveConfig)({
      browser: {
        isolated: !persistent,
        ...persistent && userDataDir ? { userDataDir } : {},
        launchOptions: {
          headless,
          ...extensionArgs ? { args: extensionArgs } : {}
        },
        contextOptions: {
          viewport: { width: 1280, height: 900 },
          recordHar: { path: this._harPath, mode: "minimal" },
          ignoreHTTPSErrors: true,
          serviceWorkers: "block"
        }
      },
      capabilities: ["testing"],
      outputDir: this._tempDir,
      snapshot: { mode: "full" },
      // Always return full snapshots — incremental diffs cause empty snapshots after clicks
      timeouts: {
        action: 15e3,
        // 15s — modals and dynamic content need more time
        navigation: 6e4
      }
    });
    if (loadExtension?.length && config.browser.launchOptions?.channel === "chrome")
      delete config.browser.launchOptions.channel;
    const factory = (0, import_browserContextFactory.contextFactory)(config);
    this._browserBackend = new import_browserServerBackend.BrowserServerBackend(config, factory);
    await this._browserBackend.initialize(clientInfo);
    this._initialized = true;
    traceDebug("TraceRecordingBackend initialized", {
      persistent,
      userDataDir,
      loadExtension,
      channel: config.browser.launchOptions?.channel
    });
    this._browserBackend.context.onBrowserContextCreated = (browserContext) => this._installPopupListener(browserContext);
  }
  async listTools() {
    const browserTools = await this._browserBackend.listTools();
    return [...browserTools, (0, import_exportTool.exportZipMcpTool)(), (0, import_assertTool.assertMcpTool)()];
  }
  async callTool(name, args, progress) {
    if (!this._initialized)
      return { content: [{ type: "text", text: "### Error\nBackend not initialized. Call initialize() first." }], isError: true };
    if (name === import_exportTool.exportZipSchema.name) {
      const parsed = import_exportTool.exportZipSchema.inputSchema.parse(args || {});
      try {
        await this._browserBackend.callTool("browser_close", {});
      } catch {
      }
      const handler = (0, import_exportTool.createExportZipHandler)({
        trackedActions: this._trackedActions,
        harPath: this._harPath,
        outputDir: this._outputDir
      });
      const exportResult = await handler(parsed);
      if (!exportResult.isError) {
        this._trackedActions = [];
        this._cleanupTempDir(true);
      }
      return exportResult;
    }
    if (name === import_assertTool.assertToolSchema.name) {
      const parsed = import_assertTool.assertToolSchema.inputSchema.parse(args || {});
      return this._handleAssert(parsed);
    }
    if (name === "browser_select_option") {
      const result2 = await this._handleSelectOption(args || {});
      return result2;
    }
    if (name === "browser_tabs" && ["select", "switch"].includes(args?.action)) {
      const index = args?.index ?? 0;
      const targetUrl = this._browserBackend.context?.tabs()[index]?.page.url();
      const urlAlias = targetUrl ? aliasForUrl(targetUrl) : null;
      this._currentPageAlias = urlAlias ?? (index === 0 ? "page" : `page${index}`);
      traceDebug(`Tab switched to index ${index} (url=${targetUrl}) \u2192 pageAlias: ${this._currentPageAlias}`);
      const tabResult = await this._browserBackend.callTool(name, args);
      return tabResult;
    }
    if (name === "browser_navigate") {
      const targetUrl = args?.url;
      const currentUrl = this._browserBackend.context?.currentTab()?.page.url();
      if (targetUrl && currentUrl && this._normalizeUrl(targetUrl) === this._normalizeUrl(currentUrl)) {
        traceDebug(`Navigate to same URL detected (${targetUrl}), converting to reload`);
        const ts = Date.now();
        const reloadAlias = this._currentPageAlias;
        const tab = this._browserBackend.context.currentTab();
        this._reloading = true;
        try {
          await tab.page.reload({ waitUntil: "domcontentloaded" });
        } finally {
          this._reloading = false;
          this._currentPageAlias = reloadAlias;
          this._pendingPopupAlias = null;
        }
        this._trackedActions.push({
          toolName: "browser_navigate",
          args: { url: currentUrl },
          code: "",
          timestamp: ts,
          pageAlias: this._currentPageAlias
        });
        const snapResult = await this._browserBackend.callTool("browser_snapshot", {});
        return {
          content: [
            { type: "text", text: `### Navigation (reload)
Reloaded current page: ${currentUrl}
` },
            ...snapResult.content || []
          ]
        };
      }
    }
    const timestampBeforeAction = Date.now();
    const pageAliasBeforeAction = this._currentPageAlias;
    let preClickHoverCode = null;
    if (name === "browser_click" && args?.ref) {
      const hoverResult = await this._browserBackend.callTool("browser_hover", {
        element: args.element || "element",
        ref: args.ref
      });
      if (!hoverResult.isError)
        preClickHoverCode = (0, import_response.parseResponse)(hoverResult)?.code ?? null;
    }
    let result = await this._browserBackend.callTool(name, args);
    result = await this._improveActionSelector(name, args || {}, result, preClickHoverCode);
    if (name === "browser_fill_form" && result.isError) {
      const resultText = result.content?.[0]?.type === "text" ? result.content[0].text : "";
      if (resultText.includes("not a <select> element") || resultText.includes("selectOption") && resultText.includes("Timeout")) {
        const fields = args?.fields;
        if (fields) {
          const comboField = fields.find((f) => f.type === "combobox");
          if (comboField) {
            traceDebug("fill_form failed on combobox field, retrying with select fallback");
            this._maybeTrackAction(name, args || {}, { content: result.content, isError: false }, timestampBeforeAction, pageAliasBeforeAction);
            const selectResult = await this._handleSelectOption({
              element: comboField.name || "dropdown",
              ref: comboField.ref,
              values: [comboField.value]
            });
            return selectResult;
          }
        }
      }
    }
    this._maybeTrackAction(name, args || {}, result, timestampBeforeAction, pageAliasBeforeAction);
    return result;
  }
  serverClosed() {
    void this._autoExportAndClose().catch(import_log.logUnhandledError);
  }
  /**
   * Handle browser_select_option with automatic fallback for custom dropdowns.
   * 1. Try native selectOption first.
   * 2. If it fails (not a <select> element), click the combobox to open it,
   *    take a snapshot to find the options, then click the matching option.
   */
  async _handleSelectOption(args) {
    const result = await this._browserBackend.callTool("browser_select_option", args);
    const resultText = result.content?.[0]?.type === "text" ? result.content[0].text : "";
    if (!result.isError) {
      const code = (0, import_response.parseResponse)(result)?.code ?? "";
      const hasCssSelector = code.includes("page.locator('select") || code.includes('page.locator("select') || code.includes(".selectOption(") && !code.includes("getByTestId") && !code.includes("getByRole") && !code.includes("getByLabel");
      if (hasCssSelector && args.ref) {
        traceDebug("selectOption used CSS selector, trying to resolve a better one via hover");
        const hoverResult = await this._browserBackend.callTool("browser_hover", {
          element: args.element || "select",
          ref: args.ref
        });
        if (!hoverResult.isError) {
          const hoverCode = (0, import_response.parseResponse)(hoverResult)?.code ?? "";
          const locatorMatch = hoverCode.match(/await\s+page\.(.*?)\.hover\(\)/s);
          if (locatorMatch) {
            const locatorExpr = locatorMatch[1].trim();
            const parsed = this._codeToLocator(locatorExpr);
            if (parsed && parsed.locator.kind !== "text") {
              const values2 = args.values || [];
              const selectCode = `await page.${locatorExpr}.selectOption(${JSON.stringify(values2.length === 1 ? values2[0] : values2)});`;
              traceDebug(`Improved select selector: ${selectCode}`);
              const timestamp = Date.now();
              this._trackedActions.push({
                toolName: "browser_select_option",
                args,
                code: selectCode,
                timestamp
              });
              return result;
            }
          }
        }
      }
      this._maybeTrackAction("browser_select_option", args, result);
      return result;
    }
    const shouldFallback = resultText.includes("not a <select> element") || resultText.includes("selectOption") && resultText.includes("Timeout") || resultText.includes("not found in the current page snapshot");
    if (!shouldFallback)
      return result;
    traceDebug("selectOption failed on custom dropdown, trying combobox fallback");
    const values = args.values || [];
    if (!values.length)
      return result;
    const preSnapResult = await this._browserBackend.callTool("browser_snapshot", {});
    if (preSnapResult.isError)
      return preSnapResult;
    let snapText = preSnapResult.content?.map((c) => c.type === "text" ? c.text : "").join("") || "";
    const dropdownAlreadyOpen = /listbox[^\n]*\[ref=\w+\]/.test(snapText) && /option\s+"/.test(snapText);
    if (!dropdownAlreadyOpen) {
      const comboboxes = [...snapText.matchAll(/combobox[^\n]*\[ref=(\w+)\]/g)];
      const clickableCombo = comboboxes.find((m) => m[0].includes("cursor=pointer"));
      const comboRef = clickableCombo?.[1] || args.ref;
      const clickResult = await this._browserBackend.callTool("browser_click", { element: args.element || "dropdown", ref: comboRef });
      if (clickResult.isError)
        return { content: [{ type: "text", text: `### Error
Failed to open custom dropdown. The dropdown modal may not be visible yet. Try calling browser_snapshot first to check the page state, then ensure the form/modal is open before selecting an option.` }], isError: true };
      this._maybeTrackAction("browser_click", { element: args.element || "dropdown", ref: comboRef }, clickResult);
      const snapResult = await this._browserBackend.callTool("browser_snapshot", {});
      if (snapResult.isError)
        return snapResult;
      snapText = snapResult.content?.map((c) => c.type === "text" ? c.text : "").join("") || "";
    } else {
      traceDebug("Dropdown already open, skipping combobox click");
    }
    const targetValue = values[0];
    const candidates = [targetValue];
    const lower = targetValue.toLowerCase();
    if (lower === "true" || lower === "yes") candidates.push("Yes", "yes", "true", "True");
    else if (lower === "false" || lower === "no") candidates.push("No", "no", "false", "False");
    for (const candidate of candidates) {
      const escaped = candidate.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
      const pattern1 = new RegExp(`option\\s+"${escaped}"[^\\n]*\\[ref=(\\w+)\\]`, "i");
      const match1 = snapText.match(pattern1);
      if (match1) {
        const optionClickResult = await this._browserBackend.callTool("browser_click", { element: `${candidate} option`, ref: match1[1] });
        this._maybeTrackAction("browser_click", { element: `${candidate} option`, ref: match1[1] }, optionClickResult);
        return optionClickResult;
      }
      const pattern2 = new RegExp(`option[^\\n]*\\[ref=(\\w+)\\][^\\n]*${escaped}`, "i");
      const match2 = snapText.match(pattern2);
      if (match2) {
        const optionClickResult = await this._browserBackend.callTool("browser_click", { element: `${candidate} option`, ref: match2[1] });
        this._maybeTrackAction("browser_click", { element: `${candidate} option`, ref: match2[1] }, optionClickResult);
        return optionClickResult;
      }
    }
    const optionLines = snapText.split("\n").filter((l) => l.includes("option")).join("\n");
    return { content: [{ type: "text", text: `### Error
Opened the custom dropdown but could not find a matching option for "${targetValue}". Available options:
${optionLines}` }], isError: true };
  }
  /**
   * Handle browser_assert by using browser_hover to resolve the ref to a
   * proper Playwright selector (testid > role > text), then verify the
   * assertion against the snapshot data.
   */
  async _handleAssert(params) {
    const timestamp = Date.now();
    if (!params.expected)
      return { content: [{ type: "text", text: '### Error\n"expected" parameter is required.' }], isError: true };
    const hoverResult = await this._browserBackend.callTool("browser_hover", {
      element: params.element,
      ref: params.ref
    });
    if (hoverResult.isError) {
      const errText = hoverResult.content?.[0]?.type === "text" ? hoverResult.content[0].text : "";
      return { content: [{ type: "text", text: `### Assertion Failed
Could not resolve element ref=${params.ref}. ${errText}` }], isError: true };
    }
    const hoverCode = (0, import_response.parseResponse)(hoverResult)?.code ?? "";
    const locatorMatch = hoverCode.match(/await\s+page\.(.*?)\.hover\(\)/s);
    if (!locatorMatch) {
      return { content: [{ type: "text", text: `### Assertion Failed
Could not extract selector from hover result.` }], isError: true };
    }
    const locatorExpr = locatorMatch[1].trim();
    let parsed = this._codeToLocator(locatorExpr);
    if (!parsed) {
      return { content: [{ type: "text", text: `### Assertion Failed
Could not parse locator: ${locatorExpr}` }], isError: true };
    }
    const snapResult = await this._browserBackend.callTool("browser_snapshot", {});
    const snapText = snapResult.content?.map((c) => c.type === "text" ? c.text : "").join("") || "";
    const snapLines = snapText.split("\n");
    const refLine = snapLines.find((l) => l.includes(`[ref=${params.ref}]`)) || "";
    const textMatch = refLine.match(/^\s*-\s*\w+\s+"([^"]*)"/);
    const elementText = textMatch?.[1] || "";
    const valueMatch = refLine.match(/\]:\s*(.+)$/);
    const elementValue = valueMatch?.[1]?.trim() || "";
    if (parsed.locator.kind === "text") {
      const improved = await this._improveTextSelector(snapLines, params.ref, parsed);
      if (improved) {
        traceDebug(`Improved ambiguous text selector to: ${improved.selector}`);
        parsed = improved;
      }
    }
    let passed = false;
    let details = "";
    if (params.type === "text") {
      passed = elementText.includes(params.expected) || elementValue.includes(params.expected);
      details = passed ? `Text assertion passed: "${params.element}" contains "${params.expected}".` : `Text assertion FAILED: "${params.element}" has text "${elementText}"${elementValue ? ` / value "${elementValue}"` : ""}, expected "${params.expected}".`;
    } else {
      passed = elementValue === params.expected || elementValue.includes(params.expected);
      details = passed ? `Value assertion passed: "${params.element}" has value "${params.expected}".` : `Value assertion FAILED: "${params.element}" has value "${elementValue}", expected "${params.expected}".`;
    }
    if (passed) {
      const selectorText = parsed.locator.kind === "text" ? parsed.locator.body : parsed.locator.next?.kind === "text" ? parsed.locator.next.body : parsed.locator.options?.name ?? null;
      if (selectorText && selectorText === params.expected) {
        traceDebug(`Skipped tautological assertion: selector already matches "${params.expected}"`);
      } else {
        const assertName = params.type === "value" ? "assertValue" : "assertText";
        this._trackedActions.push({
          toolName: "browser_assert",
          args: { type: params.type, ref: params.ref, expected: params.expected },
          code: `${assertName}:${parsed.selector}:${params.expected}${params.type === "text" ? ":" + params.substring : ""}`,
          timestamp
        });
        traceDebug(`Assert: ${assertName} with selector ${parsed.selector}`);
      }
    }
    return {
      content: [{ type: "text", text: `### ${passed ? "Assertion Passed" : "Assertion Failed"}
${details}` }]
    };
  }
  /** Convert a Playwright locator expression to a selector + locator object for JSONL. */
  _codeToLocator(expr) {
    const testidMatch = expr.match(/getByTestId\(\s*['"]([^'"]+)['"]\s*\)/);
    if (testidMatch) {
      return {
        selector: `internal:testid=[data-testid="${testidMatch[1]}"s]`,
        locator: { kind: "test-id", body: testidMatch[1], options: {} }
      };
    }
    const roleMatch = expr.match(/getByRole\(\s*['"]([^'"]+)['"](?:\s*,\s*\{[^}]*name:\s*['"]([^'"]+)['"][^}]*\})?\s*\)/);
    if (roleMatch) {
      return {
        selector: roleMatch[2] ? `internal:role=${roleMatch[1]}[name="${roleMatch[2]}"i]` : `internal:role=${roleMatch[1]}`,
        locator: { kind: "role", body: roleMatch[1], options: { attrs: [], exact: false, ...roleMatch[2] ? { name: roleMatch[2] } : {} } }
      };
    }
    const labelMatch = expr.match(/getByLabel\(\s*['"]([^'"]+)['"]\s*\)/);
    if (labelMatch) {
      return {
        selector: `internal:label="${labelMatch[1]}"i`,
        locator: { kind: "label", body: labelMatch[1], options: { exact: false } }
      };
    }
    const textMatch = expr.match(/getByText\(\s*['"]([^'"]+)['"]\s*\)/);
    if (textMatch) {
      return {
        selector: `internal:text="${textMatch[1]}"i`,
        locator: { kind: "text", body: textMatch[1], options: { exact: false } }
      };
    }
    const ariaRefMatch = expr.match(/locator\(\s*['"]aria-ref=(\w+)['"]\s*\)/);
    if (ariaRefMatch) {
      return null;
    }
    return null;
  }
  /**
   * When hover resolves to a text-based locator (e.g. getByText("$849.98")),
   * check if that text appears multiple times in the snapshot (ambiguous) and
   * hover ancestor refs to find a parent with a test-id, producing a chained
   * selector like `testid >> text` that uniquely identifies the element.
   */
  async _improveTextSelector(snapLines, ref, original) {
    const textBody = original.locator.body;
    const occurrences = snapLines.filter((l) => l.includes(textBody)).length;
    if (occurrences <= 1)
      return null;
    traceDebug(`Text "${textBody}" appears ${occurrences} times in snapshot \u2014 looking for parent test-id`);
    const refPattern = `[ref=${ref}]`;
    const refIdx = snapLines.findIndex((l) => l.includes(refPattern));
    if (refIdx < 0)
      return null;
    const refIndent = snapLines[refIdx].match(/^(\s*)/)?.[1]?.length ?? 0;
    for (let i = refIdx - 1; i >= 0; i--) {
      const line = snapLines[i];
      const indent = line.match(/^(\s*)/)?.[1]?.length ?? 0;
      if (indent >= refIndent)
        continue;
      const ancestorRefMatch = line.match(/\[ref=(\w+)\]/);
      if (!ancestorRefMatch)
        continue;
      const ancestorRef = ancestorRefMatch[1];
      try {
        const ancestorHover = await this._browserBackend.callTool("browser_hover", {
          element: "parent element",
          ref: ancestorRef
        });
        if (!ancestorHover.isError) {
          const ancestorCode = (0, import_response.parseResponse)(ancestorHover)?.code ?? "";
          const ancestorLocatorMatch = ancestorCode.match(/await\s+page\.(.*?)\.hover\(\)/s);
          if (ancestorLocatorMatch) {
            const ancestorExpr = ancestorLocatorMatch[1].trim();
            const testidMatch = ancestorExpr.match(/getByTestId\(\s*['"]([^'"]+)['"]\s*\)/);
            if (testidMatch) {
              const testid = testidMatch[1];
              const chainedSelector = `internal:testid=[data-testid="${testid}"s] >> internal:text="${textBody}"i`;
              traceDebug(`Improved to chained selector: ${chainedSelector}`);
              return {
                selector: chainedSelector,
                locator: {
                  kind: "test-id",
                  body: testid,
                  options: {},
                  next: { kind: "text", body: textBody, options: { exact: false } }
                }
              };
            }
          }
        }
      } catch {
      }
      if (indent === 0)
        break;
    }
    return null;
  }
  static {
    /** Extract selector and locator info from a snapshot line for assertion tracking. */
    // Roles that map to valid Playwright getByRole() selectors.
    // 'generic', 'paragraph', etc. are NOT valid for getByRole.
    this.ASSERTABLE_ROLES = /* @__PURE__ */ new Set([
      "button",
      "link",
      "heading",
      "textbox",
      "checkbox",
      "radio",
      "combobox",
      "listbox",
      "option",
      "tab",
      "tabpanel",
      "dialog",
      "alert",
      "img",
      "navigation",
      "banner",
      "main",
      "form",
      "table",
      "row",
      "cell",
      "columnheader",
      "rowheader",
      "spinbutton",
      "slider",
      "switch",
      "menu",
      "menuitem"
    ]);
  }
  _extractLocatorForRef(refLine) {
    const testidMatch = refLine.match(/testid="([^"]+)"/);
    if (testidMatch) {
      return {
        selector: `internal:testid=[data-testid="${testidMatch[1]}"s]`,
        locator: { kind: "test-id", body: testidMatch[1], options: {} }
      };
    }
    const roleMatch = refLine.match(/^\s*-\s*(\w+)\s+"([^"]*)"/);
    if (roleMatch && TraceRecordingBackend.ASSERTABLE_ROLES.has(roleMatch[1])) {
      const role = roleMatch[1];
      const name = roleMatch[2];
      return {
        selector: `internal:role=${role}[name="${name}"i]`,
        locator: { kind: "role", body: role, options: { attrs: [], exact: false, name } }
      };
    }
    if (roleMatch && roleMatch[2]) {
      const text = roleMatch[2];
      return {
        selector: `internal:text="${text}"i`,
        locator: { kind: "text", body: text, options: { exact: false } }
      };
    }
    return null;
  }
  /**
   * Improve ambiguous selectors in action code before tracking.
   * Handles role-based selectors like getByRole("combobox") that match multiple elements.
   * Also upgrades raw CSS locators (locator('.class')) to semantic selectors.
   * preClickHoverCode: hover result taken BEFORE the click (needed for navigation clicks
   * where the element ref is no longer valid after navigation).
   */
  async _improveActionSelector(toolName, args, result, preClickHoverCode = null) {
    if (result.isError)
      return result;
    const selectorTools = /* @__PURE__ */ new Set(["browser_click", "browser_hover", "browser_type", "browser_fill"]);
    if (!selectorTools.has(toolName))
      return result;
    const parsed = (0, import_response.parseResponse)(result);
    const code = parsed?.code ?? "";
    if (!code)
      return result;
    const cssLocatorMatch = code.match(/await\s+page\.locator\(\s*(['"])[.#][^'"]+\1\s*\)\./);
    if (cssLocatorMatch) {
      const actionMatch = code.match(/\.(click|fill|hover|dblclick|check|uncheck)\s*\(/);
      const action = actionMatch?.[1] ?? "click";
      if (preClickHoverCode) {
        const locatorMatch = preClickHoverCode.match(/await\s+page\.(.*?)\.hover\(\)/s);
        if (locatorMatch) {
          const locatorExpr = locatorMatch[1].trim();
          if (!locatorExpr.startsWith("locator(")) {
            const improvedCode = `await page.${locatorExpr}.${action}();`;
            traceDebug(`Improved CSS locator via pre-hover: ${locatorExpr}`);
            return this._modifyResultCode(result, improvedCode);
          }
        }
      }
      const ref2 = args.ref;
      const snapText2 = parsed?.snapshot ?? "";
      if (ref2 && snapText2) {
        const refLine2 = snapText2.split("\n").find((l) => l.includes(`[ref=${ref2}]`)) || "";
        if (refLine2) {
          const locatorData = this._extractLocatorForRef(refLine2);
          if (locatorData) {
            const locatorCode = this._locatorToCode(locatorData);
            if (locatorCode) {
              const improvedCode = `await page.${locatorCode}.${action}();`;
              traceDebug(`Improved CSS locator from snapshot: ${locatorCode}`);
              return this._modifyResultCode(result, improvedCode);
            }
          }
        }
      }
      return result;
    }
    const roleMatch = code.match(/getByRole\(\s*['"](\w+)['"]\s*\)(?!\s*\.(?:filter|nth|first|last))/);
    if (!roleMatch)
      return result;
    const role = roleMatch[1];
    const ref = args.ref;
    if (!ref)
      return result;
    const snapText = parsed?.snapshot ?? "";
    if (!snapText)
      return result;
    const snapLines = snapText.split("\n");
    const rolePattern = new RegExp(`^\\s*-\\s*${role}\\s+`, "m");
    const roleCount = snapLines.filter((l) => rolePattern.test(l)).length;
    if (roleCount <= 1) {
      traceDebug(`Role "${role}" is unique, no improvement needed`);
      return result;
    }
    traceDebug(`Role "${role}" appears ${roleCount} times \u2014 attempting to disambiguate`);
    const refPattern = `[ref=${ref}]`;
    const refIdx = snapLines.findIndex((l) => l.includes(refPattern));
    if (refIdx < 0)
      return result;
    const refLine = snapLines[refIdx];
    const textMatch = refLine.match(/^\s*-\s*\w+\s+"([^"]*)"/);
    if (textMatch && textMatch[1]) {
      const visibleText = textMatch[1];
      const textPattern = new RegExp(`^\\s*-\\s*${role}\\s+"[^"]*${visibleText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "i");
      const textCount = snapLines.filter((l) => textPattern.test(l)).length;
      if (textCount === 1) {
        const improvedCode = code.replace(
          /getByRole\((\s*['"](\w+)['"]\s*)\)/,
          `getByRole($1).filter({ hasText: "${visibleText}" })`
        );
        traceDebug(`Improved selector with text filter: ${visibleText}`);
        return this._modifyResultCode(result, improvedCode);
      }
    }
    const refIndent = refLine.match(/^(\s*)/)?.[1]?.length ?? 0;
    for (let i = refIdx - 1; i >= 0; i--) {
      const line = snapLines[i];
      const indent = line.match(/^(\s*)/)?.[1]?.length ?? 0;
      if (indent >= refIndent)
        continue;
      const ancestorRefMatch = line.match(/\[ref=(\w+)\]/);
      if (!ancestorRefMatch)
        continue;
      const ancestorRef = ancestorRefMatch[1];
      try {
        const ancestorHover = await this._browserBackend.callTool("browser_hover", {
          element: "parent element",
          ref: ancestorRef
        });
        if (!ancestorHover.isError) {
          const ancestorCode = (0, import_response.parseResponse)(ancestorHover)?.code ?? "";
          const testidMatch = ancestorCode.match(/getByTestId\(\s*['"]([^'"]+)['"]\s*\)/);
          if (testidMatch) {
            const testid = testidMatch[1];
            const improvedCode = code.replace(
              /await\s+page\./,
              `await page.getByTestId("${testid}").`
            );
            traceDebug(`Improved selector by chaining from test-id: ${testid}`);
            return this._modifyResultCode(result, improvedCode);
          }
        }
      } catch {
      }
      if (indent === 0)
        break;
    }
    const roleRefs = [];
    for (const line of snapLines) {
      if (rolePattern.test(line)) {
        const m = line.match(/\[ref=(\w+)\]/);
        if (m)
          roleRefs.push(m[1]);
      }
    }
    const index = roleRefs.indexOf(ref);
    if (index >= 0) {
      const improvedCode = code.replace(
        /getByRole\((\s*['"](\w+)['"]\s*)\)/,
        `getByRole($1).nth(${index})`
      );
      traceDebug(`Improved selector with .nth(${index})`);
      return this._modifyResultCode(result, improvedCode);
    }
    return result;
  }
  /** Helper to modify the code in a CallToolResult. */
  _modifyResultCode(result, newCode) {
    const modified = { ...result, content: [...result.content] };
    for (let i = 0; i < modified.content.length; i++) {
      if (modified.content[i].type === "text") {
        const text = modified.content[i].text || "";
        if (text.includes("```javascript") || text.includes("await page.")) {
          const codeBlockMatch = text.match(/([\s\S]*```javascript\n)([\s\S]*?)(```[\s\S]*)/);
          if (codeBlockMatch) {
            modified.content[i] = {
              ...modified.content[i],
              text: codeBlockMatch[1] + newCode + "\n" + codeBlockMatch[3]
            };
          } else {
            const lines = text.split("\n");
            const codeLineIdx = lines.findIndex((l) => l.includes("await page."));
            if (codeLineIdx >= 0) {
              lines[codeLineIdx] = newCode;
              modified.content[i] = {
                ...modified.content[i],
                text: lines.join("\n")
              };
            }
          }
        }
      }
    }
    return modified;
  }
  /** Convert a locator object to a Playwright code expression like getByRole('link', { name: 'X' }). */
  _locatorToCode(parsed) {
    const { locator } = parsed;
    switch (locator.kind) {
      case "test-id":
        return `getByTestId('${locator.body}')`;
      case "role": {
        const name = locator.options?.name;
        const exact = locator.options?.exact;
        const opts = name ? `, { name: '${name}'${exact ? ", exact: true" : ""} }` : "";
        return `getByRole('${locator.body}'${opts})`;
      }
      case "label":
        return `getByLabel('${locator.body}')`;
      case "text":
        return `getByText('${locator.body}')`;
      default:
        return null;
    }
  }
  /**
   * Listen for new pages (popups) on the browser context.
   * When a popup opens, find the most recent click that triggered it and mark it
   * with a popupAlias signal. Switch the current page alias to the new page.
   */
  _installPopupListener(browserContext) {
    let initialPageSeen = false;
    browserContext.on("page", (newPage) => {
      if (!initialPageSeen) {
        initialPageSeen = true;
        return;
      }
      if (this._reloading) {
        traceDebug("Ignoring spurious popup opened during reload");
        return;
      }
      this._pageCount++;
      const popupAlias = `page${this._pageCount}`;
      this._currentPageAlias = popupAlias;
      this._pendingPopupAlias = popupAlias;
      traceDebug(`Popup page opened: ${popupAlias} (pending stamp)`);
      void newPage.waitForLoadState("domcontentloaded").then(() => {
        const settled = newPage.url();
        const semantic = aliasForUrl(settled);
        if (!semantic)
          return;
        if (this._currentPageAlias === popupAlias)
          this._currentPageAlias = semantic;
        if (this._pendingPopupAlias === popupAlias)
          this._pendingPopupAlias = semantic;
        traceDebug(`Popup alias upgraded: ${popupAlias} \u2192 ${semantic} (url=${settled})`);
      }).catch(() => {
      });
    });
  }
  _maybeTrackAction(toolName, args, result, timestamp, pageAliasBeforeAction) {
    if (result.isError)
      return;
    const parsed = (0, import_response.parseResponse)(result);
    const code = parsed?.code ?? "";
    if (code || import_types.ARGS_ONLY_TOOLS.has(toolName)) {
      const popupAlias = this._pendingPopupAlias ?? void 0;
      this._pendingPopupAlias = null;
      const pageAlias = popupAlias ? pageAliasBeforeAction ?? this._currentPageAlias : this._currentPageAlias;
      this._trackedActions.push({
        toolName,
        args,
        code,
        timestamp: timestamp ?? Date.now(),
        pageAlias,
        popupAlias
      });
      traceDebug(`Tracked action: ${toolName} on ${pageAlias}${popupAlias ? ` \u2192 popup:${popupAlias}` : ""} (total: ${this._trackedActions.length})`);
    }
  }
  /** Normalize a URL for comparison: strip trailing slash and fragment. */
  _normalizeUrl(url) {
    try {
      const u = new URL(url);
      u.hash = "";
      return u.toString().replace(/\/$/, "");
    } catch {
      return url.replace(/\/$/, "");
    }
  }
  /** Clean up temp directory and optionally recreate it for next session. */
  _cleanupTempDir(recreate = false) {
    try {
      import_fs.default.rmSync(this._tempDir, { recursive: true, force: true });
      traceDebug(`Cleaned up temp directory: ${this._tempDir}`);
      if (recreate) {
        this._tempDir = import_fs.default.mkdtempSync(import_path.default.join(import_os.default.tmpdir(), "skyramp-trace-"));
        this._harPath = import_path.default.join(this._tempDir, "skyramp_network.har");
        traceDebug(`Created new temp directory: ${this._tempDir}`);
      }
    } catch (e) {
      traceDebug(`Failed to clean up temp directory ${this._tempDir}: ${e}`);
    }
  }
  async _autoExportAndClose() {
    if (this._trackedActions.length) {
      try {
        const outputZip = import_path.default.resolve(this._outputDir, "skyramp_export.zip");
        const { jsonl: jsonlContent, actionCount } = (0, import_skyRampExport.buildJsonlContent)(this._trackedActions, "chromium", this._harPath);
        await (0, import_skyRampExport.writeSkyrampZip)(outputZip, jsonlContent, this._harPath);
        process.stderr.write(`[skyramp] Auto-exported ${actionCount} actions to ${outputZip}
`);
      } catch {
      }
    }
    this._browserBackend?.serverClosed();
    this._cleanupTempDir(false);
  }
}
function splitExtensionPaths(value) {
  if (!value)
    return void 0;
  const parts = value.split(/[,;]/).map((p) => p.trim()).filter(Boolean);
  return parts.length ? parts : void 0;
}
function aliasForUrl(url) {
  if (!url)
    return null;
  if (/^chrome-extension:\/\/[a-p]{32}\/popup\//.test(url))
    return "popupPage";
  return null;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  TraceRecordingBackend
});
