# HG changeset patch
# User Matt A. Tobin <email@mattatobin.com>
# Date 1722343300 18000
#      Tue Jul 30 07:41:40 2024 -0500
# Node ID 04b7b3fda5f991ce0913f3391a57d9839f946fe8
# Parent  56357ac123e4f2989867d6fa59e18f93215efa08
No Bug - Adapt a new component so that SeaMonkey can run.

diff --git a/suite/branding/seamonkey/locales/en-US/brand.ftl b/suite/branding/seamonkey/locales/en-US/brand.ftl
new file mode 100644
--- /dev/null
+++ b/suite/branding/seamonkey/locales/en-US/brand.ftl
@@ -0,0 +1,21 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+## SeaMonkey Brand
+##
+## SeaMonkey must be treated as a brand, and kept in English.
+## It cannot be:
+## - Transliterated.
+## - Translated.
+##
+## Reference: https://www.mozilla.org/styleguide/communications/translation/
+
+-brand-shorter-name = SeaMonkey
+-brand-short-name = SeaMonkey
+-brand-full-name = SeaMonkey
+# This brand name can be used in messages where the product name needs to
+# remain unchanged across different versions (Daily, Beta, etc.).
+-brand-product-name = SeaMonkey
+-vendor-short-name = SeaMonkey e.V.
+trademarkInfo = { " " }
diff --git a/suite/branding/seamonkey/locales/jar.mn b/suite/branding/seamonkey/locales/jar.mn
--- a/suite/branding/seamonkey/locales/jar.mn
+++ b/suite/branding/seamonkey/locales/jar.mn
@@ -1,6 +1,9 @@
 #filter substitution
 
+[localization] @AB_CD@.jar:
+  branding                                 (en-US/**/*.ftl)
+
 @AB_CD@.jar:
 % locale branding @AB_CD@ %locale/@AB_CD@/branding/
   locale/@AB_CD@/branding/brand.dtd        (%brand.dtd)
   locale/@AB_CD@/branding/brand.properties (%brand.properties)
