tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

GenAI.sys.mjs (43391B)


      1 /**
      2 * This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
      5 */
      6 
      7 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
      8 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      9 
     10 const lazy = {};
     11 ChromeUtils.defineESModuleGetters(lazy, {
     12  ASRouterTargeting: "resource:///modules/asrouter/ASRouterTargeting.sys.mjs",
     13  ContentAnalysisUtils: "resource://gre/modules/ContentAnalysisUtils.sys.mjs",
     14  EveryWindow: "resource:///modules/EveryWindow.sys.mjs",
     15  NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
     16  PrefUtils: "moz-src:///toolkit/modules/PrefUtils.sys.mjs",
     17  SidebarManager:
     18    "moz-src:///browser/components/sidebar/SidebarManager.sys.mjs",
     19 });
     20 ChromeUtils.defineLazyGetter(
     21  lazy,
     22  "l10n",
     23  () => new Localization(["browser/genai.ftl"])
     24 );
     25 XPCOMUtils.defineLazyPreferenceGetter(
     26  lazy,
     27  "chatEnabled",
     28  "browser.ml.chat.enabled",
     29  null,
     30  (_pref, _old, val) => onChatEnabledChange(val)
     31 );
     32 XPCOMUtils.defineLazyPreferenceGetter(
     33  lazy,
     34  "chatHideLocalhost",
     35  "browser.ml.chat.hideLocalhost",
     36  null,
     37  reorderChatProviders
     38 );
     39 XPCOMUtils.defineLazyPreferenceGetter(
     40  lazy,
     41  "chatMaxLength",
     42  "browser.ml.chat.maxLength"
     43 );
     44 XPCOMUtils.defineLazyPreferenceGetter(lazy, "chatMenu", "browser.ml.chat.menu");
     45 XPCOMUtils.defineLazyPreferenceGetter(
     46  lazy,
     47  "chatNimbus",
     48  "browser.ml.chat.nimbus"
     49 );
     50 XPCOMUtils.defineLazyPreferenceGetter(
     51  lazy,
     52  "chatOpenSidebarOnProviderChange",
     53  "browser.ml.chat.openSidebarOnProviderChange",
     54  true
     55 );
     56 XPCOMUtils.defineLazyPreferenceGetter(lazy, "chatPage", "browser.ml.chat.page");
     57 XPCOMUtils.defineLazyPreferenceGetter(
     58  lazy,
     59  "chatPageMenuBadge",
     60  "browser.ml.chat.page.menuBadge"
     61 );
     62 XPCOMUtils.defineLazyPreferenceGetter(
     63  lazy,
     64  "chatPromptPrefix",
     65  "browser.ml.chat.prompt.prefix"
     66 );
     67 XPCOMUtils.defineLazyPreferenceGetter(
     68  lazy,
     69  "chatProvider",
     70  "browser.ml.chat.provider",
     71  null,
     72  (_pref, _old, val) => onChatProviderChange(val)
     73 );
     74 XPCOMUtils.defineLazyPreferenceGetter(
     75  lazy,
     76  "chatProviders",
     77  "browser.ml.chat.providers",
     78  "claude,chatgpt,copilot,gemini,lechat",
     79  reorderChatProviders
     80 );
     81 XPCOMUtils.defineLazyPreferenceGetter(
     82  lazy,
     83  "chatShortcuts",
     84  "browser.ml.chat.shortcuts",
     85  null,
     86  (_pref, _old, val) => onChatShortcutsChange(val)
     87 );
     88 XPCOMUtils.defineLazyPreferenceGetter(
     89  lazy,
     90  "chatShortcutsCustom",
     91  "browser.ml.chat.shortcuts.custom"
     92 );
     93 XPCOMUtils.defineLazyPreferenceGetter(
     94  lazy,
     95  "chatShortcutsIgnoreFields",
     96  "browser.ml.chat.shortcuts.ignoreFields",
     97  "input",
     98  updateIgnoredInputs
     99 );
    100 XPCOMUtils.defineLazyPreferenceGetter(
    101  lazy,
    102  "chatSidebar",
    103  "browser.ml.chat.sidebar"
    104 );
    105 XPCOMUtils.defineLazyPreferenceGetter(lazy, "sidebarRevamp", "sidebar.revamp");
    106 XPCOMUtils.defineLazyPreferenceGetter(
    107  lazy,
    108  "sidebarTools",
    109  "sidebar.main.tools"
    110 );
    111 XPCOMUtils.defineLazyPreferenceGetter(
    112  lazy,
    113  "shortcutMouseoverCount",
    114  "browser.ml.chat.shortcut.onboardingMouseoverCount",
    115  0
    116 );
    117 
    118 XPCOMUtils.defineLazyServiceGetter(
    119  lazy,
    120  "parserUtils",
    121  "@mozilla.org/parserutils;1",
    122  Ci.nsIParserUtils
    123 );
    124 
    125 export const GenAI = {
    126  // Cache of potentially localized prompt
    127  chatPromptPrefix: "",
    128 
    129  // Any chat provider can be used and those that match the URLs in this object
    130  // will allow for additional UI shown such as populating dropdown with a name,
    131  // showing links, and other special behaviors needed for individual providers.
    132  chatProviders: new Map([
    133    [
    134      "https://claude.ai/new",
    135      {
    136        iconUrl: "chrome://browser/content/genai/assets/brands/claude.svg",
    137        id: "claude",
    138        link1:
    139          "https://www.anthropic.com/legal/archive/6370fb23-12ed-41d9-a4a2-28866dee3105",
    140        link2:
    141          "https://www.anthropic.com/legal/archive/7197103a-5e27-4ee4-93b1-f2d4c39ba1e7",
    142        link3:
    143          "https://www.anthropic.com/legal/archive/628feec9-7df9-4d38-bc69-fbf104df47b0",
    144        linksId: "genai-settings-chat-claude-links",
    145        maxLength: 14150,
    146        name: "Anthropic Claude",
    147        supportAutoSubmit: true,
    148        tooltipId: "genai-onboarding-claude-tooltip",
    149      },
    150    ],
    151    [
    152      "https://chatgpt.com",
    153      {
    154        iconUrl: "chrome://browser/content/genai/assets/brands/chatgpt.svg",
    155        id: "chatgpt",
    156        link1: "https://openai.com/terms",
    157        link2: "https://openai.com/privacy",
    158        linksId: "genai-settings-chat-chatgpt-links",
    159        maxLength: 9350,
    160        name: "ChatGPT",
    161        supportAutoSubmit: true,
    162        tooltipId: "genai-onboarding-chatgpt-tooltip",
    163      },
    164    ],
    165    [
    166      "https://copilot.microsoft.com/?form=MOZCMC",
    167      {
    168        iconUrl: "chrome://browser/content/genai/assets/brands/copilot.svg",
    169        id: "copilot",
    170        link1: "https://www.bing.com/new/termsofuse",
    171        link2: "https://go.microsoft.com/fwlink/?LinkId=521839",
    172        linksId: "genai-settings-chat-copilot-links",
    173        maxLength: 3260,
    174        name: "Copilot",
    175        tooltipId: "genai-onboarding-copilot-tooltip",
    176      },
    177    ],
    178    [
    179      "https://gemini.google.com",
    180      {
    181        header: "X-Firefox-Gemini",
    182        iconUrl: "chrome://browser/content/genai/assets/brands/gemini.svg",
    183        id: "gemini",
    184        link1: "https://policies.google.com/terms",
    185        link2: "https://policies.google.com/terms/generative-ai/use-policy",
    186        link3: "https://support.google.com/gemini?p=privacy_notice",
    187        linksId: "genai-settings-chat-gemini-links",
    188        // Max header length is around 55000, but spaces are encoded with %20
    189        // for header instead of + for query parameter
    190        maxLength: 45000,
    191        name: "Google Gemini",
    192        tooltipId: "genai-onboarding-gemini-tooltip",
    193      },
    194    ],
    195    [
    196      "https://huggingface.co/chat",
    197      {
    198        iconUrl: "chrome://browser/content/genai/assets/brands/huggingchat.svg",
    199        id: "huggingchat",
    200        link1: "https://huggingface.co/chat/privacy",
    201        link2: "https://huggingface.co/privacy",
    202        linksId: "genai-settings-chat-huggingchat-links",
    203        maxLength: 8192,
    204        name: "HuggingChat",
    205        tooltipId: "genai-onboarding-huggingchat-tooltip",
    206      },
    207    ],
    208    [
    209      "https://chat.mistral.ai/chat",
    210      {
    211        iconUrl: "chrome://browser/content/genai/assets/brands/lechat.svg",
    212        id: "lechat",
    213        link1: "https://mistral.ai/terms/#terms-of-service-le-chat",
    214        link2: "https://mistral.ai/terms/#privacy-policy",
    215        linksId: "genai-settings-chat-lechat-links",
    216        maxLength: 13350,
    217        name: "Le Chat Mistral",
    218        tooltipId: "genai-onboarding-lechat-tooltip",
    219      },
    220    ],
    221    [
    222      "http://localhost:8080",
    223      {
    224        id: "localhost",
    225        link1: "https://llamafile.ai",
    226        linksId: "genai-settings-chat-localhost-links",
    227        maxLength: 8192,
    228        name: "localhost",
    229      },
    230    ],
    231  ]),
    232 
    233  /**
    234   * Retrieves the current chat provider information based on the
    235   * preference setting
    236   *
    237   * @returns {object} An object containing the current chat provider's
    238   *                   information, such as name, iconUrl, etc. If no
    239   *                   provider is set, returns an empty object.
    240   */
    241  get currentChatProviderInfo() {
    242    return {
    243      iconUrl: "chrome://global/skin/icons/highlights.svg",
    244      ...this.chatProviders.get(lazy.chatProvider),
    245    };
    246  },
    247 
    248  /**
    249   * Determine if chat entrypoints can be shown
    250   *
    251   * @returns {bool} can show
    252   */
    253  get canShowChatEntrypoint() {
    254    return (
    255      lazy.chatEnabled &&
    256      lazy.chatProvider != "" &&
    257      // Chatbot needs to be a tool if new sidebar
    258      (!lazy.sidebarRevamp || lazy.sidebarTools.includes("aichat"))
    259    );
    260  },
    261 
    262  /**
    263   * Handle startup tasks like telemetry, adding listeners.
    264   */
    265  init() {
    266    // Allow other callers to init even though we now automatically init
    267    if (this._initialized) {
    268      return;
    269    }
    270    this._initialized = true;
    271 
    272    // Access getters for side effects of observing pref changes
    273    lazy.chatEnabled;
    274    lazy.chatHideLocalhost;
    275    lazy.chatProvider;
    276    lazy.chatProviders;
    277    lazy.chatShortcuts;
    278    lazy.chatShortcutsIgnoreFields;
    279 
    280    // Apply initial ordering of providers
    281    reorderChatProviders();
    282    updateIgnoredInputs();
    283 
    284    // Handle nimbus feature pref setting
    285    const feature = lazy.NimbusFeatures.chatbot;
    286    feature.onUpdate(() => {
    287      const enrollment = feature.getEnrollmentMetadata();
    288      if (!enrollment) {
    289        return;
    290      }
    291 
    292      // Enforce minimum version by skipping pref changes until Firefox restarts
    293      // with the appropriate version
    294      if (
    295        Services.vc.compare(
    296          // Support betas, e.g., 132.0b1, instead of MOZ_APP_VERSION
    297          AppConstants.MOZ_APP_VERSION_DISPLAY,
    298          // Check configured version or compare with unset handled as 0
    299          feature.getVariable("minVersion")
    300        ) < 0
    301      ) {
    302        return;
    303      }
    304 
    305      // Set prefs on any branch if we have a new enrollment slug, otherwise
    306      // only set default branch as those only last for the session
    307      const slug = enrollment.slug + ":" + enrollment.branch;
    308      const newEnroll = slug != lazy.chatNimbus;
    309      const setPref = ([pref, { branch = "user", value = null }]) => {
    310        if (newEnroll || branch == "default") {
    311          lazy.PrefUtils.setPref("browser.ml.chat." + pref, value, { branch });
    312        }
    313      };
    314      setPref(["nimbus", { value: slug }]);
    315      Object.entries(feature.getVariable("prefs") ?? {}).forEach(setPref);
    316 
    317      // Show sidebar badge on new enrollment
    318      if (feature.getVariable("badgeSidebar") && newEnroll) {
    319        Services.prefs.setBoolPref("sidebar.notification.badge.aichat", true);
    320      }
    321    });
    322 
    323    // Record glean metrics after applying nimbus prefs
    324    Glean.genaiChatbot.badges.set(
    325      Object.entries({
    326        footer: "browser.ml.chat.page.footerBadge",
    327        menu: "browser.ml.chat.page.menuBadge",
    328        sidebar: "sidebar.notification.badge.aichat",
    329      })
    330        .reduce((acc, [key, pref]) => {
    331          if (Services.prefs.getBoolPref(pref)) {
    332            acc.push(key);
    333          }
    334          return acc;
    335        }, [])
    336        .join(",")
    337    );
    338    Glean.genaiChatbot.enabled.set(lazy.chatEnabled);
    339    Glean.genaiChatbot.menu.set(lazy.chatMenu);
    340    Glean.genaiChatbot.page.set(lazy.chatPage);
    341    Glean.genaiChatbot.provider.set(this.getProviderId());
    342    Glean.genaiChatbot.shortcuts.set(lazy.chatShortcuts);
    343    Glean.genaiChatbot.shortcutsCustom.set(lazy.chatShortcutsCustom);
    344    Glean.genaiChatbot.sidebar.set(lazy.chatSidebar);
    345  },
    346 
    347  /**
    348   * Convert provider to id.
    349   *
    350   * @param {string} provider url defaulting to current pref
    351   * @returns {string} id or custom or none
    352   */
    353  getProviderId(provider = lazy.chatProvider) {
    354    const { id } = this.chatProviders.get(provider) ?? {};
    355    return id ?? (provider ? "custom" : "none");
    356  },
    357 
    358  /**
    359   * Add chat items to menu or popup.
    360   *
    361   * @param {MozBrowser} browser providing context
    362   * @param {object} extraContext e.g., selection text
    363   * @param {Function} itemAdder creates and returns the item
    364   * @param {string} entry name
    365   * @param {Function} cleanup optional on item activation
    366   * @returns {object} context used for selecting prompts
    367   */
    368  async addAskChatItems(browser, extraContext, itemAdder, entry, cleanup) {
    369    // Prepare context used for both targeting and handling prompts
    370    const window = browser.ownerGlobal;
    371    const tab = window?.gBrowser?.getTabForBrowser(browser);
    372    const uri = browser.currentURI;
    373    const context = {
    374      ...extraContext,
    375      entry,
    376      provider: lazy.chatProvider,
    377      tabTitle: (tab?._labelIsContentTitle && tab?.label) || "",
    378      url: uri?.asciiHost + uri?.filePath,
    379      window,
    380    };
    381 
    382    // Add items that pass along context for handling
    383    (await this.getContextualPrompts(context)).forEach(promptObj => {
    384      const item = itemAdder(promptObj, context);
    385      item?.addEventListener("command", () => {
    386        this.handleAskChat(promptObj, context);
    387        cleanup?.(item);
    388      });
    389    });
    390 
    391    return context;
    392  },
    393 
    394  /**
    395   * Setup helpers and callbacks for ai shortcut button.
    396   *
    397   * @param {MozButton} aiActionButton instance for the browser window
    398   */
    399  initializeAIShortcut(aiActionButton) {
    400    if (aiActionButton.initialized) {
    401      return;
    402    }
    403    aiActionButton.initialized = true;
    404 
    405    const setAIButtonAriaLabel = (chatProviderName = "localhost") => {
    406      document.l10n.setAttributes(aiActionButton, "genai-shortcut-button", {
    407        provider: chatProviderName,
    408      });
    409    };
    410 
    411    const document = aiActionButton.ownerDocument;
    412    const initialChatProvider = this.chatProviders.get(lazy.chatProvider);
    413    setAIButtonAriaLabel(initialChatProvider?.name);
    414    const buttonActiveState = "icon";
    415    const buttonDefaultState = "icon ghost";
    416    const chatShortcutsOptionsPanel = document.getElementById(
    417      "chat-shortcuts-options-panel"
    418    );
    419    const selectionShortcutActionPanel = document.getElementById(
    420      "selection-shortcut-action-panel"
    421    );
    422    aiActionButton.hide = () => {
    423      chatShortcutsOptionsPanel.hidePopup();
    424      selectionShortcutActionPanel.hidePopup();
    425    };
    426    aiActionButton.iconSrc = "chrome://global/skin/icons/highlights.svg";
    427    aiActionButton.setAttribute("type", buttonDefaultState);
    428    chatShortcutsOptionsPanel.addEventListener("popuphidden", () =>
    429      aiActionButton.setAttribute("type", buttonDefaultState)
    430    );
    431    chatShortcutsOptionsPanel.firstChild.id = "ask-chat-shortcuts";
    432 
    433    // Helper to show rounded warning numbers
    434    const roundDownToNearestHundred = number => {
    435      return Math.floor(number / 100) * 100;
    436    };
    437 
    438    /**
    439     * Create a warning message bar.
    440     *
    441     * @param {{
    442     *   name: string,
    443     *   maxLength: number,
    444     * }} chatProvider attributes for the warning
    445     * @returns { mozMessageBarEl } MozMessageBar warning message bar
    446     */
    447    const createMessageBarWarning = chatProvider => {
    448      const mozMessageBarEl = this.createWarningEl(
    449        document,
    450        "ask-chat-shortcut-warning",
    451        null
    452      );
    453 
    454      // If provider is not defined, use generic warning message
    455      const translationId = chatProvider?.name
    456        ? "genai-shortcuts-selected-warning"
    457        : "genai-shortcuts-selected-warning-generic";
    458 
    459      document.l10n.setAttributes(mozMessageBarEl, translationId, {
    460        provider: chatProvider?.name,
    461        maxLength: roundDownToNearestHundred(
    462          this.estimateSelectionLimit(chatProvider?.maxLength)
    463        ),
    464        selectionLength: roundDownToNearestHundred(
    465          aiActionButton.data.selection.length
    466        ),
    467      });
    468 
    469      return mozMessageBarEl;
    470    };
    471 
    472    // build the ask popup
    473    const buildPopup = async () => {
    474      aiActionButton.setAttribute("type", buttonActiveState);
    475      const vbox = chatShortcutsOptionsPanel.querySelector("vbox");
    476      vbox.innerHTML = "";
    477 
    478      const showWarning = this.isContextTooLong(aiActionButton.data.selection);
    479      const chatProvider = this.chatProviders.get(lazy.chatProvider);
    480 
    481      if (initialChatProvider !== chatProvider?.name) {
    482        setAIButtonAriaLabel(chatProvider?.name);
    483      }
    484 
    485      // Show warning if selection is too long
    486      if (showWarning) {
    487        vbox.appendChild(createMessageBarWarning(chatProvider));
    488      }
    489 
    490      const addItem = () => {
    491        const button = vbox.appendChild(
    492          document.createXULElement("toolbarbutton")
    493        );
    494        button.className = "subviewbutton";
    495        button.setAttribute("tabindex", "0");
    496        return button;
    497      };
    498 
    499      const browser = document.ownerGlobal.gBrowser.selectedBrowser;
    500      const context = await this.addAskChatItems(
    501        browser,
    502        aiActionButton.data,
    503        promptObj => {
    504          const button = addItem();
    505          button.textContent = promptObj.label;
    506          return button;
    507        },
    508        "shortcuts",
    509        aiActionButton.hide
    510      );
    511 
    512      // Add custom textarea box if configured
    513      if (lazy.chatShortcutsCustom) {
    514        const textAreaEl = vbox.appendChild(document.createElement("textarea"));
    515        document.l10n.setAttributes(
    516          textAreaEl,
    517          chatProvider?.name
    518            ? "genai-input-ask-provider"
    519            : "genai-input-ask-generic",
    520          { provider: chatProvider?.name }
    521        );
    522 
    523        textAreaEl.className = "ask-chat-shortcuts-custom-prompt";
    524        textAreaEl.addEventListener("mouseover", () => textAreaEl.focus());
    525        textAreaEl.addEventListener("keydown", event => {
    526          if (event.key == "Enter" && !event.shiftKey) {
    527            this.handleAskChat({ value: textAreaEl.value }, context);
    528            aiActionButton.hide();
    529          }
    530        });
    531 
    532        // For Content Analysis, we need to specify the URL that the data is being sent to.
    533        // In this case it's not the URL in the browsingContext (like it is in other cases),
    534        // but the URL of the chatProvider is close enough to where the content will eventually
    535        // be sent.
    536        lazy.ContentAnalysisUtils.setupContentAnalysisEventsForTextElement(
    537          textAreaEl,
    538          browser.browsingContext,
    539          Services.io.newURI(lazy.chatProvider)
    540        );
    541 
    542        const resetHeight = () => {
    543          textAreaEl.style.height = "auto";
    544          textAreaEl.style.height = textAreaEl.scrollHeight + "px";
    545        };
    546 
    547        textAreaEl.addEventListener("input", resetHeight);
    548        chatShortcutsOptionsPanel.addEventListener("popupshown", resetHeight, {
    549          once: true,
    550        });
    551      }
    552 
    553      // Allow hiding these shortcuts
    554      vbox.appendChild(document.createXULElement("toolbarseparator"));
    555      const hider = addItem();
    556      document.l10n.setAttributes(hider, "genai-shortcuts-hide");
    557      hider.addEventListener("command", () => {
    558        Services.prefs.setBoolPref("browser.ml.chat.shortcuts", false);
    559        Glean.genaiChatbot.shortcutsHideClick.record({
    560          selection: aiActionButton.data.selection.length,
    561        });
    562      });
    563 
    564      chatShortcutsOptionsPanel.openPopup(
    565        selectionShortcutActionPanel,
    566        "after_start",
    567        0,
    568        10
    569      );
    570      Glean.genaiChatbot.shortcutsExpanded.record({
    571        selection: aiActionButton.data.selection.length,
    572        provider: this.getProviderId(),
    573        warning: showWarning,
    574      });
    575    };
    576 
    577    // ask popup shows on mouseover only in the first two times
    578    const hasMouseoverOnPopup = () => {
    579      const mouseoverCounter = lazy.shortcutMouseoverCount;
    580      const maxMouseoverCount = 2;
    581 
    582      if (mouseoverCounter >= maxMouseoverCount) {
    583        return;
    584      }
    585 
    586      if (chatShortcutsOptionsPanel.state == "closed") {
    587        Services.prefs.setIntPref(
    588          "browser.ml.chat.shortcut.onboardingMouseoverCount",
    589          mouseoverCounter + 1
    590        );
    591        buildPopup();
    592      }
    593    };
    594 
    595    aiActionButton.addEventListener("mouseover", hasMouseoverOnPopup);
    596 
    597    // Detect click to build and toggle the popup
    598    aiActionButton.addEventListener("click", async () => {
    599      if (chatShortcutsOptionsPanel.state != "closed") {
    600        chatShortcutsOptionsPanel.hidePopup();
    601        return;
    602      }
    603      buildPopup();
    604    });
    605  },
    606 
    607  /**
    608   * Handle messages from content to show or hide shortcuts.
    609   *
    610   * @param {string} name of message
    611   * @param {{
    612   *   inputType: string,
    613   *   selection: string,
    614   *   delay: number,
    615   *   x: number,
    616   *   y: number,
    617   * }} data for the message
    618   * @param {MozBrowser} browser that provided the message
    619   */
    620  handleShortcutsMessage(name, data, browser) {
    621    const isInBrowserStack = browser?.closest(".browserStack");
    622 
    623    if (
    624      !isInBrowserStack ||
    625      !browser ||
    626      this.ignoredInputs.has(data.inputType) ||
    627      !lazy.chatShortcuts ||
    628      !this.canShowChatEntrypoint
    629    ) {
    630      return;
    631    }
    632 
    633    const window = browser.ownerGlobal;
    634    const { document, devicePixelRatio } = window;
    635    const aiActionButton = document.getElementById("ai-action-button");
    636    this.initializeAIShortcut(aiActionButton);
    637 
    638    switch (name) {
    639      case "GenAI:HideShortcuts":
    640        aiActionButton.hide();
    641        break;
    642      case "GenAI:ShowShortcuts": {
    643        // Save the latest selection so it can be used by popup
    644        aiActionButton.data = data;
    645 
    646        Glean.genaiChatbot.shortcutsDisplayed.record({
    647          delay: data.delay,
    648          inputType: data.inputType,
    649          selection: data.selection.length,
    650        });
    651 
    652        // Position the shortcuts relative to the browser's top-left corner
    653        const screenYBase = data.screenYDevPx / devicePixelRatio;
    654        const safeSpace = window.outerHeight - 40;
    655        // Remove padding if the popup would be offscreen
    656        const bottomPadding = screenYBase > safeSpace ? 0 : 40;
    657        const screenX = data.screenXDevPx / devicePixelRatio;
    658        const screenY = screenYBase + bottomPadding;
    659 
    660        aiActionButton
    661          .closest("panel")
    662          .openPopup(
    663            browser,
    664            "before_start",
    665            screenX - browser.screenX,
    666            screenY - browser.screenY
    667          );
    668        break;
    669      }
    670    }
    671  },
    672 
    673  /**
    674   * Determine whether a warning should be shown depending on provider max length
    675   *
    676   * @param {string} selection selected text from context
    677   */
    678  isContextTooLong(selection) {
    679    const chatProvider = this.chatProviders.get(lazy.chatProvider);
    680    const selectionLength = selection.length;
    681 
    682    return (
    683      this.estimateSelectionLimit(chatProvider?.maxLength) < selectionLength
    684    );
    685  },
    686 
    687  /**
    688   * Create <moz-message-bar> warning element
    689   *
    690   * @param {Document} document the element
    691   * @param {string | null} className css class to apply
    692   * @param {string | null} dismissable attribute setting
    693   */
    694  createWarningEl(document, className, dismissable) {
    695    const mozMessageBarEl = document.createElement("moz-message-bar");
    696 
    697    mozMessageBarEl.dataset.l10nAttrs = "heading,message";
    698    mozMessageBarEl.setAttribute("type", "warning");
    699    if (dismissable) {
    700      mozMessageBarEl.setAttribute("dismissable", true);
    701    }
    702 
    703    if (className) {
    704      mozMessageBarEl.className = className;
    705    }
    706 
    707    return mozMessageBarEl;
    708  },
    709 
    710  /**
    711   * Build prompts menu to ask chat for context menu.
    712   *
    713   * @param {MozMenu} menu element to update
    714   * @param {object} contextMenu object containing utility and states for building the context menu
    715   */
    716  async buildAskChatMenu(menu, contextMenu) {
    717    const {
    718      browser,
    719      selectionInfo,
    720      showItem = this.showItem,
    721      source,
    722      contextTabs = null,
    723    } = contextMenu;
    724 
    725    // DO NOT show menu when inside an extension panel
    726    const uri = browser.browsingContext?.currentURI.spec;
    727    if (uri?.startsWith("moz-extension:")) {
    728      showItem(menu, false);
    729      return;
    730    }
    731 
    732    // Page feature can be shown without provider unless disabled via menu
    733    // or revamp sidebar excludes chatbot
    734    const isPageFeatureAllowed =
    735      lazy.chatPage &&
    736      (lazy.chatProvider != "" || lazy.chatMenu) &&
    737      (!lazy.sidebarRevamp || lazy.sidebarTools.includes("aichat"));
    738 
    739    let canShow = false;
    740    switch (source) {
    741      case "page":
    742        canShow = this.canShowChatEntrypoint || isPageFeatureAllowed;
    743        break;
    744      case "tab":
    745        canShow = isPageFeatureAllowed && contextTabs?.length === 1;
    746        break;
    747      case "tool":
    748        canShow = lazy.chatPage;
    749        break;
    750    }
    751    if (!canShow) {
    752      showItem(menu, false);
    753      return;
    754    }
    755 
    756    const provider = this.chatProviders.get(lazy.chatProvider)?.name;
    757    const doc = menu.ownerDocument;
    758 
    759    // Only "page" and "tab" contexts need a <menu> submenu
    760    if (source !== "tool") {
    761      if (provider) {
    762        doc.l10n.setAttributes(menu, "genai-menu-ask-provider-2", { provider });
    763      } else {
    764        doc.l10n.setAttributes(
    765          menu,
    766          lazy.chatProvider
    767            ? "genai-menu-ask-generic-2"
    768            : "genai-menu-no-provider-2"
    769        );
    770      }
    771      menu.menupopup?.remove();
    772    }
    773 
    774    // NOTE: Show the menu item synchronously, before any `await`.
    775    showItem(menu, true);
    776 
    777    // Determine if we have selection or should use page content
    778    const context = {
    779      contentType: "selection",
    780      selection: selectionInfo?.fullText ?? "",
    781    };
    782    if (lazy.chatPage && !context.selection) {
    783      // Get page content for prompts when no selection
    784      await this.addPageContext(browser, context);
    785    }
    786    const addItem = () =>
    787      source === "tool"
    788        ? menu.appendChild(doc.createXULElement("menuitem"))
    789        : menu.appendItem("");
    790    await this.addAskChatItems(
    791      browser,
    792      context,
    793      promptObj => {
    794        const { contentType, selection } = context;
    795        const item = addItem();
    796        item.setAttribute("label", promptObj.label);
    797 
    798        // Disabled menu if page is invalid
    799        if (contentType === "page" && !selection) {
    800          item.disabled = true;
    801        }
    802        if (promptObj.badge && lazy.chatPageMenuBadge) {
    803          item.setAttribute("badge", promptObj.badge);
    804        }
    805 
    806        return item;
    807      },
    808      source,
    809      item => {
    810        // Currently only summarize page shows a badge, so remove when clicked
    811        if (item.hasAttribute("badge")) {
    812          Services.prefs.setBoolPref("browser.ml.chat.page.menuBadge", false);
    813        }
    814      }
    815    );
    816 
    817    // For page which currently only shows 1 prompt, make it less empty with an
    818    // Open or Choose options depending on provider
    819    if (context.contentType == "page") {
    820      const openItem = addItem();
    821      if (provider) {
    822        doc.l10n.setAttributes(openItem, "genai-menu-open-provider", {
    823          provider,
    824        });
    825      } else {
    826        doc.l10n.setAttributes(
    827          openItem,
    828          lazy.chatProvider
    829            ? "genai-menu-open-generic"
    830            : "genai-menu-choose-chatbot"
    831        );
    832      }
    833      openItem.addEventListener("command", () => {
    834        const window = browser.ownerGlobal;
    835        window.SidebarController.show("viewGenaiChatSidebar");
    836        Glean.genaiChatbot.contextmenuChoose.record({
    837          provider: this.getProviderId(),
    838        });
    839      });
    840    }
    841 
    842    // Add remove provider option
    843    const popup = source === "tool" ? menu : menu.menupopup;
    844    popup.appendChild(doc.createXULElement("menuseparator"));
    845    const removeItem = addItem();
    846    doc.l10n.setAttributes(
    847      removeItem,
    848      provider ? "genai-menu-remove-provider" : "genai-menu-remove-generic",
    849      { provider }
    850    );
    851    removeItem.addEventListener("command", () => {
    852      Glean.genaiChatbot.contextmenuRemove.record({
    853        provider: this.getProviderId(),
    854      });
    855      if (lazy.chatProvider) {
    856        Services.prefs.clearUserPref("browser.ml.chat.provider");
    857      } else if (source === "tool") {
    858        // When there's no provider set this menu should remove chatbot as a tool
    859        lazy.SidebarManager.updateToolsPref("aichat", true);
    860      } else {
    861        Services.prefs.setBoolPref("browser.ml.chat.menu", false);
    862      }
    863    });
    864  },
    865 
    866  /**
    867   *
    868   * Build the buildAskChatMenu item for the tab context menu
    869   *
    870   * @param {MozMenu} menu the tab menu to update
    871   * @param {object} tabContextMenu the tab context menu instance
    872   * @returns {promise} resolve when the menu item is configured
    873   */
    874  async buildTabMenu(menu, tabContextMenu) {
    875    const { contextTab, contextTabs } = tabContextMenu;
    876 
    877    const browser = contextTab?.linkedBrowser;
    878    await this.buildAskChatMenu(menu, {
    879      browser,
    880      selectionInfo: null,
    881      showItem: (item, shouldShow) => {
    882        const separator = item.nextElementSibling;
    883        this.showItem(item, shouldShow);
    884 
    885        if (separator && separator.localName === "menuseparator") {
    886          this.showItem(separator, shouldShow);
    887        }
    888      },
    889      source: "tab",
    890      contextTabs,
    891    });
    892  },
    893 
    894  /**
    895   * Toggle the visibility of the chatbot menu item
    896   *
    897   * @param {MozMenu} item the chatbot menu item element
    898   * @param {boolean} shouldShow whether to show or hide the item
    899   */
    900  showItem(item, shouldShow) {
    901    item.hidden = !shouldShow;
    902  },
    903 
    904  /**
    905   * Get prompts from prefs evaluated with context
    906   *
    907   * @param {object} context data used for targeting
    908   * @returns {promise} array of matching prompt objects
    909   */
    910  async getContextualPrompts(context) {
    911    // Treat prompt objects as messages to reuse targeting capabilities
    912    const messages = [];
    913    const toFormat = [];
    914    Services.prefs.getChildList("browser.ml.chat.prompts.").forEach(pref => {
    915      try {
    916        const promptObj = {
    917          label: Services.prefs.getStringPref(pref),
    918          targeting: "true",
    919          value: "",
    920        };
    921        try {
    922          // Prompts can be JSON with label, value, targeting and other keys
    923          Object.assign(promptObj, JSON.parse(promptObj.label));
    924 
    925          // Ignore provided id (if any) for modified prefs
    926          if (Services.prefs.prefHasUserValue(pref)) {
    927            promptObj.id = null;
    928          }
    929        } catch (ex) {}
    930        messages.push(promptObj);
    931        if (promptObj.l10nId) {
    932          toFormat.push(promptObj);
    933        }
    934      } catch (ex) {
    935        console.error("Failed to get prompt pref " + pref, ex);
    936      }
    937    });
    938 
    939    // Apply localized attributes for prompts
    940    (await lazy.l10n.formatMessages(toFormat.map(obj => obj.l10nId))).forEach(
    941      (msg, idx) =>
    942        msg?.attributes.forEach(attr => (toFormat[idx][attr.name] = attr.value))
    943    );
    944 
    945    // Specially handle page summarization prompt
    946    if (context.contentType == "page") {
    947      for (const promptObj of toFormat) {
    948        if (promptObj.id == "summarize") {
    949          const [badge, label] = await lazy.l10n.formatValues([
    950            "genai-menu-new-badge",
    951            "genai-menu-summarize-page",
    952          ]);
    953          promptObj.badge = badge;
    954          promptObj.label = label;
    955        }
    956      }
    957    }
    958 
    959    return lazy.ASRouterTargeting.findMatchingMessage({
    960      messages,
    961      returnAll: true,
    962      trigger: { context },
    963    });
    964  },
    965 
    966  /**
    967   * Approximately adjust query limit for encoding and other text in prompt,
    968   * e.g., page title, per-prompt instructions. Generally more conservative as
    969   * going over the limit results in server errors.
    970   *
    971   * @param {number} maxLength optional of the provider request URI
    972   * @returns {number} adjusted length estimate
    973   */
    974  estimateSelectionLimit(maxLength = lazy.chatMaxLength) {
    975    // Could try to be smarter including the selected text with URI encoding,
    976    // base URI length, other parts of the prompt (especially for custom)
    977    return Math.round(maxLength * 0.85) - 500;
    978  },
    979 
    980  /**
    981   * Updates chat prompt prefix.
    982   */
    983  async prepareChatPromptPrefix() {
    984    if (
    985      !this.chatPromptPrefix ||
    986      this.chatLastPrefix != lazy.chatPromptPrefix
    987    ) {
    988      try {
    989        // Check json for localized prefix
    990        const prefixObj = JSON.parse(lazy.chatPromptPrefix);
    991        this.chatPromptPrefix = (
    992          await lazy.l10n.formatMessages([
    993            {
    994              id: prefixObj.l10nId,
    995              args: {
    996                selection: `%selection|${this.estimateSelectionLimit(
    997                  this.chatProviders.get(lazy.chatProvider)?.maxLength
    998                )}%`,
    999                tabTitle: "%tabTitle|50%",
   1000                url: "%url%",
   1001              },
   1002            },
   1003          ])
   1004        )[0].value;
   1005      } catch (ex) {
   1006        // Treat as plain text prefix
   1007        this.chatPromptPrefix = lazy.chatPromptPrefix;
   1008      }
   1009      if (this.chatPromptPrefix) {
   1010        this.chatPromptPrefix += "\n\n";
   1011      }
   1012      this.chatLastPrefix = lazy.chatPromptPrefix;
   1013    }
   1014  },
   1015 
   1016  /**
   1017   * Build a prompt with context.
   1018   *
   1019   * @param {MozMenuItem} item Use value falling back to label
   1020   * @param {object} context Placeholder keys with values to replace
   1021   * @param {Document} document Document for sanitizing context values
   1022   * @returns {string} Prompt with placeholders replaced
   1023   */
   1024  buildChatPrompt(item, context = {}, document = null) {
   1025    // Combine prompt prefix with the item then replace placeholders from the
   1026    // original prompt (and not from context)
   1027    return (this.chatPromptPrefix + (item.value || item.label)).replace(
   1028      // Handle %placeholder% as key|options
   1029      /\%(\w+)(?:\|([^%]+))?\%/g,
   1030      (placeholder, key, options) => {
   1031        // Currently only supporting numeric options for slice with `undefined`
   1032        // resulting in whole string. Also remove fake int tags from untrusted content.
   1033        const value = context[key];
   1034        let sanitized;
   1035 
   1036        // Sanitize and truncate context values before sending prompt
   1037        // otherwise return placeholder
   1038        if (value !== undefined) {
   1039          const contextElement = document.createElement("div");
   1040          sanitized = lazy.parserUtils.parseFragment(
   1041            value,
   1042            Ci.nsIParserUtils.SanitizerDropForms |
   1043              Ci.nsIParserUtils.SanitizerDropMedia,
   1044            false,
   1045            Services.io.newURI("about:blank"),
   1046            contextElement
   1047          ).textContent;
   1048 
   1049          if (options) {
   1050            sanitized = sanitized.slice(0, Number(options));
   1051          }
   1052 
   1053          sanitized = sanitized
   1054            .replace(/&/g, "&amp;")
   1055            .replace(/</g, "&lt;")
   1056            .replace(/>/g, "&gt;")
   1057            .replace(/"/g, "&quot;")
   1058            .replace(/'/g, "&#39;");
   1059        } else {
   1060          sanitized = placeholder;
   1061        }
   1062 
   1063        return `<${key}>${sanitized}</${key}>`;
   1064      }
   1065    );
   1066  },
   1067 
   1068  /**
   1069   * Update context with page content.
   1070   *
   1071   * @param {MozBrowser} browser for the tab to get content
   1072   * @param {object} context optional existing context to update
   1073   * @returns {object} updated context
   1074   */
   1075  async addPageContext(browser, context = {}) {
   1076    context.contentType = "page";
   1077    try {
   1078      Object.assign(
   1079        context,
   1080        await browser?.browsingContext?.currentWindowContext
   1081          .getActor("GenAI")
   1082          .sendQuery("GetReadableText")
   1083      );
   1084    } catch (ex) {
   1085      console.warn("Failed to get page content", ex);
   1086    }
   1087    return context;
   1088  },
   1089 
   1090  /**
   1091   * Summarize the current page content.
   1092   *
   1093   * @param {Window} window chrome window with tabs
   1094   * @param {string} entry name
   1095   */
   1096  async summarizeCurrentPage(window, entry) {
   1097    const browser = window.gBrowser.selectedBrowser;
   1098    await this.addAskChatItems(
   1099      browser,
   1100      await this.addPageContext(browser),
   1101      (promptObj, context) => {
   1102        if (promptObj.id === "summarize") {
   1103          this.handleAskChat(promptObj, context);
   1104        }
   1105      },
   1106      entry
   1107    );
   1108  },
   1109 
   1110  /**
   1111   * Set up automatic prompt submission for ChatGPT and Claude
   1112   *
   1113   * @param {Browser} browser - current browser
   1114   * @param {string} prompt - prompt text
   1115   * @param {object} context of how the prompt should be handled
   1116   */
   1117  setupAutoSubmit(browser, prompt, context) {
   1118    const sendAutoSubmit = (br, promptText) => {
   1119      const wgp = br.browsingContext?.currentWindowGlobal;
   1120      const actor = wgp?.getActor("GenAI");
   1121      if (!actor) {
   1122        return;
   1123      }
   1124 
   1125      try {
   1126        actor.sendAsyncMessage("AutoSubmit", {
   1127          promptText,
   1128        });
   1129      } catch (e) {
   1130        console.error("error message: ", e);
   1131      }
   1132    };
   1133 
   1134    if (lazy.chatSidebar) {
   1135      const injector = {
   1136        async onStateChange(_wp, _req, flags) {
   1137          const stopDoc =
   1138            flags & Ci.nsIWebProgressListener.STATE_STOP &&
   1139            flags & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
   1140          if (!stopDoc) {
   1141            return;
   1142          }
   1143 
   1144          const wgp = browser.browsingContext?.currentWindowGlobal;
   1145          if (!wgp || wgp.isInitialDocument) {
   1146            return;
   1147          }
   1148 
   1149          try {
   1150            browser.webProgress?.removeProgressListener(injector);
   1151          } catch {}
   1152          await sendAutoSubmit(browser, prompt);
   1153        },
   1154        QueryInterface: ChromeUtils.generateQI([
   1155          "nsIWebProgressListener",
   1156          "nsISupportsWeakReference",
   1157        ]),
   1158      };
   1159 
   1160      browser.webProgress?.addProgressListener(
   1161        injector,
   1162        Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT
   1163      );
   1164    } else {
   1165      // Tab mode:
   1166      const gBrowser = context.window.gBrowser;
   1167      const targetBrowser = browser;
   1168 
   1169      const tabListener = {
   1170        async onLocationChange(br, _wp, _req, location) {
   1171          if (br !== targetBrowser) {
   1172            return;
   1173          }
   1174 
   1175          const spec = location?.spec || "";
   1176          if (spec === "about:blank") {
   1177            return;
   1178          }
   1179 
   1180          try {
   1181            gBrowser.removeTabsProgressListener(tabListener);
   1182          } catch {}
   1183          await sendAutoSubmit(browser, prompt);
   1184        },
   1185        QueryInterface: ChromeUtils.generateQI([
   1186          "nsIwebProgressListener",
   1187          "nsISupportsWeakReference",
   1188        ]),
   1189      };
   1190 
   1191      gBrowser.addTabsProgressListener(tabListener);
   1192    }
   1193  },
   1194 
   1195  /**
   1196   * Handle selected prompt by opening tab or sidebar.
   1197   *
   1198   * @param {object} promptObj to convert to string
   1199   * @param {object} context of how the prompt should be handled
   1200   */
   1201  async handleAskChat(promptObj, context) {
   1202    // Record up to 3 types of event telemetry for backwards compatibility
   1203    const isPageSummarizeRequest =
   1204      promptObj.id == "summarize" && context.contentType == "page";
   1205    if (isPageSummarizeRequest) {
   1206      Glean.genaiChatbot.summarizePage.record({
   1207        provider: this.getProviderId(),
   1208        reader_mode: context.readerMode,
   1209        selection: context.selection?.length ?? 0,
   1210        source: context.entry,
   1211      });
   1212    }
   1213    if (["page", "shortcuts"].includes(context.entry)) {
   1214      Glean.genaiChatbot[
   1215        context.entry == "page"
   1216          ? "contextmenuPromptClick"
   1217          : "shortcutsPromptClick"
   1218      ].record({
   1219        prompt: promptObj.id ?? "custom",
   1220        provider: this.getProviderId(),
   1221        selection: context.selection?.length ?? 0,
   1222      });
   1223    }
   1224    Glean.genaiChatbot.promptClick.record({
   1225      content_type: context.contentType,
   1226      prompt: promptObj.id ?? "custom",
   1227      provider: this.getProviderId(),
   1228      reader_mode: context.readerMode,
   1229      selection: context.selection?.length ?? 0,
   1230      source: context.entry,
   1231    });
   1232 
   1233    // If no provider is configured, open sidebar and wait once for onboarding
   1234    const { SidebarController } = context.window;
   1235 
   1236    if (!lazy.chatProvider) {
   1237      await SidebarController.show("viewGenaiChatSidebar");
   1238      await SidebarController.browser.contentWindow.onboardingPromise;
   1239      if (!lazy.chatProvider) {
   1240        return;
   1241      }
   1242    }
   1243 
   1244    // Build prompt after provider is confirmed to use correct length limits
   1245    await this.prepareChatPromptPrefix();
   1246    const prompt = this.buildChatPrompt(
   1247      promptObj,
   1248      {
   1249        ...context,
   1250      },
   1251      context.window.document
   1252    );
   1253 
   1254    // Pass the prompt via GET url ?q= param or request header
   1255    const {
   1256      header,
   1257      queryParam = "q",
   1258      supportAutoSubmit,
   1259    } = this.chatProviders.get(lazy.chatProvider) ?? {};
   1260    const url = new URL(lazy.chatProvider);
   1261    const options = {
   1262      inBackground: false,
   1263      relatedToCurrent: true,
   1264      triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
   1265        {}
   1266      ),
   1267    };
   1268 
   1269    if (header) {
   1270      options.headers = Cc[
   1271        "@mozilla.org/io/string-input-stream;1"
   1272      ].createInstance(Ci.nsIStringInputStream);
   1273      options.headers.setByteStringData(
   1274        `${header}: ${encodeURIComponent(prompt)}\r\n`
   1275      );
   1276    } else {
   1277      url.searchParams.set(queryParam, prompt);
   1278    }
   1279 
   1280    // Get the desired browser to handle the prompt url request
   1281    let browser;
   1282    if (lazy.chatSidebar) {
   1283      await SidebarController.show("viewGenaiChatSidebar");
   1284      browser = await SidebarController.browser.contentWindow.browserPromise;
   1285      if (!browser) {
   1286        console.error("Failed to get chat sidebar browser");
   1287        return;
   1288      }
   1289      const showWarning =
   1290        isPageSummarizeRequest && this.isContextTooLong(context.selection);
   1291 
   1292      await SidebarController.browser.contentWindow.onNewPrompt({
   1293        show: showWarning,
   1294        ...(showWarning
   1295          ? { contextLength: context.selection?.length ?? 0 }
   1296          : {}),
   1297      });
   1298    } else {
   1299      browser = context.window.gBrowser.addTab("", options).linkedBrowser;
   1300    }
   1301    browser.fixupAndLoadURIString(url, options);
   1302 
   1303    // Run autosubmit only for chatGPT, Claude, or mochitest
   1304    if (
   1305      supportAutoSubmit ||
   1306      lazy.chatProvider?.includes("file_chat-autosubmit.html")
   1307    ) {
   1308      this.setupAutoSubmit(browser, prompt, context);
   1309    }
   1310  },
   1311 };
   1312 
   1313 /**
   1314 * Ensure the chat sidebar get closed.
   1315 *
   1316 * @param {bool} value New pref value
   1317 */
   1318 function onChatEnabledChange(value) {
   1319  if (!value) {
   1320    lazy.EveryWindow.readyWindows.forEach(({ SidebarController }) => {
   1321      if (
   1322        SidebarController.isOpen &&
   1323        SidebarController.currentID == "viewGenaiChatSidebar"
   1324      ) {
   1325        SidebarController.hide();
   1326      }
   1327    });
   1328  }
   1329 }
   1330 
   1331 /**
   1332 * Ensure the chat sidebar is shown to reflect changed provider.
   1333 *
   1334 * @param {string} value New pref value
   1335 */
   1336 function onChatProviderChange(value) {
   1337  if (value && lazy.chatEnabled && lazy.chatOpenSidebarOnProviderChange) {
   1338    Services.wm
   1339      .getMostRecentWindow("navigator:browser")
   1340      ?.SidebarController.show("viewGenaiChatSidebar");
   1341  }
   1342 
   1343  // Recalculate query limit on provider change
   1344  GenAI.chatLastPrefix = null;
   1345 
   1346  // Refreshes the sidebar icon and label for all open windows
   1347  lazy.EveryWindow.readyWindows.forEach(window => {
   1348    window.SidebarController.addOrUpdateExtension("viewGenaiChatSidebar", {});
   1349  });
   1350 }
   1351 
   1352 /**
   1353 * Ensure the chat shortcuts get hidden.
   1354 *
   1355 * @param {bool} value New pref value
   1356 */
   1357 function onChatShortcutsChange(value) {
   1358  if (!value) {
   1359    lazy.EveryWindow.readyWindows.forEach(window => {
   1360      const selectionShortcutActionPanel = window.document.getElementById(
   1361        "selection-shortcut-action-panel"
   1362      );
   1363 
   1364      selectionShortcutActionPanel.hidePopup();
   1365    });
   1366  }
   1367 }
   1368 
   1369 /**
   1370 * Update the ordering of chat providers Map.
   1371 */
   1372 function reorderChatProviders() {
   1373  // Figure out which providers to include in order
   1374  const ordered = lazy.chatProviders.split(",");
   1375  if (!lazy.chatHideLocalhost) {
   1376    ordered.push("localhost");
   1377  }
   1378 
   1379  // Convert the url keys to lookup by id
   1380  const idToKey = new Map([...GenAI.chatProviders].map(([k, v]) => [v.id, k]));
   1381 
   1382  // Remove providers in the desired order and make them shown
   1383  const toSet = [];
   1384  ordered.forEach(id => {
   1385    const key = idToKey.get(id);
   1386    const val = GenAI.chatProviders.get(key);
   1387    if (val) {
   1388      val.hidden = false;
   1389      toSet.push([key, val]);
   1390      GenAI.chatProviders.delete(key);
   1391    }
   1392  });
   1393 
   1394  // Hide unremoved providers before re-adding visible ones in order
   1395  GenAI.chatProviders.forEach(val => (val.hidden = true));
   1396  toSet.forEach(args => GenAI.chatProviders.set(...args));
   1397 }
   1398 
   1399 /**
   1400 * Update ignored input fields Set.
   1401 */
   1402 function updateIgnoredInputs() {
   1403  GenAI.ignoredInputs = new Set(
   1404    // Skip empty string as no input type is ""
   1405    lazy.chatShortcutsIgnoreFields.split(",").filter(v => v)
   1406  );
   1407 }
   1408 
   1409 // Initialize on first import
   1410 GenAI.init();