diff --git a/suite/buoy/ATTN-CC-SUITE-PATCHERS.txt b/suite/buoy/ATTN-CC-SUITE-PATCHERS.txt
new file mode 100644
--- /dev/null
+++ b/suite/buoy/ATTN-CC-SUITE-PATCHERS.txt
@@ -0,0 +1,3 @@
+Please exclude this component from any scripted or manual upgrades to the rest
+of the suite (unless you are specifically updating this component).
+It's handling is a special case and should be done seperately.
\ No newline at end of file
diff --git a/suite/buoy/ATTN-L10N-TRANSLATORS.txt b/suite/buoy/ATTN-L10N-TRANSLATORS.txt
new file mode 100644
--- /dev/null
+++ b/suite/buoy/ATTN-L10N-TRANSLATORS.txt
@@ -0,0 +1,3 @@
+This component can be ignored and does not need to be translated. Its only
+purpose is to faciliate SeaMonkey Reconstruction as well as specific
+testing cases.
\ No newline at end of file
diff --git a/suite/buoy/ZZ-buoy-prefs.js b/suite/buoy/ZZ-buoy-prefs.js
new file mode 100644
--- /dev/null
+++ b/suite/buoy/ZZ-buoy-prefs.js
@@ -0,0 +1,2 @@
+//pref("toolkit.defaultChromeURI", "chrome://buoy/content/buoy.xhtml");
+pref("prompts.contentPromptSubDialog", false);
diff --git a/suite/buoy/content/buoy.css b/suite/buoy/content/buoy.css
new file mode 100644
--- /dev/null
+++ b/suite/buoy/content/buoy.css
@@ -0,0 +1,9 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+html, body {
+  height: 100%;
+}
+
diff --git a/suite/buoy/content/buoy.js b/suite/buoy/content/buoy.js
new file mode 100644
--- /dev/null
+++ b/suite/buoy/content/buoy.js
@@ -0,0 +1,37 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { EzE10SUtils } = ChromeUtils.import("resource:///modules/EzE10SUtils.jsm");
+
+ChromeUtils.defineESModuleGetters(this, {
+  BrowserToolboxLauncher: "resource://devtools/client/framework/browser-toolbox/Launcher.sys.mjs",
+});
+
+Object.defineProperty(this, "BrowserConsoleManager", {
+  get() {
+    let { loader } = ChromeUtils.importESModule("resource://devtools/shared/loader/Loader.sys.mjs");
+    return loader.require("devtools/client/webconsole/browser-console-manager").BrowserConsoleManager;
+  },
+  configurable: true,
+  enumerable: true,
+});
+
+var MainBuoyFunctions = {
+  startup: function() {
+    var browser = document.getElementById("main-browser");
+    var homepage = "about:config";
+    EzE10SUtils.loadAboutBlank(browser);
+    EzE10SUtils.loadURI(browser, homepage);
+  },
+  openURL: function(aURL) {
+    EzE10SUtils.loadURI(document.getElementById("main-browser"), aURL);
+  },
+  devtools: function() { BrowserToolboxLauncher.init(); },
+  quitApp: function() { Services.startup.quit(Services.startup.eAttemptQuit); },
+}
+
+function openWebLinkIn(url, where, params) {
+  MainBuoyFunctions.openURL(url);
+}
diff --git a/suite/buoy/content/buoy.xhtml b/suite/buoy/content/buoy.xhtml
new file mode 100644
--- /dev/null
+++ b/suite/buoy/content/buoy.xhtml
@@ -0,0 +1,65 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!--  Mozilla DocType Reference:
+      xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+      xmlns:xbl="http://www.mozilla.org/xbl"
+      xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+      xmlns:html="http://www.w3.org/1999/xhtml"
+      xmlns:svg="http://www.w3.org/2000/svg"
+      xmlns:em="http://www.mozilla.org/2004/em-rdf#"
+-->
+
+<!DOCTYPE html>
+
+<html id="main-window"
+      xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:html="http://www.w3.org/1999/xhtml"
+      xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+      windowtype="buoy:main"
+      screenX="10"
+      screenY="10"
+      width="640px"
+      height="480px"
+      scrolling="false"
+      persist="screenX screenY width height sizemode">
+  <head>
+    <title>SeaMonkey Buoy</title>
+    <link rel="stylesheet" href="chrome://global/skin/global.css" />
+    <link rel="stylesheet" href="chrome://buoy/content/buoy.css" />
+    <script defer="defer" src="chrome://global/content/customElements.js" />
+    <script defer="defer" src="chrome://buoy/content/buoy.js" />
+    <script>
+      window.addEventListener("load", MainBuoyFunctions.startup);
+    </script>
+  </head>
+  <body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+    <commandset id="mainCommandSet">
+      <command id="cmd_Exit" oncommand="MainBuoyFunctions.quitApp();"/>
+      <command id="cmd_DevTools" oncommand="MainBuoyFunctions.devtools();"/>
+    </commandset>
+
+    <vbox flex="1">    
+      <toolbox id="buoy-toolbox">
+        <menubar id="main-menubar">
+          <menu id="file-menu" label="File">
+            <menupopup id="file-popup">
+              <menuitem label="DevTools" command="cmd_DevTools"/>
+              <menuitem label="Exit" command="cmd_Exit"/>
+            </menupopup>
+          </menu>
+        </menubar>
+      </toolbox>
+      <browser id="main-browser"
+               flex="1"
+               type="content"
+               primary="true"
+               maychangeremoteness="true"
+               nodefaultsrc="true" />
+    </vbox>
+  </body>
+</html>
+
diff --git a/suite/buoy/devtools/all-devtools.js b/suite/buoy/devtools/all-devtools.js
new file mode 100644
--- /dev/null
+++ b/suite/buoy/devtools/all-devtools.js
@@ -0,0 +1,368 @@
+// Developer Tools related preferences
+pref("devtools.chrome.enabled", true);
+pref("devtools.debugger.remote-enabled", true);
+pref("devtools.selfxss.count", 5);
+// Enable extensionStorage storage actor by default
+pref("devtools.storage.extensionStorage.enabled", true);
+
+// Toolbox preferences
+pref("devtools.toolbox.footer.height", 250);
+pref("devtools.toolbox.sidebar.width", 500);
+pref("devtools.toolbox.host", "bottom");
+pref("devtools.toolbox.previousHost", "right");
+pref("devtools.toolbox.selectedTool", "inspector");
+pref("devtools.toolbox.sideEnabled", true);
+pref("devtools.toolbox.zoomValue", "1");
+pref("devtools.toolbox.splitconsoleEnabled", false);
+pref("devtools.toolbox.splitconsoleHeight", 100);
+pref("devtools.toolbox.tabsOrder", "");
+pref("devtools.netmonitor.features.newEditAndResend", false);
+
+// The fission pref for enabling the "Multiprocess Browser Toolbox", which will
+// make it possible to debug anything in Firefox (See Bug 1570639 for more
+// information).
+pref("devtools.browsertoolbox.fission", true);
+
+// When the Multiprocess Browser Toolbox is enabled, you can configure the scope of it:
+// - "everything" will enable debugging absolutely everything in the browser
+//   All processes, all documents, all workers, all add-ons.
+// - "parent-process" will restrict debugging to the parent process
+//   All privileged javascript, documents and workers running in the parent process.
+pref("devtools.browsertoolbox.scope", "everything");
+
+// Toolbox Button preferences
+pref("devtools.command-button-pick.enabled", true);
+pref("devtools.command-button-frames.enabled", true);
+pref("devtools.command-button-splitconsole.enabled", true);
+pref("devtools.command-button-responsive.enabled", true);
+pref("devtools.command-button-screenshot.enabled", false);
+pref("devtools.command-button-rulers.enabled", false);
+pref("devtools.command-button-measure.enabled", false);
+pref("devtools.command-button-noautohide.enabled", false);
+pref("devtools.command-button-errorcount.enabled", true);
+  pref("devtools.command-button-experimental-prefs.enabled", true);
+
+// Inspector preferences
+// Enable the Inspector
+pref("devtools.inspector.enabled", true);
+// What was the last active sidebar in the inspector
+pref("devtools.inspector.selectedSidebar", "layoutview");
+pref("devtools.inspector.activeSidebar", "layoutview");
+pref("devtools.inspector.remote", false);
+
+// Enable the 3 pane mode in the inspector
+pref("devtools.inspector.three-pane-enabled", true);
+// Enable the 3 pane mode in the chrome inspector
+pref("devtools.inspector.chrome.three-pane-enabled", false);
+// Collapse pseudo-elements by default in the rule-view
+pref("devtools.inspector.show_pseudo_elements", false);
+// The default size for image preview tooltips in the rule-view/computed-view/markup-view
+pref("devtools.inspector.imagePreviewTooltipSize", 300);
+// Enable user agent style inspection in rule-view
+pref("devtools.inspector.showUserAgentStyles", false);
+// Show native anonymous content and user agent shadow roots
+pref("devtools.inspector.showAllAnonymousContent", false);
+// Enable the inline CSS compatibility warning in inspector rule view
+pref("devtools.inspector.ruleview.inline-compatibility-warning.enabled", false);
+// Enable the compatibility tool in the inspector.
+pref("devtools.inspector.compatibility.enabled", true);
+// Enable color scheme simulation in the inspector.
+pref("devtools.inspector.color-scheme-simulation.enabled", true);
+
+// Grid highlighter preferences
+pref("devtools.gridinspector.gridOutlineMaxColumns", 50);
+pref("devtools.gridinspector.gridOutlineMaxRows", 50);
+pref("devtools.gridinspector.showGridAreas", false);
+pref("devtools.gridinspector.showGridLineNumbers", false);
+pref("devtools.gridinspector.showInfiniteLines", false);
+// Max number of grid highlighters that can be displayed
+pref("devtools.gridinspector.maxHighlighters", 3);
+
+// Whether or not simplified highlighters should be used when
+// prefers-reduced-motion is enabled.
+pref("devtools.inspector.simple-highlighters-reduced-motion", false);
+
+// Whether or not the box model panel is opened in the layout view
+pref("devtools.layout.boxmodel.opened", true);
+// Whether or not the flexbox panel is opened in the layout view
+pref("devtools.layout.flexbox.opened", true);
+// Whether or not the flexbox container panel is opened in the layout view
+pref("devtools.layout.flex-container.opened", true);
+// Whether or not the flexbox item panel is opened in the layout view
+pref("devtools.layout.flex-item.opened", true);
+// Whether or not the grid inspector panel is opened in the layout view
+pref("devtools.layout.grid.opened", true);
+
+// Enable hovering Box Model values and jumping to their source CSS rule in the
+// rule-view.
+#if defined(NIGHTLY_BUILD)
+  pref("devtools.layout.boxmodel.highlightProperty", true);
+#else
+  pref("devtools.layout.boxmodel.highlightProperty", false);
+#endif
+
+// By how many times eyedropper will magnify pixels
+pref("devtools.eyedropper.zoom", 6);
+
+// Enable to collapse attributes that are too long.
+pref("devtools.markup.collapseAttributes", true);
+// Length to collapse attributes
+pref("devtools.markup.collapseAttributeLength", 120);
+// Whether to auto-beautify the HTML on copy.
+pref("devtools.markup.beautifyOnCopy", false);
+// Whether or not the DOM mutation breakpoints context menu are enabled in the
+// markup view.
+pref("devtools.markup.mutationBreakpoints.enabled", true);
+
+// DevTools default color unit
+pref("devtools.defaultColorUnit", "authored");
+
+// Enable the Memory tools
+pref("devtools.memory.enabled", true);
+
+pref("devtools.memory.custom-census-displays", "{}");
+pref("devtools.memory.custom-label-displays", "{}");
+pref("devtools.memory.custom-tree-map-displays", "{}");
+
+pref("devtools.memory.max-individuals", 1000);
+pref("devtools.memory.max-retaining-paths", 10);
+
+// Enable the Performance tools
+pref("devtools.performance.enabled", true);
+// But not the pop-up.
+pref("devtools.performance.popup.feature-flag", false);
+// Override the default preset, which is "web-developer" on beta and release.
+pref("devtools.performance.recording.preset", "firefox-platform");
+pref("devtools.performance.recording.preset.remote", "firefox-platform");
+
+// The default cache UI setting
+pref("devtools.cache.disabled", false);
+
+// The default service workers UI setting
+pref("devtools.serviceWorkers.testing.enabled", false);
+
+// Enable the Network Monitor
+pref("devtools.netmonitor.enabled", true);
+
+pref("devtools.netmonitor.features.search", true);
+pref("devtools.netmonitor.features.requestBlocking", true);
+
+// Enable the Application panel
+pref("devtools.application.enabled", false);
+
+// Enable the custom formatters feature
+// This preference represents the user's choice to enable the custom formatters feature.
+// While the preference above will be removed once the feature is stable, this one is menat to stay.
+pref("devtools.custom-formatters.enabled", false);
+
+// The default Network Monitor UI settings
+pref("devtools.netmonitor.panes-network-details-width", 550);
+pref("devtools.netmonitor.panes-network-details-height", 450);
+pref("devtools.netmonitor.panes-search-width", 550);
+pref("devtools.netmonitor.panes-search-height", 450);
+pref("devtools.netmonitor.filters", "[\"all\"]");
+pref("devtools.netmonitor.visibleColumns",
+  "[\"status\",\"method\",\"domain\",\"file\",\"initiator\",\"type\",\"transferred\",\"contentSize\",\"waterfall\"]"
+);
+pref("devtools.netmonitor.columnsData",
+  '[{"name":"status","minWidth":30,"width":5}, {"name":"method","minWidth":30,"width":5}, {"name":"domain","minWidth":30,"width":10}, {"name":"file","minWidth":30,"width":25}, {"name":"url","minWidth":30,"width":25},{"name":"initiator","minWidth":30,"width":10},{"name":"type","minWidth":30,"width":5},{"name":"transferred","minWidth":30,"width":10},{"name":"contentSize","minWidth":30,"width":5},{"name":"waterfall","minWidth":150,"width":15}]');
+pref("devtools.netmonitor.msg.payload-preview-height", 128);
+pref("devtools.netmonitor.msg.visibleColumns",
+  '["data", "time"]'
+);
+pref("devtools.netmonitor.msg.displayed-messages.limit", 500);
+
+pref("devtools.netmonitor.response.ui.limit", 10240);
+
+// Save request/response bodies yes/no.
+pref("devtools.netmonitor.saveRequestAndResponseBodies", true);
+
+// The default Network monitor HAR export setting
+pref("devtools.netmonitor.har.defaultLogDir", "");
+pref("devtools.netmonitor.har.defaultFileName", "%hostname_Archive [%date]");
+pref("devtools.netmonitor.har.jsonp", false);
+pref("devtools.netmonitor.har.jsonpCallback", "");
+pref("devtools.netmonitor.har.includeResponseBodies", true);
+pref("devtools.netmonitor.har.compress", false);
+pref("devtools.netmonitor.har.forceExport", false);
+pref("devtools.netmonitor.har.pageLoadedTimeout", 1500);
+pref("devtools.netmonitor.har.enableAutoExportToFile", false);
+
+pref("devtools.netmonitor.features.webSockets", true);
+
+// netmonitor audit
+pref("devtools.netmonitor.audits.slow", 500);
+
+// Disable the EventSource Inspector.
+pref("devtools.netmonitor.features.serverSentEvents", false);
+
+// Enable the Storage Inspector
+pref("devtools.storage.enabled", true);
+
+// Enable the Style Editor.
+pref("devtools.styleeditor.enabled", true);
+pref("devtools.styleeditor.autocompletion-enabled", true);
+pref("devtools.styleeditor.showMediaSidebar", true);
+pref("devtools.styleeditor.mediaSidebarWidth", 238);
+pref("devtools.styleeditor.navSidebarWidth", 245);
+pref("devtools.styleeditor.transitions", true);
+
+// Screenshot Option Settings.
+pref("devtools.screenshot.clipboard.enabled", false);
+pref("devtools.screenshot.audio.enabled", true);
+
+// Make sure the DOM panel is hidden by default
+pref("devtools.dom.enabled", false);
+
+// Enable the Accessibility panel.
+pref("devtools.accessibility.enabled", true);
+
+// Web console filters
+pref("devtools.webconsole.filter.error", true);
+pref("devtools.webconsole.filter.warn", true);
+pref("devtools.webconsole.filter.info", true);
+pref("devtools.webconsole.filter.log", true);
+pref("devtools.webconsole.filter.debug", true);
+pref("devtools.webconsole.filter.css", false);
+pref("devtools.webconsole.filter.net", false);
+pref("devtools.webconsole.filter.netxhr", false);
+
+// Webconsole autocomplete preference
+pref("devtools.webconsole.input.autocomplete",true);
+  pref("devtools.webconsole.input.context", false);
+
+// Set to true to eagerly show the results of webconsole terminal evaluations
+// when they don't have side effects.
+pref("devtools.webconsole.input.eagerEvaluation", true);
+
+// Browser console filters
+pref("devtools.browserconsole.filter.error", true);
+pref("devtools.browserconsole.filter.warn", true);
+pref("devtools.browserconsole.filter.info", true);
+pref("devtools.browserconsole.filter.log", true);
+pref("devtools.browserconsole.filter.debug", true);
+pref("devtools.browserconsole.filter.css", false);
+pref("devtools.browserconsole.filter.net", false);
+pref("devtools.browserconsole.filter.netxhr", false);
+
+// Max number of inputs to store in web console history.
+pref("devtools.webconsole.inputHistoryCount", 300);
+
+// Persistent logging: |true| if you want the relevant tool to keep all of the
+// logged messages after reloading the page, |false| if you want the output to
+// be cleared each time page navigation happens.
+pref("devtools.webconsole.persistlog", false);
+pref("devtools.netmonitor.persistlog", false);
+
+// Web Console timestamp: |true| if you want the logs and instructions
+// in the Web Console to display a timestamp, or |false| to not display
+// any timestamps.
+pref("devtools.webconsole.timestampMessages", false);
+
+// Enable the webconsole sidebar toggle in Nightly builds.
+  pref("devtools.webconsole.sidebarToggle", false);
+
+// Saved editor mode state in the console.
+pref("devtools.webconsole.input.editor", false);
+pref("devtools.browserconsole.input.editor", false);
+
+// Editor width for webconsole and browserconsole.
+pref("devtools.webconsole.input.editorWidth", 0);
+pref("devtools.browserconsole.input.editorWidth", 0);
+
+// Display an onboarding UI for the Editor mode.
+pref("devtools.webconsole.input.editorOnboarding", true);
+
+// Enable message grouping in the console, true by default
+pref("devtools.webconsole.groupWarningMessages", true);
+
+// Saved state of the Display content messages checkbox in the browser console.
+pref("devtools.browserconsole.contentMessages", true);
+
+// Enable network monitoring the browser toolbox console/browser console.
+pref("devtools.browserconsole.enableNetworkMonitoring", false);
+
+// Enable client-side mapping service for source maps
+pref("devtools.source-map.client-service.enabled", true);
+
+// The number of lines that are displayed in the web console.
+pref("devtools.hud.loglimit", 10000);
+
+// The developer tools editor configuration:
+// - tabsize: how many spaces to use when a Tab character is displayed.
+// - expandtab: expand Tab characters to spaces.
+// - keymap: which keymap to use (can be 'default', 'emacs' or 'vim')
+// - autoclosebrackets: whether to permit automatic bracket/quote closing.
+// - detectindentation: whether to detect the indentation from the file
+// - enableCodeFolding: Whether to enable code folding or not.
+pref("devtools.editor.tabsize", 2);
+pref("devtools.editor.expandtab", true);
+pref("devtools.editor.keymap", "default");
+pref("devtools.editor.autoclosebrackets", true);
+pref("devtools.editor.detectindentation", true);
+pref("devtools.editor.enableCodeFolding", true);
+pref("devtools.editor.autocomplete", true);
+
+// The angle of the viewport.
+pref("devtools.responsive.viewport.angle", 0);
+// The width of the viewport.
+pref("devtools.responsive.viewport.width", 320);
+// The height of the viewport.
+pref("devtools.responsive.viewport.height", 480);
+// The pixel ratio of the viewport.
+pref("devtools.responsive.viewport.pixelRatio", 0);
+// Whether or not the viewports are left aligned.
+pref("devtools.responsive.leftAlignViewport.enabled", false);
+// Whether to reload when touch simulation is toggled
+pref("devtools.responsive.reloadConditions.touchSimulation", false);
+// Whether to reload when user agent is changed
+pref("devtools.responsive.reloadConditions.userAgent", false);
+// Whether to show the notification about reloading to apply emulation
+pref("devtools.responsive.reloadNotification.enabled", true);
+// Whether or not touch simulation is enabled.
+pref("devtools.responsive.touchSimulation.enabled", false);
+// The user agent of the viewport.
+pref("devtools.responsive.userAgent", "");
+
+// Show the custom user agent input in Nightly builds.
+#if defined(NIGHTLY_BUILD)
+  pref("devtools.responsive.showUserAgentInput", true);
+#else
+  pref("devtools.responsive.showUserAgentInput", false);
+#endif
+
+// Show tab debug targets for This Firefox (on by default for local builds).
+  pref("devtools.aboutdebugging.local-tab-debugging", true);
+
+// Show process debug targets.
+pref("devtools.aboutdebugging.process-debugging", true);
+// Stringified array of network locations that users can connect to.
+pref("devtools.aboutdebugging.network-locations", "[]");
+// Debug target pane collapse/expand settings.
+pref("devtools.aboutdebugging.collapsibilities.installedExtension", false);
+pref("devtools.aboutdebugging.collapsibilities.otherWorker", false);
+pref("devtools.aboutdebugging.collapsibilities.serviceWorker", false);
+pref("devtools.aboutdebugging.collapsibilities.sharedWorker", false);
+pref("devtools.aboutdebugging.collapsibilities.tab", false);
+pref("devtools.aboutdebugging.collapsibilities.temporaryExtension", false);
+
+// about:debugging: only show system and hidden extensions in local builds by
+// default.
+  pref("devtools.aboutdebugging.showHiddenAddons", true);
+
+// Map top-level await expressions in the console
+pref("devtools.debugger.features.map-await-expression", true);
+
+// This relies on javascript.options.asyncstack as well or it has no effect.
+pref("devtools.debugger.features.async-captured-stacks", true);
+pref("devtools.debugger.features.async-live-stacks", false);
+
+// Disable autohide for DevTools popups and tooltips.
+// This is currently not exposed by any UI to avoid making
+// about:devtools-toolbox tabs unusable by mistake.
+pref("devtools.popup.disable_autohide", false);
+
+// Enable overflow debugging in the inspector.
+pref("devtools.overflow.debugging.enabled", true);
+// Enable drag to edit properties in the inspector rule view.
+pref("devtools.inspector.draggable_properties", true);
diff --git a/suite/buoy/devtools/components.conf b/suite/buoy/devtools/components.conf
new file mode 100644
--- /dev/null
+++ b/suite/buoy/devtools/components.conf
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+  {
+    'cid': '{089694e9-106a-4704-abf7-62a88545e194}',
+    'contract_ids': ['@xulvolution.org/generic/devtools-startup-clh;1'],
+    'esModule': 'resource:///modules/devtools-loader.sys.mjs',
+    'constructor': 'DevToolsStartup',
+    'categories': {'command-line-handler': 'm-aaa-xre-devtools'},
+  },
+]
diff --git a/suite/buoy/devtools/devtools-loader.sys.mjs b/suite/buoy/devtools/devtools-loader.sys.mjs
new file mode 100644
--- /dev/null
+++ b/suite/buoy/devtools/devtools-loader.sys.mjs
@@ -0,0 +1,118 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+  NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
+});
+
+function resolveURIInternal(aCmdLine, aArgument) {
+  var uri = aCmdLine.resolveURI(aArgument);
+
+  if (!(uri instanceof Ci.nsIFileURL)) {
+    return uri;
+  }
+
+  try {
+    if (uri.file.exists()) {
+      return uri;
+    }
+  } catch (e) {
+    console.error(e);
+  }
+
+  // We have interpreted the argument as a relative file URI, but the file
+  // doesn't exist. Try URI fixup heuristics: see bug 290782.
+
+  try {
+    uri = Services.uriFixup.getFixupURIInfo(aArgument, 0).preferredURI;
+  } catch (e) {
+    console.error(e);
+  }
+
+  return uri;
+}
+
+export function DevToolsStartup() {}
+
+DevToolsStartup.prototype = {
+  QueryInterface: ChromeUtils.generateQI(["nsICommandLineHandler"]),
+
+  helpInfo: "",
+  handle(cmdLine) {
+    this.initialize();
+
+    // We want to overwrite the -devtools flag and open the toolbox instead
+    const devtoolsFlag = cmdLine.handleFlag("devtools", false);
+    if (devtoolsFlag) {
+      this.handleDevToolsFlag(cmdLine);
+    }
+
+    var chromeFlag = cmdLine.handleFlagWithParam("chrome", false);
+    if (chromeFlag) {
+      // The parameter specifies the window to open.
+      this.handleChromeFlag(cmdLine, chromeFlag);
+    }
+  },
+
+  handleDevToolsFlag(cmdLine) {
+    const { BrowserToolboxLauncher } = ChromeUtils.importESModule(
+      "resource://devtools/client/framework/browser-toolbox/Launcher.sys.mjs"
+    );
+    BrowserToolboxLauncher.init();
+
+    if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
+      cmdLine.preventDefault = true;
+    }
+  },
+
+  handleChromeFlag(cmdLine, chromeParam) {
+    try {
+      const argstring = Cc["@mozilla.org/supports-string;1"].createInstance(
+        Ci.nsISupportsString
+      );
+
+      const _uri = resolveURIInternal(cmdLine, chromeParam);
+
+      // only load URIs which do not inherit chrome privs
+      if (!Services.io.URIChainHasFlags(_uri, Ci.nsIProtocolHandler.URI_INHERITS_SECURITY_CONTEXT)) {
+        Services.ww.openWindow(null, _uri.spec, "_blank", "chrome,dialog=no,all", argstring);
+        cmdLine.preventDefault = true;
+      }
+    } catch (e) { dump(e); }
+  },
+
+  initialize() {
+    const { loader, require, DevToolsLoader } = ChromeUtils.importESModule(
+      "resource://devtools/shared/loader/Loader.sys.mjs"
+    );
+    const { DevToolsServer } = require("devtools/server/devtools-server");
+    const { gDevTools } = require("devtools/client/framework/devtools");
+
+    // Make sure our root actor is always registered, no matter how devtools are called.
+    const devtoolsRegisterActors =
+      DevToolsServer.registerActors.bind(DevToolsServer);
+    DevToolsServer.registerActors = function (options) {
+      devtoolsRegisterActors(options);
+      if (options.root) {
+        const {
+          createRootActor,
+        } = require("resource:///modules/devtools-root-actor.js");
+        DevToolsServer.setRootActor(createRootActor);
+      }
+    };
+
+    // Make the loader visible to the debugger by default and for the already
+    // loaded instance. Thunderbird now also provides the Browser Toolbox for
+    // chrome debugging, which uses its own separate loader instance.
+    DevToolsLoader.prototype.invisibleToDebugger = false;
+    loader.invisibleToDebugger = false;
+    DevToolsServer.allowChromeProcess = true;
+
+    // Initialize and load the toolkit/browser actors. This will also call above function to set the
+    // Thunderbird root actor
+    DevToolsServer.init();
+    DevToolsServer.registerAllActors();
+  },
+};
diff --git a/suite/buoy/devtools/devtools-root-actor.js b/suite/buoy/devtools/devtools-root-actor.js
new file mode 100644
--- /dev/null
+++ b/suite/buoy/devtools/devtools-root-actor.js
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* globals loader, require, exports */
+
+/**
+ * Actors for Developer Tools.
+ */
+
+var { ActorRegistry } = require("devtools/server/actors/utils/actor-registry");
+
+loader.lazyRequireGetter(
+  this,
+  "RootActor",
+  "devtools/server/actors/root",
+  true
+);
+loader.lazyRequireGetter(
+  this,
+  "sendShutdownEvent",
+  "devtools/server/actors/webbrowser",
+  true
+);
+loader.lazyRequireGetter(
+  this,
+  "WorkerDescriptorActorList",
+  "devtools/server/actors/worker/worker-descriptor-actor-list",
+  true
+);
+loader.lazyRequireGetter(
+  this,
+  "ServiceWorkerRegistrationActorList",
+  "devtools/server/actors/worker/service-worker-registration-list",
+  true
+);
+loader.lazyRequireGetter(
+  this,
+  "ProcessActorList",
+  "devtools/server/actors/process",
+  true
+);
+
+/**
+ * Create the basic root actor for any XRE Application.
+ *
+ * @param aConnection       The debugger connection to create the actor for.
+ * @returns The actor for the connection.
+ */
+exports.createRootActor = function (aConnection) {
+  const parameters = {
+    workerList: new WorkerDescriptorActorList(aConnection, {}),
+    serviceWorkerRegistrationList: new ServiceWorkerRegistrationActorList(
+      aConnection
+    ),
+    processList: new ProcessActorList(),
+    globalActorFactories: ActorRegistry.globalActorFactories,
+    onShutdown: sendShutdownEvent,
+  };
+
+  // Create the root actor and set the application type
+  const rootActor = new RootActor(aConnection, parameters);
+  rootActor.applicationType = "generic";
+
+  return rootActor;
+};
+
diff --git a/suite/buoy/devtools/moz.build b/suite/buoy/devtools/moz.build
new file mode 100644
--- /dev/null
+++ b/suite/buoy/devtools/moz.build
@@ -0,0 +1,15 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXTRA_JS_MODULES += [
+    "devtools-loader.sys.mjs",
+    "devtools-root-actor.js",
+]
+
+XPCOM_MANIFESTS += [
+    "components.conf",
+]
+
+JS_PREFERENCE_FILES += ['all-devtools.js']
diff --git a/suite/buoy/jar.mn b/suite/buoy/jar.mn
new file mode 100644
--- /dev/null
+++ b/suite/buoy/jar.mn
@@ -0,0 +1,16 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#filter substitution
+
+buoy.jar:
+% content buoy %content/buoy/ contentaccessible=yes
+  content/buoy/buoy.css            (content/buoy.css)
+  content/buoy/buoy.js             (content/buoy.js)
+  content/buoy/buoy.xhtml          (content/buoy.xhtml)
+
+[localization] @AB_CD@.jar:
+  browser                          (moz-l10n/browser/**/*.ftl)
+  buoy                             (locale/**/*.ftl)
+
diff --git a/suite/buoy/modules/BrowserWindowTracker.sys.mjs b/suite/buoy/modules/BrowserWindowTracker.sys.mjs
new file mode 100644
--- /dev/null
+++ b/suite/buoy/modules/BrowserWindowTracker.sys.mjs
@@ -0,0 +1,6 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This module is deliberately not implemented. It only exists to keep
+// the automated tests happy. See bug 1782621.
diff --git a/suite/buoy/modules/CustomizableUI.sys.mjs b/suite/buoy/modules/CustomizableUI.sys.mjs
new file mode 100644
--- /dev/null
+++ b/suite/buoy/modules/CustomizableUI.sys.mjs
@@ -0,0 +1,360 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file is a copy of a file with the same name in Firefox. Only the
+// pieces we're using, and a few pieces the devtools rely on such as the
+// constants, remain.
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+  PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs",
+});
+
+/**
+ * gPanelsForWindow is a list of known panels in a window which we may need to close
+ * should command events fire which target them.
+ */
+var gPanelsForWindow = new WeakMap();
+
+var CustomizableUIInternal = {
+  addPanelCloseListeners(aPanel) {
+    Services.els.addSystemEventListener(aPanel, "click", this, false);
+    Services.els.addSystemEventListener(aPanel, "keypress", this, false);
+    const win = aPanel.ownerGlobal;
+    if (!gPanelsForWindow.has(win)) {
+      gPanelsForWindow.set(win, new Set());
+    }
+    gPanelsForWindow.get(win).add(this._getPanelForNode(aPanel));
+  },
+
+  removePanelCloseListeners(aPanel) {
+    Services.els.removeSystemEventListener(aPanel, "click", this, false);
+    Services.els.removeSystemEventListener(aPanel, "keypress", this, false);
+    const win = aPanel.ownerGlobal;
+    const panels = gPanelsForWindow.get(win);
+    if (panels) {
+      panels.delete(this._getPanelForNode(aPanel));
+    }
+  },
+
+  handleEvent(aEvent) {
+    switch (aEvent.type) {
+      case "click":
+      case "keypress":
+        this.maybeAutoHidePanel(aEvent);
+        break;
+    }
+  },
+
+  _getPanelForNode(aNode) {
+    return aNode.closest("panel");
+  },
+
+  /*
+   * If people put things in the panel which need more than single-click interaction,
+   * we don't want to close it. Right now we check for text inputs and menu buttons.
+   * We also check for being outside of any toolbaritem/toolbarbutton, ie on a blank
+   * part of the menu.
+   */
+  _isOnInteractiveElement(aEvent) {
+    function getMenuPopupForDescendant(aNode) {
+      let lastPopup = null;
+      while (
+        aNode &&
+        aNode.parentNode &&
+        aNode.parentNode.localName.startsWith("menu")
+      ) {
+        lastPopup = aNode.localName == "menupopup" ? aNode : lastPopup;
+        aNode = aNode.parentNode;
+      }
+      return lastPopup;
+    }
+
+    let target = aEvent.target;
+    const panel = this._getPanelForNode(aEvent.currentTarget);
+    // This can happen in e.g. customize mode. If there's no panel,
+    // there's clearly nothing for us to close; pretend we're interactive.
+    if (!panel) {
+      return true;
+    }
+    // We keep track of:
+    // whether we're in an input container (text field)
+    let inInput = false;
+    // whether we're in a popup/context menu
+    let inMenu = false;
+    // whether we're in a toolbarbutton/toolbaritem
+    let inItem = false;
+    // whether the current menuitem has a valid closemenu attribute
+    let menuitemCloseMenu = "auto";
+
+    // While keeping track of that, we go from the original target back up,
+    // to the panel if we have to. We bail as soon as we find an input,
+    // a toolbarbutton/item, or the panel:
+    while (target) {
+      // Skip out of iframes etc:
+      if (target.nodeType == target.DOCUMENT_NODE) {
+        if (!target.defaultView) {
+          // Err, we're done.
+          break;
+        }
+        // Find containing browser or iframe element in the parent doc.
+        target = target.defaultView.docShell.chromeEventHandler;
+        if (!target) {
+          break;
+        }
+      }
+      const tagName = target.localName;
+      inInput = tagName == "input";
+      inItem = tagName == "toolbaritem" || tagName == "toolbarbutton";
+      const isMenuItem = tagName == "menuitem";
+      inMenu = inMenu || isMenuItem;
+
+      if (isMenuItem && target.hasAttribute("closemenu")) {
+        const closemenuVal = target.getAttribute("closemenu");
+        menuitemCloseMenu =
+          closemenuVal == "single" || closemenuVal == "none"
+            ? closemenuVal
+            : "auto";
+      }
+
+      // Keep the menu open and break out of the loop if the click happened on
+      // the ShadowRoot or a disabled menu item.
+      if (
+        target.nodeType == target.DOCUMENT_FRAGMENT_NODE ||
+        target.getAttribute("disabled") == "true"
+      ) {
+        return true;
+      }
+
+      // This isn't in the loop condition because we want to break before
+      // changing |target| if any of these conditions are true
+      if (inInput || inItem || target == panel) {
+        break;
+      }
+      // We need specific code for popups: the item on which they were invoked
+      // isn't necessarily in their parentNode chain:
+      if (isMenuItem) {
+        const topmostMenuPopup = getMenuPopupForDescendant(target);
+        target =
+          (topmostMenuPopup && topmostMenuPopup.triggerNode) ||
+          target.parentNode;
+      } else {
+        target = target.parentNode;
+      }
+    }
+
+    // If the user clicked a menu item...
+    if (inMenu) {
+      // We care if we're in an input also,
+      // or if the user specified closemenu!="auto":
+      if (inInput || menuitemCloseMenu != "auto") {
+        return true;
+      }
+      // Otherwise, we're probably fine to close the panel
+      return false;
+    }
+    // If we're not in a menu, and we *are* in a type="menu" toolbarbutton,
+    // we'll now interact with the menu
+    if (inItem && target.getAttribute("type") == "menu") {
+      return true;
+    }
+    return inInput || !inItem;
+  },
+
+  hidePanelForNode(aNode) {
+    const panel = this._getPanelForNode(aNode);
+    if (panel) {
+      lazy.PanelMultiView.hidePopup(panel);
+    }
+  },
+
+  maybeAutoHidePanel(aEvent) {
+    const eventType = aEvent.type;
+    if (eventType == "keypress" && aEvent.keyCode != aEvent.DOM_VK_RETURN) {
+      return;
+    }
+
+    if (eventType == "click" && aEvent.button != 0) {
+      return;
+    }
+
+    // We don't check preventDefault - it makes sense that this was prevented,
+    // but we probably still want to close the panel. If consumers don't want
+    // this to happen, they should specify the closemenu attribute.
+    if (eventType != "command" && this._isOnInteractiveElement(aEvent)) {
+      return;
+    }
+
+    // We can't use event.target because we might have passed an anonymous
+    // content boundary as well, and so target points to the outer element in
+    // that case. Unfortunately, this means we get anonymous child nodes instead
+    // of the real ones, so looking for the 'stoooop, don't close me' attributes
+    // is more involved.
+    let target = aEvent.originalTarget;
+    while (target.parentNode && target.localName != "panel") {
+      if (
+        target.getAttribute("closemenu") == "none" ||
+        target.getAttribute("widget-type") == "view" ||
+        target.getAttribute("widget-type") == "button-and-view"
+      ) {
+        return;
+      }
+      target = target.parentNode;
+    }
+
+    // If we get here, we can actually hide the popup:
+    this.hidePanelForNode(aEvent.target);
+  },
+};
+Object.freeze(CustomizableUIInternal);
+
+export var CustomizableUI = {
+  /**
+   * Constant reference to the ID of the navigation toolbar.
+   */
+  AREA_NAVBAR: "nav-bar",
+  /**
+   * Constant reference to the ID of the menubar's toolbar.
+   */
+  AREA_MENUBAR: "toolbar-menubar",
+  /**
+   * Constant reference to the ID of the tabstrip toolbar.
+   */
+  AREA_TABSTRIP: "TabsToolbar",
+  /**
+   * Constant reference to the ID of the bookmarks toolbar.
+   */
+  AREA_BOOKMARKS: "PersonalToolbar",
+  /**
+   * Constant reference to the ID of the non-dymanic (fixed) list in the overflow panel.
+   */
+  AREA_FIXED_OVERFLOW_PANEL: "widget-overflow-fixed-list",
+
+  /**
+   * Constant indicating the area is a menu panel.
+   */
+  TYPE_MENU_PANEL: "menu-panel",
+  /**
+   * Constant indicating the area is a toolbar.
+   */
+  TYPE_TOOLBAR: "toolbar",
+
+  /**
+   * Constant indicating a XUL-type provider.
+   */
+  PROVIDER_XUL: "xul",
+  /**
+   * Constant indicating an API-type provider.
+   */
+  PROVIDER_API: "api",
+  /**
+   * Constant indicating dynamic (special) widgets: spring, spacer, and separator.
+   */
+  PROVIDER_SPECIAL: "special",
+
+  /**
+   * Constant indicating the widget is built-in
+   */
+  SOURCE_BUILTIN: "builtin",
+  /**
+   * Constant indicating the widget is externally provided
+   * (e.g. by add-ons or other items not part of the builtin widget set).
+   */
+  SOURCE_EXTERNAL: "external",
+
+  /**
+   * Constant indicating the reason the event was fired was a window closing
+   */
+  REASON_WINDOW_CLOSED: "window-closed",
+  /**
+   * Constant indicating the reason the event was fired was an area being
+   * unregistered separately from window closing mechanics.
+   */
+  REASON_AREA_UNREGISTERED: "area-unregistered",
+
+  /**
+   * Add a widget to an area.
+   * If the area to which you try to add is not known to CustomizableUI,
+   * this will throw.
+   * If the area to which you try to add is the same as the area in which
+   * the widget is currently placed, this will do the same as
+   * moveWidgetWithinArea.
+   * If the widget cannot be removed from its original location, this will
+   * no-op.
+   *
+   * This will fire an onWidgetAdded notification,
+   * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification
+   * for each window CustomizableUI knows about.
+   *
+   * @param aWidgetId the ID of the widget to add
+   * @param aArea     the ID of the area to add the widget to
+   * @param aPosition the position at which to add the widget. If you do not
+   *                  pass a position, the widget will be added to the end
+   *                  of the area.
+   */
+  addWidgetToArea(aWidgetId, aArea, aPosition) {},
+  /**
+   * Remove a widget from its area. If the widget cannot be removed from its
+   * area, or is not in any area, this will no-op. Otherwise, this will fire an
+   * onWidgetRemoved notification, and an onWidgetBeforeDOMChange and
+   * onWidgetAfterDOMChange notification for each window CustomizableUI knows
+   * about.
+   *
+   * @param aWidgetId the ID of the widget to remove
+   */
+  removeWidgetFromArea(aWidgetId) {},
+  /**
+   * Get the placement of a widget. This is by far the best way to obtain
+   * information about what the state of your widget is. The internals of
+   * this call are cheap (no DOM necessary) and you will know where the user
+   * has put your widget.
+   *
+   * @param aWidgetId the ID of the widget whose placement you want to know
+   * @returns
+   *   {
+   *     area: "somearea", // The ID of the area where the widget is placed
+   *     position: 42 // the index in the placements array corresponding to
+   *                  // your widget.
+   *   }
+   *
+   *   OR
+   *
+   *   null // if the widget is not placed anywhere (ie in the palette)
+   */
+  getPlacementOfWidget(aWidgetId, aOnlyRegistered = true, aDeadAreas = false) {
+    return null;
+  },
+  /**
+   * Add listeners to a panel that will close it. For use from the menu panel
+   * and overflowable toolbar implementations, unlikely to be useful for
+   * consumers.
+   *
+   * @param aPanel the panel to which listeners should be attached.
+   */
+  addPanelCloseListeners(aPanel) {
+    CustomizableUIInternal.addPanelCloseListeners(aPanel);
+  },
+  /**
+   * Remove close listeners that have been added to a panel with
+   * addPanelCloseListeners. For use from the menu panel and overflowable
+   * toolbar implementations, unlikely to be useful for consumers.
+   *
+   * @param aPanel the panel from which listeners should be removed.
+   */
+  removePanelCloseListeners(aPanel) {
+    CustomizableUIInternal.removePanelCloseListeners(aPanel);
+  },
+  /**
+   * Notify toolbox(es) of a particular event. If you don't pass aWindow,
+   * all toolboxes will be notified. For use from Customize Mode only,
+   * do not use otherwise.
+   *
+   * @param aEvent the name of the event to send.
+   * @param aDetails optional, the details of the event.
+   * @param aWindow optional, the window in which to send the event.
+   */
+  dispatchToolboxEvent(aEvent, aDetails = {}, aWindow = null) {},
+};
+Object.freeze(CustomizableUI);
diff --git a/suite/buoy/modules/EzE10SUtils.jsm b/suite/buoy/modules/EzE10SUtils.jsm
new file mode 100644
--- /dev/null
+++ b/suite/buoy/modules/EzE10SUtils.jsm
@@ -0,0 +1,107 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This module is assists with dealing with arbitrary browser elements
+ * in an e10s only world.
+ *
+ * This is forked from comm/mail/modules/MailE10SUtils.jsm
+ *
+ * XXXTobin: Convert to sys.mjs
+ */
+
+const EXPORTED_SYMBOLS = ["EzE10SUtils"];
+
+const { E10SUtils } = ChromeUtils.importESModule(
+  "resource://gre/modules/E10SUtils.sys.mjs"
+);
+const { ExtensionParent } = ChromeUtils.importESModule(
+  "resource://gre/modules/ExtensionParent.sys.mjs"
+);
+
+var EzE10SUtils = {
+  /**
+   * Loads about:blank in `browser` without switching remoteness. about:blank
+   * can load in a local browser or a remote browser, and `loadURI` will make
+   * it load in a remote browser even if you don't want it to.
+   *
+   * @param {nsIBrowser} browser
+   */
+  loadAboutBlank(browser) {
+    if (!browser.currentURI || browser.currentURI.spec == "about:blank") {
+      return;
+    }
+    browser.loadURI(Services.io.newURI("about:blank"), {
+      triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+      remoteTypeOverride: browser.remoteType,
+    });
+  },
+
+  /**
+   * Loads `uri` in `browser`, changing to a remote/local browser if necessary.
+   *
+   * @see `nsIWebNavigation.loadURI`
+   *
+   * @param {nsIBrowser} browser
+   * @param {string} uri
+   * @param {object} params
+   */
+  loadURI(browser, uri, params = {}) {
+    const multiProcess = browser.ownerGlobal.docShell.QueryInterface(
+      Ci.nsILoadContext
+    ).useRemoteTabs;
+    const remoteSubframes = browser.ownerGlobal.docShell.QueryInterface(
+      Ci.nsILoadContext
+    ).useRemoteSubframes;
+
+    const isRemote = browser.getAttribute("remote") == "true";
+    const remoteType = E10SUtils.getRemoteTypeForURI(
+      uri,
+      multiProcess,
+      remoteSubframes
+    );
+    const shouldBeRemote = remoteType !== E10SUtils.NOT_REMOTE;
+
+    if (shouldBeRemote != isRemote) {
+      this.changeRemoteness(browser, remoteType);
+    }
+
+    params.triggeringPrincipal =
+      params.triggeringPrincipal ||
+      Services.scriptSecurityManager.getSystemPrincipal();
+    browser.fixupAndLoadURIString(uri, params);
+  },
+
+  /**
+   * Force `browser` to be a remote/local browser.
+   *
+   * @see E10SUtils.jsm for remote types.
+   *
+   * @param {nsIBrowser} browser - the browser to enforce the remoteness of.
+   * @param {string} remoteType - the remoteness to enforce.
+   * @returns {boolean} true if any change happened on the browser (which would
+   *    not be the case if its remoteness is already in the correct state).
+   */
+  changeRemoteness(browser, remoteType) {
+    if (browser.remoteType == remoteType) {
+      return false;
+    }
+
+    browser.destroy();
+
+    if (remoteType) {
+      browser.setAttribute("remote", "true");
+      browser.setAttribute("remoteType", remoteType);
+    } else {
+      browser.setAttribute("remote", "false");
+      browser.removeAttribute("remoteType");
+    }
+
+    browser.changeRemoteness({ remoteType });
+    browser.construct();
+    ExtensionParent.apiManager.emit("extension-browser-inserted", browser);
+
+    return true;
+  },
+};
diff --git a/suite/buoy/modules/PanelMultiView.sys.mjs b/suite/buoy/modules/PanelMultiView.sys.mjs
new file mode 100644
--- /dev/null
+++ b/suite/buoy/modules/PanelMultiView.sys.mjs
@@ -0,0 +1,1700 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Allows a popup panel to host multiple subviews. The main view shown when the
+ * panel is opened may slide out to display a subview, which in turn may lead to
+ * other subviews in a cascade menu pattern.
+ *
+ * The <panel> element should contain a <panelmultiview> element. Views are
+ * declared using <panelview> elements that are usually children of the main
+ * <panelmultiview> element, although they don't need to be, as views can also
+ * be imported into the panel from other panels or popup sets.
+ *
+ * The panel should be opened asynchronously using the openPopup static method
+ * on the PanelMultiView object. This will display the view specified using the
+ * mainViewId attribute on the contained <panelmultiview> element.
+ *
+ * Specific subviews can slide in using the showSubView method, and backwards
+ * navigation can be done using the goBack method or through a button in the
+ * subview headers.
+ *
+ * The process of displaying the main view or a new subview requires multiple
+ * steps to be completed, hence at any given time the <panelview> element may
+ * be in different states:
+ *
+ * -- Open or closed
+ *
+ *    All the <panelview> elements start "closed", meaning that they are not
+ *    associated to a <panelmultiview> element and can be located anywhere in
+ *    the document. When the openPopup or showSubView methods are called, the
+ *    relevant view becomes "open" and the <panelview> element may be moved to
+ *    ensure it is a descendant of the <panelmultiview> element.
+ *
+ *    The "ViewShowing" event is fired at this point, when the view is not
+ *    visible yet. The event is allowed to cancel the operation, in which case
+ *    the view is closed immediately.
+ *
+ *    Closing the view does not move the node back to its original position.
+ *
+ * -- Visible or invisible
+ *
+ *    This indicates whether the view is visible in the document from a layout
+ *    perspective, regardless of whether it is currently scrolled into view. In
+ *    fact, all subviews are already visible before they start sliding in.
+ *
+ *    Before scrolling into view, a view may become visible but be placed in a
+ *    special off-screen area of the document where layout and measurements can
+ *    take place asynchronously.
+ *
+ *    When navigating forward, an open view may become invisible but stay open
+ *    after sliding out of view. The last known size of these views is still
+ *    taken into account for determining the overall panel size.
+ *
+ *    When navigating backwards, an open subview will first become invisible and
+ *    then will be closed.
+ *
+ * -- Active or inactive
+ *
+ *    This indicates whether the view is fully scrolled into the visible area
+ *    and ready to receive mouse and keyboard events. An active view is always
+ *    visible, but a visible view may be inactive. For example, during a scroll
+ *    transition, both views will be inactive.
+ *
+ *    When a view becomes active, the ViewShown event is fired synchronously,
+ *    and the showSubView and goBack methods can be called for navigation.
+ *
+ *    For the main view of the panel, the ViewShown event is dispatched during
+ *    the "popupshown" event, which means that other "popupshown" handlers may
+ *    be called before the view is active. Thus, code that needs to perform
+ *    further navigation automatically should either use the ViewShown event or
+ *    wait for an event loop tick, like BrowserTestUtils.waitForEvent does.
+ *
+ * -- Navigating with the keyboard
+ *
+ *    An open view may keep state related to keyboard navigation, even if it is
+ *    invisible. When a view is closed, keyboard navigation state is cleared.
+ *
+ * This diagram shows how <panelview> nodes move during navigation:
+ *
+ *   In this <panelmultiview>     In other panels    Action
+ *             ┌───┬───┬───┐        ┌───┬───┐
+ *             │(A)│ B │ C │        │ D │ E │          Open panel
+ *             └───┴───┴───┘        └───┴───┘
+ *         ┌───┬───┬───┐            ┌───┬───┐
+ *         │{A}│(C)│ B │            │ D │ E │          Show subview C
+ *         └───┴───┴───┘            └───┴───┘
+ *     ┌───┬───┬───┬───┐            ┌───┐
+ *     │{A}│{C}│(D)│ B │            │ E │              Show subview D
+ *     └───┴───┴───┴───┘            └───┘
+ *       │ ┌───┬───┬───┬───┐        ┌───┐
+ *       │ │{A}│(C)│ D │ B │        │ E │              Go back
+ *       │ └───┴───┴───┴───┘        └───┘
+ *       │   │   │
+ *       │   │   └── Currently visible view
+ *       │   │   │
+ *       └───┴───┴── Open views
+ */
+
+const lazy = {};
+
+ChromeUtils.defineLazyGetter(lazy, "gBundle", function () {
+  return Services.strings.createBundle(
+    "chrome://messenger/locale/messenger.properties"
+  );
+});
+
+/**
+ * Safety timeout after which asynchronous events will be canceled if any of the
+ * registered blockers does not return.
+ */
+const BLOCKERS_TIMEOUT_MS = 10000;
+
+const TRANSITION_PHASES = Object.freeze({
+  START: 1,
+  PREPARE: 2,
+  TRANSITION: 3,
+});
+
+const gNodeToObjectMap = new WeakMap();
+const gWindowsWithUnloadHandler = new WeakSet();
+
+/**
+ * Allows associating an object to a node lazily using a weak map.
+ *
+ * Classes deriving from this one may be easily converted to Custom Elements,
+ * although they would lose the ability of being associated lazily.
+ */
+var AssociatedToNode = class {
+  constructor(node) {
+    /**
+     * Node associated to this object.
+     */
+    this.node = node;
+
+    /**
+     * This promise is resolved when the current set of blockers set by event
+     * handlers have all been processed.
+     */
+    this._blockersPromise = Promise.resolve();
+  }
+
+  /**
+   * Retrieves the instance associated with the given node, constructing a new
+   * one if necessary. When the last reference to the node is released, the
+   * object instance will be garbage collected as well.
+   */
+  static forNode(node) {
+    let associatedToNode = gNodeToObjectMap.get(node);
+    if (!associatedToNode) {
+      associatedToNode = new this(node);
+      gNodeToObjectMap.set(node, associatedToNode);
+    }
+    return associatedToNode;
+  }
+
+  get document() {
+    return this.node.ownerDocument;
+  }
+
+  get window() {
+    return this.node.ownerGlobal;
+  }
+
+  _getBoundsWithoutFlushing(element) {
+    return this.window.windowUtils.getBoundsWithoutFlushing(element);
+  }
+
+  /**
+   * Dispatches a custom event on this element.
+   *
+   * @param  {string}    eventName Name of the event to dispatch.
+   * @param  {object}    [detail]  Event detail object. Optional.
+   * @param  {boolean}   cancelable If the event can be canceled.
+   * @returns {boolean} `true` if the event was canceled by an event handler, `false`
+   *                   otherwise.
+   */
+  dispatchCustomEvent(eventName, detail, cancelable = false) {
+    const event = new this.window.CustomEvent(eventName, {
+      detail,
+      bubbles: true,
+      cancelable,
+    });
+    this.node.dispatchEvent(event);
+    return event.defaultPrevented;
+  }
+
+  /**
+   * Dispatches a custom event on this element and waits for any blocking
+   * promises registered using the "addBlocker" function on the details object.
+   * If this function is called again, the event is only dispatched after all
+   * the previously registered blockers have returned.
+   *
+   * The event can be canceled either by resolving any blocking promise to the
+   * boolean value "false" or by calling preventDefault on the event. Rejections
+   * and exceptions will be reported and will cancel the event.
+   *
+   * Blocking should be used sporadically because it slows down the interface.
+   * Also, non-reentrancy is not strictly guaranteed because a safety timeout of
+   * BLOCKERS_TIMEOUT_MS is implemented, after which the event will be canceled.
+   * This helps to prevent deadlocks if any of the event handlers does not
+   * resolve a blocker promise.
+   *
+   * @note Since there is no use case for dispatching different asynchronous
+   *       events in parallel for the same element, this function will also wait
+   *       for previous blockers when the event name is different.
+   *
+   * @param eventName
+   *        Name of the custom event to dispatch.
+   *
+   * @resolves True if the event was canceled by a handler, false otherwise.
+   */
+  async dispatchAsyncEvent(eventName) {
+    // Wait for all the previous blockers before dispatching the event.
+    const blockersPromise = this._blockersPromise.catch(() => {});
+    return (this._blockersPromise = blockersPromise.then(async () => {
+      const blockers = new Set();
+      let cancel = this.dispatchCustomEvent(
+        eventName,
+        {
+          addBlocker(promise) {
+            // Any exception in the blocker will cancel the operation.
+            blockers.add(
+              promise.catch(ex => {
+                console.error(ex);
+                return true;
+              })
+            );
+          },
+        },
+        true
+      );
+      if (blockers.size) {
+        const timeoutPromise = new Promise((resolve, reject) => {
+          this.window.setTimeout(reject, BLOCKERS_TIMEOUT_MS);
+        });
+        try {
+          const results = await Promise.race([
+            Promise.all(blockers),
+            timeoutPromise,
+          ]);
+          cancel = cancel || results.some(result => result === false);
+        } catch (ex) {
+          console.error(
+            new Error(`One of the blockers for ${eventName} timed out.`)
+          );
+          return true;
+        }
+      }
+      return cancel;
+    }));
+  }
+};
+
+/**
+ * This is associated to <panelmultiview> elements.
+ */
+export class PanelMultiView extends AssociatedToNode {
+  /**
+   * Tries to open the specified <panel> and displays the main view specified
+   * with the "mainViewId" attribute on the <panelmultiview> node it contains.
+   *
+   * If the panel does not contain a <panelmultiview>, it is opened directly.
+   * This allows consumers like page actions to accept different panel types.
+   *
+   * @see The non-static openPopup method for details.
+   */
+  static async openPopup(panelNode, ...args) {
+    const panelMultiViewNode = panelNode.querySelector("panelmultiview");
+    if (panelMultiViewNode) {
+      return this.forNode(panelMultiViewNode).openPopup(...args);
+    }
+    panelNode.openPopup(...args);
+    return true;
+  }
+
+  /**
+   * Closes the specified <panel> which contains a <panelmultiview> node.
+   *
+   * If the panel does not contain a <panelmultiview>, it is closed directly.
+   * This allows consumers like page actions to accept different panel types.
+   *
+   * @see The non-static hidePopup method for details.
+   */
+  static hidePopup(panelNode) {
+    const panelMultiViewNode = panelNode.querySelector("panelmultiview");
+    if (panelMultiViewNode) {
+      this.forNode(panelMultiViewNode).hidePopup();
+    } else {
+      panelNode.hidePopup();
+    }
+  }
+
+  /**
+   * Removes the specified <panel> from the document, ensuring that any
+   * <panelmultiview> node it contains is destroyed properly.
+   *
+   * If the viewCacheId attribute is present on the <panelmultiview> element,
+   * imported subviews will be moved out again to the element it specifies, so
+   * that the panel element can be removed safely.
+   *
+   * If the panel does not contain a <panelmultiview>, it is removed directly.
+   * This allows consumers like page actions to accept different panel types.
+   */
+  static removePopup(panelNode) {
+    try {
+      const panelMultiViewNode = panelNode.querySelector("panelmultiview");
+      if (panelMultiViewNode) {
+        const panelMultiView = this.forNode(panelMultiViewNode);
+        panelMultiView._moveOutKids();
+        panelMultiView.disconnect();
+      }
+    } finally {
+      // Make sure to remove the panel element even if disconnecting fails.
+      panelNode.remove();
+    }
+  }
+
+  /**
+   * Ensures that when the specified window is closed all the <panelmultiview>
+   * node it contains are destroyed properly.
+   */
+  static ensureUnloadHandlerRegistered(window) {
+    if (gWindowsWithUnloadHandler.has(window)) {
+      return;
+    }
+
+    window.addEventListener(
+      "unload",
+      () => {
+        for (const panelMultiViewNode of window.document.querySelectorAll(
+          "panelmultiview"
+        )) {
+          this.forNode(panelMultiViewNode).disconnect();
+        }
+      },
+      { once: true }
+    );
+
+    gWindowsWithUnloadHandler.add(window);
+  }
+
+  get _panel() {
+    return this.node.parentNode;
+  }
+
+  set _transitioning(val) {
+    if (val) {
+      this.node.setAttribute("transitioning", "true");
+    } else {
+      this.node.removeAttribute("transitioning");
+    }
+  }
+
+  get _screenManager() {
+    if (this.__screenManager) {
+      return this.__screenManager;
+    }
+    return (this.__screenManager = Cc[
+      "@mozilla.org/gfx/screenmanager;1"
+    ].getService(Ci.nsIScreenManager));
+  }
+
+  constructor(node) {
+    super(node);
+    this._openPopupPromise = Promise.resolve(false);
+    this._openPopupCancelCallback = () => {};
+  }
+
+  connect() {
+    this.connected = true;
+
+    PanelMultiView.ensureUnloadHandlerRegistered(this.window);
+
+    const viewContainer = (this._viewContainer =
+      this.document.createXULElement("box"));
+    viewContainer.classList.add("panel-viewcontainer");
+
+    const viewStack = (this._viewStack = this.document.createXULElement("box"));
+    viewStack.classList.add("panel-viewstack");
+    viewContainer.append(viewStack);
+
+    const offscreenViewContainer = this.document.createXULElement("box");
+    offscreenViewContainer.classList.add("panel-viewcontainer", "offscreen");
+
+    const offscreenViewStack = (this._offscreenViewStack =
+      this.document.createXULElement("box"));
+    offscreenViewStack.classList.add("panel-viewstack");
+    offscreenViewContainer.append(offscreenViewStack);
+
+    this.node.prepend(offscreenViewContainer);
+    this.node.prepend(viewContainer);
+
+    this.openViews = [];
+
+    this._panel.addEventListener("popupshowing", this);
+    this._panel.addEventListener("popuppositioned", this);
+    this._panel.addEventListener("popuphidden", this);
+    this._panel.addEventListener("popupshown", this);
+
+    // Proxy these public properties and methods, as used elsewhere by various
+    // parts of the browser, to this instance.
+    ["goBack", "showSubView"].forEach(method => {
+      Object.defineProperty(this.node, method, {
+        enumerable: true,
+        value: (...args) => this[method](...args),
+      });
+    });
+  }
+
+  disconnect() {
+    // Guard against re-entrancy.
+    if (!this.node || !this.connected) {
+      return;
+    }
+
+    this._panel.removeEventListener("mousemove", this);
+    this._panel.removeEventListener("popupshowing", this);
+    this._panel.removeEventListener("popuppositioned", this);
+    this._panel.removeEventListener("popupshown", this);
+    this._panel.removeEventListener("popuphidden", this);
+    this.window.removeEventListener("keydown", this, true);
+    this.node =
+      this._openPopupPromise =
+      this._openPopupCancelCallback =
+      this._viewContainer =
+      this._viewStack =
+      this._transitionDetails =
+        null;
+  }
+
+  /**
+   * Tries to open the panel associated with this PanelMultiView, and displays
+   * the main view specified with the "mainViewId" attribute.
+   *
+   * The hidePopup method can be called while the operation is in progress to
+   * prevent the panel from being displayed. View events may also cancel the
+   * operation, so there is no guarantee that the panel will become visible.
+   *
+   * The "popuphidden" event will be fired either when the operation is canceled
+   * or when the popup is closed later. This event can be used for example to
+   * reset the "open" state of the anchor or tear down temporary panels.
+   *
+   * If this method is called again before the panel is shown, the result
+   * depends on the operation currently in progress. If the operation was not
+   * canceled, the panel is opened using the arguments from the previous call,
+   * and this call is ignored. If the operation was canceled, it will be
+   * retried again using the arguments from this call.
+   *
+   * It's not necessary for the <panelmultiview> binding to be connected when
+   * this method is called, but the containing panel must have its display
+   * turned on, for example it shouldn't have the "hidden" attribute.
+   *
+   * @param anchor
+   *        The node to anchor the popup to.
+   * @param options
+   *        Either options to use or a string position. This is forwarded to
+   *        the openPopup method of the panel.
+   * @param args
+   *        Additional arguments to be forwarded to the openPopup method of the
+   *        panel.
+   *
+   * @resolves With true as soon as the request to display the panel has been
+   *           sent, or with false if the operation was canceled. The state of
+   *           the panel at this point is not guaranteed. It may be still
+   *           showing, completely shown, or completely hidden.
+   * @rejects If an exception is thrown at any point in the process before the
+   *          request to display the panel is sent.
+   */
+  async openPopup(anchor, options, ...args) {
+    // Set up the function that allows hidePopup or a second call to showPopup
+    // to cancel the specific panel opening operation that we're starting below.
+    // This function must be synchronous, meaning we can't use Promise.race,
+    // because hidePopup wants to dispatch the "popuphidden" event synchronously
+    // even if the panel has not been opened yet.
+    let canCancel = true;
+    const cancelCallback = (this._openPopupCancelCallback = () => {
+      // If the cancel callback is called and the panel hasn't been prepared
+      // yet, cancel showing it. Setting canCancel to false will prevent the
+      // popup from opening. If the panel has opened by the time the cancel
+      // callback is called, canCancel will be false already, and we will not
+      // fire the "popuphidden" event.
+      if (canCancel && this.node) {
+        canCancel = false;
+        this.dispatchCustomEvent("popuphidden");
+      }
+    });
+
+    // Create a promise that is resolved with the result of the last call to
+    // this method, where errors indicate that the panel was not opened.
+    const openPopupPromise = this._openPopupPromise.catch(() => {
+      return false;
+    });
+
+    // Make the preparation done before showing the panel non-reentrant. The
+    // promise created here will be resolved only after the panel preparation is
+    // completed, even if a cancellation request is received in the meantime.
+    return (this._openPopupPromise = openPopupPromise.then(async wasShown => {
+      // The panel may have been destroyed in the meantime.
+      if (!this.node) {
+        return false;
+      }
+      // If the panel has been already opened there is nothing more to do. We
+      // check the actual state of the panel rather than setting some state in
+      // our handler of the "popuphidden" event because this has a lower chance
+      // of locking indefinitely if events aren't raised in the expected order.
+      if (wasShown && ["open", "showing"].includes(this._panel.state)) {
+        return true;
+      }
+      try {
+        if (!this.connected) {
+          this.connect();
+        }
+        // Allow any of the ViewShowing handlers to prevent showing the main view.
+        if (!(await this._showMainView())) {
+          cancelCallback();
+        }
+      } catch (ex) {
+        cancelCallback();
+        throw ex;
+      }
+      // If a cancellation request was received there is nothing more to do.
+      if (!canCancel || !this.node) {
+        return false;
+      }
+      // We have to set canCancel to false before opening the popup because the
+      // hidePopup method of PanelMultiView can be re-entered by event handlers.
+      // If the openPopup call fails, however, we still have to dispatch the
+      // "popuphidden" event even if canCancel was set to false.
+      try {
+        canCancel = false;
+        this._panel.openPopup(anchor, options, ...args);
+
+        // On Windows, if another popup is hiding while we call openPopup, the
+        // call won't fail but the popup won't open. In this case, we have to
+        // dispatch an artificial "popuphidden" event to reset our state.
+        if (this._panel.state == "closed" && this.openViews.length) {
+          this.dispatchCustomEvent("popuphidden");
+          return false;
+        }
+
+        if (
+          options &&
+          typeof options == "object" &&
+          options.triggerEvent &&
+          options.triggerEvent.type == "keypress" &&
+          this.openViews.length
+        ) {
+          // This was opened via the keyboard, so focus the first item.
+          this.openViews[0].focusWhenActive = true;
+        }
+
+        return true;
+      } catch (ex) {
+        this.dispatchCustomEvent("popuphidden");
+        throw ex;
+      }
+    }));
+  }
+
+  /**
+   * Closes the panel associated with this PanelMultiView.
+   *
+   * If the openPopup method was called but the panel has not been displayed
+   * yet, the operation is canceled and the panel will not be displayed, but the
+   * "popuphidden" event is fired synchronously anyways.
+   *
+   * This means that by the time this method returns all the operations handled
+   * by the "popuphidden" event are completed, for example resetting the "open"
+   * state of the anchor, and the panel is already invisible.
+   */
+  hidePopup() {
+    if (!this.node || !this.connected) {
+      return;
+    }
+
+    // If we have already reached the _panel.openPopup call in the openPopup
+    // method, we can call hidePopup. Otherwise, we have to cancel the latest
+    // request to open the panel, which will have no effect if the request has
+    // been canceled already.
+    if (["open", "showing"].includes(this._panel.state)) {
+      this._panel.hidePopup();
+    } else {
+      this._openPopupCancelCallback();
+    }
+
+    // We close all the views synchronously, so that they are ready to be opened
+    // in other PanelMultiView instances. The "popuphidden" handler may also
+    // call this function, but the second time openViews will be empty.
+    this.closeAllViews();
+  }
+
+  /**
+   * Move any child subviews into the element defined by "viewCacheId" to make
+   * sure they will not be removed together with the <panelmultiview> element.
+   */
+  _moveOutKids() {
+    const viewCacheId = this.node.getAttribute("viewCacheId");
+    if (!viewCacheId) {
+      return;
+    }
+
+    // Node.children and Node.children is live to DOM changes like the
+    // ones we're about to do, so iterate over a static copy:
+    const subviews = Array.from(this._viewStack.children);
+    const viewCache = this.document.getElementById(viewCacheId);
+    for (const subview of subviews) {
+      viewCache.appendChild(subview);
+    }
+  }
+
+  /**
+   * Slides in the specified view as a subview.
+   *
+   * @param viewIdOrNode
+   *        DOM element or string ID of the <panelview> to display.
+   * @param anchor
+   *        DOM element that triggered the subview, which will be highlighted
+   *        and whose "label" attribute will be used for the title of the
+   *        subview when a "title" attribute is not specified.
+   */
+  showSubView(viewIdOrNode, anchor) {
+    this._showSubView(viewIdOrNode, anchor).catch(console.error);
+  }
+  async _showSubView(viewIdOrNode, anchor) {
+    const viewNode =
+      typeof viewIdOrNode == "string"
+        ? this.document.getElementById(viewIdOrNode)
+        : viewIdOrNode;
+    if (!viewNode) {
+      console.error(new Error(`Subview ${viewIdOrNode} doesn't exist.`));
+      return;
+    }
+
+    if (!this.openViews.length) {
+      console.error(new Error(`Cannot show a subview in a closed panel.`));
+      return;
+    }
+
+    const prevPanelView = this.openViews[this.openViews.length - 1];
+    const nextPanelView = PanelView.forNode(viewNode);
+    if (this.openViews.includes(nextPanelView)) {
+      console.error(new Error(`Subview ${viewNode.id} is already open.`));
+      return;
+    }
+
+    // Do not re-enter the process if navigation is already in progress. Since
+    // there is only one active view at any given time, we can do this check
+    // safely, even considering that during the navigation process the actual
+    // view to which prevPanelView refers will change.
+    if (!prevPanelView.active) {
+      return;
+    }
+    // If prevPanelView._doingKeyboardActivation is true, it will be reset to
+    // false synchronously. Therefore, we must capture it before we use any
+    // "await" statements.
+    const doingKeyboardActivation = prevPanelView._doingKeyboardActivation;
+    // Marking the view that is about to scrolled out of the visible area as
+    // inactive will prevent re-entrancy and also disable keyboard navigation.
+    // From this point onwards, "await" statements can be used safely.
+    prevPanelView.active = false;
+
+    // Provide visual feedback while navigation is in progress, starting before
+    // the transition starts and ending when the previous view is invisible.
+    if (anchor) {
+      anchor.setAttribute("open", "true");
+    }
+    try {
+      // If the ViewShowing event cancels the operation we have to re-enable
+      // keyboard navigation, but this must be avoided if the panel was closed.
+      if (!(await this._openView(nextPanelView))) {
+        if (prevPanelView.isOpenIn(this)) {
+          // We don't raise a ViewShown event because nothing actually changed.
+          // Technically we should use a different state flag just because there
+          // is code that could check the "active" property to determine whether
+          // to wait for a ViewShown event later, but this only happens in
+          // regression tests and is less likely to be a technique used in
+          // production code, where use of ViewShown is less common.
+          prevPanelView.active = true;
+        }
+        return;
+      }
+
+      prevPanelView.captureKnownSize();
+
+      // The main view of a panel can be a subview in another one. Make sure to
+      // reset all the properties that may be set on a subview.
+      nextPanelView.mainview = false;
+      // The header may change based on how the subview was opened.
+      nextPanelView.headerText =
+        viewNode.getAttribute("title") ||
+        (anchor && anchor.getAttribute("label"));
+      // The constrained width of subviews may also vary between panels.
+      nextPanelView.minMaxWidth = prevPanelView.knownWidth;
+
+      if (anchor) {
+        viewNode.classList.add("PanelUI-subView");
+      }
+
+      await this._transitionViews(prevPanelView.node, viewNode, false, anchor);
+    } finally {
+      if (anchor) {
+        anchor.removeAttribute("open");
+      }
+    }
+
+    nextPanelView.focusWhenActive = doingKeyboardActivation;
+    this._activateView(nextPanelView);
+  }
+
+  /**
+   * Navigates backwards by sliding out the most recent subview.
+   */
+  goBack() {
+    this._goBack().catch(console.error);
+  }
+  async _goBack() {
+    if (this.openViews.length < 2) {
+      // This may be called by keyboard navigation or external code when only
+      // the main view is open.
+      return;
+    }
+
+    const prevPanelView = this.openViews[this.openViews.length - 1];
+    const nextPanelView = this.openViews[this.openViews.length - 2];
+
+    // Like in the showSubView method, do not re-enter navigation while it is
+    // in progress, and make the view inactive immediately. From this point
+    // onwards, "await" statements can be used safely.
+    if (!prevPanelView.active) {
+      return;
+    }
+
+    prevPanelView.active = false;
+
+    prevPanelView.captureKnownSize();
+
+    await this._transitionViews(prevPanelView.node, nextPanelView.node, true);
+
+    this._closeLatestView();
+
+    this._activateView(nextPanelView);
+  }
+
+  /**
+   * Prepares the main view before showing the panel.
+   */
+  async _showMainView() {
+    const nextPanelView = PanelView.forNode(
+      this.document.getElementById(this.node.getAttribute("mainViewId"))
+    );
+
+    // If the view is already open in another panel, close the panel first.
+    const oldPanelMultiViewNode = nextPanelView.node.panelMultiView;
+    if (oldPanelMultiViewNode) {
+      PanelMultiView.forNode(oldPanelMultiViewNode).hidePopup();
+      // Wait for a layout flush after hiding the popup, otherwise the view may
+      // not be displayed correctly for some time after the new panel is opened.
+      // This is filed as bug 1441015.
+      await this.window.promiseDocumentFlushed(() => {});
+    }
+
+    if (!(await this._openView(nextPanelView))) {
+      return false;
+    }
+
+    // The main view of a panel can be a subview in another one. Make sure to
+    // reset all the properties that may be set on a subview.
+    nextPanelView.mainview = true;
+    nextPanelView.headerText = "";
+    nextPanelView.minMaxWidth = 0;
+
+    // Ensure the view will be visible once the panel is opened.
+    nextPanelView.visible = true;
+
+    return true;
+  }
+
+  /**
+   * Opens the specified PanelView and dispatches the ViewShowing event, which
+   * can be used to populate the subview or cancel the operation.
+   *
+   * This also clears all the attributes and styles that may be left by a
+   * transition that was interrupted.
+   *
+   * @resolves With true if the view was opened, false otherwise.
+   */
+  async _openView(panelView) {
+    if (panelView.node.parentNode != this._viewStack) {
+      this._viewStack.appendChild(panelView.node);
+    }
+
+    panelView.node.panelMultiView = this.node;
+    this.openViews.push(panelView);
+
+    const canceled = await panelView.dispatchAsyncEvent("ViewShowing");
+
+    // The panel can be hidden while we are processing the ViewShowing event.
+    // This results in all the views being closed synchronously, and at this
+    // point the ViewHiding event has already been dispatched for all of them.
+    if (!this.openViews.length) {
+      return false;
+    }
+
+    // Check if the event requested cancellation but the panel is still open.
+    if (canceled) {
+      // Handlers for ViewShowing can't know if a different handler requested
+      // cancellation, so this will dispatch a ViewHiding event to give a chance
+      // to clean up.
+      this._closeLatestView();
+      return false;
+    }
+
+    // Clean up all the attributes and styles related to transitions. We do this
+    // here rather than when the view is closed because we are likely to make
+    // other DOM modifications soon, which isn't the case when closing.
+    const { style } = panelView.node;
+    style.removeProperty("outline");
+    style.removeProperty("width");
+
+    return true;
+  }
+
+  /**
+   * Activates the specified view and raises the ViewShown event, unless the
+   * view was closed in the meantime.
+   */
+  _activateView(panelView) {
+    if (panelView.isOpenIn(this)) {
+      panelView.active = true;
+      if (panelView.focusWhenActive) {
+        panelView.focusFirstNavigableElement(false, true);
+        panelView.focusWhenActive = false;
+      }
+      panelView.dispatchCustomEvent("ViewShown");
+    }
+  }
+
+  /**
+   * Closes the most recent PanelView and raises the ViewHiding event.
+   *
+   * @note The ViewHiding event is not cancelable and should probably be renamed
+   *       to ViewHidden or ViewClosed instead, see bug 1438507.
+   */
+  _closeLatestView() {
+    const panelView = this.openViews.pop();
+    panelView.clearNavigation();
+    panelView.dispatchCustomEvent("ViewHiding");
+    panelView.node.panelMultiView = null;
+    // Views become invisible synchronously when they are closed, and they won't
+    // become visible again until they are opened. When this is called at the
+    // end of backwards navigation, the view is already invisible.
+    panelView.visible = false;
+  }
+
+  /**
+   * Closes all the views that are currently open.
+   */
+  closeAllViews() {
+    // Raise ViewHiding events for open views in reverse order.
+    while (this.openViews.length) {
+      this._closeLatestView();
+    }
+  }
+
+  /**
+   * Apply a transition to 'slide' from the currently active view to the next
+   * one.
+   * Sliding the next subview in means that the previous panelview stays where it
+   * is and the active panelview slides in from the left in LTR mode, right in
+   * RTL mode.
+   *
+   * @param {panelview} previousViewNode Node that is currently displayed, but
+   *                                     is about to be transitioned away. This
+   *                                     must be already inactive at this point.
+   * @param {panelview} viewNode - Node that will becode the active view,
+   *                                     after the transition has finished.
+   * @param {boolean}   reverse          Whether we're navigation back to a
+   *                                     previous view or forward to a next view.
+   */
+  async _transitionViews(previousViewNode, viewNode, reverse) {
+    const { window } = this;
+
+    const nextPanelView = PanelView.forNode(viewNode);
+    const prevPanelView = PanelView.forNode(previousViewNode);
+
+    const details = (this._transitionDetails = {
+      phase: TRANSITION_PHASES.START,
+    });
+
+    // Set the viewContainer dimensions to make sure only the current view is
+    // visible.
+    const olderView = reverse ? nextPanelView : prevPanelView;
+    this._viewContainer.style.minHeight = olderView.knownHeight + "px";
+    this._viewContainer.style.height = prevPanelView.knownHeight + "px";
+    this._viewContainer.style.width = prevPanelView.knownWidth + "px";
+    // Lock the dimensions of the window that hosts the popup panel.
+    const rect = this._getBoundsWithoutFlushing(this._panel);
+    this._panel.style.width = rect.width + "px";
+    this._panel.style.height = rect.height + "px";
+
+    let viewRect;
+    if (reverse) {
+      // Use the cached size when going back to a previous view, but not when
+      // reopening a subview, because its contents may have changed.
+      viewRect = {
+        width: nextPanelView.knownWidth,
+        height: nextPanelView.knownHeight,
+      };
+      nextPanelView.visible = true;
+    } else if (viewNode.customRectGetter) {
+      // We use a customRectGetter for WebExtensions panels, because they need
+      // to query the size from an embedded browser. The presence of this
+      // getter also provides an indication that the view node shouldn't be
+      // moved around, otherwise the state of the browser would get disrupted.
+      const width = prevPanelView.knownWidth;
+      const height = prevPanelView.knownHeight;
+      viewRect = Object.assign({ height, width }, viewNode.customRectGetter());
+      nextPanelView.visible = true;
+      // Until the header is visible, it has 0 height.
+      // Wait for layout before measuring it
+      const header = viewNode.firstElementChild;
+      if (header && header.classList.contains("panel-header")) {
+        viewRect.height += await window.promiseDocumentFlushed(() => {
+          return this._getBoundsWithoutFlushing(header).height;
+        });
+      }
+    } else {
+      this._offscreenViewStack.style.minHeight = olderView.knownHeight + "px";
+      this._offscreenViewStack.appendChild(viewNode);
+      nextPanelView.visible = true;
+
+      viewRect = await window.promiseDocumentFlushed(() => {
+        return this._getBoundsWithoutFlushing(viewNode);
+      });
+      // Bail out if the panel was closed in the meantime.
+      if (!nextPanelView.isOpenIn(this)) {
+        return;
+      }
+
+      // Place back the view after all the other views that are already open in
+      // order for the transition to work as expected.
+      this._viewStack.appendChild(viewNode);
+
+      this._offscreenViewStack.style.removeProperty("min-height");
+    }
+
+    this._transitioning = true;
+    details.phase = TRANSITION_PHASES.PREPARE;
+
+    // The 'magic' part: build up the amount of pixels to move right or left.
+    const moveToLeft =
+      (this.window.RTL_UI && !reverse) || (!this.window.RTL_UI && reverse);
+    const deltaX = prevPanelView.knownWidth;
+    const deepestNode = reverse ? previousViewNode : viewNode;
+
+    // With a transition when navigating backwards - user hits the 'back'
+    // button - we need to make sure that the views are positioned in a way
+    // that a translateX() unveils the previous view from the right direction.
+    if (reverse) {
+      this._viewStack.style.marginInlineStart = "-" + deltaX + "px";
+    }
+
+    // Set the transition style and listen for its end to clean up and make sure
+    // the box sizing becomes dynamic again.
+    // Somehow, putting these properties in PanelUI.css doesn't work for newly
+    // shown nodes in a XUL parent node.
+    this._viewStack.style.transition =
+      "transform var(--animation-easing-function)" +
+      " var(--panelui-subview-transition-duration)";
+    this._viewStack.style.willChange = "transform";
+    // Use an outline instead of a border so that the size is not affected.
+    deepestNode.style.outline = "1px solid var(--panel-separator-color)";
+
+    // Now that all the elements are in place for the start of the transition,
+    // give the layout code a chance to set the initial values.
+    await window.promiseDocumentFlushed(() => {});
+    // Bail out if the panel was closed in the meantime.
+    if (!nextPanelView.isOpenIn(this)) {
+      return;
+    }
+
+    // Now set the viewContainer dimensions to that of the new view, which
+    // kicks of the height animation.
+    this._viewContainer.style.height = viewRect.height + "px";
+    this._viewContainer.style.width = viewRect.width + "px";
+    this._panel.style.removeProperty("width");
+    this._panel.style.removeProperty("height");
+
+    // We're setting the width property to prevent flickering during the
+    // sliding animation with smaller views.
+    viewNode.style.width = viewRect.width + "px";
+
+    // Kick off the transition!
+    details.phase = TRANSITION_PHASES.TRANSITION;
+
+    // If we're going to show the main view, we can remove the
+    // min-height property on the view container.
+    if (viewNode.getAttribute("mainview")) {
+      this._viewContainer.style.removeProperty("min-height");
+    }
+
+    this._viewStack.style.transform =
+      "translateX(" + (moveToLeft ? "" : "-") + deltaX + "px)";
+
+    await new Promise(resolve => {
+      details.resolve = resolve;
+      this._viewContainer.addEventListener(
+        "transitionend",
+        (details.listener = ev => {
+          // It's quite common that `height` on the view container doesn't need
+          // to transition, so we make sure to do all the work on the transform
+          // transition-end, because that is guaranteed to happen.
+          if (ev.target != this._viewStack || ev.propertyName != "transform") {
+            return;
+          }
+          this._viewContainer.removeEventListener(
+            "transitionend",
+            details.listener
+          );
+          delete details.listener;
+          resolve();
+        })
+      );
+      this._viewContainer.addEventListener(
+        "transitioncancel",
+        (details.cancelListener = ev => {
+          if (ev.target != this._viewStack) {
+            return;
+          }
+          this._viewContainer.removeEventListener(
+            "transitioncancel",
+            details.cancelListener
+          );
+          delete details.cancelListener;
+          resolve();
+        })
+      );
+    });
+
+    // Bail out if the panel was closed during the transition.
+    if (!nextPanelView.isOpenIn(this)) {
+      return;
+    }
+    prevPanelView.visible = false;
+
+    // This will complete the operation by removing any transition properties.
+    nextPanelView.node.style.removeProperty("width");
+    deepestNode.style.removeProperty("outline");
+    this._cleanupTransitionPhase();
+
+    nextPanelView.focusSelectedElement();
+  }
+
+  /**
+   * Attempt to clean up the attributes and properties set by `_transitionViews`
+   * above. Which attributes and properties depends on the phase the transition
+   * was left from.
+   */
+  _cleanupTransitionPhase() {
+    if (!this._transitionDetails) {
+      return;
+    }
+
+    const { phase, resolve, listener, cancelListener } =
+      this._transitionDetails;
+    this._transitionDetails = null;
+
+    if (phase >= TRANSITION_PHASES.START) {
+      this._panel.style.removeProperty("width");
+      this._panel.style.removeProperty("height");
+      this._viewContainer.style.removeProperty("height");
+      this._viewContainer.style.removeProperty("width");
+    }
+    if (phase >= TRANSITION_PHASES.PREPARE) {
+      this._transitioning = false;
+      this._viewStack.style.removeProperty("margin-inline-start");
+      this._viewStack.style.removeProperty("transition");
+    }
+    if (phase >= TRANSITION_PHASES.TRANSITION) {
+      this._viewStack.style.removeProperty("transform");
+      if (listener) {
+        this._viewContainer.removeEventListener("transitionend", listener);
+      }
+      if (cancelListener) {
+        this._viewContainer.removeEventListener(
+          "transitioncancel",
+          cancelListener
+        );
+      }
+      if (resolve) {
+        resolve();
+      }
+    }
+  }
+
+  _calculateMaxHeight(aEvent) {
+    // While opening the panel, we have to limit the maximum height of any
+    // view based on the space that will be available. We cannot just use
+    // window.screen.availTop and availHeight because these may return an
+    // incorrect value when the window spans multiple screens.
+    const anchor = this._panel.anchorNode;
+    const anchorRect = anchor.getBoundingClientRect();
+
+    const screen = this._screenManager.screenForRect(
+      anchor.screenX,
+      anchor.screenY,
+      anchorRect.width,
+      anchorRect.height
+    );
+    const availTop = {},
+      availHeight = {};
+    screen.GetAvailRect({}, availTop, {}, availHeight);
+    const cssAvailTop = availTop.value / screen.defaultCSSScaleFactor;
+
+    // The distance from the anchor to the available margin of the screen is
+    // based on whether the panel will open towards the top or the bottom.
+    let maxHeight;
+    if (aEvent.alignmentPosition.startsWith("before_")) {
+      maxHeight = anchor.screenY - cssAvailTop;
+    } else {
+      const anchorScreenBottom = anchor.screenY + anchorRect.height;
+      const cssAvailHeight = availHeight.value / screen.defaultCSSScaleFactor;
+      maxHeight = cssAvailTop + cssAvailHeight - anchorScreenBottom;
+    }
+
+    // To go from the maximum height of the panel to the maximum height of
+    // the view stack, we need to subtract the height of the arrow and the
+    // height of the opposite margin, but we cannot get their actual values
+    // because the panel is not visible yet. However, we know that this is
+    // currently 11px on Mac, 13px on Windows, and 13px on Linux. We also
+    // want an extra margin, both for visual reasons and to prevent glitches
+    // due to small rounding errors. So, we just use a value that makes
+    // sense for all platforms. If the arrow visuals change significantly,
+    // this value will be easy to adjust.
+    const EXTRA_MARGIN_PX = 20;
+    maxHeight -= EXTRA_MARGIN_PX;
+    return maxHeight;
+  }
+
+  handleEvent(aEvent) {
+    // Only process actual popup events from the panel or events we generate
+    // ourselves, but not from menus being shown from within the panel.
+    if (
+      aEvent.type.startsWith("popup") &&
+      aEvent.target != this._panel &&
+      aEvent.target != this.node
+    ) {
+      return;
+    }
+    switch (aEvent.type) {
+      case "keydown":
+        // Since we start listening for the "keydown" event when the popup is
+        // already showing and stop listening when the panel is hidden, we
+        // always have at least one view open.
+        const currentView = this.openViews[this.openViews.length - 1];
+        currentView.keyNavigation(aEvent);
+        break;
+      case "mousemove":
+        this.openViews.forEach(panelView => panelView.clearNavigation());
+        break;
+      case "popupshowing": {
+        this._viewContainer.setAttribute("panelopen", "true");
+        if (!this.node.hasAttribute("disablekeynav")) {
+          // We add the keydown handler on the window so that it handles key
+          // presses when a panel appears but doesn't get focus, as happens
+          // when a button to open a panel is clicked with the mouse.
+          // However, this means the listener is on an ancestor of the panel,
+          // which means that handlers such as ToolbarKeyboardNavigator are
+          // deeper in the tree. Therefore, this must be a capturing listener
+          // so we get the event first.
+          this.window.addEventListener("keydown", this, true);
+          this._panel.addEventListener("mousemove", this);
+        }
+        break;
+      }
+      case "popuppositioned": {
+        if (this._panel.state == "showing") {
+          const maxHeight = this._calculateMaxHeight(aEvent);
+          this._viewStack.style.maxHeight = maxHeight + "px";
+          this._offscreenViewStack.style.maxHeight = maxHeight + "px";
+        }
+        break;
+      }
+      case "popupshown":
+        // The main view is always open and visible when the panel is first
+        // shown, so we can check the height of the description elements it
+        // contains and notify consumers using the ViewShown event. In order to
+        // minimize flicker we need to allow synchronous reflows, and we still
+        // make sure the ViewShown event is dispatched synchronously.
+        const mainPanelView = this.openViews[0];
+        this._activateView(mainPanelView);
+        break;
+      case "popuphidden": {
+        // WebExtensions consumers can hide the popup from viewshowing, or
+        // mid-transition, which disrupts our state:
+        this._transitioning = false;
+        this._viewContainer.removeAttribute("panelopen");
+        this._cleanupTransitionPhase();
+        this.window.removeEventListener("keydown", this, true);
+        this._panel.removeEventListener("mousemove", this);
+        this.closeAllViews();
+
+        // Clear the main view size caches. The dimensions could be different
+        // when the popup is opened again, e.g. through touch mode sizing.
+        this._viewContainer.style.removeProperty("min-height");
+        this._viewStack.style.removeProperty("max-height");
+        this._viewContainer.style.removeProperty("width");
+        this._viewContainer.style.removeProperty("height");
+
+        this.dispatchCustomEvent("PanelMultiViewHidden");
+        break;
+      }
+    }
+  }
+}
+
+/**
+ * This is associated to <panelview> elements.
+ */
+export class PanelView extends AssociatedToNode {
+  constructor(node) {
+    super(node);
+
+    /**
+     * Indicates whether the view is active. When this is false, consumers can
+     * wait for the ViewShown event to know when the view becomes active.
+     */
+    this.active = false;
+
+    /**
+     * Specifies whether the view should be focused when active. When this
+     * is true, the first navigable element in the view will be focused
+     * when the view becomes active. This should be set to true when the view
+     * is activated from the keyboard. It will be set to false once the view
+     * is active.
+     */
+    this.focusWhenActive = false;
+  }
+
+  /**
+   * Indicates whether the view is open in the specified PanelMultiView object.
+   */
+  isOpenIn(panelMultiView) {
+    return this.node.panelMultiView == panelMultiView.node;
+  }
+
+  /**
+   * The "mainview" attribute is set before the panel is opened when this view
+   * is displayed as the main view, and is removed before the <panelview> is
+   * displayed as a subview. The same view element can be displayed as a main
+   * view and as a subview at different times.
+   */
+  set mainview(value) {
+    if (value) {
+      this.node.setAttribute("mainview", true);
+    } else {
+      this.node.removeAttribute("mainview");
+    }
+  }
+
+  /**
+   * Determines whether the view is visible. Setting this to false also resets
+   * the "active" property.
+   */
+  set visible(value) {
+    if (value) {
+      this.node.setAttribute("visible", true);
+    } else {
+      this.node.removeAttribute("visible");
+      this.active = false;
+      this.focusWhenActive = false;
+    }
+  }
+
+  /**
+   * Constrains the width of this view using the "min-width" and "max-width"
+   * styles. Setting this to zero removes the constraints.
+   */
+  set minMaxWidth(value) {
+    const style = this.node.style;
+    if (value) {
+      style.minWidth = style.maxWidth = value + "px";
+    } else {
+      style.removeProperty("min-width");
+      style.removeProperty("max-width");
+    }
+  }
+
+  /**
+   * Adds a header with the given title, or removes it if the title is empty.
+   */
+  set headerText(value) {
+    // If the header already exists, update or remove it as requested.
+    let header = this.node.firstElementChild;
+    if (header && header.classList.contains("panel-header")) {
+      if (value) {
+        header.querySelector(".panel-header > h1 > span").textContent = value;
+      } else {
+        header.remove();
+      }
+      return;
+    }
+
+    // The header doesn't exist, only create it if needed.
+    if (!value) {
+      return;
+    }
+
+    header = this.document.createXULElement("box");
+    header.classList.add("panel-header");
+
+    const backButton = this.document.createXULElement("toolbarbutton");
+    backButton.className =
+      "subviewbutton subviewbutton-iconic subviewbutton-back";
+    backButton.setAttribute("closemenu", "none");
+    backButton.setAttribute("tabindex", "0");
+
+    backButton.setAttribute(
+      "aria-label",
+      lazy.gBundle.GetStringFromName("panel.back")
+    );
+
+    backButton.addEventListener("command", () => {
+      // The panelmultiview element may change if the view is reused.
+      this.node.panelMultiView.goBack();
+      backButton.blur();
+    });
+
+    const h1 = this.document.createElement("h1");
+    const span = this.document.createElement("span");
+    span.textContent = value;
+    h1.appendChild(span);
+
+    header.append(backButton, h1);
+    this.node.prepend(header);
+  }
+
+  /**
+   * Populates the "knownWidth" and "knownHeight" properties with the current
+   * dimensions of the view. These may be zero if the view is invisible.
+   *
+   * These values are relevant during transitions and are retained for backwards
+   * navigation if the view is still open but is invisible.
+   */
+  captureKnownSize() {
+    const rect = this._getBoundsWithoutFlushing(this.node);
+    this.knownWidth = rect.width;
+    this.knownHeight = rect.height;
+  }
+
+  /**
+   * Determine whether an element can only be navigated to with tab/shift+tab,
+   * not the arrow keys.
+   */
+  _isNavigableWithTabOnly(element) {
+    const tag = element.localName;
+    return (
+      tag == "menulist" ||
+      tag == "input" ||
+      tag == "textarea" ||
+      // Allow tab to reach embedded documents in extension panels.
+      tag == "browser"
+    );
+  }
+
+  /**
+   * Make a TreeWalker for keyboard navigation.
+   *
+   * @param {boolean} arrowKey If `true`, elements only navigable with tab are
+   *        excluded.
+   */
+  _makeNavigableTreeWalker(arrowKey) {
+    const filter = node => {
+      if (node.disabled) {
+        return NodeFilter.FILTER_REJECT;
+      }
+      const bounds = this._getBoundsWithoutFlushing(node);
+      if (bounds.width == 0 || bounds.height == 0) {
+        return NodeFilter.FILTER_REJECT;
+      }
+      if (
+        node.tagName == "button" ||
+        node.tagName == "toolbarbutton" ||
+        node.classList.contains("text-link") ||
+        (!arrowKey && this._isNavigableWithTabOnly(node))
+      ) {
+        // Set the tabindex attribute to make sure the node is focusable.
+        if (!node.hasAttribute("tabindex")) {
+          node.setAttribute("tabindex", "-1");
+        }
+        return NodeFilter.FILTER_ACCEPT;
+      }
+      return NodeFilter.FILTER_SKIP;
+    };
+    return this.document.createTreeWalker(
+      this.node,
+      NodeFilter.SHOW_ELEMENT,
+      filter
+    );
+  }
+
+  /**
+   * Get a TreeWalker which finds elements navigable with tab/shift+tab.
+   */
+  get _tabNavigableWalker() {
+    if (!this.__tabNavigableWalker) {
+      this.__tabNavigableWalker = this._makeNavigableTreeWalker(false);
+    }
+    return this.__tabNavigableWalker;
+  }
+
+  /**
+   * Get a TreeWalker which finds elements navigable with up/down arrow keys.
+   */
+  get _arrowNavigableWalker() {
+    if (!this.__arrowNavigableWalker) {
+      this.__arrowNavigableWalker = this._makeNavigableTreeWalker(true);
+    }
+    return this.__arrowNavigableWalker;
+  }
+
+  /**
+   * Element that is currently selected with the keyboard, or null if no element
+   * is selected. Since the reference is held weakly, it can become null or
+   * undefined at any time.
+   */
+  get selectedElement() {
+    return this._selectedElement && this._selectedElement.get();
+  }
+  set selectedElement(value) {
+    if (!value) {
+      delete this._selectedElement;
+    } else {
+      this._selectedElement = Cu.getWeakReference(value);
+    }
+  }
+
+  /**
+   * Focuses and moves keyboard selection to the first navigable element.
+   * This is a no-op if there are no navigable elements.
+   *
+   * @param {boolean} homeKey - `true` if this is for the home key.
+   * @param {boolean} skipBack - `true` if the Back button should be skipped.
+   */
+  focusFirstNavigableElement(homeKey = false, skipBack = false) {
+    // The home key is conceptually similar to the up/down arrow keys.
+    const walker = homeKey
+      ? this._arrowNavigableWalker
+      : this._tabNavigableWalker;
+    walker.currentNode = walker.root;
+    this.selectedElement = walker.firstChild();
+    if (
+      skipBack &&
+      walker.currentNode &&
+      walker.currentNode.classList.contains("subviewbutton-back") &&
+      walker.nextNode()
+    ) {
+      this.selectedElement = walker.currentNode;
+    }
+    this.focusSelectedElement(/* byKey */ true);
+  }
+
+  /**
+   * Focuses and moves keyboard selection to the last navigable element.
+   * This is a no-op if there are no navigable elements.
+   *
+   * @param {boolean} endKey - `true` if this is for the end key.
+   */
+  focusLastNavigableElement(endKey = false) {
+    // The end key is conceptually similar to the up/down arrow keys.
+    const walker = endKey
+      ? this._arrowNavigableWalker
+      : this._tabNavigableWalker;
+    walker.currentNode = walker.root;
+    this.selectedElement = walker.lastChild();
+    this.focusSelectedElement(/* byKey */ true);
+  }
+
+  /**
+   * Based on going up or down, select the previous or next focusable element.
+   *
+   * @param {boolean} isDown - whether we're going down (true) or up (false).
+   * @param {boolean} arrowKey - `true` if this is for the up/down arrow keys.
+   *
+   * @returns {DOMNode} the element we selected.
+   */
+  moveSelection(isDown, arrowKey = false) {
+    const walker = arrowKey
+      ? this._arrowNavigableWalker
+      : this._tabNavigableWalker;
+    const oldSel = this.selectedElement;
+    let newSel;
+    if (oldSel) {
+      walker.currentNode = oldSel;
+      newSel = isDown ? walker.nextNode() : walker.previousNode();
+    }
+    // If we couldn't find something, select the first or last item:
+    if (!newSel) {
+      walker.currentNode = walker.root;
+      newSel = isDown ? walker.firstChild() : walker.lastChild();
+    }
+    this.selectedElement = newSel;
+    return newSel;
+  }
+
+  /**
+   * Allow for navigating subview buttons using the arrow keys and the Enter key.
+   * The Up and Down keys can be used to navigate the list up and down and the
+   * Enter, Right or Left - depending on the text direction - key can be used to
+   * simulate a click on the currently selected button.
+   * The Right or Left key - depending on the text direction - can be used to
+   * navigate to the previous view, functioning as a shortcut for the view's
+   * back button.
+   * Thus, in LTR mode:
+   *  - The Right key functions the same as the Enter key, simulating a click
+   *  - The Left key triggers a navigation back to the previous view.
+   *
+   * Key navigation is only enabled while the view is active, meaning that this
+   * method will return early if it is invoked during a sliding transition.
+   *
+   * @param {KeyEvent} event
+   */
+  /* eslint-disable-next-line complexity */
+  keyNavigation(event) {
+    if (!this.active) {
+      return;
+    }
+
+    let focus = this.document.activeElement;
+    // Make sure the focus is actually inside the panel. (It might not be if
+    // the panel was opened with the mouse.) If it isn't, we don't care
+    // about it for our purposes.
+    // We use Node.compareDocumentPosition because Node.contains doesn't
+    // behave as expected for anonymous content; e.g. the input inside a
+    // textbox.
+    if (
+      focus &&
+      !(
+        this.node.compareDocumentPosition(focus) &
+        Node.DOCUMENT_POSITION_CONTAINED_BY
+      )
+    ) {
+      focus = null;
+    }
+
+    // Extension panels contain embedded documents. We can't manage
+    // keyboard navigation within those.
+    if (focus && focus.tagName == "browser") {
+      return;
+    }
+
+    const stop = () => {
+      event.stopPropagation();
+      event.preventDefault();
+    };
+
+    // If the focused element is only navigable with tab, it wants the arrow
+    // keys, etc. We shouldn't handle any keys except tab and shift+tab.
+    // We make a function for this for performance reasons: we only want to
+    // check this for keys we potentially care about, not *all* keys.
+    const tabOnly = () => {
+      // We use the real focus rather than this.selectedElement because focus
+      // might have been moved without keyboard navigation (e.g. mouse click)
+      // and this.selectedElement is only updated for keyboard navigation.
+      return focus && this._isNavigableWithTabOnly(focus);
+    };
+
+    // If a context menu is open, we must let it handle all keys.
+    // Normally, this just happens, but because we have a capturing window
+    // keydown listener, our listener takes precedence.
+    // Again, we only want to do this check on demand for performance.
+    const isContextMenuOpen = () => {
+      if (!focus) {
+        return false;
+      }
+      const contextNode = focus.closest("[context]");
+      if (!contextNode) {
+        return false;
+      }
+      const context = contextNode.getAttribute("context");
+      const popup = this.document.getElementById(context);
+      return popup && popup.state == "open";
+    };
+
+    const keyCode = event.code;
+    switch (keyCode) {
+      case "ArrowDown":
+      case "ArrowUp":
+        if (tabOnly()) {
+          break;
+        }
+      // Fall-through...
+      case "Tab": {
+        if (isContextMenuOpen()) {
+          break;
+        }
+        stop();
+        const isDown =
+          keyCode == "ArrowDown" || (keyCode == "Tab" && !event.shiftKey);
+        const button = this.moveSelection(isDown, keyCode != "Tab");
+        Services.focus.setFocus(button, Services.focus.FLAG_BYKEY);
+        break;
+      }
+      case "Home":
+        if (tabOnly() || isContextMenuOpen()) {
+          break;
+        }
+        stop();
+        this.focusFirstNavigableElement(true);
+        break;
+      case "End":
+        if (tabOnly() || isContextMenuOpen()) {
+          break;
+        }
+        stop();
+        this.focusLastNavigableElement(true);
+        break;
+      case "ArrowLeft":
+      case "ArrowRight": {
+        if (tabOnly() || isContextMenuOpen()) {
+          break;
+        }
+        stop();
+        if (
+          (!this.window.RTL_UI && keyCode == "ArrowLeft") ||
+          (this.window.RTL_UI && keyCode == "ArrowRight")
+        ) {
+          this.node.panelMultiView.goBack();
+          break;
+        }
+        // If the current button is _not_ one that points to a subview, pressing
+        // the arrow key shouldn't do anything.
+        const button = this.selectedElement;
+        if (!button || !button.classList.contains("subviewbutton-nav")) {
+          break;
+        }
+      }
+      // Fall-through...
+      case "Space":
+      case "NumpadEnter":
+      case "Enter": {
+        if (tabOnly() || isContextMenuOpen()) {
+          break;
+        }
+        const button = this.selectedElement;
+        if (!button) {
+          break;
+        }
+        stop();
+
+        this._doingKeyboardActivation = true;
+        // Unfortunately, 'tabindex' doesn't execute the default action, so
+        // we explicitly do this here.
+        // We are sending a command event, a mousedown event and then a click
+        // event. This is done in order to mimic a "real" mouse click event.
+        // Normally, the command event executes the action, then the click event
+        // closes the menu. However, in some cases (e.g. the Library button),
+        // there is no command event handler and the mousedown event executes the
+        // action instead.
+        button.doCommand();
+        let dispEvent = new event.target.ownerGlobal.MouseEvent("mousedown", {
+          bubbles: true,
+        });
+        button.dispatchEvent(dispEvent);
+        dispEvent = new event.target.ownerGlobal.MouseEvent("click", {
+          bubbles: true,
+        });
+        button.dispatchEvent(dispEvent);
+        this._doingKeyboardActivation = false;
+        break;
+      }
+    }
+  }
+
+  /**
+   * Focus the last selected element in the view, if any.
+   *
+   * @param byKey {Boolean} whether focus was moved by the user pressing a key.
+   *                        Needed to ensure we show focus styles in the right cases.
+   */
+  focusSelectedElement(byKey = false) {
+    const selected = this.selectedElement;
+    if (selected) {
+      const flag = byKey ? "FLAG_BYKEY" : "FLAG_BYELEMENTFOCUS";
+      Services.focus.setFocus(selected, Services.focus[flag]);
+    }
+  }
+
+  /**
+   * Clear all traces of keyboard navigation happening right now.
+   */
+  clearNavigation() {
+    const selected = this.selectedElement;
+    if (selected) {
+      selected.blur();
+      this.selectedElement = null;
+    }
+  }
+}
diff --git a/suite/buoy/modules/SessionStore.sys.mjs b/suite/buoy/modules/SessionStore.sys.mjs
new file mode 100644
--- /dev/null
+++ b/suite/buoy/modules/SessionStore.sys.mjs
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This is a shim for SessionStore in moz-central to prevent bug 1713801. Only
+ * the methods that appear to be hit by comm-central are implemented.
+ */
+export var SessionStore = {
+  updateSessionStoreFromTablistener(aBrowser, aBrowsingContext, aData) {},
+  maybeExitCrashedState() {},
+};
diff --git a/suite/buoy/modules/moz.build b/suite/buoy/modules/moz.build
new file mode 100644
--- /dev/null
+++ b/suite/buoy/modules/moz.build
@@ -0,0 +1,16 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXTRA_JS_MODULES += [
+    "BrowserWindowTracker.sys.mjs",
+    "CustomizableUI.sys.mjs",
+    "EzE10SUtils.jsm",
+    "PanelMultiView.sys.mjs",
+]
+
+EXTRA_JS_MODULES.sessionstore += [
+    "SessionStore.sys.mjs",
+]
+
diff --git a/suite/buoy/moz-l10n/browser/appExtensionFields.ftl b/suite/buoy/moz-l10n/browser/appExtensionFields.ftl
new file mode 100644
--- /dev/null
+++ b/suite/buoy/moz-l10n/browser/appExtensionFields.ftl
@@ -0,0 +1,10 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+## Theme names and descriptions used in the Themes panel in about:addons
+
+# "Auto" is short for automatic. It can be localized without limitations.
+extension-default-theme-name-auto=System theme — auto
+extension-default-theme-description=Follow the operating system setting for buttons, menus, and windows.
+
diff --git a/suite/buoy/moz-l10n/browser/branding/brandings.ftl b/suite/buoy/moz-l10n/browser/branding/brandings.ftl
new file mode 100644
--- /dev/null
+++ b/suite/buoy/moz-l10n/browser/branding/brandings.ftl
@@ -0,0 +1,17 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+## The following feature names must be treated as a brand.
+##
+## They cannot be:
+## - Transliterated.
+## - Translated.
+##
+## Declension should be avoided where possible, leaving the original
+## brand unaltered in prominent UI positions.
+##
+## For further details, consult:
+## https://mozilla-l10n.github.io/styleguides/mozilla_general/#brands-copyright-and-trademark
+
+-profiler-brand-name = Firefox Profiler
diff --git a/suite/buoy/moz.build b/suite/buoy/moz.build
new file mode 100644
--- /dev/null
+++ b/suite/buoy/moz.build
@@ -0,0 +1,8 @@
+DIRS += [
+    "devtools",
+    "modules"
+]
+
+JS_PREFERENCE_FILES += ["ZZ-buoy-prefs.js"]
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/suite/mailnews/components/moz.build b/suite/mailnews/components/moz.build
--- a/suite/mailnews/components/moz.build
+++ b/suite/mailnews/components/moz.build
@@ -1,11 +1,12 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DIRS += [
+    "../../../mail/extensions/openpgp",
     "compose",
     "prefs",
     "addrbook",
     "smime",
 ]
diff --git a/suite/moz.build b/suite/moz.build
--- a/suite/moz.build
+++ b/suite/moz.build
@@ -6,16 +6,17 @@
 with Files("**"):
     BUG_COMPONENT = ("SeaMonkey", "General")
 
 CONFIGURE_SUBST_FILES += ["installer/Makefile"]
 
 DIRS += [
     "base",
     "browser",
+    "buoy",
     "components",
     "editor",
     "extensions",
     "locales",
     "mailnews",
     "modules",
     "themes/classic",
     "themes/modern",
diff --git a/suite/moz.configure b/suite/moz.configure
--- a/suite/moz.configure
+++ b/suite/moz.configure
@@ -9,16 +9,19 @@ set_define("MOZ_SUITE", True)
 
 imply_option("MOZ_APP_BASENAME", "SeaMonkey")
 imply_option("MOZ_APP_ID", "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}")
 imply_option("MOZ_APP_VENDOR", "Mozilla")
 # Include the DevTools client, not just the server (which is the default)
 imply_option("MOZ_DEVTOOLS", "all")
 imply_option("NSS_EXTRA_SYMBOLS_FILE", "../comm/mailnews/nss-extra.symbols")
 
+set_config("MOZ_SANDBOX_NULL_BLOCKLIST_OOP", True)
+set_define("MOZ_SANDBOX_NULL_BLOCKLIST_OOP", True)
+
 imply_option('--enable-default-browser-agent', False)
 
 @depends(target_is_windows, target_has_linux_kernel)
 def bundled_fonts(is_windows, is_linux):
     if is_windows or is_linux:
         return True
 
 
