Merge f-t to m-c, a=merge
authorPhil Ringnalda <philringnalda@gmail.com>
Thu, 10 Sep 2015 13:22:31 -0700
changeset 294212 b8f7944f92373e26b778453702e57478905f30fe
parent 294192 18f5b185fb971492bcb820ec4881141939ea497b (current diff)
parent 294211 4c29498bbb5caac795eec2cfae6181fba0902739 (diff)
child 294255 7671701d15cadd2b343fc27685231e0689bb71df
push id5245
push userraliiev@mozilla.com
push dateThu, 29 Oct 2015 11:30:51 +0000
treeherdermozilla-beta@dac831dc1bd0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone43.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge f-t to m-c, a=merge
browser/devtools/webconsole/NetworkPanel.xhtml
browser/devtools/webconsole/network-panel.js
browser/devtools/webconsole/test/browser_netpanel_longstring_expand.js
browser/devtools/webconsole/test/browser_webconsole_bug_594477_clickable_output.js
browser/devtools/webconsole/test/browser_webconsole_bug_602572_log_bodies_checkbox.js
browser/devtools/webconsole/test/browser_webconsole_bug_618311_close_panels.js
browser/devtools/webconsole/test/browser_webconsole_network_panel.js
browser/themes/shared/devtools/webconsole_networkpanel.css
--- a/addon-sdk/source/lib/node/os.js
+++ b/addon-sdk/source/lib/node/os.js
@@ -6,23 +6,37 @@
 
 module.metadata = {
   "stability": "unstable"
 };
 
 const { Cc, Ci } = require('chrome');
 const system = require('../sdk/system');
 const runtime = require('../sdk/system/runtime');
-const oscpu = Cc["@mozilla.org/network/protocol;1?name=http"]
-                 .getService(Ci.nsIHttpProtocolHandler).oscpu;
-const hostname = Cc["@mozilla.org/network/dns-service;1"]
-                 .getService(Ci.nsIDNSService).myHostName;
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
 const isWindows = system.platform === 'win32';
 const endianness = ((new Uint32Array((new Uint8Array([1,2,3,4])).buffer))[0] === 0x04030201) ? 'LE' : 'BE';
 
+XPCOMUtils.defineLazyGetter(this, "oscpu", () => {
+  try {
+    return Cc["@mozilla.org/network/protocol;1?name=http"].getService(Ci.nsIHttpProtocolHandler).oscpu;
+  } catch (e) {
+    return "";
+  }
+});
+
+XPCOMUtils.defineLazyGetter(this, "hostname", () => {
+  try {
+    // On some platforms (Linux according to try), this service does not exist and fails.
+    return Cc["@mozilla.org/network/dns-service;1"].getService(Ci.nsIDNSService).myHostName;
+  } catch (e) {
+    return "";
+  }
+});
+
 /**
  * Returns a path to a temp directory
  */
 exports.tmpdir = () => system.pathFor('TmpD');
 
 /**
  * Returns the endianness of the architecture: either 'LE' or 'BE'
  */
--- a/browser/base/content/abouthome/aboutHome.js
+++ b/browser/base/content/abouthome/aboutHome.js
@@ -16,16 +16,17 @@ const DEFAULT_SNIPPETS_URLS = [
 
 const SNIPPETS_UPDATE_INTERVAL_MS = 14400000; // 4 hours.
 
 // IndexedDB storage constants.
 const DATABASE_NAME = "abouthome";
 const DATABASE_VERSION = 1;
 const DATABASE_STORAGE = "persistent";
 const SNIPPETS_OBJECTSTORE_NAME = "snippets";
+let searchText, findKey;
 
 // This global tracks if the page has been set up before, to prevent double inits
 let gInitialized = false;
 let gObserver = new MutationObserver(function (mutations) {
   for (let mutation of mutations) {
     if (mutation.attributeName == "snippetsVersion") {
       if (!gInitialized) {
         ensureSnippetsMapThen(loadSnippets);
@@ -49,16 +50,26 @@ window.addEventListener("pageshow", func
   document.dispatchEvent(event);
 });
 
 window.addEventListener("pagehide", function() {
   window.gObserver.disconnect();
   window.removeEventListener("resize", fitToWidth);
 });
 
+// make Accel+f focus the search box
+window.addEventListener("keypress", ev => {
+  // Make Ctrl/Cmd+f focus the search box.
+  let modifiers = ev.ctrlKey + ev.altKey + ev.shiftKey + ev.metaKey;
+  if (ev.getModifierState("Accel") && modifiers == 1 && ev.key == findKey) {
+    searchText.focus();
+    ev.preventDefault();
+  }
+});
+
 // This object has the same interface as Map and is used to store and retrieve
 // the snippets data.  It is lazily initialized by ensureSnippetsMapThen(), so
 // be sure its callback returned before trying to use it.
 let gSnippetsMap;
 let gSnippetsMapCallbacks = [];
 
 /**
  * Ensure the snippets map is properly initialized.
@@ -174,21 +185,22 @@ function onSearchSubmit(aEvent)
 
 let gContentSearchController;
 
 function setupSearch()
 {
   // The "autofocus" attribute doesn't focus the form element
   // immediately when the element is first drawn, so the
   // attribute is also used for styling when the page first loads.
-  let searchText = document.getElementById("searchText");
+  searchText = document.getElementById("searchText");
   searchText.addEventListener("blur", function searchText_onBlur() {
     searchText.removeEventListener("blur", searchText_onBlur);
     searchText.removeAttribute("autofocus");
   });
+  findKey = searchText.dataset.findkey;
 
   if (!gContentSearchController) {
     gContentSearchController =
       new ContentSearchUIController(searchText, searchText.parentNode,
                                     "abouthome", "homepage");
   }
 }
 
--- a/browser/base/content/abouthome/aboutHome.xhtml
+++ b/browser/base/content/abouthome/aboutHome.xhtml
@@ -37,17 +37,18 @@
   <body dir="&locale.dir;">
     <div class="spacer"/>
     <div id="topSection">
       <div id="brandLogo"></div>
 
       <div id="searchIconAndTextContainer">
         <div id="searchIcon"/>
         <input type="text" name="q" value="" id="searchText" maxlength="256"
-               aria-label="&contentSearchInput.label;" autofocus="autofocus" dir="auto"/>
+               aria-label="&contentSearchInput.label;" autofocus="autofocus"
+               dir="auto" data-findkey="&find.commandkey;"/>
         <input id="searchSubmit" type="button" value="" onclick="onSearchSubmit(event)"
                aria-label="&contentSearchSubmit.label;"/>
       </div>
 
       <div id="snippetContainer">
         <div id="defaultSnippets" hidden="true">
           <span id="defaultSnippet1">&abouthome.defaultSnippet1.v1;</span>
           <span id="defaultSnippet2">&abouthome.defaultSnippet2.v1;</span>
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -2360,37 +2360,53 @@ function BrowserViewSourceOfDocument(aAr
     let outerWindowID = requestor.getInterface(Ci.nsIDOMWindowUtils)
                                  .outerWindowID;
     let URL = browser.currentURI.spec;
     args = { browser, outerWindowID, URL };
   } else {
     args = aArgsOrDocument;
   }
 
-  let inTab = Services.prefs.getBoolPref("view_source.tab");
-  if (inTab) {
-    let viewSourceURL = `view-source:${args.URL}`;
-    let tabBrowser = gBrowser;
-    // In the case of sidebars and chat windows, gBrowser is defined but null,
-    // because no #content element exists.  For these cases, we need to find
-    // the most recent browser window.
-    // In the case of popups, we need to find a non-popup browser window.
-    if (!tabBrowser || !window.toolbar.visible) {
-      // This returns only non-popup browser windows by default.
-      let browserWindow = RecentWindow.getMostRecentBrowserWindow();
-      tabBrowser = browserWindow.gBrowser;
-    }
-    let tab = tabBrowser.loadOneTab(viewSourceURL, {
-      relatedToCurrent: true,
-      inBackground: false
+  let viewInternal = () => {
+    let inTab = Services.prefs.getBoolPref("view_source.tab");
+    if (inTab) {
+      let viewSourceURL = `view-source:${args.URL}`;
+      let tabBrowser = gBrowser;
+      // In the case of sidebars and chat windows, gBrowser is defined but null,
+      // because no #content element exists.  For these cases, we need to find
+      // the most recent browser window.
+      // In the case of popups, we need to find a non-popup browser window.
+      if (!tabBrowser || !window.toolbar.visible) {
+        // This returns only non-popup browser windows by default.
+        let browserWindow = RecentWindow.getMostRecentBrowserWindow();
+        tabBrowser = browserWindow.gBrowser;
+      }
+      let tab = tabBrowser.loadOneTab(viewSourceURL, {
+        relatedToCurrent: true,
+        inBackground: false
+      });
+      args.viewSourceBrowser = tabBrowser.getBrowserForTab(tab);
+      top.gViewSourceUtils.viewSourceInBrowser(args);
+    } else {
+      top.gViewSourceUtils.viewSource(args);
+    }
+  }
+
+  // Check if external view source is enabled.  If so, try it.  If it fails,
+  // fallback to internal view source.
+  if (Services.prefs.getBoolPref("view_source.editor.external")) {
+    top.gViewSourceUtils
+       .openInExternalEditor(args, null, null, null, result => {
+      if (!result) {
+        viewInternal();
+      }
     });
-    args.viewSourceBrowser = tabBrowser.getBrowserForTab(tab);
-    top.gViewSourceUtils.viewSourceInBrowser(args);
   } else {
-    top.gViewSourceUtils.viewSource(args);
+    // Display using internal view source
+    viewInternal();
   }
 }
 
 /**
  * Opens the View Source dialog for the source loaded in the root
  * top-level document of the browser. This is really just a
  * convenience wrapper around BrowserViewSourceOfDocument.
  *
@@ -6790,24 +6806,16 @@ var gIdentityHandler = {
   get _permissionsContainer () {
     delete this._permissionsContainer;
     return this._permissionsContainer = document.getElementById("identity-popup-permissions");
   },
   get _permissionList () {
     delete this._permissionList;
     return this._permissionList = document.getElementById("identity-popup-permission-list");
   },
-  get _permissionSubviewListPageFunctionality () {
-    delete this._permissionSubviewListPageFunctionality;
-    return this._permissionSubviewListPageFunctionality = document.getElementById("permission-subview-list-page-functionality");
-  },
-  get _permissionSubviewListSystemAccess () {
-    delete this._permissionSubviewListSystemAccess;
-    return this._permissionSubviewListSystemAccess = document.getElementById("permission-subview-list-system-access");
-  },
 
   /**
    * Rebuild cache of the elements that may or may not exist depending
    * on whether there's a location bar.
    */
   _cacheElements : function() {
     delete this._identityBox;
     delete this._identityIcons;
@@ -6818,18 +6826,16 @@ var gIdentityHandler = {
     delete this._permissionList;
     this._identityBox = document.getElementById("identity-box");
     this._identityIcons = document.getElementById("identity-icons");
     this._identityIconLabel = document.getElementById("identity-icon-label");
     this._identityIconCountryLabel = document.getElementById("identity-icon-country-label");
     this._identityIcon = document.getElementById("page-proxy-favicon");
     this._permissionsContainer = document.getElementById("identity-popup-permissions");
     this._permissionList = document.getElementById("identity-popup-permission-list");
-    this._permissionSubviewListPageFunctionality = document.getElementById("permission-subview-list-page-functionality");
-    this._permissionSubviewListSystemAccess = document.getElementById("permission-subview-list-system-access");
   },
 
   /**
    * Handler for mouseclicks on the "More Information" button in the
    * "identity-popup" panel.
    */
   handleMoreInfoClick : function(event) {
     displaySecurityInfo();
@@ -7290,43 +7296,29 @@ var gIdentityHandler = {
     dt.setData("text/x-moz-url", urlString);
     dt.setData("text/uri-list", value);
     dt.setData("text/plain", value);
     dt.setData("text/html", htmlString);
     dt.setDragImage(gProxyFavIcon, 16, 16);
   },
 
   updateSitePermissions: function () {
-    // Clear all lists and then repopulate them.
-    this._permissionList.textContent = "";
-    this._permissionSubviewListPageFunctionality.textContent = "";
-    this._permissionSubviewListSystemAccess.textContent = "";
+    while (this._permissionList.hasChildNodes())
+      this._permissionList.removeChild(this._permissionList.lastChild);
 
     let uri = gBrowser.currentURI;
 
-    for (let permission of SitePermissions.listPageFunctionalityPermissions()) {
+    for (let permission of SitePermissions.listPermissions()) {
       let state = SitePermissions.get(uri, permission);
-      let item = this._createPermissionItem(permission, state);
-      this._permissionSubviewListPageFunctionality.appendChild(item);
-    }
-
-    for (let permission of SitePermissions.listSystemAccessPermissions()) {
-      let state = SitePermissions.get(uri, permission);
+
+      if (state == SitePermissions.UNKNOWN)
+        continue;
+
       let item = this._createPermissionItem(permission, state);
-      this._permissionSubviewListSystemAccess.appendChild(item);
-    }
-
-    for (let permission of SitePermissions.listPermissions()) {
-      // Add to the main view only if there is a known / non-default
-      // value for the permission for this site.
-      let state = SitePermissions.get(uri, permission);
-      if (state != SitePermissions.UNKNOWN) {
-        let item = this._createPermissionItem(permission, state);
-        this._permissionList.appendChild(item);
-      }
+      this._permissionList.appendChild(item);
     }
 
     this._permissionsContainer.hidden = !this._permissionList.hasChildNodes();
   },
 
   setPermission: function (aPermission, aState) {
     if (aState == SitePermissions.getDefault(aPermission))
       SitePermissions.remove(gBrowser.currentURI, aPermission);
@@ -7335,40 +7327,30 @@ var gIdentityHandler = {
   },
 
   _createPermissionItem: function (aPermission, aState) {
     let menulist = document.createElement("menulist");
     let menupopup = document.createElement("menupopup");
     for (let state of SitePermissions.getAvailableStates(aPermission)) {
       let menuitem = document.createElement("menuitem");
       menuitem.setAttribute("value", state);
-      let label = SitePermissions.getStateLabel(aPermission, state);
-      menuitem.setAttribute("label", label);
-      menuitem.setAttribute("tooltiptext", label);
+      menuitem.setAttribute("label", SitePermissions.getStateLabel(aPermission, state));
       menupopup.appendChild(menuitem);
     }
     menulist.appendChild(menupopup);
-    let value = aState;
-    if (aState == SitePermissions.UNKNOWN) {
-      value = SitePermissions.getDefault(aPermission);
-    }
-    menulist.setAttribute("value", value);
+    menulist.setAttribute("value", aState);
     menulist.setAttribute("oncommand", "gIdentityHandler.setPermission('" +
                                        aPermission + "', this.value)");
     menulist.setAttribute("id", "identity-popup-permission:" + aPermission);
-    menulist.setAttribute("class", "identity-popup-permission");
 
     let label = document.createElement("label");
-    let labelText = SitePermissions.getPermissionLabel(aPermission);
     label.setAttribute("flex", "1");
     label.setAttribute("class", "identity-popup-permission-label");
     label.setAttribute("control", menulist.getAttribute("id"));
-    label.setAttribute("crop", "end");
-    label.setAttribute("value", labelText);
-    label.setAttribute("tooltiptext", labelText);
+    label.setAttribute("value", SitePermissions.getPermissionLabel(aPermission));
 
     let container = document.createElement("hbox");
     container.setAttribute("align", "center");
     container.appendChild(label);
     container.appendChild(menulist);
     return container;
   }
 };
--- a/browser/base/content/test/general/browser_aboutHome.js
+++ b/browser/base/content/test/general/browser_aboutHome.js
@@ -415,16 +415,32 @@ let gTests = [
       searchController.selectedIndex = 1;
       EventUtils.synthesizeMouseAtCenter(row, {button: 0}, gBrowser.contentWindow);
       yield loadPromise;
       ok(input.value == "x", "Input value did not change");
     });
   }
 },
 {
+  desc: "Cmd+f should focus the search box in the page",
+  setup: function () {},
+  run: Task.async(function* () {
+    let doc = gBrowser.selectedBrowser.contentDocument;
+    let logo = doc.getElementById("brandLogo");
+    let searchInput = doc.getElementById("searchText");
+
+    EventUtils.synthesizeMouseAtCenter(logo, {});
+    isnot(searchInput, doc.activeElement, "Search input should not be the active element.");
+
+    EventUtils.synthesizeKey("f", { accelKey: true });
+    yield promiseWaitForCondition(() => doc.activeElement === searchInput);
+    is(searchInput, doc.activeElement, "Search input should be the active element.");
+  })
+},
+{
   desc: "Cmd+k should focus the search box in the page when the search box in the toolbar is absent",
   setup: function () {
     // Remove the search bar from toolbar
     CustomizableUI.removeWidgetFromArea("search-container");
   },
   run: Task.async(function* () {
     let doc = gBrowser.selectedBrowser.contentDocument;
     let logo = doc.getElementById("brandLogo");
--- a/browser/base/content/test/general/browser_permissions.js
+++ b/browser/base/content/test/general/browser_permissions.js
@@ -33,52 +33,8 @@ add_task(function* testMainViewVisible()
   gIdentityHandler._identityPopup.hidden = true;
 
   gIdentityHandler.setPermission("install", SitePermissions.getDefault("install"));
 
   gIdentityHandler._identityBox.click();
   ok(is_hidden(gIdentityHandler._permissionsContainer), "The container is hidden");
   gIdentityHandler._identityPopup.hidden = true;
 });
-
-add_task(function* testSubviewListing() {
-  let {gIdentityHandler} = gBrowser.ownerGlobal;
-  gIdentityHandler.setPermission("install", 1);
-
-  info("Opening control center and expanding permissions subview");
-  gIdentityHandler._identityBox.click();
-
-  info("Checking 'Page Functionality' permissions");
-  let pageFunctionalityMenulists = gIdentityHandler._permissionSubviewListPageFunctionality.querySelectorAll("menulist");
-  let pageFunctionalityPerms = SitePermissions.listPageFunctionalityPermissions();
-  is(pageFunctionalityMenulists.length, pageFunctionalityPerms.length, "One menulist for each permission");
-
-  for (let i = 0; i < pageFunctionalityMenulists.length; i++) {
-    let menulist = pageFunctionalityMenulists[i];
-    let perm = pageFunctionalityPerms[i];
-    let expectedValue = SitePermissions.get(gBrowser.currentURI, perm);
-    if (expectedValue == SitePermissions.UNKNOWN) {
-      expectedValue = SitePermissions.getDefault(perm);
-    }
-
-    is(menulist.id, "identity-popup-permission:" + perm, "Correct id for menulist: " + perm);
-    is(menulist.value, expectedValue, "Correct value on menulist: " + perm);
-  }
-
-  info("Checking 'System Access' permissions");
-  let systemAccessMenulists = gIdentityHandler._permissionSubviewListSystemAccess.querySelectorAll("menulist");
-  let systemAccessPerms = SitePermissions.listSystemAccessPermissions();
-  is(systemAccessMenulists.length, systemAccessPerms.length, "One menulist for each permission");
-
-  for (let i = 0; i < systemAccessMenulists.length; i++) {
-    let menulist = systemAccessMenulists[i];
-    let perm = systemAccessPerms[i];
-    let expectedValue = SitePermissions.get(gBrowser.currentURI, perm);
-    if (expectedValue == SitePermissions.UNKNOWN) {
-      expectedValue = SitePermissions.getDefault(perm);
-    }
-
-    is(menulist.id, "identity-popup-permission:" + perm, "Correct id for menulist: " + perm);
-    is(menulist.value, expectedValue, "Correct value on menulist: " + perm);
-  }
-
-  gIdentityHandler._identityPopup.hidden = true;
-});
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -1101,29 +1101,44 @@ file, You can obtain one at http://mozil
           }
         ]]></body>
       </method>
     </implementation>
   </binding>
 
   <binding id="urlbar-rich-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-rich-result-popup">
 
-    <content ignorekeys="true" level="top" consumeoutsideclicks="never">
-      <xul:hbox anonid="search-suggestions-notification" align="center">
+    <content ignorekeys="true" level="top" consumeoutsideclicks="never"
+             aria-owns="search-suggestions-notification richlistbox">
+      <xul:hbox anonid="search-suggestions-notification"
+                align="center"
+                role="alert"
+                aria-describedby="search-suggestions-notification-text">
         <xul:description flex="1">
           &urlbar.searchSuggestionsNotification.question;
-          <xul:label anonid="search-suggestions-notification-learn-more"
+          <!-- Several things here are to make the label accessibile via an
+               accesskey so that a11y doesn't suck: the accesskey, using an
+               onclick handler instead of an href attribute, the control
+               attribute, and having the control attribute refer to a valid ID
+               that is the label itself. -->
+          <xul:label id="search-suggestions-notification-learn-more"
                      class="text-link"
-                     value="&urlbar.searchSuggestionsNotification.learnMore;"/>
+                     role="link"
+                     value="&urlbar.searchSuggestionsNotification.learnMore;"
+                     accesskey="&urlbar.searchSuggestionsNotification.learnMore.accesskey;"
+                     onclick="document.getBindingParent(this).openSearchSuggestionsNotificationLearnMoreURL();"
+                     control="search-suggestions-notification-learn-more"/>
         </xul:description>
         <xul:button anonid="search-suggestions-notification-disable"
                     label="&urlbar.searchSuggestionsNotification.disable;"
+                    accesskey="&urlbar.searchSuggestionsNotification.disable.accesskey;"
                     onclick="document.getBindingParent(this).dismissSearchSuggestionsNotification(false);"/>
         <xul:button anonid="search-suggestions-notification-enable"
                     label="&urlbar.searchSuggestionsNotification.enable;"
+                    accesskey="&urlbar.searchSuggestionsNotification.enable.accesskey;"
                     onclick="document.getBindingParent(this).dismissSearchSuggestionsNotification(true);"/>
       </xul:hbox>
       <xul:richlistbox anonid="richlistbox" class="autocomplete-richlistbox"
                        flex="1"/>
       <xul:hbox anonid="footer">
         <children/>
       </xul:hbox>
     </content>
@@ -1138,26 +1153,29 @@ file, You can obtain one at http://mozil
       </field>
 
       <field name="searchSuggestionsNotification" readonly="true">
         document.getAnonymousElementByAttribute(
           this, "anonid", "search-suggestions-notification"
         );
       </field>
 
-      <field name="searchSuggestionsNotificationLearnMoreLink" readonly="true">
-        document.getAnonymousElementByAttribute(
-          this, "anonid", "search-suggestions-notification-learn-more"
-        );
-      </field>
-
       <field name="footer" readonly="true">
         document.getAnonymousElementByAttribute(this, "anonid", "footer");
       </field>
 
+      <method name="openSearchSuggestionsNotificationLearnMoreURL">
+        <body><![CDATA[
+        let url = Services.urlFormatter.formatURL(
+          Services.prefs.getCharPref("app.support.baseURL") + "suggestions"
+        );
+        openUILinkIn(url, "tab");
+        ]]></body>
+      </method>
+
       <method name="dismissSearchSuggestionsNotification">
         <parameter name="enableSuggestions"/>
         <body><![CDATA[
           Services.prefs.setBoolPref(
             "browser.urlbar.suggest.searches", enableSuggestions
           );
           Services.prefs.setBoolPref(
             "browser.urlbar.userMadeSearchSuggestionsChoice", true
@@ -1255,39 +1273,37 @@ file, You can obtain one at http://mozil
         </body>
       </method>
 
       <method name="_showSearchSuggestionsNotification">
         <parameter name="aInput"/>
         <parameter name="aElement"/>
         <body>
           <![CDATA[
-          // Set the learn-more link href.
-          let link = this.searchSuggestionsNotificationLearnMoreLink;
-          if (!link.hasAttribute("href")) {
-            let url = Services.urlFormatter.formatURL(
-              Services.prefs.getCharPref("app.support.baseURL") + "suggestions"
-            );
-            link.setAttribute("href", url);
-          }
-
           // With the notification shown, the listbox's height can sometimes be
           // too small when it's flexed, as it normally is.  Also, it can start
           // out slightly scrolled down.  Both problems appear together, most
           // often when the popup is very narrow and the notification's text
           // must wrap.  Work around them by removing the flex.
           //
           // But without flexing the listbox, the listbox's height animation
           // sometimes fails to complete, leaving the popup too tall.  Work
           // around that problem by disabling the listbox animation.
           this.richlistbox.flex = 0;
           this.setAttribute("dontanimate", "true");
 
           this.classList.add("showSearchSuggestionsNotification");
           this._updateFooterVisibility();
+
+          // This event allows accessibility APIs to see the notification.
+          if (!this.popupOpen) {
+            let event = document.createEvent("Events");
+            event.initEvent("AlertActive", true, true);
+            this.searchSuggestionsNotification.dispatchEvent(event);
+          }
           ]]>
         </body>
       </method>
 
       <method name="searchSuggestionsNotificationWasDismissed">
         <parameter name="enableSuggestions"/>
         <body>
           <![CDATA[
--- a/browser/components/controlcenter/content/panel.inc.xul
+++ b/browser/components/controlcenter/content/panel.inc.xul
@@ -82,22 +82,16 @@
 
       <!-- Permissions Section -->
       <hbox id="identity-popup-permissions" class="identity-popup-section">
         <vbox id="identity-popup-permissions-content" flex="1">
           <label class="identity-popup-headline"
                  value="&identity.permissions;"/>
           <vbox id="identity-popup-permission-list"/>
         </vbox>
-        <button id="identity-popup-permissions-expander"
-                class="identity-popup-expander"
-#ifndef NIGHTLY_BUILD
-                hidden="true"
-#endif
-                oncommand="gIdentityHandler.toggleSubView('permissions', this)"/>
       </hbox>
     </panelview>
 
     <!-- Security SubView -->
     <panelview id="identity-popup-securityView" flex="1">
       <vbox id="identity-popup-securityView-header">
         <label observes="identity-popup-content-host"/>
         <description class="identity-popup-connection-not-secure"
@@ -157,32 +151,10 @@
                 oncommand="gIdentityHandler.enableMixedContentProtection()"/>
 
         <!-- More Security Information -->
         <button label="&identity.moreInfoLinkText2;"
                 oncommand="gIdentityHandler.handleMoreInfoClick(event);"/>
       </vbox>
 
     </panelview>
-
-    <!-- Permissions SubView -->
-    <panelview id="identity-popup-permissionsView" flex="1">
-      <vbox id="identity-popup-permissionsView-header">
-        <label class="identity-popup-headline"
-               value="&identity.permissions;"
-               crop="end"/>
-      </vbox>
-
-      <vbox id="identity-popup-permissionsView-body">
-        <vbox id="identity-popup-permission-subview-list">
-          <label class="identity-popup-subheadline"
-                 value="&identity.permissionsPageFunctionality;"
-                 crop="end"/>
-          <vbox id="permission-subview-list-page-functionality"></vbox>
-          <label class="identity-popup-subheadline"
-                 value="&identity.permissionsSystemAccess;"
-                 crop="end"/>
-          <vbox id="permission-subview-list-system-access"></vbox>
-        </vbox>
-      </vbox>
-    </panelview>
   </panelmultiview>
 </panel>
--- a/browser/components/loop/content/css/contacts.css
+++ b/browser/components/loop/content/css/contacts.css
@@ -160,39 +160,33 @@ html[dir="rtl"] .contact-filter {
   display: block;
   z-index: 1;
 }
 
 .contact > .details {
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
-}
-
-.contact:hover > .details {
-  /* Hovering the contact shows the icons/ buttons, which takes up horizontal
-   * space. This causes the fixed-size avatar to resize horizontally, so we assign
-   * a flex value equivalent to the maximum pixel value to avoid the resizing
-   * to happen. Consider this a hack. */
-  flex: 190;
+  flex: auto;
 }
 
 .contact > .avatar {
   width: 40px;
   height: 40px;
   background-color: #ccc;
   border-radius: 50%;
   -moz-margin-end: 10px;
   overflow: hidden;
   box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.3);
   background-image: url("../shared/img/audio-call-avatar.svg");
   background-repeat: no-repeat;
   background-color: #4ba6e7;
   background-size: contain;
   -moz-user-select: none;
+  flex: none;
 }
 
 /*
  * Loop through all 12 default avatars.
  */
 .contact:nth-child(12n + 1) > .avatar.defaultAvatar {
   background-image: url("../shared/img/avatars.svg#blue-avatar");
   background-color: #4A90E2;
@@ -300,19 +294,20 @@ html[dir="rtl"] .contact-filter {
   color: #4a4a4a;
   font-size: 11px;
   line-height: 14px;
 }
 
 .icons {
   cursor: pointer;
   display: none;
-  -moz-margin-start: auto;
+  -moz-margin-start: 10px;
   color: #fff;
   -moz-user-select: none;
+  flex: none;
 }
 
 .icons:hover {
   display: block;
 }
 
 .icons i {
   display: inline-block;
--- a/browser/components/loop/content/js/contacts.js
+++ b/browser/components/loop/content/js/contacts.js
@@ -650,19 +650,16 @@ loop.contacts = (function(_, mozL10n) {
         );
       };
 
       // If no contacts to show and filter is set, then none match the search.
       if (!shownContacts.available && !shownContacts.blocked &&
           this.state.filter) {
         return (
           React.createElement("div", {className: "contact-search-list-empty"}, 
-            React.createElement("p", {className: "panel-text-large"}, 
-              mozL10n.get("no_search_results_message_heading")
-            ), 
             React.createElement("p", {className: "panel-text-medium"}, 
               mozL10n.get("contacts_no_search_results")
             )
           )
         );
       }
 
       // If no contacts to show and filter is not set, we don't have contacts.
--- a/browser/components/loop/content/js/contacts.jsx
+++ b/browser/components/loop/content/js/contacts.jsx
@@ -650,19 +650,16 @@ loop.contacts = (function(_, mozL10n) {
         );
       };
 
       // If no contacts to show and filter is set, then none match the search.
       if (!shownContacts.available && !shownContacts.blocked &&
           this.state.filter) {
         return (
           <div className="contact-search-list-empty">
-            <p className="panel-text-large">
-              {mozL10n.get("no_search_results_message_heading")}
-            </p>
             <p className="panel-text-medium">
               {mozL10n.get("contacts_no_search_results")}
             </p>
           </div>
         );
       }
 
       // If no contacts to show and filter is not set, we don't have contacts.
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -7,17 +7,17 @@ loop.conversation = (function(mozL10n) {
   "use strict";
 
   var sharedMixins = loop.shared.mixins;
   var sharedActions = loop.shared.actions;
 
   var CallControllerView = loop.conversationViews.CallControllerView;
   var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
   var FeedbackView = loop.feedbackViews.FeedbackView;
-  var GenericFailureView = loop.conversationViews.GenericFailureView;
+  var DirectCallFailureView = loop.conversationViews.DirectCallFailureView;
 
   /**
    * Master controller view for handling if incoming or outgoing calls are
    * in progress, and hence, which view to display.
    */
   var AppControllerView = React.createClass({displayName: "AppControllerView",
     mixins: [
       Backbone.Events,
@@ -79,17 +79,21 @@ loop.conversation = (function(mozL10n) {
         case "room": {
           return (React.createElement(DesktopRoomConversationView, {
             dispatcher: this.props.dispatcher, 
             mozLoop: this.props.mozLoop, 
             onCallTerminated: this.handleCallTerminated, 
             roomStore: this.props.roomStore}));
         }
         case "failed": {
-          return React.createElement(GenericFailureView, {cancelCall: this.closeWindow});
+          return (React.createElement(DirectCallFailureView, {
+            contact: {}, 
+            dispatcher: this.props.dispatcher, 
+            mozLoop: this.props.mozLoop, 
+            outgoing: false}));
         }
         default: {
           // If we don't have a windowType, we don't know what we are yet,
           // so don't display anything.
           return null;
         }
       }
     }
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -7,17 +7,17 @@ loop.conversation = (function(mozL10n) {
   "use strict";
 
   var sharedMixins = loop.shared.mixins;
   var sharedActions = loop.shared.actions;
 
   var CallControllerView = loop.conversationViews.CallControllerView;
   var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
   var FeedbackView = loop.feedbackViews.FeedbackView;
-  var GenericFailureView = loop.conversationViews.GenericFailureView;
+  var DirectCallFailureView = loop.conversationViews.DirectCallFailureView;
 
   /**
    * Master controller view for handling if incoming or outgoing calls are
    * in progress, and hence, which view to display.
    */
   var AppControllerView = React.createClass({
     mixins: [
       Backbone.Events,
@@ -79,17 +79,21 @@ loop.conversation = (function(mozL10n) {
         case "room": {
           return (<DesktopRoomConversationView
             dispatcher={this.props.dispatcher}
             mozLoop={this.props.mozLoop}
             onCallTerminated={this.handleCallTerminated}
             roomStore={this.props.roomStore} />);
         }
         case "failed": {
-          return <GenericFailureView cancelCall={this.closeWindow} />;
+          return (<DirectCallFailureView
+            contact={{}}
+            dispatcher={this.props.dispatcher}
+            mozLoop={this.props.mozLoop}
+            outgoing={false} />);
         }
         default: {
           // If we don't have a windowType, we don't know what we are yet,
           // so don't display anything.
           return null;
         }
       }
     }
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -308,62 +308,16 @@ loop.conversationViews = (function(mozL1
             )
           )
         )
       );
     }
   });
 
   /**
-   * Something went wrong view. Displayed when there's a big problem.
-   */
-  var GenericFailureView = React.createClass({displayName: "GenericFailureView",
-    mixins: [
-      sharedMixins.AudioMixin,
-      sharedMixins.DocumentTitleMixin
-    ],
-
-    propTypes: {
-      cancelCall: React.PropTypes.func.isRequired,
-      failureReason: React.PropTypes.string
-    },
-
-    componentDidMount: function() {
-      this.play("failure");
-    },
-
-    render: function() {
-      this.setTitle(mozL10n.get("generic_failure_title"));
-
-      var errorString;
-      switch (this.props.failureReason) {
-        case FAILURE_DETAILS.NO_MEDIA:
-        case FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA:
-          errorString = mozL10n.get("no_media_failure_message");
-          break;
-        default:
-          errorString = mozL10n.get("generic_failure_title");
-      }
-
-      return (
-        React.createElement("div", {className: "call-window"}, 
-          React.createElement("h2", null, errorString), 
-
-          React.createElement("div", {className: "btn-group call-action-group"}, 
-            React.createElement("button", {className: "btn btn-cancel", 
-                    onClick: this.props.cancelCall}, 
-              mozL10n.get("cancel_button")
-            )
-          )
-        )
-      );
-    }
-  });
-
-  /**
    * View for pending conversations. Displays a cancel button and appropriate
    * pending/ringing strings.
    */
   var PendingConversationView = React.createClass({displayName: "PendingConversationView",
     mixins: [sharedMixins.AudioMixin],
 
     propTypes: {
       callState: React.PropTypes.string,
@@ -414,157 +368,206 @@ loop.conversationViews = (function(mozL1
           )
 
         )
       );
     }
   });
 
   /**
-   * Call failed view. Displayed when a call fails.
+   * Used to display errors in direct calls and rooms to the user.
    */
-  var CallFailedView = React.createClass({displayName: "CallFailedView",
+  var FailureInfoView = React.createClass({displayName: "FailureInfoView",
+    propTypes: {
+      contact: React.PropTypes.object,
+      extraFailureMessage: React.PropTypes.string,
+      extraMessage: React.PropTypes.string,
+      failureReason: React.PropTypes.string.isRequired
+    },
+
+    /**
+     * Returns the translated message appropraite to the failure reason.
+     *
+     * @return {String} The translated message for the failure reason.
+     */
+    _getMessage: function() {
+      switch (this.props.failureReason) {
+        case FAILURE_DETAILS.USER_UNAVAILABLE:
+          var contactDisplayName = _getContactDisplayName(this.props.contact);
+          if (contactDisplayName.length) {
+            return mozL10n.get(
+              "contact_unavailable_title",
+              {"contactName": contactDisplayName});
+          }
+          return mozL10n.get("generic_contact_unavailable_title");
+        case FAILURE_DETAILS.NO_MEDIA:
+        case FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA:
+          return mozL10n.get("no_media_failure_message");
+        default:
+          return mozL10n.get("generic_failure_message");
+      }
+    },
+
+    _renderExtraMessage: function() {
+      if (this.props.extraMessage) {
+        return React.createElement("p", {className: "failure-info-extra"}, this.props.extraMessage);
+      }
+      return null;
+    },
+
+    _renderExtraFailureMessage: function() {
+      if (this.props.extraFailureMessage) {
+        return React.createElement("p", {className: "failure-info-extra-failure"}, this.props.extraFailureMessage);
+      }
+      return null;
+    },
+
+    render: function() {
+      return (
+        React.createElement("div", {className: "failure-info"}, 
+          React.createElement("div", {className: "failure-info-logo"}), 
+          React.createElement("h2", {className: "failure-info-message"}, this._getMessage()), 
+          this._renderExtraMessage(), 
+          this._renderExtraFailureMessage()
+        )
+      );
+    }
+  });
+
+  /**
+   * Direct Call failure view. Displayed when a call fails.
+   */
+  var DirectCallFailureView = React.createClass({displayName: "DirectCallFailureView",
     mixins: [
       Backbone.Events,
       loop.store.StoreMixin("conversationStore"),
       sharedMixins.AudioMixin,
       sharedMixins.WindowCloseMixin
     ],
 
     propTypes: {
-      contact: React.PropTypes.object.isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       // This is used by the UI showcase.
       emailLinkError: React.PropTypes.bool,
+      mozLoop: React.PropTypes.object.isRequired,
       outgoing: React.PropTypes.bool.isRequired
     },
 
     getInitialState: function() {
-      return {
+      return _.extend({
         emailLinkError: this.props.emailLinkError,
         emailLinkButtonDisabled: false
-      };
+      }, this.getStoreState());
     },
 
     componentDidMount: function() {
       this.play("failure");
       this.listenTo(this.getStore(), "change:emailLink",
                     this._onEmailLinkReceived);
       this.listenTo(this.getStore(), "error:emailLink",
                     this._onEmailLinkError);
     },
 
     componentWillUnmount: function() {
       this.stopListening(this.getStore());
     },
 
     _onEmailLinkReceived: function() {
       var emailLink = this.getStoreState().emailLink;
-      var contactEmail = _getPreferredEmail(this.props.contact).value;
+      var contactEmail = _getPreferredEmail(this.state.contact).value;
       sharedUtils.composeCallUrlEmail(emailLink, contactEmail, null, "callfailed");
       this.closeWindow();
     },
 
     _onEmailLinkError: function() {
       this.setState({
         emailLinkError: true,
         emailLinkButtonDisabled: false
       });
     },
 
-    _renderError: function() {
-      if (!this.state.emailLinkError) {
-        return;
-      }
-      return React.createElement("p", {className: "error"}, mozL10n.get("unable_retrieve_url"));
-    },
-
-    _getTitleMessage: function() {
-      switch (this.getStoreState().callStateReason) {
-        case FAILURE_DETAILS.USER_UNAVAILABLE:
-          var contactDisplayName = _getContactDisplayName(this.props.contact);
-          if (contactDisplayName.length) {
-            return mozL10n.get(
-              "contact_unavailable_title",
-              {"contactName": contactDisplayName});
-          }
-
-          return mozL10n.get("generic_contact_unavailable_title");
-        case FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA:
-          return mozL10n.get("no_media_failure_message");
-        default:
-          return mozL10n.get("generic_failure_title");
-      }
-    },
-
     retryCall: function() {
       this.props.dispatcher.dispatch(new sharedActions.RetryCall());
     },
 
     cancelCall: function() {
       this.props.dispatcher.dispatch(new sharedActions.CancelCall());
     },
 
     emailLink: function() {
       this.setState({
         emailLinkError: false,
         emailLinkButtonDisabled: true
       });
 
       this.props.dispatcher.dispatch(new sharedActions.FetchRoomEmailLink({
-        roomName: _getContactDisplayName(this.props.contact)
+        roomName: _getContactDisplayName(this.state.contact)
       }));
     },
 
-    _renderMessage: function() {
-      if (this.props.outgoing) {
-        return  (React.createElement("p", {className: "btn-label"}, mozL10n.get("generic_failure_with_reason2")));
-      }
-
-      return null;
-    },
-
     render: function() {
       var cx = React.addons.classSet;
 
       var retryClasses = cx({
         btn: true,
         "btn-info": true,
         "btn-retry": true,
         hide: !this.props.outgoing
       });
       var emailClasses = cx({
         btn: true,
         "btn-info": true,
         "btn-email": true,
         hide: !this.props.outgoing
       });
 
+      var settingsMenuItems = [
+        { id: "feedback" },
+        { id: "help" }
+      ];
+
+      var extraMessage;
+
+      if (this.props.outgoing) {
+        extraMessage = mozL10n.get("generic_failure_with_reason2");
+      }
+
+      var extraFailureMessage;
+
+      if (this.state.emailLinkError) {
+        extraFailureMessage = mozL10n.get("unable_retrieve_url");
+      }
+
       return (
-        React.createElement("div", {className: "call-window"}, 
-          React.createElement("h2", null,  this._getTitleMessage() ), 
-
-          this._renderMessage(), 
-          this._renderError(), 
+        React.createElement("div", {className: "direct-call-failure"}, 
+          React.createElement(FailureInfoView, {
+            contact: this.state.contact, 
+            extraFailureMessage: extraFailureMessage, 
+            extraMessage: extraMessage, 
+            failureReason: this.getStoreState().callStateReason}), 
 
           React.createElement("div", {className: "btn-group call-action-group"}, 
             React.createElement("button", {className: "btn btn-cancel", 
                     onClick: this.cancelCall}, 
               mozL10n.get("cancel_button")
             ), 
             React.createElement("button", {className: retryClasses, 
                     onClick: this.retryCall}, 
               mozL10n.get("retry_call_button")
             ), 
             React.createElement("button", {className: emailClasses, 
                     disabled: this.state.emailLinkButtonDisabled, 
                     onClick: this.emailLink}, 
               mozL10n.get("share_button3")
             )
-          )
+          ), 
+          React.createElement(loop.shared.views.SettingsControlButton, {
+            menuBelow: true, 
+            menuItems: settingsMenuItems, 
+            mozLoop: this.props.mozLoop})
         )
       );
     }
   });
 
   var OngoingConversationView = React.createClass({displayName: "OngoingConversationView",
     mixins: [
       sharedMixins.MediaSetupMixin
@@ -802,19 +805,19 @@ loop.conversationViews = (function(mozL1
       }
 
       switch (this.state.callState) {
         case CALL_STATES.CLOSE: {
           this._closeWindow();
           return null;
         }
         case CALL_STATES.TERMINATED: {
-          return (React.createElement(CallFailedView, {
-            contact: this.state.contact, 
+          return (React.createElement(DirectCallFailureView, {
             dispatcher: this.props.dispatcher, 
+            mozLoop: this.props.mozLoop, 
             outgoing: this.state.outgoing}));
         }
         case CALL_STATES.ONGOING: {
           return (React.createElement(OngoingConversationView, {
             audio: { enabled: !this.state.audioMuted, visible: true}, 
             conversationStore: this.getStore(), 
             dispatcher: this.props.dispatcher, 
             mediaConnected: this.state.mediaConnected, 
@@ -841,17 +844,17 @@ loop.conversationViews = (function(mozL1
       }
     }
   });
 
   return {
     PendingConversationView: PendingConversationView,
     CallIdentifierView: CallIdentifierView,
     ConversationDetailView: ConversationDetailView,
-    CallFailedView: CallFailedView,
     _getContactDisplayName: _getContactDisplayName,
-    GenericFailureView: GenericFailureView,
+    FailureInfoView: FailureInfoView,
+    DirectCallFailureView: DirectCallFailureView,
     AcceptCallView: AcceptCallView,
     OngoingConversationView: OngoingConversationView,
     CallControllerView: CallControllerView
   };
 
 })(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -308,62 +308,16 @@ loop.conversationViews = (function(mozL1
             </div>
           </div>
         </div>
       );
     }
   });
 
   /**
-   * Something went wrong view. Displayed when there's a big problem.
-   */
-  var GenericFailureView = React.createClass({
-    mixins: [
-      sharedMixins.AudioMixin,
-      sharedMixins.DocumentTitleMixin
-    ],
-
-    propTypes: {
-      cancelCall: React.PropTypes.func.isRequired,
-      failureReason: React.PropTypes.string
-    },
-
-    componentDidMount: function() {
-      this.play("failure");
-    },
-
-    render: function() {
-      this.setTitle(mozL10n.get("generic_failure_title"));
-
-      var errorString;
-      switch (this.props.failureReason) {
-        case FAILURE_DETAILS.NO_MEDIA:
-        case FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA:
-          errorString = mozL10n.get("no_media_failure_message");
-          break;
-        default:
-          errorString = mozL10n.get("generic_failure_title");
-      }
-
-      return (
-        <div className="call-window">
-          <h2>{errorString}</h2>
-
-          <div className="btn-group call-action-group">
-            <button className="btn btn-cancel"
-                    onClick={this.props.cancelCall}>
-              {mozL10n.get("cancel_button")}
-            </button>
-          </div>
-        </div>
-      );
-    }
-  });
-
-  /**
    * View for pending conversations. Displays a cancel button and appropriate
    * pending/ringing strings.
    */
   var PendingConversationView = React.createClass({
     mixins: [sharedMixins.AudioMixin],
 
     propTypes: {
       callState: React.PropTypes.string,
@@ -414,157 +368,206 @@ loop.conversationViews = (function(mozL1
           </div>
 
         </ConversationDetailView>
       );
     }
   });
 
   /**
-   * Call failed view. Displayed when a call fails.
+   * Used to display errors in direct calls and rooms to the user.
    */
-  var CallFailedView = React.createClass({
+  var FailureInfoView = React.createClass({
+    propTypes: {
+      contact: React.PropTypes.object,
+      extraFailureMessage: React.PropTypes.string,
+      extraMessage: React.PropTypes.string,
+      failureReason: React.PropTypes.string.isRequired
+    },
+
+    /**
+     * Returns the translated message appropraite to the failure reason.
+     *
+     * @return {String} The translated message for the failure reason.
+     */
+    _getMessage: function() {
+      switch (this.props.failureReason) {
+        case FAILURE_DETAILS.USER_UNAVAILABLE:
+          var contactDisplayName = _getContactDisplayName(this.props.contact);
+          if (contactDisplayName.length) {
+            return mozL10n.get(
+              "contact_unavailable_title",
+              {"contactName": contactDisplayName});
+          }
+          return mozL10n.get("generic_contact_unavailable_title");
+        case FAILURE_DETAILS.NO_MEDIA:
+        case FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA:
+          return mozL10n.get("no_media_failure_message");
+        default:
+          return mozL10n.get("generic_failure_message");
+      }
+    },
+
+    _renderExtraMessage: function() {
+      if (this.props.extraMessage) {
+        return <p className="failure-info-extra">{this.props.extraMessage}</p>;
+      }
+      return null;
+    },
+
+    _renderExtraFailureMessage: function() {
+      if (this.props.extraFailureMessage) {
+        return <p className="failure-info-extra-failure">{this.props.extraFailureMessage}</p>;
+      }
+      return null;
+    },
+
+    render: function() {
+      return (
+        <div className="failure-info">
+          <div className="failure-info-logo" />
+          <h2 className="failure-info-message">{this._getMessage()}</h2>
+          {this._renderExtraMessage()}
+          {this._renderExtraFailureMessage()}
+        </div>
+      );
+    }
+  });
+
+  /**
+   * Direct Call failure view. Displayed when a call fails.
+   */
+  var DirectCallFailureView = React.createClass({
     mixins: [
       Backbone.Events,
       loop.store.StoreMixin("conversationStore"),
       sharedMixins.AudioMixin,
       sharedMixins.WindowCloseMixin
     ],
 
     propTypes: {
-      contact: React.PropTypes.object.isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       // This is used by the UI showcase.
       emailLinkError: React.PropTypes.bool,
+      mozLoop: React.PropTypes.object.isRequired,
       outgoing: React.PropTypes.bool.isRequired
     },
 
     getInitialState: function() {
-      return {
+      return _.extend({
         emailLinkError: this.props.emailLinkError,
         emailLinkButtonDisabled: false
-      };
+      }, this.getStoreState());
     },
 
     componentDidMount: function() {
       this.play("failure");
       this.listenTo(this.getStore(), "change:emailLink",
                     this._onEmailLinkReceived);
       this.listenTo(this.getStore(), "error:emailLink",
                     this._onEmailLinkError);
     },
 
     componentWillUnmount: function() {
       this.stopListening(this.getStore());
     },
 
     _onEmailLinkReceived: function() {
       var emailLink = this.getStoreState().emailLink;
-      var contactEmail = _getPreferredEmail(this.props.contact).value;
+      var contactEmail = _getPreferredEmail(this.state.contact).value;
       sharedUtils.composeCallUrlEmail(emailLink, contactEmail, null, "callfailed");
       this.closeWindow();
     },
 
     _onEmailLinkError: function() {
       this.setState({
         emailLinkError: true,
         emailLinkButtonDisabled: false
       });
     },
 
-    _renderError: function() {
-      if (!this.state.emailLinkError) {
-        return;
-      }
-      return <p className="error">{mozL10n.get("unable_retrieve_url")}</p>;
-    },
-
-    _getTitleMessage: function() {
-      switch (this.getStoreState().callStateReason) {
-        case FAILURE_DETAILS.USER_UNAVAILABLE:
-          var contactDisplayName = _getContactDisplayName(this.props.contact);
-          if (contactDisplayName.length) {
-            return mozL10n.get(
-              "contact_unavailable_title",
-              {"contactName": contactDisplayName});
-          }
-
-          return mozL10n.get("generic_contact_unavailable_title");
-        case FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA:
-          return mozL10n.get("no_media_failure_message");
-        default:
-          return mozL10n.get("generic_failure_title");
-      }
-    },
-
     retryCall: function() {
       this.props.dispatcher.dispatch(new sharedActions.RetryCall());
     },
 
     cancelCall: function() {
       this.props.dispatcher.dispatch(new sharedActions.CancelCall());
     },
 
     emailLink: function() {
       this.setState({
         emailLinkError: false,
         emailLinkButtonDisabled: true
       });
 
       this.props.dispatcher.dispatch(new sharedActions.FetchRoomEmailLink({
-        roomName: _getContactDisplayName(this.props.contact)
+        roomName: _getContactDisplayName(this.state.contact)
       }));
     },
 
-    _renderMessage: function() {
-      if (this.props.outgoing) {
-        return  (<p className="btn-label">{mozL10n.get("generic_failure_with_reason2")}</p>);
-      }
-
-      return null;
-    },
-
     render: function() {
       var cx = React.addons.classSet;
 
       var retryClasses = cx({
         btn: true,
         "btn-info": true,
         "btn-retry": true,
         hide: !this.props.outgoing
       });
       var emailClasses = cx({
         btn: true,
         "btn-info": true,
         "btn-email": true,
         hide: !this.props.outgoing
       });
 
+      var settingsMenuItems = [
+        { id: "feedback" },
+        { id: "help" }
+      ];
+
+      var extraMessage;
+
+      if (this.props.outgoing) {
+        extraMessage = mozL10n.get("generic_failure_with_reason2");
+      }
+
+      var extraFailureMessage;
+
+      if (this.state.emailLinkError) {
+        extraFailureMessage = mozL10n.get("unable_retrieve_url");
+      }
+
       return (
-        <div className="call-window">
-          <h2>{ this._getTitleMessage() }</h2>
-
-          {this._renderMessage()}
-          {this._renderError()}
+        <div className="direct-call-failure">
+          <FailureInfoView
+            contact={this.state.contact}
+            extraFailureMessage={extraFailureMessage}
+            extraMessage={extraMessage}
+            failureReason={this.getStoreState().callStateReason}/>
 
           <div className="btn-group call-action-group">
             <button className="btn btn-cancel"
                     onClick={this.cancelCall}>
               {mozL10n.get("cancel_button")}
             </button>
             <button className={retryClasses}
                     onClick={this.retryCall}>
               {mozL10n.get("retry_call_button")}
             </button>
             <button className={emailClasses}
                     disabled={this.state.emailLinkButtonDisabled}
                     onClick={this.emailLink}>
               {mozL10n.get("share_button3")}
             </button>
           </div>
+          <loop.shared.views.SettingsControlButton
+            menuBelow={true}
+            menuItems={settingsMenuItems}
+            mozLoop={this.props.mozLoop} />
         </div>
       );
     }
   });
 
   var OngoingConversationView = React.createClass({
     mixins: [
       sharedMixins.MediaSetupMixin
@@ -802,19 +805,19 @@ loop.conversationViews = (function(mozL1
       }
 
       switch (this.state.callState) {
         case CALL_STATES.CLOSE: {
           this._closeWindow();
           return null;
         }
         case CALL_STATES.TERMINATED: {
-          return (<CallFailedView
-            contact={this.state.contact}
+          return (<DirectCallFailureView
             dispatcher={this.props.dispatcher}
+            mozLoop={this.props.mozLoop}
             outgoing={this.state.outgoing} />);
         }
         case CALL_STATES.ONGOING: {
           return (<OngoingConversationView
             audio={{ enabled: !this.state.audioMuted, visible: true }}
             conversationStore={this.getStore()}
             dispatcher={this.props.dispatcher}
             mediaConnected={this.state.mediaConnected}
@@ -841,17 +844,17 @@ loop.conversationViews = (function(mozL1
       }
     }
   });
 
   return {
     PendingConversationView: PendingConversationView,
     CallIdentifierView: CallIdentifierView,
     ConversationDetailView: ConversationDetailView,
-    CallFailedView: CallFailedView,
     _getContactDisplayName: _getContactDisplayName,
-    GenericFailureView: GenericFailureView,
+    FailureInfoView: FailureInfoView,
+    DirectCallFailureView: DirectCallFailureView,
     AcceptCallView: AcceptCallView,
     OngoingConversationView: OngoingConversationView,
     CallControllerView: CallControllerView
   };
 
 })(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/content/js/roomStore.js
+++ b/browser/components/loop/content/js/roomStore.js
@@ -319,17 +319,17 @@ loop.store = loop.store || {};
         error: actionData.error,
         pendingCreation: false
       });
 
       // XXX Needs a more descriptive error - bug 1109151.
       this._notifications.set({
         id: "create-room-error",
         level: "error",
-        message: mozL10n.get("generic_failure_title")
+        message: mozL10n.get("generic_failure_message")
       });
     },
 
     /**
      * Copy a room url.
      *
      * @param  {sharedActions.CopyRoomUrl} actionData The action data.
      */
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -69,16 +69,60 @@ loop.roomViews = (function(mozL10n) {
       return _.extend({
         // Used by the UI showcase.
         roomState: this.props.roomState || storeState.roomState,
         savingContext: false
       }, storeState);
     }
   };
 
+  /**
+   * Something went wrong view. Displayed when there's a big problem.
+   */
+  var RoomFailureView = React.createClass({displayName: "RoomFailureView",
+    mixins: [ sharedMixins.AudioMixin ],
+
+    propTypes: {
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      failureReason: React.PropTypes.string,
+      mozLoop: React.PropTypes.object.isRequired
+    },
+
+    componentDidMount: function() {
+      this.play("failure");
+    },
+
+    handleRejoinCall: function() {
+      this.props.dispatcher.dispatch(new sharedActions.JoinRoom());
+    },
+
+    render: function() {
+      var settingsMenuItems = [
+        { id: "feedback" },
+        { id: "help" }
+      ];
+      return (
+        React.createElement("div", {className: "room-failure"}, 
+          React.createElement(loop.conversationViews.FailureInfoView, {
+            failureReason: this.props.failureReason}), 
+          React.createElement("div", {className: "btn-group call-action-group"}, 
+            React.createElement("button", {className: "btn btn-info btn-rejoin", 
+                    onClick: this.handleRejoinCall}, 
+              mozL10n.get("rejoin_button")
+            )
+          ), 
+          React.createElement(loop.shared.views.SettingsControlButton, {
+            menuBelow: true, 
+            menuItems: settingsMenuItems, 
+            mozLoop: this.props.mozLoop})
+        )
+      );
+    }
+  });
+
   var SocialShareDropdown = React.createClass({displayName: "SocialShareDropdown",
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       roomUrl: React.PropTypes.string,
       show: React.PropTypes.bool.isRequired,
       socialShareProviders: React.PropTypes.array
     },
 
@@ -721,19 +765,20 @@ loop.roomViews = (function(mozL10n) {
       var roomData = this.props.roomStore.getStoreState("activeRoom");
 
       switch(this.state.roomState) {
         case ROOM_STATES.FAILED:
         case ROOM_STATES.FULL: {
           // Note: While rooms are set to hold a maximum of 2 participants, the
           //       FULL case should never happen on desktop.
           return (
-            React.createElement(loop.conversationViews.GenericFailureView, {
-              cancelCall: this.closeWindow, 
-              failureReason: this.state.failureReason})
+            React.createElement(RoomFailureView, {
+              dispatcher: this.props.dispatcher, 
+              failureReason: this.state.failureReason, 
+              mozLoop: this.props.mozLoop})
           );
         }
         case ROOM_STATES.ENDED: {
           // When conversation ended we either display a feedback form or
           // close the window. This is decided in the AppControllerView.
           return null;
         }
         default: {
@@ -799,15 +844,16 @@ loop.roomViews = (function(mozL10n) {
           );
         }
       }
     }
   });
 
   return {
     ActiveRoomStoreMixin: ActiveRoomStoreMixin,
+    RoomFailureView: RoomFailureView,
     SocialShareDropdown: SocialShareDropdown,
     DesktopRoomEditContextView: DesktopRoomEditContextView,
     DesktopRoomConversationView: DesktopRoomConversationView,
     DesktopRoomInvitationView: DesktopRoomInvitationView
   };
 
 })(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -69,16 +69,60 @@ loop.roomViews = (function(mozL10n) {
       return _.extend({
         // Used by the UI showcase.
         roomState: this.props.roomState || storeState.roomState,
         savingContext: false
       }, storeState);
     }
   };
 
+  /**
+   * Something went wrong view. Displayed when there's a big problem.
+   */
+  var RoomFailureView = React.createClass({
+    mixins: [ sharedMixins.AudioMixin ],
+
+    propTypes: {
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      failureReason: React.PropTypes.string,
+      mozLoop: React.PropTypes.object.isRequired
+    },
+
+    componentDidMount: function() {
+      this.play("failure");
+    },
+
+    handleRejoinCall: function() {
+      this.props.dispatcher.dispatch(new sharedActions.JoinRoom());
+    },
+
+    render: function() {
+      var settingsMenuItems = [
+        { id: "feedback" },
+        { id: "help" }
+      ];
+      return (
+        <div className="room-failure">
+          <loop.conversationViews.FailureInfoView
+            failureReason={this.props.failureReason} />
+          <div className="btn-group call-action-group">
+            <button className="btn btn-info btn-rejoin"
+                    onClick={this.handleRejoinCall}>
+              {mozL10n.get("rejoin_button")}
+            </button>
+          </div>
+          <loop.shared.views.SettingsControlButton
+            menuBelow={true}
+            menuItems={settingsMenuItems}
+            mozLoop={this.props.mozLoop} />
+        </div>
+      );
+    }
+  });
+
   var SocialShareDropdown = React.createClass({
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       roomUrl: React.PropTypes.string,
       show: React.PropTypes.bool.isRequired,
       socialShareProviders: React.PropTypes.array
     },
 
@@ -721,19 +765,20 @@ loop.roomViews = (function(mozL10n) {
       var roomData = this.props.roomStore.getStoreState("activeRoom");
 
       switch(this.state.roomState) {
         case ROOM_STATES.FAILED:
         case ROOM_STATES.FULL: {
           // Note: While rooms are set to hold a maximum of 2 participants, the
           //       FULL case should never happen on desktop.
           return (
-            <loop.conversationViews.GenericFailureView
-              cancelCall={this.closeWindow}
-              failureReason={this.state.failureReason} />
+            <RoomFailureView
+              dispatcher={this.props.dispatcher}
+              failureReason={this.state.failureReason}
+              mozLoop={this.props.mozLoop} />
           );
         }
         case ROOM_STATES.ENDED: {
           // When conversation ended we either display a feedback form or
           // close the window. This is decided in the AppControllerView.
           return null;
         }
         default: {
@@ -799,15 +844,16 @@ loop.roomViews = (function(mozL10n) {
           );
         }
       }
     }
   });
 
   return {
     ActiveRoomStoreMixin: ActiveRoomStoreMixin,
+    RoomFailureView: RoomFailureView,
     SocialShareDropdown: SocialShareDropdown,
     DesktopRoomEditContextView: DesktopRoomEditContextView,
     DesktopRoomConversationView: DesktopRoomConversationView,
     DesktopRoomInvitationView: DesktopRoomInvitationView
   };
 
 })(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -203,16 +203,17 @@ html[dir="rtl"] .conversation-toolbar-bt
   content: url("../img/svg/video-mute-hover.svg");
 }
 
 .btn-settings {
   width: 28px;
   height: 28px;
   background-size: 28px;
   background-image: url("../img/svg/settings.svg");
+  background-color: transparent;
 }
 
 .btn-settings:hover,
 .btn-settings:active {
   background-image: url("../img/svg/settings-hover.svg");
 }
 
 .btn-screen-share {
@@ -266,68 +267,139 @@ html[dir="rtl"] .conversation-toolbar-bt
 
 /* Call ended view */
 .call-ended p {
   text-align: center;
 }
 
 /* General Call (incoming or outgoing). */
 
+/* XXX call-window currently relates to multiple things like the AcceptCallView,
+   DirectCallFailureView, PendingConversationView. It doesn't relate to the direct
+   call media views. We should probably make this more explicit at some stage. */
+
 /*
  * Height matches the height of the docked window
  * but the UI breaks when you pop out
  * Bug 1040985
  */
 .call-window {
   display: flex;
   flex-direction: column;
   align-items: center;
   justify-content: space-between;
   min-height: 230px;
 }
 
-.call-window > .btn-label {
-  text-align: center;
-}
-
-.call-window > .error {
-  text-align: center;
-  color: #f00;
-  font-size: 90%;
-}
-
 .call-action-group {
   display: flex;
-  padding: 2.5em 4px 0 4px;
+  padding: 0 4px;
   width: 100%;
 }
 
 .call-action-group > .btn,
 .room-context > .btn {
-  min-height: 26px;
-  border-radius: 2px;
+  min-height: 30px;
+  border-radius: 4px;
   margin: 0 4px;
   min-width: 64px;
 }
 
+.call-action-group > .btn {
+  max-width: 48%;
+  flex-grow: 1;
+}
+
+.call-action-group > .btn-rejoin {
+  max-width: 100%;
+}
+
 .call-action-group .btn-group-chevron,
 .call-action-group .btn-group {
   width: 100%;
 }
 
-/* XXX Once we get the incoming call avatar, bug 1047435, the H2 should
- * disappear from our markup, and we should remove this rule entirely.
- */
-.call-window h2 {
-  font-size: 1.5em;
-  font-weight: normal;
+.direct-call-failure,
+.room-failure {
+  /* This flex allows us to not calculate the height of the logo area
+     versus the buttons */
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: space-between;
+  min-height: 230px;
+  height: 100%;
+}
+
+.direct-call-failure > .call-action-group,
+.room-failure > .call-action-group {
+  flex: none;
+  margin: 1rem 0 2rem;
+}
+
+.direct-call-failure > .failure-info,
+.room-failure > .failure-info {
+  flex: auto;
+}
+
+.direct-call-failure > .settings-control,
+.room-failure > .settings-control {
+  position: absolute;
+  top: 1rem;
+  right: .5rem;
+}
+
+html[dir="rtl"] .direct-call-failure > .settings-control,
+html[dir="rtl"] .room-failure > .settings-control {
+  left: .5rem;
+  right: auto;
+}
+
+.failure-info {
   text-align: center;
-  /* compensate for reset.css overriding this; values borrowed from
-     Firefox Mac html.css */
-  margin: 0.83em 0;
+  /* This flex is designed to set the logo in a standard place, but if the
+     text below needs more space (due to multi-line), then the logo will move
+     higher out the way */
+  display: flex;
+  flex-direction: column;
+  /* Matches 4px padding of .btn-group plus 4px of margin for .btn */
+  padding: 0 0.8rem;
+}
+
+.failure-info-logo {
+  height: 90px;
+  background-image: url("../img/sad_hello_icon_64x64.svg");
+  background-position: center center;
+  background-size: contain;
+  background-repeat: no-repeat;
+  flex: 1;
+  background-size: 90px 90px;
+  /* Don't let the logo take up too much space, e.g. if there's only one line of
+     text. */
+  max-height: calc(90px + 4rem);
+  margin-top: 1rem;
+}
+
+.failure-info-message {
+  margin: 0.25rem 0px;
+  text-align: center;
+  font-weight: bold;
+  font-size: 1.2rem;
+  color: #333;
+  flex: none;
+}
+
+.failure-info-extra,
+.failure-info-extra-failure {
+  margin: 0.25rem 0;
+  flex: none;
+}
+
+.failure-info-extra-failure {
+  color: #f00;
 }
 
 .fx-embedded-call-button-spacer {
   display: flex;
   flex: 1;
 }
 
 /*
@@ -391,16 +463,21 @@ html[dir="rtl"] .conversation-toolbar-bt
   right: 14px;
 }
 
 html[dir="rtl"] .settings-menu.dropdown-menu {
   left: 14px;
   right: auto;
 }
 
+.settings-menu.dropdown-menu.menu-below {
+  top: 11.5rem;
+  bottom: auto;
+}
+
 /* Expired call url page */
 
 .expired-url-info {
   width: 400px;
   margin: 0 auto;
 }
 
 .promote-firefox {
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/img/sad_hello_icon_64x64.svg
@@ -0,0 +1,1 @@
+<svg width="180" height="173" viewBox="0 0 180 173" xmlns="http://www.w3.org/2000/svg"><title>Firefox-Hello_icon_64x64 7 + Oval 49 + Bitmap 62</title><path d="M89.994 0C40.304 0 0 35.475 0 79.218c0 21.78 9.993 41.513 26.146 55.832-2.806 9.91-8.362 23.362-19.34 36.492 1.88 3.327 32.794-8.41 54.588-17.188 8.993 2.64 18.597 4.084 28.6 4.084 49.706 0 90.006-35.47 90.006-79.22C180 35.475 139.7 0 89.994 0zm26.922 47.5c6.566 0 11.894 5.343 11.894 11.923 0 6.598-5.328 11.93-11.894 11.93-6.576 0-11.907-5.332-11.907-11.93 0-6.58 5.33-11.923 11.906-11.923zm-54.3 0c6.564 0 11.903 5.343 11.903 11.923 0 6.598-5.34 11.93-11.905 11.93-6.578 0-11.908-5.332-11.908-11.93 0-6.58 5.33-11.923 11.908-11.923zm60.346 42.758l-64.72 24.27c-2.09.784-3.15 3.115-2.366 5.207.784 2.092 3.115 3.15 5.207 2.367l64.72-24.27c2.09-.784 3.15-3.116 2.366-5.207-.785-2.092-3.117-3.152-5.208-2.367z" fill="#9B9B9B" fill-rule="evenodd"/></svg>
\ No newline at end of file
--- a/browser/components/loop/content/shared/js/store.js
+++ b/browser/components/loop/content/shared/js/store.js
@@ -112,25 +112,26 @@ loop.store.createStore = (function() {
  *       mixins: [StoreMixin("roomStore")]
  *     });
  */
 loop.store.StoreMixin = (function() {
   "use strict";
 
   var _stores = {};
   function StoreMixin(id) {
-    function _getStore() {
-      if (!_stores[id]) {
-        throw new Error("Unavailable store " + id);
-      }
-      return _stores[id];
-    }
     return {
       getStore: function() {
-        return _getStore();
+        // Allows the ui-showcase to override the specified store.
+        if (id in this.props) {
+          return this.props[id];
+        }
+        if (!_stores[id]) {
+          throw new Error("Unavailable store " + id);
+        }
+        return _stores[id];
       },
       getStoreState: function() {
         return this.getStore().getStoreState();
       },
       componentWillMount: function() {
         this.getStore().on("change", function() {
           this.setState(this.getStoreState());
         }, this);
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -180,25 +180,33 @@ loop.shared.views = (function(_, mozL10n
     }
   });
 
   /**
    * Settings control button.
    */
   var SettingsControlButton = React.createClass({displayName: "SettingsControlButton",
     propTypes: {
+      // Set to true if the menu should be below the button rather than above.
+      menuBelow: React.PropTypes.bool,
       menuItems: React.PropTypes.array,
       mozLoop: React.PropTypes.object
     },
 
     mixins: [
       sharedMixins.DropdownMenuMixin(),
       React.addons.PureRenderMixin
     ],
 
+    getDefaultProps: function() {
+      return {
+        menuBelow: false
+      };
+    },
+
     /**
      * Show or hide the settings menu
      */
     handleClick: function(event) {
       event.preventDefault();
       this.toggleDropdownMenu();
     },
 
@@ -299,23 +307,24 @@ loop.shared.views = (function(_, mozL10n
       if (!menuItemRows || !menuItemRows.length) {
         return null;
       }
 
       var cx = React.addons.classSet;
       var settingsDropdownMenuClasses = cx({
         "settings-menu": true,
         "dropdown-menu": true,
+        "menu-below": this.props.menuBelow,
         "hide": !this.state.showMenu
       });
       return (
-        React.createElement("div", null, 
+        React.createElement("div", {className: "settings-control"}, 
           React.createElement("button", {className: "btn btn-settings transparent-button", 
              onClick: this.toggleDropdownMenu, 
-             ref: "menu-button", 
+             ref: "anchor", 
              title: mozL10n.get("settings_menu_button_tooltip")}), 
           React.createElement("ul", {className: settingsDropdownMenuClasses, ref: "menu"}, 
             menuItemRows
           )
         )
       );
     }
   });
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -180,25 +180,33 @@ loop.shared.views = (function(_, mozL10n
     }
   });
 
   /**
    * Settings control button.
    */
   var SettingsControlButton = React.createClass({
     propTypes: {
+      // Set to true if the menu should be below the button rather than above.
+      menuBelow: React.PropTypes.bool,
       menuItems: React.PropTypes.array,
       mozLoop: React.PropTypes.object
     },
 
     mixins: [
       sharedMixins.DropdownMenuMixin(),
       React.addons.PureRenderMixin
     ],
 
+    getDefaultProps: function() {
+      return {
+        menuBelow: false
+      };
+    },
+
     /**
      * Show or hide the settings menu
      */
     handleClick: function(event) {
       event.preventDefault();
       this.toggleDropdownMenu();
     },
 
@@ -299,23 +307,24 @@ loop.shared.views = (function(_, mozL10n
       if (!menuItemRows || !menuItemRows.length) {
         return null;
       }
 
       var cx = React.addons.classSet;
       var settingsDropdownMenuClasses = cx({
         "settings-menu": true,
         "dropdown-menu": true,
+        "menu-below": this.props.menuBelow,
         "hide": !this.state.showMenu
       });
       return (
-        <div>
+        <div className="settings-control">
           <button className="btn btn-settings transparent-button"
              onClick={this.toggleDropdownMenu}
-             ref="menu-button"
+             ref="anchor"
              title={mozL10n.get("settings_menu_button_tooltip")} />
           <ul className={settingsDropdownMenuClasses} ref="menu">
             {menuItemRows}
           </ul>
         </div>
       );
     }
   });
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -35,16 +35,17 @@ browser.jar:
   content/browser/loop/shared/img/helloicon.svg                 (content/shared/img/helloicon.svg)
   content/browser/loop/shared/img/icon_32.png                   (content/shared/img/icon_32.png)
   content/browser/loop/shared/img/icon_64.png                   (content/shared/img/icon_64.png)
   content/browser/loop/shared/img/spinner.svg                   (content/shared/img/spinner.svg)
   # XXX could get rid of the png spinner usages and replace them with the svg
   # one?
   content/browser/loop/shared/img/spinner.png                   (content/shared/img/spinner.png)
   content/browser/loop/shared/img/spinner@2x.png                (content/shared/img/spinner@2x.png)
+  content/browser/loop/shared/img/sad_hello_icon_64x64.svg      (content/shared/img/sad_hello_icon_64x64.svg)
   content/browser/loop/shared/img/chatbubble-arrow-left.svg     (content/shared/img/chatbubble-arrow-left.svg)
   content/browser/loop/shared/img/chatbubble-arrow-right.svg    (content/shared/img/chatbubble-arrow-right.svg)
   content/browser/loop/shared/img/audio-inverse-14x14.png       (content/shared/img/audio-inverse-14x14.png)
   content/browser/loop/shared/img/audio-inverse-14x14@2x.png    (content/shared/img/audio-inverse-14x14@2x.png)
   content/browser/loop/shared/img/facemute-14x14.png            (content/shared/img/facemute-14x14.png)
   content/browser/loop/shared/img/facemute-14x14@2x.png         (content/shared/img/facemute-14x14@2x.png)
   content/browser/loop/shared/img/hangup-inverse-14x14.png      (content/shared/img/hangup-inverse-14x14.png)
   content/browser/loop/shared/img/hangup-inverse-14x14@2x.png   (content/shared/img/hangup-inverse-14x14@2x.png)
--- a/browser/components/loop/modules/MozLoopService.jsm
+++ b/browser/components/loop/modules/MozLoopService.jsm
@@ -340,17 +340,17 @@ let MozLoopServiceInternal = {
       } else {
         messageString = "session_expired_error_description";
       }
     } else if (error.code >= 500 && error.code < 600) {
       messageString = "service_not_available";
       detailsString = "try_again_later";
       detailsButtonLabelString = "retry_button";
     } else {
-      messageString = "generic_failure_title";
+      messageString = "generic_failure_message";
     }
 
     error.friendlyMessage = this.localizedStrings.get(messageString);
 
     // Default to the generic "retry_button" text even though the button won't be shown if
     // error.friendlyDetails is null.
     error.friendlyDetailsButtonLabel = detailsButtonLabelString ?
                                          this.localizedStrings.get(detailsButtonLabelString) :
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -114,19 +114,20 @@ loop.standaloneRoomViews = (function(moz
         case ROOM_STATES.SESSION_CONNECTED:
           this.setState({ waitToRenderWaiting: false });
           this.props.dispatcher.dispatch(new sharedActions.TileShown());
           break;
       }
     },
 
     componentDidUpdate: function() {
-      // Start a timer once from the earliest waiting state if we need to wait
-      // before showing a message.
-      if (this.props.roomState === ROOM_STATES.JOINING &&
+      // Start a timer once from the earliest waiting state or from the state
+      // after someone else leaves if we need to wait before showing a message.
+      if ((this.props.roomState === ROOM_STATES.JOINING ||
+           this.props.roomState === ROOM_STATES.SESSION_CONNECTED) &&
           this.state.waitToRenderWaiting &&
           this._waitTimer === undefined) {
         this._waitTimer = setTimeout(this._allowRenderWaiting,
           this.constructor.RENDER_WAITING_DELAY);
       }
     },
 
     componentWillReceiveProps: function(nextProps) {
@@ -224,17 +225,17 @@ loop.standaloneRoomViews = (function(moz
           // there's another participant that will momentarily appear.
           if (this.state.waitToRenderWaiting) {
             return null;
           }
 
           return (
             React.createElement("div", {className: "room-inner-info-area"}, 
               React.createElement("p", {className: "empty-room-message"}, 
-                mozL10n.get("rooms_only_occupant_label")
+                mozL10n.get("rooms_only_occupant_label2")
               ), 
               React.createElement("p", {className: "room-waiting-area"}, 
                 mozL10n.get("rooms_read_while_wait_offer"), 
                 React.createElement("a", {href: loop.config.tilesSupportUrl, 
                   onClick: this.recordTilesSupport, 
                   rel: "noreferrer", 
                   target: "_blank"}, 
                   React.createElement("i", {className: "room-waiting-help"})
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -114,19 +114,20 @@ loop.standaloneRoomViews = (function(moz
         case ROOM_STATES.SESSION_CONNECTED:
           this.setState({ waitToRenderWaiting: false });
           this.props.dispatcher.dispatch(new sharedActions.TileShown());
           break;
       }
     },
 
     componentDidUpdate: function() {
-      // Start a timer once from the earliest waiting state if we need to wait
-      // before showing a message.
-      if (this.props.roomState === ROOM_STATES.JOINING &&
+      // Start a timer once from the earliest waiting state or from the state
+      // after someone else leaves if we need to wait before showing a message.
+      if ((this.props.roomState === ROOM_STATES.JOINING ||
+           this.props.roomState === ROOM_STATES.SESSION_CONNECTED) &&
           this.state.waitToRenderWaiting &&
           this._waitTimer === undefined) {
         this._waitTimer = setTimeout(this._allowRenderWaiting,
           this.constructor.RENDER_WAITING_DELAY);
       }
     },
 
     componentWillReceiveProps: function(nextProps) {
@@ -224,17 +225,17 @@ loop.standaloneRoomViews = (function(moz
           // there's another participant that will momentarily appear.
           if (this.state.waitToRenderWaiting) {
             return null;
           }
 
           return (
             <div className="room-inner-info-area">
               <p className="empty-room-message">
-                {mozL10n.get("rooms_only_occupant_label")}
+                {mozL10n.get("rooms_only_occupant_label2")}
               </p>
               <p className="room-waiting-area">
                 {mozL10n.get("rooms_read_while_wait_offer")}
                 <a href={loop.config.tilesSupportUrl}
                   onClick={this.recordTilesSupport}
                   rel="noreferrer"
                   target="_blank">
                   <i className="room-waiting-help"></i>
--- a/browser/components/loop/standalone/content/l10n/en-US/loop.properties
+++ b/browser/components/loop/standalone/content/l10n/en-US/loop.properties
@@ -1,17 +1,17 @@
 ## LOCALIZATION NOTE: In this file, don't translate the part between {{..}}
 restart_call=Rejoin
 conversation_has_ended=Your conversation has ended.
 call_timeout_notification_text=Your call did not go through.
 missing_conversation_info=Missing conversation information.
 network_disconnected=The network connection terminated abruptly.
 peer_ended_conversation2=The person you were calling has ended the conversation.
 call_failed_title=Call failed.
-generic_failure_title=Something went wrong.
+generic_failure_message=We're having technical difficulties…
 generic_failure_with_reason2=You can try again or email a link to be reached at later.
 generic_failure_no_reason2=Would you like to try again?
 retry_call_button=Retry
 unable_retrieve_call_info=Unable to retrieve conversation information.
 hangup_button_title=Hang up
 hangup_button_caption2=Exit
 mute_local_audio_button_title=Mute your audio
 unmute_local_audio_button_title=Unmute your audio
@@ -92,17 +92,17 @@ rooms_default_room_name_template=Convers
 ## LOCALIZATION_NOTE(rooms_welcome_title): {{conversationName}} will be replaced
 ## by the user specified conversation name.
 rooms_welcome_title=Welcome to {{conversationName}}
 rooms_leave_button_label=Leave
 rooms_list_copy_url_tooltip=Copy Link
 rooms_list_delete_tooltip=Delete conversation
 rooms_list_deleteConfirmation_label=Are you sure?
 rooms_new_room_button_label=Start a conversation
-rooms_only_occupant_label=You're the first one here.
+rooms_only_occupant_label2=You're the only one here.
 rooms_panel_title=Choose a conversation or start a new one
 rooms_room_full_label=There are already two people in this conversation.
 rooms_room_full_call_to_action_nonFx_label=Download {{brandShortname}} to start your own
 rooms_room_full_call_to_action_label=Learn more about {{clientShortname}} »
 rooms_room_joined_label=Someone has joined the conversation!
 rooms_room_join_label=Join the conversation
 rooms_display_name_guest=Guest
 rooms_unavailable_notification_message=Sorry, you cannot join this conversation. The link may be expired or invalid.
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -255,206 +255,268 @@ describe("loop.conversationViews", funct
         React.addons.TestUtils.Simulate.click(cancelBtn);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("name", "cancelCall"));
       });
   });
 
-  describe("CallFailedView", function() {
+  describe("FailureInfoView", function() {
+    function mountTestComponent(options) {
+      return TestUtils.renderIntoDocument(
+        React.createElement(loop.conversationViews.FailureInfoView, options));
+    }
+
+    it("should display a generic failure message by default", function() {
+      view = mountTestComponent({
+        failureReason: "fake"
+      });
+
+      var message = view.getDOMNode().querySelector(".failure-info-message");
+
+      expect(message.textContent).eql("generic_failure_message");
+    });
+
+    it("should display a no media message for the no media reason", function() {
+      view = mountTestComponent({
+        failureReason: FAILURE_DETAILS.NO_MEDIA
+      });
+
+      var message = view.getDOMNode().querySelector(".failure-info-message");
+
+      expect(message.textContent).eql("no_media_failure_message");
+    });
+
+    it("should display a no media message for the unable to publish reason", function() {
+      view = mountTestComponent({
+        failureReason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
+      });
+
+      var message = view.getDOMNode().querySelector(".failure-info-message");
+
+      expect(message.textContent).eql("no_media_failure_message");
+    });
+
+    it("should display a user unavailable message for the unavailable reason", function() {
+      view = mountTestComponent({
+        contact: {email: [{value: "test@test.tld"}]},
+        failureReason: FAILURE_DETAILS.USER_UNAVAILABLE
+      });
+
+      var message = view.getDOMNode().querySelector(".failure-info-message");
+
+      expect(message.textContent).eql("contact_unavailable_title");
+    });
+
+    it("should display a generic unavailable message if the contact doesn't have a display name", function() {
+      view = mountTestComponent({
+        contact: {
+          tel: [{"pref": true, type: "work", value: ""}]
+        },
+        failureReason: FAILURE_DETAILS.USER_UNAVAILABLE
+      });
+
+      var message = view.getDOMNode().querySelector(".failure-info-message");
+
+      expect(message.textContent).eql("generic_contact_unavailable_title");
+    });
+
+    it("should display an extra message", function() {
+      view = mountTestComponent({
+        extraMessage: "Fake message",
+        failureReason: FAILURE_DETAILS.UNKNOWN
+      });
+
+      var extraMessage = view.getDOMNode().querySelector(".failure-info-extra");
+
+      expect(extraMessage.textContent).eql("Fake message");
+    });
+
+    it("should display an extra failure message", function() {
+      view = mountTestComponent({
+        extraFailureMessage: "Fake failure message",
+        failureReason: FAILURE_DETAILS.UNKNOWN
+      });
+
+      var extraFailureMessage = view.getDOMNode().querySelector(".failure-info-extra-failure");
+
+      expect(extraFailureMessage.textContent).eql("Fake failure message");
+    });
+  });
+
+  describe("DirectCallFailureView", function() {
     var fakeAudio, composeCallUrlEmail;
 
     var fakeContact = {email: [{value: "test@test.tld"}]};
 
     function mountTestComponent(options) {
-      options = options || {};
-      return TestUtils.renderIntoDocument(
-        React.createElement(loop.conversationViews.CallFailedView, {
+      var props = _.extend({
           dispatcher: dispatcher,
-          contact: options.contact,
+          mozLoop: fakeMozLoop,
           outgoing: true
-        }));
+        }, options);
+      return TestUtils.renderIntoDocument(
+        React.createElement(loop.conversationViews.DirectCallFailureView, props));
     }
 
     beforeEach(function() {
       fakeAudio = {
         play: sinon.spy(),
         pause: sinon.spy(),
         removeAttribute: sinon.spy()
       };
       sandbox.stub(window, "Audio").returns(fakeAudio);
       composeCallUrlEmail = sandbox.stub(sharedUtils, "composeCallUrlEmail");
+      conversationStore.setStoreState({
+        callStateReason: FAILURE_DETAILS.UNKNOWN,
+        contact: fakeContact
+      });
+    });
+
+    it("should not display the retry button for incoming calls", function() {
+      view = mountTestComponent({outgoing: false});
+
+      var retryBtn = view.getDOMNode().querySelector(".btn-retry");
+
+      expect(retryBtn.classList.contains("hide")).eql(true);
+    });
+
+    it("should not display the email button for incoming calls", function() {
+      view = mountTestComponent({outgoing: false});
+
+      var retryBtn = view.getDOMNode().querySelector(".btn-email");
+
+      expect(retryBtn.classList.contains("hide")).eql(true);
     });
 
     it("should dispatch a retryCall action when the retry button is pressed",
       function() {
-        view = mountTestComponent({contact: fakeContact});
+        view = mountTestComponent();
 
         var retryBtn = view.getDOMNode().querySelector(".btn-retry");
 
         React.addons.TestUtils.Simulate.click(retryBtn);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("name", "retryCall"));
       });
 
     it("should dispatch a cancelCall action when the cancel button is pressed",
       function() {
-        view = mountTestComponent({contact: fakeContact});
+        view = mountTestComponent();
 
         var cancelBtn = view.getDOMNode().querySelector(".btn-cancel");
 
         React.addons.TestUtils.Simulate.click(cancelBtn);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("name", "cancelCall"));
       });
 
     it("should dispatch a fetchRoomEmailLink action when the email button is pressed",
       function() {
-        view = mountTestComponent({contact: fakeContact});
+        view = mountTestComponent();
 
         var emailLinkBtn = view.getDOMNode().querySelector(".btn-email");
 
         React.addons.TestUtils.Simulate.click(emailLinkBtn);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("name", "fetchRoomEmailLink"));
         sinon.assert.calledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("roomName", "test@test.tld"));
       });
 
     it("should name the created room using the contact name when available",
       function() {
-        view = mountTestComponent({contact: {
-          email: [{value: "test@test.tld"}],
-          name: ["Mr Fake ContactName"]
-        }});
+        conversationStore.setStoreState({
+          contact: {
+            email: [{value: "test@test.tld"}],
+            name: ["Mr Fake ContactName"]
+          }
+        });
+
+        view = mountTestComponent();
 
         var emailLinkBtn = view.getDOMNode().querySelector(".btn-email");
 
         React.addons.TestUtils.Simulate.click(emailLinkBtn);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("roomName", "Mr Fake ContactName"));
       });
 
     it("should disable the email link button once the action is dispatched",
       function() {
-        view = mountTestComponent({contact: fakeContact});
+        view = mountTestComponent();
         var emailLinkBtn = view.getDOMNode().querySelector(".btn-email");
         React.addons.TestUtils.Simulate.click(emailLinkBtn);
 
         expect(view.getDOMNode().querySelector(".btn-email").disabled).eql(true);
       });
 
     it("should compose an email once the email link is received", function() {
-      view = mountTestComponent({contact: fakeContact});
+      view = mountTestComponent();
       conversationStore.setStoreState({emailLink: "http://fake.invalid/"});
 
       sinon.assert.calledOnce(composeCallUrlEmail);
       sinon.assert.calledWithExactly(composeCallUrlEmail,
         "http://fake.invalid/", "test@test.tld", null, "callfailed");
     });
 
     it("should close the conversation window once the email link is received",
       function() {
-        view = mountTestComponent({contact: fakeContact});
+        view = mountTestComponent();
 
         conversationStore.setStoreState({emailLink: "http://fake.invalid/"});
 
         sinon.assert.calledOnce(fakeWindow.close);
       });
 
     it("should display an error message in case email link retrieval failed",
       function() {
-        view = mountTestComponent({contact: fakeContact});
+        view = mountTestComponent();
 
         conversationStore.trigger("error:emailLink");
 
-        expect(view.getDOMNode().querySelector(".error")).not.eql(null);
+        expect(view.getDOMNode().querySelector(".failure-info-extra-failure")).not.eql(null);
       });
 
     it("should allow retrying to get a call url if it failed previously",
       function() {
-        view = mountTestComponent({contact: fakeContact});
+        view = mountTestComponent();
 
         conversationStore.trigger("error:emailLink");
 
         expect(view.getDOMNode().querySelector(".btn-email").disabled).eql(false);
       });
 
     it("should play a failure sound, once", function() {
-      view = mountTestComponent({contact: fakeContact});
+      view = mountTestComponent();
 
       sinon.assert.calledOnce(navigator.mozLoop.getAudioBlob);
       sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob,
                                      "failure", sinon.match.func);
       sinon.assert.calledOnce(fakeAudio.play);
       expect(fakeAudio.loop).to.equal(false);
     });
 
-    it("should show 'something went wrong' when the reason is WEBSOCKET_REASONS.MEDIA_FAIL",
-      function () {
-        conversationStore.setStoreState({callStateReason: WEBSOCKET_REASONS.MEDIA_FAIL});
-
-        view = mountTestComponent({contact: fakeContact});
-
-        sinon.assert.calledWith(document.mozL10n.get, "generic_failure_title");
-      });
-
-    it("should show 'something went wrong' when the reason is 'setup'",
-      function () {
-        conversationStore.setStoreState({callStateReason: "setup"});
-
-        view = mountTestComponent({contact: fakeContact});
-
-        sinon.assert.calledWithExactly(document.mozL10n.get,
-          "generic_failure_title");
+    it("should display an additional message for outgoing calls", function() {
+      view = mountTestComponent({
+        outgoing: true
       });
 
-    it("should show 'contact unavailable' when the reason is FAILURE_DETAILS.USER_UNAVAILABLE",
-      function () {
-        conversationStore.setStoreState({callStateReason: FAILURE_DETAILS.USER_UNAVAILABLE});
-
-        view = mountTestComponent({contact: fakeContact});
-
-        sinon.assert.calledWithExactly(document.mozL10n.get,
-          "contact_unavailable_title",
-          {contactName: loop.conversationViews
-                            ._getContactDisplayName(fakeContact)});
-      });
-
-    it("should show 'no media' when the reason is FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA",
-      function () {
-        conversationStore.setStoreState({callStateReason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA});
+      var extraMessage = view.getDOMNode().querySelector(".failure-info-extra");
 
-        view = mountTestComponent({contact: fakeContact});
-
-        sinon.assert.calledWithExactly(document.mozL10n.get, "no_media_failure_message");
-      });
-
-    it("should display a generic contact unavailable msg when the reason is" +
-       " FAILURE_DETAILS.USER_UNAVAILABLE and no display name is available", function() {
-        conversationStore.setStoreState({
-          callStateReason: FAILURE_DETAILS.USER_UNAVAILABLE
-        });
-        var phoneOnlyContact = {
-          tel: [{"pref": true, type: "work", value: ""}]
-        };
-
-        view = mountTestComponent({contact: phoneOnlyContact});
-
-        sinon.assert.calledWith(document.mozL10n.get,
-          "generic_contact_unavailable_title");
+      expect(extraMessage.textContent).eql("generic_failure_with_reason2");
     });
   });
 
   describe("OngoingConversationView", function() {
     function mountTestComponent(extraProps) {
       var props = _.extend({
         conversationStore: conversationStore,
         dispatcher: dispatcher,
@@ -616,28 +678,29 @@ describe("loop.conversationViews", funct
         callerId: "fakeId"
       });
 
       mountTestComponent({contact: contact});
 
       expect(fakeWindow.document.title).eql("fakeId");
     });
 
-    it("should render the CallFailedView when the call state is 'terminated'",
+    it("should render the DirectCallFailureView when the call state is 'terminated'",
       function() {
         conversationStore.setStoreState({
           callState: CALL_STATES.TERMINATED,
           contact: contact,
+          callStateReason: WEBSOCKET_REASONS.CLOSED,
           outgoing: true
         });
 
         view = mountTestComponent();
 
         TestUtils.findRenderedComponentWithType(view,
-          loop.conversationViews.CallFailedView);
+          loop.conversationViews.DirectCallFailureView);
     });
 
     it("should render the PendingConversationView for outgoing calls when the call state is 'gather'",
       function() {
         conversationStore.setStoreState({
           callState: CALL_STATES.GATHER,
           contact: contact,
           outgoing: true
@@ -690,26 +753,25 @@ describe("loop.conversationViews", funct
 
     it("should update the rendered views when the state is changed.",
       function() {
         conversationStore.setStoreState({
           callState: CALL_STATES.GATHER,
           contact: contact,
           outgoing: true
         });
-
         view = mountTestComponent();
-
         TestUtils.findRenderedComponentWithType(view,
           loop.conversationViews.PendingConversationView);
-
-        conversationStore.setStoreState({callState: CALL_STATES.TERMINATED});
-
+        conversationStore.setStoreState({
+          callState: CALL_STATES.TERMINATED,
+          callStateReason: WEBSOCKET_REASONS.CLOSED
+        });
         TestUtils.findRenderedComponentWithType(view,
-          loop.conversationViews.CallFailedView);
+          loop.conversationViews.DirectCallFailureView);
     });
 
     it("should call onCallTerminated when the call is finished", function() {
       conversationStore.setStoreState({
         callState: CALL_STATES.ONGOING
       });
 
       view = mountTestComponent({
@@ -885,73 +947,9 @@ describe("loop.conversationViews", funct
         TestUtils.Simulate.click(buttonBlock);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.DeclineCall({blockCaller: true}));
       });
     });
   });
-
-  describe("GenericFailureView", function() {
-    var callView, fakeAudio;
-
-    function mountTestComponent(props) {
-      return TestUtils.renderIntoDocument(
-        React.createElement(loop.conversationViews.GenericFailureView, props));
-    }
-
-    beforeEach(function() {
-      fakeAudio = {
-        play: sinon.spy(),
-        pause: sinon.spy(),
-        removeAttribute: sinon.spy()
-      };
-      navigator.mozLoop.doNotDisturb = false;
-      sandbox.stub(window, "Audio").returns(fakeAudio);
-    });
-
-    it("should play a failure sound, once", function() {
-      callView = mountTestComponent({cancelCall: function() {}});
-
-      sinon.assert.calledOnce(navigator.mozLoop.getAudioBlob);
-      sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob,
-                                     "failure", sinon.match.func);
-      sinon.assert.calledOnce(fakeAudio.play);
-      expect(fakeAudio.loop).to.equal(false);
-    });
-
-    it("should set the title to generic_failure_title", function() {
-      callView = mountTestComponent({cancelCall: function() {}});
-
-      expect(fakeWindow.document.title).eql("generic_failure_title");
-    });
-
-    it("should show 'no media' for FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA reason",
-       function() {
-         callView = mountTestComponent({
-           cancelCall: function() {},
-           failureReason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
-         });
-
-         expect(callView.getDOMNode().querySelector("h2").textContent)
-         .eql("no_media_failure_message");
-     });
-
-    it("should show 'no media' for FAILURE_DETAILS.NO_MEDIA reason", function() {
-      callView = mountTestComponent({
-        cancelCall: function() {},
-        failureReason: FAILURE_DETAILS.NO_MEDIA
-      });
-
-      expect(callView.getDOMNode().querySelector("h2").textContent)
-          .eql("no_media_failure_message");
-    });
-
-    it("should show 'generic_failure_title' when no reason is specified",
-       function() {
-         callView = mountTestComponent({cancelCall: function() {}});
-
-         expect(callView.getDOMNode().querySelector("h2").textContent)
-            .eql("generic_failure_title");
-     });
-  });
 });
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -5,16 +5,17 @@
 describe("loop.conversation", function() {
   "use strict";
 
   var expect = chai.expect;
   var FeedbackView = loop.feedbackViews.FeedbackView;
   var TestUtils = React.addons.TestUtils;
   var sharedActions = loop.shared.actions;
   var sharedModels = loop.shared.models;
+  var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
   var fakeWindow, sandbox, getLoopPrefStub, setLoopPrefStub, mozL10nGet;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
     setLoopPrefStub = sandbox.stub();
 
     navigator.mozLoop = {
       doNotDisturb: true,
@@ -216,23 +217,30 @@ describe("loop.conversation", function()
       activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
 
       ccView = mountTestComponent();
 
       TestUtils.findRenderedComponentWithType(ccView,
         loop.roomViews.DesktopRoomConversationView);
     });
 
-    it("should display the GenericFailureView for failures", function() {
-      conversationAppStore.setStoreState({windowType: "failed"});
+    it("should display the DirectCallFailureView for failures", function() {
+      conversationAppStore.setStoreState({
+        contact: {},
+        outgoing: false,
+        windowType: "failed"
+      });
+      conversationStore.setStoreState({
+        callStateReason: FAILURE_DETAILS.UNKNOWN
+      });
 
       ccView = mountTestComponent();
 
       TestUtils.findRenderedComponentWithType(ccView,
-        loop.conversationViews.GenericFailureView);
+        loop.conversationViews.DirectCallFailureView);
     });
 
     it("should set the correct title when rendering feedback view", function() {
       conversationAppStore.setStoreState({showFeedbackForm: true});
 
       ccView = mountTestComponent();
 
       sinon.assert.calledWithExactly(mozL10nGet, "conversation_has_ended");
--- a/browser/components/loop/test/desktop-local/roomViews_test.js
+++ b/browser/components/loop/test/desktop-local/roomViews_test.js
@@ -6,28 +6,31 @@ describe("loop.roomViews", function () {
 
   var expect = chai.expect;
   var TestUtils = React.addons.TestUtils;
   var sharedActions = loop.shared.actions;
   var sharedUtils = loop.shared.utils;
   var sharedViews = loop.shared.views;
   var ROOM_STATES = loop.store.ROOM_STATES;
   var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
+  var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
 
-  var sandbox, dispatcher, roomStore, activeRoomStore, fakeWindow,
-    fakeMozLoop, fakeContextURL;
+  var sandbox, dispatcher, roomStore, activeRoomStore, view;
+  var fakeWindow, fakeMozLoop, fakeContextURL;
   var favicon = "data:image/x-icon;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
 
     dispatcher = new loop.Dispatcher();
 
     fakeMozLoop = {
-      getAudioBlob: sinon.stub(),
+      getAudioBlob: sinon.spy(function(name, callback) {
+        callback(null, new Blob([new ArrayBuffer(10)], {type: "audio/ogg"}));
+      }),
       getLoopPref: sinon.stub(),
       getSelectedTabMetadata: sinon.stub().callsArgWith(0, {
         favicon: favicon,
         previews: [],
         title: ""
       }),
       openURL: sinon.stub(),
       rooms: {
@@ -79,21 +82,23 @@ describe("loop.roomViews", function () {
       textChatStore: textChatStore
     });
 
     fakeContextURL = {
       description: "An invalid page",
       location: "http://invalid.com",
       thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
     };
+    sandbox.stub(dispatcher, "dispatch");
   });
 
   afterEach(function() {
     sandbox.restore();
     loop.shared.mixins.setRootObject(window);
+    view = null;
   });
 
   describe("ActiveRoomStoreMixin", function() {
     it("should merge initial state", function() {
       var TestView = React.createClass({
         mixins: [loop.roomViews.ActiveRoomStoreMixin],
         getInitialState: function() {
           return {foo: "bar"};
@@ -123,27 +128,69 @@ describe("loop.roomViews", function () {
         }));
 
       activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
 
       expect(testView.state.roomState).eql(ROOM_STATES.READY);
     });
   });
 
-  describe("DesktopRoomInvitationView", function() {
-    var view;
+  describe("RoomFailureView", function() {
+    var fakeAudio;
+
+    function mountTestComponent(props) {
+      props = _.extend({
+        dispatcher: dispatcher,
+        failureReason: FAILURE_DETAILS.UNKNOWN,
+        mozLoop: fakeMozLoop
+      });
+      return TestUtils.renderIntoDocument(
+        React.createElement(loop.roomViews.RoomFailureView, props));
+    }
 
     beforeEach(function() {
-      sandbox.stub(dispatcher, "dispatch");
+      fakeAudio = {
+        play: sinon.spy(),
+        pause: sinon.spy(),
+        removeAttribute: sinon.spy()
+      };
+      sandbox.stub(window, "Audio").returns(fakeAudio);
+    });
+
+    it("should render the FailureInfoView", function() {
+      view = mountTestComponent();
+
+      TestUtils.findRenderedComponentWithType(view,
+        loop.conversationViews.FailureInfoView);
     });
 
-    afterEach(function() {
-      view = null;
+    it("should dispatch a JoinRoom action when the rejoin call button is pressed", function() {
+      view = mountTestComponent();
+
+      var rejoinBtn = view.getDOMNode().querySelector(".btn-rejoin");
+
+      React.addons.TestUtils.Simulate.click(rejoinBtn);
+
+      sinon.assert.calledOnce(dispatcher.dispatch);
+      sinon.assert.calledWithExactly(dispatcher.dispatch,
+        new sharedActions.JoinRoom());
     });
 
+    it("should play a failure sound, once", function() {
+      view = mountTestComponent();
+
+      sinon.assert.calledOnce(fakeMozLoop.getAudioBlob);
+      sinon.assert.calledWithExactly(fakeMozLoop.getAudioBlob,
+                                     "failure", sinon.match.func);
+      sinon.assert.calledOnce(fakeAudio.play);
+      expect(fakeAudio.loop).to.equal(false);
+    });
+  });
+
+  describe("DesktopRoomInvitationView", function() {
     function mountTestComponent(props) {
       props = _.extend({
         dispatcher: dispatcher,
         mozLoop: fakeMozLoop,
         roomData: {},
         savingContext: false,
         show: true,
         showEditContext: false
@@ -285,20 +332,19 @@ describe("loop.roomViews", function () {
         });
 
         expect(view.getDOMNode().querySelector(".room-context")).to.not.eql(null);
       });
     });
   });
 
   describe("DesktopRoomConversationView", function() {
-    var view, onCallTerminatedStub;
+    var onCallTerminatedStub;
 
     beforeEach(function() {
-      sandbox.stub(dispatcher, "dispatch");
       fakeMozLoop.getLoopPref = function(prefName) {
         if (prefName === "contextInConversations.enabled") {
           return true;
         }
         return "test";
       };
       onCallTerminatedStub = sandbox.stub();
     });
@@ -439,34 +485,40 @@ describe("loop.roomViews", function () {
       it("should set document.title to store.serverData.roomName", function() {
         mountTestComponent();
 
         activeRoomStore.setStoreState({roomName: "fakeName"});
 
         expect(fakeWindow.document.title).to.equal("fakeName");
       });
 
-      it("should render the GenericFailureView if the roomState is `FAILED`",
+      it("should render the RoomFailureView if the roomState is `FAILED`",
         function() {
-          activeRoomStore.setStoreState({roomState: ROOM_STATES.FAILED});
+          activeRoomStore.setStoreState({
+            failureReason: FAILURE_DETAILS.UNKNOWN,
+            roomState: ROOM_STATES.FAILED
+          });
 
           view = mountTestComponent();
 
           TestUtils.findRenderedComponentWithType(view,
-            loop.conversationViews.GenericFailureView);
+            loop.roomViews.RoomFailureView);
         });
 
-      it("should render the GenericFailureView if the roomState is `FULL`",
+      it("should render the RoomFailureView if the roomState is `FULL`",
         function() {
-          activeRoomStore.setStoreState({roomState: ROOM_STATES.FULL});
+          activeRoomStore.setStoreState({
+            failureReason: FAILURE_DETAILS.UNKNOWN,
+            roomState: ROOM_STATES.FULL
+          });
 
           view = mountTestComponent();
 
           TestUtils.findRenderedComponentWithType(view,
-            loop.conversationViews.GenericFailureView);
+            loop.roomViews.RoomFailureView);
         });
 
       it("should render the DesktopRoomInvitationView if roomState is `JOINED`",
         function() {
           activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINED});
 
           view = mountTestComponent();
 
@@ -666,30 +718,28 @@ describe("loop.roomViews", function () {
         React.addons.TestUtils.Simulate.click(editButton);
 
         expect(view.getDOMNode().querySelector(".room-context")).to.eql(null);
       });
     });
   });
 
   describe("SocialShareDropdown", function() {
-    var view, fakeProvider;
+    var fakeProvider;
 
     beforeEach(function() {
-      sandbox.stub(dispatcher, "dispatch");
-
       fakeProvider = {
         name: "foo",
         origin: "https://foo",
         iconURL: "http://example.com/foo.png"
       };
     });
 
     afterEach(function() {
-      view = fakeProvider = null;
+      fakeProvider = null;
     });
 
     function mountTestComponent(props) {
       props = _.extend({
         dispatcher: dispatcher,
         show: true
       }, props);
       return TestUtils.renderIntoDocument(
@@ -762,22 +812,16 @@ describe("loop.roomViews", function () {
             roomUrl: "http://example.com",
             previews: []
           }));
       });
     });
   });
 
   describe("DesktopRoomEditContextView", function() {
-    var view;
-
-    afterEach(function() {
-      view = null;
-    });
-
     function mountTestComponent(props) {
       props = _.extend({
         dispatcher: dispatcher,
         mozLoop: fakeMozLoop,
         savingContext: false,
         show: true,
         roomData: {
           roomToken: "fakeToken"
@@ -852,18 +896,16 @@ describe("loop.roomViews", function () {
         expect(node.querySelector(".checkbox-wrapper").classList.contains("hide")).to.eql(true);
       });
     });
 
     describe("Update Room", function() {
       var roomNameBox;
 
       beforeEach(function() {
-        sandbox.stub(dispatcher, "dispatch");
-
         view = mountTestComponent({
           editMode: true,
           roomData: {
             roomToken: "fakeToken",
             roomName: "fakeName",
             roomContextUrls: [fakeContextURL]
           }
         });
--- a/browser/components/loop/test/shared/views_test.js
+++ b/browser/components/loop/test/shared/views_test.js
@@ -320,16 +320,41 @@ describe("loop.shared.views", function()
       var comp = mountTestComponent({ menuItems: settingsMenuItems} );
 
       expect(comp.state.showMenu).eql(false);
       TestUtils.Simulate.click(comp.getDOMNode().querySelector(".btn-settings"));
 
       expect(comp.state.showMenu).eql(true);
     });
 
+    it("should have a `menu-below` class on the dropdown when the prop is set.", function() {
+      var settingsMenuItems = [
+        { id: "help" }
+      ];
+      var comp = mountTestComponent({
+        menuBelow: true,
+        menuItems: settingsMenuItems
+      });
+      var menuItems = comp.getDOMNode().querySelector(".settings-menu");
+
+      expect(menuItems.classList.contains("menu-below")).eql(true);
+    });
+
+    it("should not have a `menu-below` class on the dropdown when the prop is not set.", function() {
+      var settingsMenuItems = [
+        { id: "help" }
+      ];
+      var comp = mountTestComponent({
+        menuItems: settingsMenuItems
+      });
+      var menuItems = comp.getDOMNode().querySelector(".settings-menu");
+
+      expect(menuItems.classList.contains("menu-below")).eql(false);
+    });
+
     it("should show edit Context on menu when the option is enabled", function() {
       var settingsMenuItems = [
         {
           id: "edit",
           enabled: true,
           visible: true,
           onClick: function() {}
         }
--- a/browser/components/loop/test/standalone/standaloneRoomViews_test.js
+++ b/browser/components/loop/test/standalone/standaloneRoomViews_test.js
@@ -204,16 +204,27 @@ describe("loop.standaloneRoomViews", fun
 
       it("should not dispatch a `TileShown` action after a wait when in the HAS_PARTICIPANTS state",
         function() {
           activeRoomStore.setStoreState({roomState: ROOM_STATES.HAS_PARTICIPANTS});
           clock.tick(loop.standaloneRoomViews.StandaloneRoomInfoArea.RENDER_WAITING_DELAY);
 
           sinon.assert.notCalled(dispatch);
         });
+
+      it("should dispatch a `TileShown` action after a wait when a participant leaves",
+        function() {
+          activeRoomStore.setStoreState({roomState: ROOM_STATES.HAS_PARTICIPANTS});
+          clock.tick(loop.standaloneRoomViews.StandaloneRoomInfoArea.RENDER_WAITING_DELAY);
+          activeRoomStore.remotePeerDisconnected();
+          clock.tick(loop.standaloneRoomViews.StandaloneRoomInfoArea.RENDER_WAITING_DELAY);
+
+          sinon.assert.calledOnce(dispatch);
+          sinon.assert.calledWithExactly(dispatch, new sharedActions.TileShown());
+        });
     });
 
     describe("#componentWillReceiveProps", function() {
       var view;
 
       beforeEach(function() {
         view = mountTestComponent();
 
--- a/browser/components/loop/test/xpcshell/test_loopservice_hawk_errors.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_hawk_errors.js
@@ -108,17 +108,17 @@ add_task(function* error_404() {
   yield MozLoopServiceInternal.hawkRequestInternal(LOOP_SESSION_TYPE.GUEST, "/404", "GET").then(
     () => Assert.ok(false, "Should have rejected"),
     (error) => {
       MozLoopServiceInternal.setError("testing", error);
       Assert.strictEqual(MozLoopService.errors.size, 1, "Should be one error");
 
       let err = MozLoopService.errors.get("testing");
       Assert.strictEqual(err.code, 404);
-      Assert.strictEqual(err.friendlyMessage, getLoopString("generic_failure_title"));
+      Assert.strictEqual(err.friendlyMessage, getLoopString("generic_failure_message"));
       Assert.equal(err.friendlyDetails, null);
   });
 });
 
 add_task(cleanup_between_tests);
 
 add_task(function* error_500() {
   yield MozLoopServiceInternal.hawkRequestInternal(LOOP_SESSION_TYPE.GUEST, "/500", "GET").then(
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -21,17 +21,18 @@
   var ContactDetailsForm = loop.contacts.ContactDetailsForm;
   var ContactDropdown = loop.contacts.ContactDropdown;
   var ContactDetail = loop.contacts.ContactDetail;
   var GettingStartedView = loop.panel.GettingStartedView;
   // 1.2. Conversation Window
   var AcceptCallView = loop.conversationViews.AcceptCallView;
   var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
   var OngoingConversationView = loop.conversationViews.OngoingConversationView;
-  var CallFailedView = loop.conversationViews.CallFailedView;
+  var DirectCallFailureView = loop.conversationViews.DirectCallFailureView;
+  var RoomFailureView = loop.roomViews.RoomFailureView;
   var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
 
   // 2. Standalone webapp
   var HomeView = loop.webapp.HomeView;
   var UnsupportedBrowserView  = loop.webapp.UnsupportedBrowserView;
   var UnsupportedDeviceView   = loop.webapp.UnsupportedDeviceView;
   var StandaloneRoomView      = loop.standaloneRoomViews.StandaloneRoomView;
 
@@ -39,16 +40,17 @@
   var ConversationToolbar = loop.shared.views.ConversationToolbar;
   var FeedbackView = loop.feedbackViews.FeedbackView;
   var Checkbox = loop.shared.views.Checkbox;
   var TextChatView = loop.shared.views.chat.TextChatView;
 
   // Store constants
   var ROOM_STATES = loop.store.ROOM_STATES;
   var CALL_TYPES = loop.shared.utils.CALL_TYPES;
+  var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
   var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
 
   // Local helpers
   function returnTrue() {
     return true;
   }
 
   function returnFalse() {
@@ -378,16 +380,24 @@
     return store;
   }
 
   var conversationStores = [];
   for (var index = 0; index < 5; index++) {
     conversationStores[index] = makeConversationStore();
   }
 
+  conversationStores[0].setStoreState({
+    callStateReason: FAILURE_DETAILS.NO_MEDIA
+  });
+  conversationStores[1].setStoreState({
+    callStateReason: FAILURE_DETAILS.USER_UNAVAILABLE,
+    contact: fakeManyContacts[0]
+  });
+
   // Update the text chat store with the room info.
   textChatStore.updateRoomInfo(new sharedActions.UpdateRoomInfo({
     roomName: "A Very Long Conversation Name",
     roomUrl: "http://showcase",
     urls: [{
       description: "A wonderful page!",
       location: "http://wonderful.invalid"
       // use the fallback thumbnail
@@ -1145,45 +1155,55 @@
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(DesktopPendingConversationView, {callState: "gather", 
                                                 contact: mockContact, 
                                                 dispatcher: dispatcher})
               )
             )
           ), 
 
-          React.createElement(Section, {name: "CallFailedView"}, 
-            React.createElement(FramedExample, {dashed: true, 
-                           height: 272, 
-                           summary: "Call Failed - Incoming", 
-                           width: 300}, 
+          React.createElement(Section, {name: "DirectCallFailureView"}, 
+            React.createElement(FramedExample, {
+              dashed: true, 
+              height: 254, 
+              summary: "Call Failed - Incoming", 
+              width: 298}, 
               React.createElement("div", {className: "fx-embedded"}, 
-                React.createElement(CallFailedView, {dispatcher: dispatcher, 
-                                outgoing: false, 
-                                store: conversationStores[0]})
+                React.createElement(DirectCallFailureView, {
+                  conversationStore: conversationStores[0], 
+                  dispatcher: dispatcher, 
+                  mozLoop: navigator.mozLoop, 
+                  outgoing: false})
               )
             ), 
-            React.createElement(FramedExample, {dashed: true, 
-                           height: 272, 
-                           summary: "Call Failed - Outgoing", 
-                           width: 300}, 
+            React.createElement(FramedExample, {
+              dashed: true, 
+              height: 254, 
+              summary: "Call Failed - Outgoing", 
+              width: 298}, 
               React.createElement("div", {className: "fx-embedded"}, 
-                React.createElement(CallFailedView, {dispatcher: dispatcher, 
-                                outgoing: true, 
-                                store: conversationStores[1]})
+                React.createElement(DirectCallFailureView, {
+                  conversationStore: conversationStores[1], 
+                  dispatcher: dispatcher, 
+                  mozLoop: navigator.mozLoop, 
+                  outgoing: true})
               )
             ), 
-            React.createElement(FramedExample, {dashed: true, 
-                           height: 272, 
-                           summary: "Call Failed — with call URL error", 
-                           width: 300}, 
+            React.createElement(FramedExample, {
+              dashed: true, 
+              height: 254, 
+              summary: "Call Failed — with call URL error", 
+              width: 298}, 
               React.createElement("div", {className: "fx-embedded"}, 
-                React.createElement(CallFailedView, {dispatcher: dispatcher, emailLinkError: true, 
-                                outgoing: true, 
-                                store: conversationStores[0]})
+                React.createElement(DirectCallFailureView, {
+                  conversationStore: conversationStores[0], 
+                  dispatcher: dispatcher, 
+                  emailLinkError: true, 
+                  mozLoop: navigator.mozLoop, 
+                  outgoing: true})
               )
             )
           ), 
 
           React.createElement(Section, {name: "OngoingConversationView"}, 
             React.createElement(FramedExample, {dashed: true, 
                            height: 394, 
                            onContentsRendered: conversationStores[0].forcedUpdate, 
@@ -1329,16 +1349,31 @@
                            summary: "Standalone Unsupported Device", 
                            width: 480}, 
               React.createElement("div", {className: "standalone"}, 
                 React.createElement(UnsupportedDeviceView, {platform: "ios"})
               )
             )
           ), 
 
+          React.createElement(Section, {name: "RoomFailureView"}, 
+            React.createElement(FramedExample, {
+              dashed: true, 
+              height: 254, 
+              summary: "", 
+              width: 298}, 
+              React.createElement("div", {className: "fx-embedded"}, 
+                React.createElement(RoomFailureView, {
+                  dispatcher: dispatcher, 
+                  failureReason: FAILURE_DETAILS.UNKNOWN, 
+                  mozLoop: navigator.mozLoop})
+              )
+            )
+          ), 
+
           React.createElement(Section, {name: "DesktopRoomConversationView"}, 
             React.createElement(FramedExample, {height: 398, 
                            onContentsRendered: invitationRoomStore.activeRoomStore.forcedUpdate, 
                            summary: "Desktop room conversation (invitation, text-chat inclusion/scrollbars don't happen in real client)", 
                            width: 298}, 
               React.createElement("div", {className: "fx-embedded"}, 
                 React.createElement(DesktopRoomConversationView, {
                   dispatcher: dispatcher, 
@@ -1757,17 +1792,17 @@
         setTimeout(waitForQueuedFrames, 500);
         return;
       }
       // Put the title back, in case views changed it.
       document.title = "Loop UI Components Showcase";
 
       // This simulates the mocha layout for errors which means we can run
       // this alongside our other unit tests but use the same harness.
-      var expectedWarningsCount = 3;
+      var expectedWarningsCount = 0;
       var warningsMismatch = caughtWarnings.length !== expectedWarningsCount;
       var resultsElement = document.querySelector("#results");
       var divFailuresNode = document.createElement("div");
       var pCompleteNode = document.createElement("p");
       var emNode = document.createElement("em");
 
       if (uncaughtError || warningsMismatch) {
         var liTestFail = document.createElement("li");
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -21,17 +21,18 @@
   var ContactDetailsForm = loop.contacts.ContactDetailsForm;
   var ContactDropdown = loop.contacts.ContactDropdown;
   var ContactDetail = loop.contacts.ContactDetail;
   var GettingStartedView = loop.panel.GettingStartedView;
   // 1.2. Conversation Window
   var AcceptCallView = loop.conversationViews.AcceptCallView;
   var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
   var OngoingConversationView = loop.conversationViews.OngoingConversationView;
-  var CallFailedView = loop.conversationViews.CallFailedView;
+  var DirectCallFailureView = loop.conversationViews.DirectCallFailureView;
+  var RoomFailureView = loop.roomViews.RoomFailureView;
   var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
 
   // 2. Standalone webapp
   var HomeView = loop.webapp.HomeView;
   var UnsupportedBrowserView  = loop.webapp.UnsupportedBrowserView;
   var UnsupportedDeviceView   = loop.webapp.UnsupportedDeviceView;
   var StandaloneRoomView      = loop.standaloneRoomViews.StandaloneRoomView;
 
@@ -39,16 +40,17 @@
   var ConversationToolbar = loop.shared.views.ConversationToolbar;
   var FeedbackView = loop.feedbackViews.FeedbackView;
   var Checkbox = loop.shared.views.Checkbox;
   var TextChatView = loop.shared.views.chat.TextChatView;
 
   // Store constants
   var ROOM_STATES = loop.store.ROOM_STATES;
   var CALL_TYPES = loop.shared.utils.CALL_TYPES;
+  var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
   var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
 
   // Local helpers
   function returnTrue() {
     return true;
   }
 
   function returnFalse() {
@@ -378,16 +380,24 @@
     return store;
   }
 
   var conversationStores = [];
   for (var index = 0; index < 5; index++) {
     conversationStores[index] = makeConversationStore();
   }
 
+  conversationStores[0].setStoreState({
+    callStateReason: FAILURE_DETAILS.NO_MEDIA
+  });
+  conversationStores[1].setStoreState({
+    callStateReason: FAILURE_DETAILS.USER_UNAVAILABLE,
+    contact: fakeManyContacts[0]
+  });
+
   // Update the text chat store with the room info.
   textChatStore.updateRoomInfo(new sharedActions.UpdateRoomInfo({
     roomName: "A Very Long Conversation Name",
     roomUrl: "http://showcase",
     urls: [{
       description: "A wonderful page!",
       location: "http://wonderful.invalid"
       // use the fallback thumbnail
@@ -1145,45 +1155,55 @@
               <div className="fx-embedded">
                 <DesktopPendingConversationView callState={"gather"}
                                                 contact={mockContact}
                                                 dispatcher={dispatcher} />
               </div>
             </FramedExample>
           </Section>
 
-          <Section name="CallFailedView">
-            <FramedExample dashed={true}
-                           height={272}
-                           summary="Call Failed - Incoming"
-                           width={300}>
+          <Section name="DirectCallFailureView">
+            <FramedExample
+              dashed={true}
+              height={254}
+              summary="Call Failed - Incoming"
+              width={298}>
               <div className="fx-embedded">
-                <CallFailedView dispatcher={dispatcher}
-                                outgoing={false}
-                                store={conversationStores[0]} />
+                <DirectCallFailureView
+                  conversationStore={conversationStores[0]}
+                  dispatcher={dispatcher}
+                  mozLoop={navigator.mozLoop}
+                  outgoing={false} />
               </div>
             </FramedExample>
-            <FramedExample dashed={true}
-                           height={272}
-                           summary="Call Failed - Outgoing"
-                           width={300}>
+            <FramedExample
+              dashed={true}
+              height={254}
+              summary="Call Failed - Outgoing"
+              width={298}>
               <div className="fx-embedded">
-                <CallFailedView dispatcher={dispatcher}
-                                outgoing={true}
-                                store={conversationStores[1]} />
+                <DirectCallFailureView
+                  conversationStore={conversationStores[1]}
+                  dispatcher={dispatcher}
+                  mozLoop={navigator.mozLoop}
+                  outgoing={true} />
               </div>
             </FramedExample>
-            <FramedExample dashed={true}
-                           height={272}
-                           summary="Call Failed — with call URL error"
-                           width={300}>
+            <FramedExample
+              dashed={true}
+              height={254}
+              summary="Call Failed — with call URL error"
+              width={298}>
               <div className="fx-embedded">
-                <CallFailedView dispatcher={dispatcher} emailLinkError={true}
-                                outgoing={true}
-                                store={conversationStores[0]} />
+                <DirectCallFailureView
+                  conversationStore={conversationStores[0]}
+                  dispatcher={dispatcher}
+                  emailLinkError={true}
+                  mozLoop={navigator.mozLoop}
+                  outgoing={true} />
               </div>
             </FramedExample>
           </Section>
 
           <Section name="OngoingConversationView">
             <FramedExample dashed={true}
                            height={394}
                            onContentsRendered={conversationStores[0].forcedUpdate}
@@ -1329,16 +1349,31 @@
                            summary="Standalone Unsupported Device"
                            width={480}>
               <div className="standalone">
                 <UnsupportedDeviceView platform="ios"/>
               </div>
             </FramedExample>
           </Section>
 
+          <Section name="RoomFailureView">
+            <FramedExample
+              dashed={true}
+              height={254}
+              summary=""
+              width={298}>
+              <div className="fx-embedded">
+                <RoomFailureView
+                  dispatcher={dispatcher}
+                  failureReason={FAILURE_DETAILS.UNKNOWN}
+                  mozLoop={navigator.mozLoop} />
+              </div>
+            </FramedExample>
+          </Section>
+
           <Section name="DesktopRoomConversationView">
             <FramedExample height={398}
                            onContentsRendered={invitationRoomStore.activeRoomStore.forcedUpdate}
                            summary="Desktop room conversation (invitation, text-chat inclusion/scrollbars don't happen in real client)"
                            width={298}>
               <div className="fx-embedded">
                 <DesktopRoomConversationView
                   dispatcher={dispatcher}
@@ -1757,17 +1792,17 @@
         setTimeout(waitForQueuedFrames, 500);
         return;
       }
       // Put the title back, in case views changed it.
       document.title = "Loop UI Components Showcase";
 
       // This simulates the mocha layout for errors which means we can run
       // this alongside our other unit tests but use the same harness.
-      var expectedWarningsCount = 3;
+      var expectedWarningsCount = 0;
       var warningsMismatch = caughtWarnings.length !== expectedWarningsCount;
       var resultsElement = document.querySelector("#results");
       var divFailuresNode = document.createElement("div");
       var pCompleteNode = document.createElement("p");
       var emNode = document.createElement("em");
 
       if (uncaughtError || warningsMismatch) {
         var liTestFail = document.createElement("li");
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -11,17 +11,16 @@ browser.jar:
     content/browser/devtools/readdir.js                                (projecteditor/lib/helpers/readdir.js)
     content/browser/devtools/projecteditor-loader.xul                        (projecteditor/chrome/content/projecteditor-loader.xul)
     content/browser/devtools/projecteditor-test.xul                          (projecteditor/chrome/content/projecteditor-test.xul)
     content/browser/devtools/projecteditor-loader.js                         (projecteditor/chrome/content/projecteditor-loader.js)
     content/browser/devtools/netmonitor.xul                            (netmonitor/netmonitor.xul)
     content/browser/devtools/netmonitor.css                            (netmonitor/netmonitor.css)
     content/browser/devtools/netmonitor-controller.js                  (netmonitor/netmonitor-controller.js)
     content/browser/devtools/netmonitor-view.js                        (netmonitor/netmonitor-view.js)
-    content/browser/devtools/NetworkPanel.xhtml                        (webconsole/NetworkPanel.xhtml)
     content/browser/devtools/webconsole.xul                            (webconsole/webconsole.xul)
 *   content/browser/devtools/scratchpad.xul                            (scratchpad/scratchpad.xul)
     content/browser/devtools/scratchpad.js                             (scratchpad/scratchpad.js)
     content/browser/devtools/splitview.css                             (shared/splitview.css)
     content/browser/devtools/theme-switching.js                        (shared/theme-switching.js)
     content/browser/devtools/frame-script-utils.js                     (shared/frame-script-utils.js)
     content/browser/devtools/styleeditor.xul                           (styleeditor/styleeditor.xul)
     content/browser/devtools/styleeditor.css                           (styleeditor/styleeditor.css)
--- a/browser/devtools/netmonitor/netmonitor-controller.js
+++ b/browser/devtools/netmonitor/netmonitor-controller.js
@@ -318,16 +318,47 @@ let NetMonitorController = {
       this._currentActivity = aType;
       return reconfigureTab({ cacheDisabled: true, performReload: false }).then(standBy);
     }
     this._currentActivity = ACTIVITY_TYPE.NONE;
     return promise.reject(new Error("Invalid activity type"));
   },
 
   /**
+   * Selects the specified request in the waterfall and opens the details view.
+   *
+   * @param string requestId
+   *        The actor ID of the request to inspect.
+   * @return object
+   *         A promise resolved once the task finishes.
+   */
+  inspectRequest: function(requestId) {
+    // Look for the request in the existing ones or wait for it to appear, if
+    // the network monitor is still loading.
+    let deferred = promise.defer();
+    let request = null;
+    let inspector = function() {
+      let predicate = i => i.value === requestId;
+      request = NetMonitorView.RequestsMenu.getItemForPredicate(predicate);
+      if (request) {
+        window.off(EVENTS.REQUEST_ADDED, inspector);
+        NetMonitorView.RequestsMenu.filterOn("all");
+        NetMonitorView.RequestsMenu.selectedItem = request;
+        deferred.resolve();
+      }
+    }
+
+    inspector();
+    if (!request) {
+      window.on(EVENTS.REQUEST_ADDED, inspector);
+    }
+    return deferred.promise;
+  },
+
+  /**
    * Getter that tells if the server supports sending custom network requests.
    * @type boolean
    */
   get supportsCustomRequest() {
     return this.webConsoleClient &&
            (this.webConsoleClient.traits.customNetworkRequest ||
             !this._target.isApp);
   },
deleted file mode 100644
--- a/browser/devtools/webconsole/NetworkPanel.xhtml
+++ /dev/null
@@ -1,124 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
-  "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
-<!ENTITY % webConsoleDTD SYSTEM "chrome://browser/locale/devtools/webConsole.dtd" >
-%webConsoleDTD;
-]>
-
-<!-- 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 xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
-<head>
-  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
-  <link rel="stylesheet" href="chrome://browser/skin/devtools/webconsole_networkpanel.css" type="text/css"/>
-</head>
-<body role="application">
-<table id="header">
-  <tr>
-    <th class="property-name"
-        scope="row">&networkPanel.requestURLColon;</th>
-    <td class="property-value"
-        id="headUrl"></td>
-  </tr>
-  <tr>
-    <th class="property-name"
-        scope="row">&networkPanel.requestMethodColon;</th>
-    <td class="property-value"
-        id="headMethod"></td>
-  </tr>
-  <tr>
-    <th class="property-name"
-        scope="row">&networkPanel.statusCodeColon;</th>
-    <td class="property-value"
-        id="headStatus"></td>
-  </tr>
-</table>
-
-<div class="group">
-  <h1>
-    &networkPanel.requestHeaders;
-    <span id="requestHeadersInfo" class="info"></span>
-  </h1>
-  <table class="property-table" id="requestHeadersContent"></table>
-
-  <div id="requestCookie" style="display:none">
-    <h1>&networkPanel.requestCookie;</h1>
-    <table class="property-table" id="requestCookieContent"></table>
-  </div>
-
-  <div id="requestBody" style="display:none">
-    <h1>&networkPanel.requestBody;</h1>
-    <table class="property-table" id="requestBodyContent"></table>
-  </div>
-  <div id="requestFormData" style="display:none">
-    <h1>&networkPanel.requestFormData;</h1>
-    <table class="property-table" id="requestFormDataContent"></table>
-  </div>
-  <p id="requestBodyFetchLink" style="display:none"></p>
-</div>
-
-<div class="group" id="responseContainer" style="display:none">
-  <h1>
-    &networkPanel.responseHeaders;
-    <span id="responseHeadersInfo" class="info">&Delta;</span>
-  </h1>
-  <table class="property-table" id="responseHeadersContent"></table>
-
-  <div id="responseCookie" style="display:none">
-    <h1>&networkPanel.responseCookie;</h1>
-    <table class="property-table" id="responseCookieContent"></table>
-  </div>
-
-  <div id="responseBody" style="display:none">
-    <h1>
-      &networkPanel.responseBody;
-      <span class="info" id="responseBodyInfo">&Delta;</span>
-    </h1>
-    <table class="property-table" id="responseBodyContent"></table>
-  </div>
-  <div id="responseBodyCached" style="display:none">
-    <h1>
-      &networkPanel.responseBodyCached;
-      <span class="info" id="responseBodyCachedInfo">&Delta;</span>
-    </h1>
-    <table class="property-table" id="responseBodyCachedContent"></table>
-  </div>
-  <div id="responseNoBody" style="display:none">
-    <h1>
-      &networkPanel.responseNoBody;
-      <span id="responseNoBodyInfo" class="info">&Delta;</span>
-    </h1>
-  </div>
-  <div id="responseBodyUnknownType" style="display:none">
-    <h1>
-      &networkPanel.responseBodyUnknownType;
-      <span id="responseBodyUnknownTypeInfo" class="info">&Delta;</span>
-    </h1>
-    <table class="property-table" id="responseBodyUnknownTypeContent"></table>
-  </div>
-  <div id="responseImage" style="display:none">
-    <h1>
-      &networkPanel.responseImage;
-      <span id="responseImageInfo" class="info"></span>
-    </h1>
-    <div id="responseImageNodeDiv">
-      <img id="responseImageNode" />
-    </div>
-  </div>
-  <div id="responseImageCached" style="display:none">
-    <h1>
-      &networkPanel.responseImageCached;
-      <span id="responseImageCachedInfo" class="info"></span>
-    </h1>
-    <div id="responseImageNodeDiv">
-      <img id="responseImageCachedNode" />
-    </div>
-  </div>
-  <p id="responseBodyFetchLink" style="display:none"></p>
-</div>
-</body>
-</html>
--- a/browser/devtools/webconsole/moz.build
+++ b/browser/devtools/webconsole/moz.build
@@ -5,12 +5,11 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 
 EXTRA_JS_MODULES.devtools.webconsole += [
     'console-commands.js',
     'console-output.js',
     'hudservice.js',
-    'network-panel.js',
     'panel.js',
     'webconsole.js',
 ]
deleted file mode 100644
--- a/browser/devtools/webconsole/network-panel.js
+++ /dev/null
@@ -1,835 +0,0 @@
-/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
-/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
-/* 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/. */
-
-"use strict";
-
-const {Cc, Ci, Cu} = require("chrome");
-
-loader.lazyGetter(this, "NetworkHelper", () => require("devtools/toolkit/webconsole/network-helper"));
-loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
-loader.lazyServiceGetter(this, "mimeService", "@mozilla.org/mime;1", "nsIMIMEService");
-
-let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils;
-
-const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
-let l10n = new WebConsoleUtils.l10n(STRINGS_URI);
-
-
-/**
- * Creates a new NetworkPanel.
- *
- * @constructor
- * @param nsIDOMNode aParent
- *        Parent node to append the created panel to.
- * @param object aHttpActivity
- *        HttpActivity to display in the panel.
- * @param object aWebConsoleFrame
- *        The parent WebConsoleFrame object that owns this network panel
- *        instance.
- */
-function NetworkPanel(aParent, aHttpActivity, aWebConsoleFrame)
-{
-  let doc = aParent.ownerDocument;
-  this.httpActivity = aHttpActivity;
-  this.webconsole = aWebConsoleFrame;
-  this._responseBodyFetch = this._responseBodyFetch.bind(this);
-  this._requestBodyFetch = this._requestBodyFetch.bind(this);
-
-  // Create the underlaying panel
-  this.panel = createElement(doc, "panel", {
-    label: l10n.getStr("NetworkPanel.label"),
-    titlebar: "normal",
-    noautofocus: "true",
-    noautohide: "true",
-    close: "true"
-  });
-
-  // Create the iframe that displays the NetworkPanel XHTML.
-  this.iframe = createAndAppendElement(this.panel, "iframe", {
-    src: "chrome://browser/content/devtools/NetworkPanel.xhtml",
-    type: "content",
-    flex: "1"
-  });
-
-  let self = this;
-
-  // Destroy the panel when it's closed.
-  this.panel.addEventListener("popuphidden", function onPopupHide() {
-    self.panel.removeEventListener("popuphidden", onPopupHide, false);
-    self.panel.parentNode.removeChild(self.panel);
-    self.panel = null;
-    self.iframe = null;
-    self.httpActivity = null;
-    self.webconsole = null;
-
-    if (self.linkNode) {
-      self.linkNode._panelOpen = false;
-      self.linkNode = null;
-    }
-  }, false);
-
-  // Set the document object and update the content once the panel is loaded.
-  this.iframe.addEventListener("load", function onLoad() {
-    if (!self.iframe) {
-      return;
-    }
-
-    self.iframe.removeEventListener("load", onLoad, true);
-    self.update();
-  }, true);
-
-  this.panel.addEventListener("popupshown", function onPopupShown() {
-    self.panel.removeEventListener("popupshown", onPopupShown, true);
-    self.update();
-  }, true);
-
-  // Create the footer.
-  let footer = createElement(doc, "hbox", { align: "end" });
-  createAndAppendElement(footer, "spacer", { flex: 1 });
-
-  createAndAppendElement(footer, "resizer", { dir: "bottomend" });
-  this.panel.appendChild(footer);
-
-  aParent.appendChild(this.panel);
-}
-exports.NetworkPanel = NetworkPanel;
-
-NetworkPanel.prototype =
-{
-  /**
-   * The current state of the output.
-   */
-  _state: 0,
-
-  /**
-   * State variables.
-   */
-  _INIT: 0,
-  _DISPLAYED_REQUEST_HEADER: 1,
-  _DISPLAYED_REQUEST_BODY: 2,
-  _DISPLAYED_RESPONSE_HEADER: 3,
-  _TRANSITION_CLOSED: 4,
-
-  _fromDataRegExp: /Content-Type\:\s*application\/x-www-form-urlencoded/,
-
-  _contentType: null,
-
-  /**
-   * Function callback invoked whenever the panel content is updated. This is
-   * used only by tests.
-   *
-   * @private
-   * @type function
-   */
-  _onUpdate: null,
-
-  get document() {
-    return this.iframe && this.iframe.contentWindow ?
-           this.iframe.contentWindow.document : null;
-  },
-
-  /**
-   * Small helper function that is nearly equal to l10n.getFormatStr
-   * except that it prefixes aName with "NetworkPanel.".
-   *
-   * @param string aName
-   *        The name of an i10n string to format. This string is prefixed with
-   *        "NetworkPanel." before calling the HUDService.getFormatStr function.
-   * @param array aArray
-   *        Values used as placeholder for the i10n string.
-   * @returns string
-   *          The i10n formated string.
-   */
-  _format: function NP_format(aName, aArray)
-  {
-    return l10n.getFormatStr("NetworkPanel." + aName, aArray);
-  },
-
-  /**
-   * Returns the content type of the response body. This is based on the
-   * response.content.mimeType property. If this value is not available, then
-   * the content type is guessed by the file extension of the request URL.
-   *
-   * @return string
-   *         Content type or empty string if no content type could be figured
-   *         out.
-   */
-  get contentType()
-  {
-    if (this._contentType) {
-      return this._contentType;
-    }
-
-    let request = this.httpActivity.request;
-    let response = this.httpActivity.response;
-
-    let contentType = "";
-    let types = response.content ?
-                (response.content.mimeType || "").split(/,|;/) : [];
-    for (let i = 0; i < types.length; i++) {
-      if (types[i] in NetworkHelper.mimeCategoryMap) {
-        contentType = types[i];
-        break;
-      }
-    }
-
-    if (contentType) {
-      this._contentType = contentType;
-      return contentType;
-    }
-
-    // Try to get the content type from the request file extension.
-    let uri = NetUtil.newURI(request.url);
-    if ((uri instanceof Ci.nsIURL) && uri.fileExtension) {
-      try {
-         contentType = mimeService.getTypeFromExtension(uri.fileExtension);
-      }
-      catch(ex) {
-        // Added to prevent failures on OS X 64. No Flash?
-        Cu.reportError(ex);
-      }
-    }
-
-    this._contentType = contentType;
-    return contentType;
-  },
-
-  /**
-   *
-   * @returns boolean
-   *          True if the response is an image, false otherwise.
-   */
-  get _responseIsImage()
-  {
-    return this.contentType &&
-           NetworkHelper.mimeCategoryMap[this.contentType] == "image";
-  },
-
-  /**
-   *
-   * @returns boolean
-   *          True if the response body contains text, false otherwise.
-   */
-  get _isResponseBodyTextData()
-  {
-    return this.contentType ?
-           NetworkHelper.isTextMimeType(this.contentType) : false;
-  },
-
-  /**
-   * Tells if the server response is cached.
-   *
-   * @returns boolean
-   *          Returns true if the server responded that the request is already
-   *          in the browser's cache, false otherwise.
-   */
-  get _isResponseCached()
-  {
-    return this.httpActivity.response.status == 304;
-  },
-
-  /**
-   * Tells if the request body includes form data.
-   *
-   * @returns boolean
-   *          Returns true if the posted body contains form data.
-   */
-  get _isRequestBodyFormData()
-  {
-    let requestBody = this.httpActivity.request.postData.text;
-    if (typeof requestBody == "object" && requestBody.type == "longString") {
-      requestBody = requestBody.initial;
-    }
-    return this._fromDataRegExp.test(requestBody);
-  },
-
-  /**
-   * Appends the node with id=aId by the text aValue.
-   *
-   * @private
-   * @param string aId
-   * @param string aValue
-   * @return nsIDOMElement
-   *         The DOM element with id=aId.
-   */
-  _appendTextNode: function NP__appendTextNode(aId, aValue)
-  {
-    let textNode = this.document.createTextNode(aValue);
-    let elem = this.document.getElementById(aId);
-    elem.appendChild(textNode);
-    return elem;
-  },
-
-  /**
-   * Generates some HTML to display the key-value pair of the aList data. The
-   * generated HTML is added to node with id=aParentId.
-   *
-   * @param string aParentId
-   *        Id of the parent node to append the list to.
-   * @oaram array aList
-   *        Array that holds the objects you want to display. Each object must
-   *        have two properties: name and value.
-   * @param boolean aIgnoreCookie
-   *        If true, the key-value named "Cookie" is not added to the list.
-   * @returns void
-   */
-  _appendList: function NP_appendList(aParentId, aList, aIgnoreCookie)
-  {
-    let parent = this.document.getElementById(aParentId);
-    let doc = this.document;
-
-    aList.sort(function(a, b) {
-      return a.name.toLowerCase() < b.name.toLowerCase();
-    });
-
-    aList.forEach((aItem) => {
-      let name = aItem.name;
-      if (aIgnoreCookie && (name == "Cookie" || name == "Set-Cookie")) {
-        return;
-      }
-
-      let value = aItem.value;
-      let longString = null;
-      if (typeof value == "object" && value.type == "longString") {
-        value = value.initial;
-        longString = true;
-      }
-
-      /**
-       * The following code creates the HTML:
-       * <tr>
-       * <th scope="row" class="property-name">${line}:</th>
-       * <td class="property-value">${aList[line]}</td>
-       * </tr>
-       * and adds it to parent.
-       */
-      let row = doc.createElement("tr");
-      let textNode = doc.createTextNode(name + ":");
-      let th = doc.createElement("th");
-      th.setAttribute("scope", "row");
-      th.setAttribute("class", "property-name");
-      th.appendChild(textNode);
-      row.appendChild(th);
-
-      textNode = doc.createTextNode(value);
-      let td = doc.createElement("td");
-      td.setAttribute("class", "property-value");
-      td.appendChild(textNode);
-
-      if (longString) {
-        let a = doc.createElement("a");
-        a.href = "#";
-        a.className = "longStringEllipsis";
-        a.addEventListener("mousedown", this._longStringClick.bind(this, aItem));
-        a.textContent = l10n.getStr("longStringEllipsis");
-        td.appendChild(a);
-      }
-
-      row.appendChild(td);
-
-      parent.appendChild(row);
-    });
-  },
-
-  /**
-   * The click event handler for the ellipsis which allows the user to retrieve
-   * the full header value.
-   *
-   * @private
-   * @param object aHeader
-   *        The header object with the |name| and |value| properties.
-   * @param nsIDOMEvent aEvent
-   *        The DOM click event object.
-   */
-  _longStringClick: function NP__longStringClick(aHeader, aEvent)
-  {
-    aEvent.preventDefault();
-
-    let longString = this.webconsole.webConsoleClient.longString(aHeader.value);
-
-    longString.substring(longString.initial.length, longString.length,
-      function NP__onLongStringSubstring(aResponse)
-      {
-        if (aResponse.error) {
-          Cu.reportError("NP__onLongStringSubstring error: " + aResponse.error);
-          return;
-        }
-
-        aHeader.value = aHeader.value.initial + aResponse.substring;
-
-        let textNode = aEvent.target.previousSibling;
-        textNode.textContent += aResponse.substring;
-        textNode.parentNode.removeChild(aEvent.target);
-      });
-  },
-
-  /**
-   * Displays the node with id=aId.
-   *
-   * @private
-   * @param string aId
-   * @return nsIDOMElement
-   *         The element with id=aId.
-   */
-  _displayNode: function NP__displayNode(aId)
-  {
-    let elem = this.document.getElementById(aId);
-    elem.style.display = "block";
-  },
-
-  /**
-   * Sets the request URL, request method, the timing information when the
-   * request started and the request header content on the NetworkPanel.
-   * If the request header contains cookie data, a list of sent cookies is
-   * generated and a special sent cookie section is displayed + the cookie list
-   * added to it.
-   *
-   * @returns void
-   */
-  _displayRequestHeader: function NP__displayRequestHeader()
-  {
-    let request = this.httpActivity.request;
-    let requestTime = new Date(this.httpActivity.startedDateTime);
-
-    this._appendTextNode("headUrl", request.url);
-    this._appendTextNode("headMethod", request.method);
-    this._appendTextNode("requestHeadersInfo",
-                         l10n.timestampString(requestTime));
-
-    this._appendList("requestHeadersContent", request.headers, true);
-
-    if (request.cookies.length > 0) {
-      this._displayNode("requestCookie");
-      this._appendList("requestCookieContent", request.cookies);
-    }
-  },
-
-  /**
-   * Displays the request body section of the NetworkPanel and set the request
-   * body content on the NetworkPanel.
-   *
-   * @returns void
-   */
-  _displayRequestBody: function NP__displayRequestBody()
-  {
-    let postData = this.httpActivity.request.postData;
-    this._displayNode("requestBody");
-    this._appendTextNode("requestBodyContent", postData.text);
-  },
-
-  /*
-   * Displays the `sent form data` section. Parses the request header for the
-   * submitted form data displays it inside of the `sent form data` section.
-   *
-   * @returns void
-   */
-  _displayRequestForm: function NP__processRequestForm()
-  {
-    let postData = this.httpActivity.request.postData.text;
-    let requestBodyLines = postData.split("\n");
-    let formData = requestBodyLines[requestBodyLines.length - 1].
-                      replace(/\+/g, " ").split("&");
-
-    function unescapeText(aText)
-    {
-      try {
-        return decodeURIComponent(aText);
-      }
-      catch (ex) {
-        return decodeURIComponent(unescape(aText));
-      }
-    }
-
-    let formDataArray = [];
-    for (let i = 0; i < formData.length; i++) {
-      let data = formData[i];
-      let idx = data.indexOf("=");
-      let key = data.substring(0, idx);
-      let value = data.substring(idx + 1);
-      formDataArray.push({
-        name: unescapeText(key),
-        value: unescapeText(value)
-      });
-    }
-
-    this._appendList("requestFormDataContent", formDataArray);
-    this._displayNode("requestFormData");
-  },
-
-  /**
-   * Displays the response section of the NetworkPanel, sets the response status,
-   * the duration between the start of the request and the receiving of the
-   * response header as well as the response header content on the the NetworkPanel.
-   *
-   * @returns void
-   */
-  _displayResponseHeader: function NP__displayResponseHeader()
-  {
-    let timing = this.httpActivity.timings;
-    let response = this.httpActivity.response;
-
-    this._appendTextNode("headStatus",
-                         [response.httpVersion, response.status,
-                          response.statusText].join(" "));
-
-    // Calculate how much time it took from the request start, until the
-    // response started to be received.
-    let deltaDuration = 0;
-    ["dns", "connect", "send", "wait"].forEach(function (aValue) {
-      let ms = timing[aValue];
-      if (ms > -1) {
-        deltaDuration += ms;
-      }
-    });
-
-    this._appendTextNode("responseHeadersInfo",
-      this._format("durationMS", [deltaDuration]));
-
-    this._displayNode("responseContainer");
-    this._appendList("responseHeadersContent", response.headers, true);
-
-    if (response.cookies.length > 0) {
-      this._displayNode("responseCookie");
-      this._appendList("responseCookieContent", response.cookies);
-    }
-  },
-
-  /**
-   * Displays the respones image section, sets the source of the image displayed
-   * in the image response section to the request URL and the duration between
-   * the receiving of the response header and the end of the request. Once the
-   * image is loaded, the size of the requested image is set.
-   *
-   * @returns void
-   */
-  _displayResponseImage: function NP__displayResponseImage()
-  {
-    let self = this;
-    let timing = this.httpActivity.timings;
-    let request = this.httpActivity.request;
-    let response = this.httpActivity.response;
-    let cached = "";
-
-    if (this._isResponseCached) {
-      cached = "Cached";
-    }
-
-    let imageNode = this.document.getElementById("responseImage" +
-                                                 cached + "Node");
-
-    let text = response.content.text;
-    if (typeof text == "object" && text.type == "longString") {
-      this._showResponseBodyFetchLink();
-    }
-    else {
-      imageNode.setAttribute("src",
-        "data:" + this.contentType + ";base64," + text);
-    }
-
-    // This function is called to set the imageInfo.
-    function setImageInfo() {
-      self._appendTextNode("responseImage" + cached + "Info",
-        self._format("imageSizeDeltaDurationMS",
-          [ imageNode.width, imageNode.height, timing.receive ]
-        )
-      );
-    }
-
-    // Check if the image is already loaded.
-    if (imageNode.width != 0) {
-      setImageInfo();
-    }
-    else {
-      // Image is not loaded yet therefore add a load event.
-      imageNode.addEventListener("load", function imageNodeLoad() {
-        imageNode.removeEventListener("load", imageNodeLoad, false);
-        setImageInfo();
-      }, false);
-    }
-
-    this._displayNode("responseImage" + cached);
-  },
-
-  /**
-   * Displays the response body section, sets the the duration between
-   * the receiving of the response header and the end of the request as well as
-   * the content of the response body on the NetworkPanel.
-   *
-   * @returns void
-   */
-  _displayResponseBody: function NP__displayResponseBody()
-  {
-    let timing = this.httpActivity.timings;
-    let response = this.httpActivity.response;
-    let cached =  this._isResponseCached ? "Cached" : "";
-
-    this._appendTextNode("responseBody" + cached + "Info",
-      this._format("durationMS", [timing.receive]));
-
-    this._displayNode("responseBody" + cached);
-
-    let text = response.content.text;
-    if (typeof text == "object") {
-      text = text.initial;
-      this._showResponseBodyFetchLink();
-    }
-
-    this._appendTextNode("responseBody" + cached + "Content", text);
-  },
-
-  /**
-   * Show the "fetch response body" link.
-   * @private
-   */
-  _showResponseBodyFetchLink: function NP__showResponseBodyFetchLink()
-  {
-    let content = this.httpActivity.response.content;
-
-    let elem = this._appendTextNode("responseBodyFetchLink",
-      this._format("fetchRemainingResponseContentLink",
-                   [content.text.length - content.text.initial.length]));
-
-    elem.style.display = "block";
-    elem.addEventListener("mousedown", this._responseBodyFetch);
-  },
-
-  /**
-   * Click event handler for the link that allows users to fetch the remaining
-   * response body.
-   *
-   * @private
-   * @param nsIDOMEvent aEvent
-   */
-  _responseBodyFetch: function NP__responseBodyFetch(aEvent)
-  {
-    aEvent.target.style.display = "none";
-    aEvent.target.removeEventListener("mousedown", this._responseBodyFetch);
-
-    let content = this.httpActivity.response.content;
-    let longString = this.webconsole.webConsoleClient.longString(content.text);
-    longString.substring(longString.initial.length, longString.length,
-      (aResponse) =>
-      {
-        if (aResponse.error) {
-          Cu.reportError("NP__onLongStringSubstring error: " + aResponse.error);
-          return;
-        }
-
-        content.text = content.text.initial + aResponse.substring;
-        let cached =  this._isResponseCached ? "Cached" : "";
-
-        if (this._responseIsImage) {
-          let imageNode = this.document.getElementById("responseImage" +
-                                                       cached + "Node");
-          imageNode.src =
-            "data:" + this.contentType + ";base64," + content.text;
-        }
-        else {
-          this._appendTextNode("responseBody" + cached + "Content",
-                               aResponse.substring);
-        }
-      });
-  },
-
-  /**
-   * Displays the `Unknown Content-Type hint` and sets the duration between the
-   * receiving of the response header on the NetworkPanel.
-   *
-   * @returns void
-   */
-  _displayResponseBodyUnknownType: function NP__displayResponseBodyUnknownType()
-  {
-    let timing = this.httpActivity.timings;
-
-    this._displayNode("responseBodyUnknownType");
-    this._appendTextNode("responseBodyUnknownTypeInfo",
-      this._format("durationMS", [timing.receive]));
-
-    this._appendTextNode("responseBodyUnknownTypeContent",
-      this._format("responseBodyUnableToDisplay.content", [this.contentType]));
-  },
-
-  /**
-   * Displays the `no response body` section and sets the the duration between
-   * the receiving of the response header and the end of the request.
-   *
-   * @returns void
-   */
-  _displayNoResponseBody: function NP_displayNoResponseBody()
-  {
-    let timing = this.httpActivity.timings;
-
-    this._displayNode("responseNoBody");
-    this._appendTextNode("responseNoBodyInfo",
-      this._format("durationMS", [timing.receive]));
-  },
-
-  /**
-   * Updates the content of the NetworkPanel's iframe.
-   *
-   * @returns void
-   */
-  update: function NP_update()
-  {
-    if (!this.document || this.document.readyState != "complete") {
-      return;
-    }
-
-    let updates = this.httpActivity.updates;
-    let timing = this.httpActivity.timings;
-    let request = this.httpActivity.request;
-    let response = this.httpActivity.response;
-
-    switch (this._state) {
-      case this._INIT:
-        this._displayRequestHeader();
-        this._state = this._DISPLAYED_REQUEST_HEADER;
-        // FALL THROUGH
-
-      case this._DISPLAYED_REQUEST_HEADER:
-        // Process the request body if there is one.
-        if (!this.httpActivity.discardRequestBody && request.postData.text) {
-          this._updateRequestBody();
-          this._state = this._DISPLAYED_REQUEST_BODY;
-        }
-        // FALL THROUGH
-
-      case this._DISPLAYED_REQUEST_BODY:
-        if (!response.headers.length || !Object.keys(timing).length) {
-          break;
-        }
-        this._displayResponseHeader();
-        this._state = this._DISPLAYED_RESPONSE_HEADER;
-        // FALL THROUGH
-
-      case this._DISPLAYED_RESPONSE_HEADER:
-        if (updates.indexOf("responseContent") == -1 ||
-            updates.indexOf("eventTimings") == -1) {
-          break;
-        }
-
-        this._state = this._TRANSITION_CLOSED;
-        if (this.httpActivity.discardResponseBody) {
-          break;
-        }
-
-        if (!response.content || !response.content.text) {
-          this._displayNoResponseBody();
-        }
-        else if (this._responseIsImage) {
-          this._displayResponseImage();
-        }
-        else if (!this._isResponseBodyTextData) {
-          this._displayResponseBodyUnknownType();
-        }
-        else if (response.content.text) {
-          this._displayResponseBody();
-        }
-        break;
-    }
-
-    if (this._onUpdate) {
-      this._onUpdate();
-    }
-  },
-
-  /**
-   * Update the panel to hold the current information we have about the request
-   * body.
-   * @private
-   */
-  _updateRequestBody: function NP__updateRequestBody()
-  {
-    let postData = this.httpActivity.request.postData;
-    if (typeof postData.text == "object" && postData.text.type == "longString") {
-      let elem = this._appendTextNode("requestBodyFetchLink",
-        this._format("fetchRemainingRequestContentLink",
-                     [postData.text.length - postData.text.initial.length]));
-
-      elem.style.display = "block";
-      elem.addEventListener("mousedown", this._requestBodyFetch);
-      return;
-    }
-
-    // Check if we send some form data. If so, display the form data special.
-    if (this._isRequestBodyFormData) {
-      this._displayRequestForm();
-    }
-    else {
-      this._displayRequestBody();
-    }
-  },
-
-  /**
-   * Click event handler for the link that allows users to fetch the remaining
-   * request body.
-   *
-   * @private
-   * @param nsIDOMEvent aEvent
-   */
-  _requestBodyFetch: function NP__requestBodyFetch(aEvent)
-  {
-    aEvent.target.style.display = "none";
-    aEvent.target.removeEventListener("mousedown", this._responseBodyFetch);
-
-    let postData = this.httpActivity.request.postData;
-    let longString = this.webconsole.webConsoleClient.longString(postData.text);
-    longString.substring(longString.initial.length, longString.length,
-       (aResponse) =>
-      {
-        if (aResponse.error) {
-          Cu.reportError("NP__onLongStringSubstring error: " + aResponse.error);
-          return;
-        }
-
-        postData.text = postData.text.initial + aResponse.substring;
-        this._updateRequestBody();
-      });
-  },
-};
-
-/**
- * Creates a DOMNode and sets all the attributes of aAttributes on the created
- * element.
- *
- * @param nsIDOMDocument aDocument
- *        Document to create the new DOMNode.
- * @param string aTag
- *        Name of the tag for the DOMNode.
- * @param object aAttributes
- *        Attributes set on the created DOMNode.
- *
- * @returns nsIDOMNode
- */
-function createElement(aDocument, aTag, aAttributes)
-{
-  let node = aDocument.createElement(aTag);
-  if (aAttributes) {
-    for (let attr in aAttributes) {
-      node.setAttribute(attr, aAttributes[attr]);
-    }
-  }
-  return node;
-}
-
-/**
- * Creates a new DOMNode and appends it to aParent.
- *
- * @param nsIDOMNode aParent
- *        A parent node to append the created element.
- * @param string aTag
- *        Name of the tag for the DOMNode.
- * @param object aAttributes
- *        Attributes set on the created DOMNode.
- *
- * @returns nsIDOMNode
- */
-function createAndAppendElement(aParent, aTag, aAttributes)
-{
-  let node = createElement(aParent.ownerDocument, aTag, aAttributes);
-  aParent.appendChild(node);
-  return node;
-}
--- a/browser/devtools/webconsole/test/browser.ini
+++ b/browser/devtools/webconsole/test/browser.ini
@@ -184,18 +184,16 @@ skip-if = buildapp == 'mulet'
 skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
 [browser_console_variables_view_while_debugging_and_inspecting.js]
 skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
 [browser_eval_in_debugger_stackframe.js]
 skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
 [browser_eval_in_debugger_stackframe2.js]
 [browser_jsterm_inspect.js]
 [browser_longstring_hang.js]
-[browser_netpanel_longstring_expand.js]
-skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
 [browser_output_breaks_after_console_dir_uninspectable.js]
 [browser_output_longstring_expand.js]
 [browser_repeated_messages_accuracy.js]
 skip-if = buildapp == 'mulet'
 [browser_result_format_as_string.js]
 [browser_warn_user_about_replaced_api.js]
 [browser_webconsole_abbreviate_source_url.js]
 [browser_webconsole_allow_mixedcontent_securityerrors.js]
@@ -222,17 +220,16 @@ skip-if = e10s # Bug 1042253 - webconsol
 [browser_webconsole_bug_588342_document_focus.js]
 skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
 [browser_webconsole_bug_588730_text_node_insertion.js]
 [browser_webconsole_bug_588967_input_expansion.js]
 [browser_webconsole_bug_589162_css_filter.js]
 [browser_webconsole_bug_592442_closing_brackets.js]
 [browser_webconsole_bug_593003_iframe_wrong_hud.js]
 skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
-[browser_webconsole_bug_594477_clickable_output.js]
 [browser_webconsole_bug_594497_history_arrow_keys.js]
 [browser_webconsole_bug_595223_file_uri.js]
 [browser_webconsole_bug_595350_multiple_windows_and_tabs.js]
 skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
 [browser_webconsole_bug_595934_message_categories.js]
 skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
 [browser_webconsole_bug_597103_deactivateHUDForContext_unfocused_window.js]
 skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
@@ -240,27 +237,24 @@ skip-if = e10s # Bug 1042253 - webconsol
 [browser_webconsole_bug_597136_network_requests_from_chrome.js]
 [browser_webconsole_bug_597460_filter_scroll.js]
 [browser_webconsole_bug_597756_reopen_closed_tab.js]
 [browser_webconsole_bug_599725_response_headers.js]
 [browser_webconsole_bug_600183_charset.js]
 [browser_webconsole_bug_601177_log_levels.js]
 [browser_webconsole_bug_601352_scroll.js]
 [browser_webconsole_bug_601667_filter_buttons.js]
-[browser_webconsole_bug_602572_log_bodies_checkbox.js]
-skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
 [browser_webconsole_bug_603750_websocket.js]
 [browser_webconsole_bug_611795.js]
 [browser_webconsole_bug_613013_console_api_iframe.js]
 [browser_webconsole_bug_613280_jsterm_copy.js]
 [browser_webconsole_bug_613642_maintain_scroll.js]
 [browser_webconsole_bug_613642_prune_scroll.js]
 [browser_webconsole_bug_614793_jsterm_scroll.js]
 [browser_webconsole_bug_618078_network_exceptions.js]
-[browser_webconsole_bug_618311_close_panels.js]
 [browser_webconsole_bug_621644_jsterm_dollar.js]
 [browser_webconsole_bug_622303_persistent_filters.js]
 [browser_webconsole_bug_623749_ctrl_a_select_all_winnt.js]
 skip-if = os != "win"
 [browser_webconsole_bug_630733_response_redirect_headers.js]
 [browser_webconsole_bug_632275_getters_document_width.js]
 [browser_webconsole_bug_632347_iterators_generators.js]
 skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
@@ -329,17 +323,16 @@ skip-if = buildapp == 'mulet' || e10s # 
 [browser_webconsole_inspect-parsed-documents.js]
 [browser_webconsole_js_input_expansion.js]
 [browser_webconsole_jsterm.js]
 skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout)
 [browser_webconsole_live_filtering_of_message_types.js]
 [browser_webconsole_live_filtering_on_search_strings.js]
 [browser_webconsole_message_node_id.js]
 [browser_webconsole_netlogging.js]
-[browser_webconsole_network_panel.js]
 [browser_webconsole_notifications.js]
 [browser_webconsole_open-links-without-callback.js]
 [browser_webconsole_promise.js]
 [browser_webconsole_output_copy_newlines.js]
 [browser_webconsole_output_order.js]
 [browser_webconsole_property_provider.js]
 skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
 [browser_webconsole_scratchpad_panel_link.js]
deleted file mode 100644
--- a/browser/devtools/webconsole/test/browser_netpanel_longstring_expand.js
+++ /dev/null
@@ -1,312 +0,0 @@
-/* vim:set ts=2 sw=2 sts=2 et: */
-/* 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/. */
-
-// Tests that the network panel works with LongStringActors.
-
-"use strict";
-
-const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/" +
-                 "test/test-console.html";
-const TEST_IMG = "http://example.com/browser/browser/devtools/webconsole/" +
-                 "test/test-image.png";
-
-const TEST_IMG_BASE64 =
-  "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAVRJ" +
-  "REFUOI2lk7FLw0AUxr+YpC1CBqcMWfsvCCLdXFzqEJCgDl1EQRGxg9AhSBEJONhFhG52UCuF" +
-  "Djq5dxD8FwoO0qGDOBQkl7vLOeWa2EQDffDBvTu+373Hu1OEEJgntGgxGD6J+7fLXKbt5VNU" +
-  "yhsKAChRBQcPFVFeWskFGH694mZroCQqCLlAwPxcgJBP254CmAD5B7C7dgHLMLF3uzoL4DQE" +
-  "od+Z5sP1FizDxGgyBqfhLID9AahX29J89bwPFgMsSEAQglAf9WobhPpScbPXr4FQHyzIADTs" +
-  "DizDRMPuIOC+zEeTMZo9BwH3EfAMACccbtfGaDKGZZg423yUZrdrg3EqxQlPr0BTdTR7joRE" +
-  "N2uqnlBmCwW1hIJagtev4f3zA16/JvfiigMSYyzqJXlw/XKUyOORMUaBor6YavgdjKa8xGOn" +
-  "idadmwtwsnMu18q83/kHSou+bFNDDr4AAAAASUVORK5CYII=";
-
-let testDriver;
-
-function test() {
-  loadTab(TEST_URI).then(() => {
-    openConsole().then(testNetworkPanel);
-  });
-}
-
-function testNetworkPanel() {
-  testDriver = testGen();
-  testDriver.next();
-}
-
-function checkIsVisible(aPanel, aList) {
-  for (let id in aList) {
-    let node = aPanel.document.getElementById(id);
-    let isVisible = aList[id];
-    is(node.style.display, (isVisible ? "block" : "none"),
-       id + " isVisible=" + isVisible);
-  }
-}
-
-function checkNodeContent(aPanel, aId, aContent) {
-  let node = aPanel.document.getElementById(aId);
-  if (node == null) {
-    ok(false, "Tried to access node " + aId + " that doesn't exist!");
-  } else if (node.textContent.indexOf(aContent) != -1) {
-    ok(true, "checking content of " + aId);
-  } else {
-    ok(false, "Got false value for " + aId + ": " + node.textContent +
-       " doesn't have " + aContent);
-  }
-}
-
-function checkNodeKeyValue(aPanel, aId, aKey, aValue) {
-  let node = aPanel.document.getElementById(aId);
-
-  let headers = node.querySelectorAll("th");
-  for (let i = 0; i < headers.length; i++) {
-    if (headers[i].textContent == (aKey + ":")) {
-      is(headers[i].nextElementSibling.textContent, aValue,
-         "checking content of " + aId + " for key " + aKey);
-      return;
-    }
-  }
-
-  ok(false, "content check failed for " + aId + ", key " + aKey);
-}
-
-function* testGen() {
-  let hud = HUDService.getHudByWindow(content);
-  let filterBox = hud.ui.filterBox;
-
-  let headerValue = (new Array(456)).join("fooz bar");
-  let headerValueGrip = {
-    type: "longString",
-    initial: headerValue.substr(0, 123),
-    length: headerValue.length,
-    actor: "faktor",
-    _fullString: headerValue,
-  };
-
-  let imageContentGrip = {
-    type: "longString",
-    initial: TEST_IMG_BASE64.substr(0, 143),
-    length: TEST_IMG_BASE64.length,
-    actor: "faktor2",
-    _fullString: TEST_IMG_BASE64,
-  };
-
-  let postDataValue = (new Array(123)).join("post me");
-  let postDataGrip = {
-    type: "longString",
-    initial: postDataValue.substr(0, 172),
-    length: postDataValue.length,
-    actor: "faktor3",
-    _fullString: postDataValue,
-  };
-
-  let httpActivity = {
-    updates: ["responseContent", "eventTimings"],
-    discardRequestBody: false,
-    discardResponseBody: false,
-    startedDateTime: (new Date()).toISOString(),
-    request: {
-      url: TEST_IMG,
-      method: "GET",
-      cookies: [],
-      headers: [
-        { name: "foo", value: "bar" },
-        { name: "loongstring", value: headerValueGrip },
-      ],
-      postData: { text: postDataGrip },
-    },
-    response: {
-      httpVersion: "HTTP/3.14",
-      status: 2012,
-      statusText: "ddahl likes tacos :)",
-      headers: [
-        { name: "Content-Type", value: "image/png" },
-      ],
-      content: { mimeType: "image/png", text: imageContentGrip },
-      cookies: [],
-    },
-    timings: { wait: 15, receive: 23 },
-  };
-
-  let networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity);
-
-  is(filterBox._netPanel, networkPanel,
-     "Network panel stored on the anchor object");
-
-  networkPanel._onUpdate = function() {
-    networkPanel._onUpdate = null;
-    executeSoon(function() {
-      testDriver.next();
-    });
-  };
-
-  yield undefined;
-
-  info("test 1: check if a header value is expandable");
-
-  checkIsVisible(networkPanel, {
-    requestCookie: false,
-    requestFormData: false,
-    requestBody: false,
-    requestBodyFetchLink: true,
-    responseContainer: true,
-    responseBody: false,
-    responseNoBody: false,
-    responseImage: true,
-    responseImageCached: false,
-    responseBodyFetchLink: true,
-  });
-
-  checkNodeKeyValue(networkPanel, "requestHeadersContent", "foo", "bar");
-  checkNodeKeyValue(networkPanel, "requestHeadersContent", "loongstring",
-                    headerValueGrip.initial + "[\u2026]");
-
-  let webConsoleClient = networkPanel.webconsole.webConsoleClient;
-  let longStringFn = webConsoleClient.longString;
-
-  let expectedGrip = headerValueGrip;
-
-  function longStringClientProvider(aLongString) {
-    is(aLongString, expectedGrip,
-       "longString grip is correct");
-
-    return {
-      initial: expectedGrip.initial,
-      length: expectedGrip.length,
-      substring: function(aStart, aEnd, aCallback) {
-        is(aStart, expectedGrip.initial.length,
-           "substring start is correct");
-        is(aEnd, expectedGrip.length,
-           "substring end is correct");
-
-        executeSoon(function() {
-          aCallback({
-            substring: expectedGrip._fullString.substring(aStart, aEnd),
-          });
-
-          executeSoon(function() {
-            testDriver.next();
-          });
-        });
-      },
-    };
-  }
-
-  webConsoleClient.longString = longStringClientProvider;
-
-  let clickable = networkPanel.document
-                  .querySelector("#requestHeadersContent .longStringEllipsis");
-  ok(clickable, "long string ellipsis is shown");
-
-  EventUtils.sendMouseEvent({ type: "mousedown"}, clickable,
-                             networkPanel.document.defaultView);
-
-  yield undefined;
-
-  clickable = networkPanel.document
-              .querySelector("#requestHeadersContent .longStringEllipsis");
-  ok(!clickable, "long string ellipsis is not shown");
-
-  checkNodeKeyValue(networkPanel, "requestHeadersContent", "loongstring",
-                    expectedGrip._fullString);
-
-  info("test 2: check that response body image fetching works");
-  expectedGrip = imageContentGrip;
-
-  let imgNode = networkPanel.document.getElementById("responseImageNode");
-  ok(!imgNode.getAttribute("src"), "no image is displayed");
-
-  clickable = networkPanel.document.querySelector("#responseBodyFetchLink");
-  EventUtils.sendMouseEvent({ type: "mousedown"}, clickable,
-                             networkPanel.document.defaultView);
-
-  yield undefined;
-
-  imgNode = networkPanel.document.getElementById("responseImageNode");
-  is(imgNode.getAttribute("src"), "data:image/png;base64," + TEST_IMG_BASE64,
-     "displayed image is correct");
-  is(clickable.style.display, "none", "#responseBodyFetchLink is not visible");
-
-  info("test 3: expand the request body");
-
-  expectedGrip = postDataGrip;
-
-  clickable = networkPanel.document.querySelector("#requestBodyFetchLink");
-  EventUtils.sendMouseEvent({ type: "mousedown"}, clickable,
-                             networkPanel.document.defaultView);
-  yield undefined;
-
-  is(clickable.style.display, "none", "#requestBodyFetchLink is not visible");
-
-  checkIsVisible(networkPanel, {
-    requestBody: true,
-    requestBodyFetchLink: false,
-  });
-
-  checkNodeContent(networkPanel, "requestBodyContent",
-                   expectedGrip._fullString);
-
-  webConsoleClient.longString = longStringFn;
-
-  networkPanel.panel.hidePopup();
-
-  info("test 4: reponse body long text");
-
-  httpActivity.response.content.mimeType = "text/plain";
-  httpActivity.response.headers[0].value = "text/plain";
-
-  expectedGrip = imageContentGrip;
-
-  // Reset response.content.text to avoid caching of the full string.
-  httpActivity.response.content.text = expectedGrip;
-
-  networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity);
-  is(filterBox._netPanel, networkPanel,
-     "Network panel stored on httpActivity object");
-
-  networkPanel._onUpdate = function() {
-    networkPanel._onUpdate = null;
-    executeSoon(function() {
-      testDriver.next();
-    });
-  };
-
-  yield undefined;
-
-  checkIsVisible(networkPanel, {
-    requestCookie: false,
-    requestFormData: false,
-    requestBody: true,
-    requestBodyFetchLink: false,
-    responseContainer: true,
-    responseBody: true,
-    responseNoBody: false,
-    responseImage: false,
-    responseImageCached: false,
-    responseBodyFetchLink: true,
-  });
-
-  checkNodeContent(networkPanel, "responseBodyContent", expectedGrip.initial);
-
-  webConsoleClient.longString = longStringClientProvider;
-
-  clickable = networkPanel.document.querySelector("#responseBodyFetchLink");
-  EventUtils.sendMouseEvent({ type: "mousedown"}, clickable,
-                             networkPanel.document.defaultView);
-
-  yield undefined;
-
-  webConsoleClient.longString = longStringFn;
-  is(clickable.style.display, "none", "#responseBodyFetchLink is not visible");
-  checkNodeContent(networkPanel, "responseBodyContent",
-                   expectedGrip._fullString);
-
-  networkPanel.panel.hidePopup();
-
-  // All done!
-  testDriver = null;
-  executeSoon(finishTest);
-
-  yield undefined;
-}
deleted file mode 100644
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_594477_clickable_output.js
+++ /dev/null
@@ -1,132 +0,0 @@
-/* vim:set ts=2 sw=2 sts=2 et: */
-/* ***** BEGIN LICENSE BLOCK *****
- * Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/
- *
- * Contributor(s):
- *  Mihai Șucan <mihai.sucan@gmail.com>
- *
- * ***** END LICENSE BLOCK ***** */
-
-"use strict";
-
-const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/" +
-                 "test/test-console.html";
-let HUD;
-let outputItem;
-let outputNode;
-
-let test = asyncTest(function* () {
-  yield loadTab(TEST_URI);
-
-  HUD = yield openConsole();
-  outputNode = HUD.outputNode;
-
-  // reload the tab
-  BrowserReload();
-  yield loadBrowser(gBrowser.selectedBrowser);
-
-  let event = yield clickEvents();
-  yield testClickAgain(event);
-  yield networkPanelHidden();
-
-  HUD = outputItem = outputNode = null;
-});
-
-function clickEvents() {
-  let deferred = promise.defer();
-
-  waitForMessages({
-    webconsole: HUD,
-    messages: [{
-      text: "test-console.html",
-      category: CATEGORY_NETWORK,
-      severity: SEVERITY_LOG,
-    }],
-  }).then(([result]) => {
-    let msg = [...result.matched][0];
-    outputItem = msg.querySelector(".message-body .url");
-    ok(outputItem, "found a network message");
-    document.addEventListener("popupshown", function onPanelShown(event) {
-      document.removeEventListener("popupshown", onPanelShown, false);
-      deferred.resolve(event);
-    }, false);
-
-    // Send the mousedown and click events such that the network panel opens.
-    EventUtils.sendMouseEvent({type: "mousedown"}, outputItem);
-    EventUtils.sendMouseEvent({type: "click"}, outputItem);
-  });
-
-  return deferred.promise;
-}
-
-function testClickAgain(event) {
-  info("testClickAgain");
-
-  let deferred = promise.defer();
-
-  document.addEventListener("popupshown", networkPanelShowFailure, false);
-
-  // The network panel should not open for the second time.
-  EventUtils.sendMouseEvent({type: "mousedown"}, outputItem);
-  EventUtils.sendMouseEvent({type: "click"}, outputItem);
-
-  executeSoon(function() {
-    document.addEventListener("popuphidden", function onHidden() {
-      document.removeEventListener("popuphidden", onHidden, false);
-      deferred.resolve();
-    }, false);
-    event.target.hidePopup();
-  });
-
-  return deferred.promise;
-}
-
-function networkPanelShowFailure() {
-  ok(false, "the network panel should not show");
-}
-
-function networkPanelHidden() {
-  let deferred = promise.defer();
-
-  info("networkPanelHidden");
-
-  // The network panel should not show because this is a mouse event that starts
-  // in a position and ends in another.
-  EventUtils.sendMouseEvent({type: "mousedown", clientX: 3, clientY: 4},
-    outputItem);
-  EventUtils.sendMouseEvent({type: "click", clientX: 5, clientY: 6},
-    outputItem);
-
-  // The network panel should not show because this is a middle-click.
-  EventUtils.sendMouseEvent({type: "mousedown", button: 1},
-    outputItem);
-  EventUtils.sendMouseEvent({type: "click", button: 1},
-    outputItem);
-
-  // The network panel should not show because this is a right-click.
-  EventUtils.sendMouseEvent({type: "mousedown", button: 2},
-    outputItem);
-  EventUtils.sendMouseEvent({type: "click", button: 2},
-    outputItem);
-
-  executeSoon(function() {
-    document.removeEventListener("popupshown", networkPanelShowFailure, false);
-
-    // Done with the network output. Now test the jsterm output and the property
-    // panel.
-    HUD.jsterm.execute("document").then((msg) => {
-      info("jsterm execute 'document' callback");
-
-      HUD.jsterm.once("variablesview-open", deferred.resolve);
-      outputItem = msg.querySelector(".message-body a");
-      ok(outputItem, "jsterm output message found");
-
-      // Send the mousedown and click events such that the property panel opens.
-      EventUtils.sendMouseEvent({type: "mousedown"}, outputItem);
-      EventUtils.sendMouseEvent({type: "click"}, outputItem);
-    });
-  });
-
-  return deferred.promise;
-}
deleted file mode 100644
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_602572_log_bodies_checkbox.js
+++ /dev/null
@@ -1,186 +0,0 @@
-/* vim:set ts=2 sw=2 sts=2 et: */
-/* ***** BEGIN LICENSE BLOCK *****
- * Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/
- *
- * Contributor(s):
- *   Mihai Șucan <mihai.sucan@gmail.com>
- *
- * ***** END LICENSE BLOCK ***** */
-
-"use strict";
-
-let menuitems = [];
-let menupopups = [];
-let huds = [];
-let tabs = [];
-let runCount = 0;
-
-const TEST_URI1 = "data:text/html;charset=utf-8,Web Console test for " +
-                  "bug 602572: log bodies checkbox. tab 1";
-const TEST_URI2 = "data:text/html;charset=utf-8,Web Console test for " +
-                  "bug 602572: log bodies checkbox. tab 2";
-
-function test() {
-  if (runCount == 0) {
-    requestLongerTimeout(2);
-  }
-
-  // open tab 2
-  function openTab() {
-    loadTab(TEST_URI2).then((tab) => {
-      tabs.push(tab.tab);
-      openConsole().then((hud) => {
-        hud.iframeWindow.requestAnimationFrame(startTest);
-      });
-    });
-  }
-
-  // open tab 1
-  loadTab(TEST_URI1).then((tab) => {
-    tabs.push(tab.tab);
-    openConsole().then((hud) => {
-      hud.iframeWindow.requestAnimationFrame(() => {
-        info("iframe1 root height " + hud.ui.rootElement.clientHeight);
-
-        openTab();
-      });
-    });
-  });
-}
-
-function startTest() {
-  // Find the relevant elements in the Web Console of tab 2.
-  let win2 = tabs[runCount * 2 + 1].linkedBrowser.contentWindow;
-  huds[1] = HUDService.getHudByWindow(win2);
-  info("startTest: iframe2 root height " + huds[1].ui.rootElement.clientHeight);
-
-  if (runCount == 0) {
-    menuitems[1] = huds[1].ui.rootElement.querySelector("#saveBodies");
-  } else {
-    menuitems[1] = huds[1].ui.rootElement
-                             .querySelector("#saveBodiesContextMenu");
-  }
-  menupopups[1] = menuitems[1].parentNode;
-
-  // Open the context menu from tab 2.
-  menupopups[1].addEventListener("popupshown", onpopupshown2, false);
-  executeSoon(function() {
-    menupopups[1].openPopup();
-  });
-}
-
-function onpopupshown2(evt) {
-  menupopups[1].removeEventListener(evt.type, onpopupshown2, false);
-
-  // By default bodies are not logged.
-  isnot(menuitems[1].getAttribute("checked"), "true",
-        "menuitems[1] is not checked");
-
-  ok(!huds[1].ui._saveRequestAndResponseBodies, "bodies are not logged");
-
-  // Enable body logging.
-  huds[1].ui.setSaveRequestAndResponseBodies(true).then(() => {
-    menupopups[1].hidePopup();
-  });
-
-  menupopups[1].addEventListener("popuphidden", function _onhidden(evtPopup) {
-    menupopups[1].removeEventListener(evtPopup.type, _onhidden, false);
-
-    info("menupopups[1] hidden");
-
-    // Reopen the context menu.
-    huds[1].ui.once("save-bodies-ui-toggled", () => testpopup2b(evtPopup));
-    menupopups[1].openPopup();
-  }, false);
-}
-
-function testpopup2b() {
-  is(menuitems[1].getAttribute("checked"), "true", "menuitems[1] is checked");
-
-  menupopups[1].addEventListener("popuphidden", function _onhidden(evtPopup) {
-    menupopups[1].removeEventListener(evtPopup.type, _onhidden, false);
-
-    info("menupopups[1] hidden");
-
-    // Switch to tab 1 and open the Web Console context menu from there.
-    gBrowser.selectedTab = tabs[runCount * 2];
-    waitForFocus(function() {
-      // Find the relevant elements in the Web Console of tab 1.
-      let win1 = tabs[runCount * 2].linkedBrowser.contentWindow;
-      huds[0] = HUDService.getHudByWindow(win1);
-
-      info("iframe1 root height " + huds[0].ui.rootElement.clientHeight);
-
-      menuitems[0] = huds[0].ui.rootElement.querySelector("#saveBodies");
-      menupopups[0] = huds[0].ui.rootElement.querySelector("menupopup");
-
-      menupopups[0].addEventListener("popupshown", onpopupshown1, false);
-      executeSoon(() => menupopups[0].openPopup());
-    }, tabs[runCount * 2].linkedBrowser.contentWindow);
-  }, false);
-
-  executeSoon(function() {
-    menupopups[1].hidePopup();
-  });
-}
-
-function onpopupshown1(evt) {
-  menupopups[0].removeEventListener(evt.type, onpopupshown1, false);
-
-  // The menuitem checkbox must not be in sync with the other tabs.
-  isnot(menuitems[0].getAttribute("checked"), "true",
-        "menuitems[0] is not checked");
-
-  // Enable body logging for tab 1 as well.
-  huds[0].ui.setSaveRequestAndResponseBodies(true).then(() => {
-    menupopups[0].hidePopup();
-  });
-
-  // Close the menu, and switch back to tab 2.
-  menupopups[0].addEventListener("popuphidden", function _onhidden(evtPopup) {
-    menupopups[0].removeEventListener(evtPopup.type, _onhidden, false);
-
-    info("menupopups[0] hidden");
-
-    gBrowser.selectedTab = tabs[runCount * 2 + 1];
-    waitForFocus(function() {
-      // Reopen the context menu from tab 2.
-      huds[1].ui.once("save-bodies-ui-toggled", () => testpopup2c(evtPopup));
-      menupopups[1].openPopup();
-    }, tabs[runCount * 2 + 1].linkedBrowser.contentWindow);
-  }, false);
-}
-
-function testpopup2c() {
-  is(menuitems[1].getAttribute("checked"), "true", "menuitems[1] is checked");
-
-  menupopups[1].addEventListener("popuphidden", function _onhidden(evtPopup) {
-    menupopups[1].removeEventListener(evtPopup.type, _onhidden, false);
-
-    info("menupopups[1] hidden");
-
-    // Done if on second run
-    closeConsole(gBrowser.selectedTab).then(function() {
-      if (runCount == 0) {
-        runCount++;
-        info("start second run");
-        executeSoon(test);
-      } else {
-        gBrowser.removeCurrentTab();
-        gBrowser.selectedTab = tabs[2];
-        gBrowser.removeCurrentTab();
-        gBrowser.selectedTab = tabs[1];
-        gBrowser.removeCurrentTab();
-        gBrowser.selectedTab = tabs[0];
-        gBrowser.removeCurrentTab();
-        huds = menuitems = menupopups = tabs = null;
-        executeSoon(finishTest);
-      }
-    });
-  }, false);
-
-  executeSoon(function() {
-    menupopups[1].hidePopup();
-  });
-}
deleted file mode 100644
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_618311_close_panels.js
+++ /dev/null
@@ -1,93 +0,0 @@
-/* vim:set ts=2 sw=2 sts=2 et: */
-/* 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/. */
-
-"use strict";
-
-const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/" +
-                 "test/test-console.html";
-
-let test = asyncTest(function* () {
-  yield loadTab(TEST_URI);
-
-  let hud = yield openConsole();
-
-  BrowserReload();
-
-  let results = yield waitForMessages({
-    webconsole: hud,
-    messages: [{
-      text: "test-console.html",
-      category: CATEGORY_NETWORK,
-      severity: SEVERITY_LOG,
-    }],
-  });
-
-  yield performTest(hud, results);
-});
-
-function performTest(HUD, results) {
-  let deferred = promise.defer();
-
-  let networkMessage = [...results[0].matched][0];
-  ok(networkMessage, "network message element");
-
-  let networkLink = networkMessage.querySelector(".url");
-  ok(networkLink, "found network message link");
-
-  let popupset = document.getElementById("mainPopupSet");
-  ok(popupset, "found #mainPopupSet");
-
-  let popupsShown = 0;
-  let hiddenPopups = 0;
-
-  let onpopupshown = function() {
-    document.removeEventListener("popupshown", onpopupshown, false);
-    popupsShown++;
-
-    executeSoon(function() {
-      let popups = popupset.querySelectorAll("panel[hudId=" + HUD.hudId + "]");
-      is(popups.length, 1, "found one popup");
-
-      document.addEventListener("popuphidden", onpopuphidden, false);
-
-      registerCleanupFunction(function() {
-        is(hiddenPopups, 1, "correct number of popups hidden");
-        if (hiddenPopups != 1) {
-          document.removeEventListener("popuphidden", onpopuphidden, false);
-        }
-      });
-
-      executeSoon(closeConsole);
-    });
-  };
-
-  let onpopuphidden = function() {
-    document.removeEventListener("popuphidden", onpopuphidden, false);
-    hiddenPopups++;
-
-    executeSoon(function() {
-      let popups = popupset.querySelectorAll("panel[hudId=" + HUD.hudId + "]");
-      is(popups.length, 0, "no popups found");
-
-      executeSoon(deferred.resolve);
-    });
-  };
-
-  document.addEventListener("popupshown", onpopupshown, false);
-
-  registerCleanupFunction(function() {
-    is(popupsShown, 1, "correct number of popups shown");
-    if (popupsShown != 1) {
-      document.removeEventListener("popupshown", onpopupshown, false);
-    }
-  });
-
-  EventUtils.sendMouseEvent({ type: "mousedown" }, networkLink,
-                              HUD.iframeWindow);
-  EventUtils.sendMouseEvent({ type: "mouseup" }, networkLink, HUD.iframeWindow);
-  EventUtils.sendMouseEvent({ type: "click" }, networkLink, HUD.iframeWindow);
-
-  return deferred.promise;
-}
--- a/browser/devtools/webconsole/test/browser_webconsole_netlogging.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_netlogging.js
@@ -186,25 +186,27 @@ function testFormSubmission() {
     let form = content.document.querySelector("form");
     form.submit();
   }`);
 }
 
 function testNetworkPanel() {
   // Open the NetworkPanel. The functionality of the NetworkPanel is tested
   // within separate test files.
-  let networkPanel = hud.ui.openNetworkPanel(hud.ui.filterBox, lastRequest);
-
-  networkPanel.panel.addEventListener("popupshown", function onPopupShown() {
-    networkPanel.panel.removeEventListener("popupshown", onPopupShown, true);
-
-    is(hud.ui.filterBox._netPanel, networkPanel,
-       "Network panel stored on anchor node");
-    ok(true, "NetworkPanel was opened");
+  hud.ui.openNetworkPanel(lastRequest.actor).then(() => {
+    let toolbox = gDevTools.getToolbox(hud.target);
+    is(toolbox.currentToolId, "netmonitor", "Network panel was opened");
+    let panel = toolbox.getCurrentPanel();
+    let selected = panel.panelWin.NetMonitorView.RequestsMenu.selectedItem;
+    is(selected.attachment.method, lastRequest.request.method,
+       "The correct request is selected");
+    is(selected.attachment.url, lastRequest.request.url,
+       "The correct request is definitely selected");
 
     // All tests are done. Shutdown.
-    networkPanel.panel.hidePopup();
     lastRequest = null;
     HUDService.lastFinishedRequest.callback = null;
     browser = requestCallback = hud = null;
     executeSoon(finishTest);
-  }, true);
+  }).then(null, error => {
+    ok(false, "Got an error: " + error.message + "\n" + error.stack);
+  });
 }
deleted file mode 100644
--- a/browser/devtools/webconsole/test/browser_webconsole_network_panel.js
+++ /dev/null
@@ -1,551 +0,0 @@
-/* vim:set ts=2 sw=2 sts=2 et: */
-/* 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/. */
-
-// Tests that the network panel works.
-
-"use strict";
-
-const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/" +
-                 "test/test-console.html";
-const TEST_IMG = "http://example.com/browser/browser/devtools/webconsole/" +
-                 "test/test-image.png";
-const TEST_ENCODING_ISO_8859_1 = "http://example.com/browser/browser/" +
-                                 "devtools/webconsole/test/" +
-                                 "test-encoding-ISO-8859-1.html";
-
-const TEST_IMG_BASE64 =
-  "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAVRJ" +
-  "REFUOI2lk7FLw0AUxr+YpC1CBqcMWfsvCCLdXFzqEJCgDl1EQRGxg9AhSBEJONhFhG52UCuF" +
-  "Djq5dxD8FwoO0qGDOBQkl7vLOeWa2EQDffDBvTu+373Hu1OEEJgntGgxGD6J+7fLXKbt5VNU" +
-  "yhsKAChRBQcPFVFeWskFGH694mZroCQqCLlAwPxcgJBP254CmAD5B7C7dgHLMLF3uzoL4DQE" +
-  "od+Z5sP1FizDxGgyBqfhLID9AahX29J89bwPFgMsSEAQglAf9WobhPpScbPXr4FQHyzIADTs" +
-  "DizDRMPuIOC+zEeTMZo9BwH3EfAMACccbtfGaDKGZZg423yUZrdrg3EqxQlPr0BTdTR7joRE" +
-  "N2uqnlBmCwW1hIJagtev4f3zA16/JvfiigMSYyzqJXlw/XKUyOORMUaBor6YavgdjKa8xGOn" +
-  "idadmwtwsnMu18q83/kHSou+bFNDDr4AAAAASUVORK5CYII=";
-
-let testDriver, hud;
-
-function test() {
-  loadTab(TEST_URI).then(() => {
-    openConsole().then(testNetworkPanel);
-  });
-}
-
-function testNetworkPanel(aHud) {
-  hud = aHud;
-  testDriver = testGen();
-  testDriver.next();
-}
-
-function checkIsVisible(aPanel, aList) {
-  for (let id in aList) {
-    let node = aPanel.document.getElementById(id);
-    let isVisible = aList[id];
-    is(node.style.display, (isVisible ? "block" : "none"),
-       id + " isVisible=" + isVisible);
-  }
-}
-
-function checkNodeContent(aPanel, aId, aContent) {
-  let node = aPanel.document.getElementById(aId);
-  if (node == null) {
-    ok(false, "Tried to access node " + aId + " that doesn't exist!");
-  } else if (node.textContent.indexOf(aContent) != -1) {
-    ok(true, "checking content of " + aId);
-  } else {
-    ok(false, "Got false value for " + aId + ": " + node.textContent +
-              " doesn't have " + aContent);
-  }
-}
-
-function checkNodeKeyValue(aPanel, aId, aKey, aValue) {
-  let node = aPanel.document.getElementById(aId);
-
-  let headers = node.querySelectorAll("th");
-  for (let i = 0; i < headers.length; i++) {
-    if (headers[i].textContent == (aKey + ":")) {
-      is(headers[i].nextElementSibling.textContent, aValue,
-         "checking content of " + aId + " for key " + aKey);
-      return;
-    }
-  }
-
-  ok(false, "content check failed for " + aId + ", key " + aKey);
-}
-
-function* testGen() {
-  let filterBox = hud.ui.filterBox;
-
-  let httpActivity = {
-    updates: [],
-    discardRequestBody: true,
-    discardResponseBody: true,
-    startedDateTime: (new Date()).toISOString(),
-    request: {
-      url: "http://www.testpage.com",
-      method: "GET",
-      cookies: [],
-      headers: [
-        { name: "foo", value: "bar" },
-      ],
-    },
-    response: {
-      headers: [],
-      content: {},
-      cookies: [],
-    },
-    timings: {},
-  };
-
-  let networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity);
-
-  is(filterBox._netPanel, networkPanel,
-     "Network panel stored on the anchor object");
-
-  networkPanel._onUpdate = function() {
-    networkPanel._onUpdate = null;
-    executeSoon(function() {
-      testDriver.next();
-    });
-  };
-
-  yield undefined;
-
-  info("test 1");
-
-  checkIsVisible(networkPanel, {
-    requestCookie: false,
-    requestFormData: false,
-    requestBody: false,
-    responseContainer: false,
-    responseBody: false,
-    responseNoBody: false,
-    responseImage: false,
-    responseImageCached: false
-  });
-
-  checkNodeContent(networkPanel, "header", "http://www.testpage.com");
-  checkNodeContent(networkPanel, "header", "GET");
-  checkNodeKeyValue(networkPanel, "requestHeadersContent", "foo", "bar");
-
-  // Test request body.
-  info("test 2: request body");
-  httpActivity.discardRequestBody = false;
-  httpActivity.request.postData = { text: "hello world" };
-  networkPanel.update();
-
-  checkIsVisible(networkPanel, {
-    requestBody: true,
-    requestFormData: false,
-    requestCookie: false,
-    responseContainer: false,
-    responseBody: false,
-    responseNoBody: false,
-    responseImage: false,
-    responseImageCached: false
-  });
-  checkNodeContent(networkPanel, "requestBodyContent", "hello world");
-
-  // Test response header.
-  info("test 3: response header");
-  httpActivity.timings.wait = 10;
-  httpActivity.response.httpVersion = "HTTP/3.14";
-  httpActivity.response.status = 999;
-  httpActivity.response.statusText = "earthquake win";
-  httpActivity.response.content.mimeType = "text/html";
-  httpActivity.response.headers.push(
-    { name: "Content-Type", value: "text/html" },
-    { name: "leaveHouses", value: "true" }
-  );
-
-  networkPanel.update();
-
-  checkIsVisible(networkPanel, {
-    requestBody: true,
-    requestFormData: false,
-    requestCookie: false,
-    responseContainer: true,
-    responseBody: false,
-    responseNoBody: false,
-    responseImage: false,
-    responseImageCached: false
-  });
-
-  checkNodeContent(networkPanel, "header", "HTTP/3.14 999 earthquake win");
-  checkNodeKeyValue(networkPanel, "responseHeadersContent", "leaveHouses",
-                    "true");
-  checkNodeContent(networkPanel, "responseHeadersInfo", "10ms");
-
-  info("test 4");
-
-  httpActivity.discardResponseBody = false;
-  httpActivity.timings.receive = 2;
-  networkPanel.update();
-
-  checkIsVisible(networkPanel, {
-    requestBody: true,
-    requestCookie: false,
-    requestFormData: false,
-    responseContainer: true,
-    responseBody: false,
-    responseNoBody: false,
-    responseImage: false,
-    responseImageCached: false
-  });
-
-  info("test 5");
-
-  httpActivity.updates.push("responseContent", "eventTimings");
-  networkPanel.update();
-
-  checkNodeContent(networkPanel, "responseNoBodyInfo", "2ms");
-  checkIsVisible(networkPanel, {
-    requestBody: true,
-    requestCookie: false,
-    responseContainer: true,
-    responseBody: false,
-    responseNoBody: true,
-    responseImage: false,
-    responseImageCached: false
-  });
-
-  networkPanel.panel.hidePopup();
-
-  // Second run: Test for cookies and response body.
-  info("test 6: cookies and response body");
-  httpActivity.request.cookies.push(
-    { name: "foo", value: "bar" },
-    { name: "hello", value: "world" }
-  );
-  httpActivity.response.content.text = "get out here";
-
-  networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity);
-  is(filterBox._netPanel, networkPanel,
-     "Network panel stored on httpActivity object");
-
-  networkPanel._onUpdate = function() {
-    networkPanel._onUpdate = null;
-    executeSoon(function() {
-      testDriver.next();
-    });
-  };
-
-  yield undefined;
-
-  checkIsVisible(networkPanel, {
-    requestBody: true,
-    requestFormData: false,
-    requestCookie: true,
-    responseContainer: true,
-    responseCookie: false,
-    responseBody: true,
-    responseNoBody: false,
-    responseImage: false,
-    responseImageCached: false
-  });
-
-  checkNodeKeyValue(networkPanel, "requestCookieContent", "foo", "bar");
-  checkNodeKeyValue(networkPanel, "requestCookieContent", "hello", "world");
-  checkNodeContent(networkPanel, "responseBodyContent", "get out here");
-  checkNodeContent(networkPanel, "responseBodyInfo", "2ms");
-
-  networkPanel.panel.hidePopup();
-
-  // Third run: Test for response cookies.
-  info("test 6b: response cookies");
-  httpActivity.response.cookies.push(
-    { name: "foobar", value: "boom" },
-    { name: "foobaz", value: "omg" }
-  );
-
-  networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity);
-  is(filterBox._netPanel, networkPanel,
-     "Network panel stored on httpActivity object");
-
-  networkPanel._onUpdate = function() {
-    networkPanel._onUpdate = null;
-    executeSoon(function() {
-      testDriver.next();
-    });
-  };
-
-  yield undefined;
-
-  checkIsVisible(networkPanel, {
-    requestBody: true,
-    requestFormData: false,
-    requestCookie: true,
-    responseContainer: true,
-    responseCookie: true,
-    responseBody: true,
-    responseNoBody: false,
-    responseImage: false,
-    responseImageCached: false,
-    responseBodyFetchLink: false,
-  });
-
-  checkNodeKeyValue(networkPanel, "responseCookieContent", "foobar", "boom");
-  checkNodeKeyValue(networkPanel, "responseCookieContent", "foobaz", "omg");
-
-  networkPanel.panel.hidePopup();
-
-  // Check image request.
-  info("test 7: image request");
-  httpActivity.response.headers[1].value = "image/png";
-  httpActivity.response.content.mimeType = "image/png";
-  httpActivity.response.content.text = TEST_IMG_BASE64;
-  httpActivity.request.url = TEST_IMG;
-
-  networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity);
-  networkPanel._onUpdate = function() {
-    networkPanel._onUpdate = null;
-    executeSoon(function() {
-      testDriver.next();
-    });
-  };
-
-  yield undefined;
-
-  checkIsVisible(networkPanel, {
-    requestBody: true,
-    requestFormData: false,
-    requestCookie: true,
-    responseContainer: true,
-    responseBody: false,
-    responseNoBody: false,
-    responseImage: true,
-    responseImageCached: false,
-    responseBodyFetchLink: false,
-  });
-
-  let imgNode = networkPanel.document.getElementById("responseImageNode");
-  is(imgNode.getAttribute("src"), "data:image/png;base64," + TEST_IMG_BASE64,
-      "Displayed image is correct");
-
-  function checkImageResponseInfo() {
-    checkNodeContent(networkPanel, "responseImageInfo", "2ms");
-    checkNodeContent(networkPanel, "responseImageInfo", "16x16px");
-  }
-
-  // Check if the image is loaded already.
-  imgNode.addEventListener("load", function onLoad() {
-    imgNode.removeEventListener("load", onLoad, false);
-    checkImageResponseInfo();
-    networkPanel.panel.hidePopup();
-    testDriver.next();
-  }, false);
-  yield undefined;
-
-  // Check cached image request.
-  info("test 8: cached image request");
-  httpActivity.response.httpVersion = "HTTP/1.1";
-  httpActivity.response.status = 304;
-  httpActivity.response.statusText = "Not Modified";
-
-  networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity);
-  networkPanel._onUpdate = function() {
-    networkPanel._onUpdate = null;
-    executeSoon(function() {
-      testDriver.next();
-    });
-  };
-
-  yield undefined;
-
-  checkIsVisible(networkPanel, {
-    requestBody: true,
-    requestFormData: false,
-    requestCookie: true,
-    responseContainer: true,
-    responseBody: false,
-    responseNoBody: false,
-    responseImage: false,
-    responseImageCached: true
-  });
-
-  imgNode = networkPanel.document.getElementById("responseImageCachedNode");
-  is(imgNode.getAttribute("src"), "data:image/png;base64," + TEST_IMG_BASE64,
-     "Displayed image is correct");
-
-  networkPanel.panel.hidePopup();
-
-  // Test sent form data.
-  info("test 9: sent form data");
-  httpActivity.request.postData.text = [
-    "Content-Type:      application/x-www-form-urlencoded",
-    "Content-Length: 59",
-    "name=rob&age=20"
-  ].join("\n");
-
-  networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity);
-  networkPanel._onUpdate = function() {
-    networkPanel._onUpdate = null;
-    executeSoon(function() {
-      testDriver.next();
-    });
-  };
-
-  yield undefined;
-
-  checkIsVisible(networkPanel, {
-    requestBody: false,
-    requestFormData: true,
-    requestCookie: true,
-    responseContainer: true,
-    responseBody: false,
-    responseNoBody: false,
-    responseImage: false,
-    responseImageCached: true
-  });
-
-  checkNodeKeyValue(networkPanel, "requestFormDataContent", "name", "rob");
-  checkNodeKeyValue(networkPanel, "requestFormDataContent", "age", "20");
-  networkPanel.panel.hidePopup();
-
-  // Test no space after Content-Type:
-  info("test 10: no space after Content-Type header in post data");
-  httpActivity.request.postData.text = "Content-Type:application/x-www-" +
-                                       "form-urlencoded\n";
-
-  networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity);
-  networkPanel._onUpdate = function() {
-    networkPanel._onUpdate = null;
-    executeSoon(function() {
-      testDriver.next();
-    });
-  };
-
-  yield undefined;
-
-  checkIsVisible(networkPanel, {
-    requestBody: false,
-    requestFormData: true,
-    requestCookie: true,
-    responseContainer: true,
-    responseBody: false,
-    responseNoBody: false,
-    responseImage: false,
-    responseImageCached: true
-  });
-
-  networkPanel.panel.hidePopup();
-
-  // Test cached data.
-
-  info("test 11: cached data");
-
-  httpActivity.request.url = TEST_ENCODING_ISO_8859_1;
-  httpActivity.response.headers[1].value = "application/json";
-  httpActivity.response.content.mimeType = "application/json";
-  httpActivity.response.content.text = "my cached data is here!";
-
-  networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity);
-  networkPanel._onUpdate = function() {
-    networkPanel._onUpdate = null;
-    executeSoon(function() {
-      testDriver.next();
-    });
-  };
-
-  yield undefined;
-
-  checkIsVisible(networkPanel, {
-    requestBody: false,
-    requestFormData: true,
-    requestCookie: true,
-    responseContainer: true,
-    responseBody: false,
-    responseBodyCached: true,
-    responseNoBody: false,
-    responseImage: false,
-    responseImageCached: false
-  });
-
-  checkNodeContent(networkPanel, "responseBodyCachedContent",
-                   "my cached data is here!");
-
-  networkPanel.panel.hidePopup();
-
-  // Test a response with a content type that can't be displayed in the
-  // NetworkPanel.
-  info("test 12: unknown content type");
-  httpActivity.response.headers[1].value = "application/x-shockwave-flash";
-  httpActivity.response.content.mimeType = "application/x-shockwave-flash";
-
-  networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity);
-  networkPanel._onUpdate = function() {
-    networkPanel._onUpdate = null;
-    executeSoon(function() {
-      testDriver.next();
-    });
-  };
-
-  yield undefined;
-
-  checkIsVisible(networkPanel, {
-    requestBody: false,
-    requestFormData: true,
-    requestCookie: true,
-    responseContainer: true,
-    responseBody: false,
-    responseBodyCached: false,
-    responseBodyUnknownType: true,
-    responseNoBody: false,
-    responseImage: false,
-    responseImageCached: false
-  });
-
-  let responseString =
-    WCUL10n.getFormatStr("NetworkPanel.responseBodyUnableToDisplay.content",
-                      ["application/x-shockwave-flash"]);
-  checkNodeContent(networkPanel,
-                   "responseBodyUnknownTypeContent",
-                   responseString);
-  networkPanel.panel.hidePopup();
-
-  /*
-
-  // This test disabled. See bug 603620.
-
-  // Test if the NetworkPanel figures out the content type based on an URL as
-  // well.
-  delete httpActivity.response.header["Content-Type"];
-  httpActivity.url = "http://www.test.com/someCrazyFile.swf?done=right&ending=txt";
-
-  networkPanel = hud.ui.openNetworkPanel(filterBox, httpActivity);
-  networkPanel.isDoneCallback = function NP_doneCallback() {
-    networkPanel.isDoneCallback = null;
-    testDriver.next();
-  }
-
-  yield undefined;
-
-  checkIsVisible(networkPanel, {
-    requestBody: false,
-    requestFormData: true,
-    requestCookie: true,
-    responseContainer: true,
-    responseBody: false,
-    responseBodyCached: false,
-    responseBodyUnknownType: true,
-    responseNoBody: false,
-    responseImage: false,
-    responseImageCached: false
-  });
-
-  // Systems without Flash installed will return an empty string here. Ignore.
-  if (networkPanel.document.getElementById("responseBodyUnknownTypeContent").textContent !== "")
-    checkNodeContent(networkPanel, "responseBodyUnknownTypeContent", responseString);
-  else
-    ok(true, "Flash not installed");
-
-  networkPanel.panel.hidePopup(); */
-
-  // All done!
-  testDriver = hud = null;
-  executeSoon(finishTest);
-
-  yield undefined;
-}
--- a/browser/devtools/webconsole/webconsole.js
+++ b/browser/devtools/webconsole/webconsole.js
@@ -15,18 +15,16 @@ loader.lazyServiceGetter(this, "clipboar
                          "@mozilla.org/widget/clipboardhelper;1",
                          "nsIClipboardHelper");
 loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm");
 loader.lazyGetter(this, "EventEmitter", () => require("devtools/toolkit/event-emitter"));
 loader.lazyGetter(this, "AutocompletePopup",
                   () => require("devtools/shared/autocomplete-popup").AutocompletePopup);
 loader.lazyGetter(this, "ToolSidebar",
                   () => require("devtools/framework/sidebar").ToolSidebar);
-loader.lazyGetter(this, "NetworkPanel",
-                  () => require("devtools/webconsole/network-panel").NetworkPanel);
 loader.lazyGetter(this, "ConsoleOutput",
                   () => require("devtools/webconsole/console-output").ConsoleOutput);
 loader.lazyGetter(this, "Messages",
                   () => require("devtools/webconsole/console-output").Messages);
 loader.lazyGetter(this, "asyncStorage",
                   () => require("devtools/toolkit/shared/async-storage"));
 loader.lazyRequireGetter(this, "EnvironmentClient", "devtools/toolkit/client/main", true);
 loader.lazyRequireGetter(this, "ObjectClient", "devtools/toolkit/client/main", true);
@@ -1668,21 +1666,17 @@ WebConsoleFrame.prototype = {
       messageNode.classList.add("mixed-content");
       this.makeMixedContentNode(body);
     }
 
     let statusNode = this.document.createElementNS(XHTML_NS, "a");
     statusNode.className = "status";
     body.appendChild(statusNode);
 
-    let onClick = () => {
-      if (!messageNode._panelOpen) {
-        this.openNetworkPanel(messageNode, networkInfo);
-      }
-    };
+    let onClick = () => this.openNetworkPanel(networkInfo.actor);
 
     this._addMessageLinkCallback(urlNode, onClick);
     this._addMessageLinkCallback(statusNode, onClick);
 
     networkInfo.node = messageNode;
 
     this._updateNetMessage(actorId);
 
@@ -1964,144 +1958,27 @@ WebConsoleFrame.prototype = {
     if (messageNode._netPanel) {
       messageNode._netPanel.update();
     }
 
     return updated;
   },
 
   /**
-   * Opens a NetworkPanel.
+   * Opens the network monitor and highlights the specified request.
    *
-   * @param nsIDOMNode aNode
-   *        The message node you want the panel to be anchored to.
-   * @param object aHttpActivity
-   *        The HTTP activity object that holds network request and response
-   *        information. This object is given to the NetworkPanel constructor.
-   * @return object
-   *         The new NetworkPanel instance.
+   * @param string requestId
+   *        The actor ID of the network request.
    */
-  openNetworkPanel: function WCF_openNetworkPanel(aNode, aHttpActivity)
+  openNetworkPanel: function WCF_openNetworkPanel(requestId)
   {
-    let actor = aHttpActivity.actor;
-
-    if (actor) {
-      this.webConsoleClient.getRequestHeaders(actor, (aResponse) => {
-        if (aResponse.error) {
-          Cu.reportError("WCF_openNetworkPanel getRequestHeaders:" +
-                         aResponse.error);
-          return;
-        }
-
-        aHttpActivity.request.headers = aResponse.headers;
-
-        this.webConsoleClient.getRequestCookies(actor, onRequestCookies);
-      });
-    }
-
-    let onRequestCookies = (aResponse) => {
-      if (aResponse.error) {
-        Cu.reportError("WCF_openNetworkPanel getRequestCookies:" +
-                       aResponse.error);
-        return;
-      }
-
-      aHttpActivity.request.cookies = aResponse.cookies;
-
-      this.webConsoleClient.getResponseHeaders(actor, onResponseHeaders);
-    };
-
-    let onResponseHeaders = (aResponse) => {
-      if (aResponse.error) {
-        Cu.reportError("WCF_openNetworkPanel getResponseHeaders:" +
-                       aResponse.error);
-        return;
-      }
-
-      aHttpActivity.response.headers = aResponse.headers;
-
-      this.webConsoleClient.getResponseCookies(actor, onResponseCookies);
-    };
-
-    let onResponseCookies = (aResponse) => {
-      if (aResponse.error) {
-        Cu.reportError("WCF_openNetworkPanel getResponseCookies:" +
-                       aResponse.error);
-        return;
-      }
-
-      aHttpActivity.response.cookies = aResponse.cookies;
-
-      this.webConsoleClient.getRequestPostData(actor, onRequestPostData);
-    };
-
-    let onRequestPostData = (aResponse) => {
-      if (aResponse.error) {
-        Cu.reportError("WCF_openNetworkPanel getRequestPostData:" +
-                       aResponse.error);
-        return;
-      }
-
-      aHttpActivity.request.postData = aResponse.postData;
-      aHttpActivity.discardRequestBody = aResponse.postDataDiscarded;
-
-      this.webConsoleClient.getResponseContent(actor, onResponseContent);
-    };
-
-    let onResponseContent = (aResponse) => {
-      if (aResponse.error) {
-        Cu.reportError("WCF_openNetworkPanel getResponseContent:" +
-                       aResponse.error);
-        return;
-      }
-
-      aHttpActivity.response.content = aResponse.content;
-      aHttpActivity.discardResponseBody = aResponse.contentDiscarded;
-
-      this.webConsoleClient.getEventTimings(actor, onEventTimings);
-    };
-
-    let onEventTimings = (aResponse) => {
-      if (aResponse.error) {
-        Cu.reportError("WCF_openNetworkPanel getEventTimings:" +
-                       aResponse.error);
-        return;
-      }
-
-      aHttpActivity.timings = aResponse.timings;
-
-      openPanel();
-    };
-
-    let openPanel = () => {
-      aNode._netPanel = netPanel;
-
-      let panel = netPanel.panel;
-      panel.openPopup(aNode, "after_pointer", 0, 0, false, false);
-      panel.sizeTo(450, 500);
-      panel.setAttribute("hudId", this.hudId);
-
-      panel.addEventListener("popuphiding", function WCF_netPanel_onHide() {
-        panel.removeEventListener("popuphiding", WCF_netPanel_onHide);
-
-        aNode._panelOpen = false;
-        aNode._netPanel = null;
-      });
-
-      aNode._panelOpen = true;
-    };
-
-    let netPanel = new NetworkPanel(this.popupset, aHttpActivity, this);
-    netPanel.linkNode = aNode;
-
-    if (!actor) {
-      openPanel();
-    }
-
-    return netPanel;
+    let toolbox = gDevTools.getToolbox(this.owner.target);
+    return toolbox.selectTool("netmonitor").then(panel => {
+      return panel.panelWin.NetMonitorController.inspectRequest(requestId);
+    });
   },
 
   /**
    * Handler for page location changes.
    *
    * @param string aURI
    *        New page location.
    * @param string aTitle
--- a/browser/locales/en-US/chrome/browser/aboutHome.dtd
+++ b/browser/locales/en-US/chrome/browser/aboutHome.dtd
@@ -32,8 +32,12 @@
 <!ENTITY abouthome.preferencesButtonUnix.label  "Preferences">
 <!ENTITY abouthome.addonsButton.label    "Add-ons">
 <!-- LOCALIZATION NOTE (abouthome.appsButton2.label): This string should be consistent with
      the Apps menu item in the Tools menu (webapps.label in browser.dtd) and the Apps toolbar button in
      Firefox's customization palette (web-apps-button.label in customizableWidgets.properties) -->
 <!ENTITY abouthome.appsButton2.label     "Apps">
 <!ENTITY abouthome.downloadsButton.label "Downloads">
 <!ENTITY abouthome.syncButton.label      "&syncBrand.shortName.label;">
+
+<!-- LOCALIZATION NOTE (find.commandkey): This is the key to use in
+     conjunction with accel (Command on Mac or Ctrl on other platforms) to find -->
+<!ENTITY find.commandkey "f">
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -405,18 +405,21 @@ These should match what Safari and other
 
 <!ENTITY openCmd.commandkey           "l">
 <!ENTITY urlbar.placeholder2          "Search or enter address">
 <!ENTITY urlbar.accesskey             "d">
 <!ENTITY urlbar.switchToTab.label     "Switch to tab:">
 
 <!ENTITY urlbar.searchSuggestionsNotification.question "Would you like to improve your search experience with suggestions?">
 <!ENTITY urlbar.searchSuggestionsNotification.learnMore "Learn more…">
+<!ENTITY urlbar.searchSuggestionsNotification.learnMore.accesskey "l">
 <!ENTITY urlbar.searchSuggestionsNotification.disable "No">
+<!ENTITY urlbar.searchSuggestionsNotification.disable.accesskey "n">
 <!ENTITY urlbar.searchSuggestionsNotification.enable "Yes">
+<!ENTITY urlbar.searchSuggestionsNotification.enable.accesskey "y">
 
 <!-- 
   Comment duplicated from browser-sets.inc:
 
   Search Command Key Logic works like this:
 
   Unix: Ctrl+J (0.8, 0.9 support)
         Ctrl+K (cross platform binding)
@@ -725,18 +728,16 @@ you can use these alternative items. Oth
 <!ENTITY identity.enableMixedContentBlocking.accesskey "E">
 <!ENTITY identity.disableMixedContentBlocking.label "Disable protection for now">
 <!ENTITY identity.disableMixedContentBlocking.accesskey "D">
 <!ENTITY identity.learnMore "Learn More">
 
 <!ENTITY identity.moreInfoLinkText2 "More Information">
 
 <!ENTITY identity.permissions "Permissions">
-<!ENTITY identity.permissionsPageFunctionality "Page Functionality">
-<!ENTITY identity.permissionsSystemAccess "System Access">
 
 <!-- Name for the tabs toolbar as spoken by screen readers.
      The word "toolbar" is appended automatically and should not be contained below! -->
 <!ENTITY tabsToolbar.label "Browser tabs">
 
 <!-- LOCALIZATION NOTE (syncTabsMenu2.label): This appears in the history menu -->
 <!ENTITY syncTabsMenu2.label     "Tabs From Other Devices">
 
--- a/browser/locales/en-US/chrome/browser/devtools/webConsole.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/webConsole.dtd
@@ -6,34 +6,16 @@
   - keep it in English, or another language commonly spoken among web developers.
   - You want to make that choice consistent across the developer tools.
   - A good criteria is the language in which you'd find the best
   - documentation on web development on the web. -->
 
 <!ENTITY window.title "Web Console">
 <!ENTITY browserConsole.title "Browser Console">
 
-<!ENTITY networkPanel.requestURLColon             "Request URL:">
-<!ENTITY networkPanel.requestMethodColon          "Request Method:">
-<!ENTITY networkPanel.statusCodeColon             "Status Code:">
-
-<!ENTITY networkPanel.requestHeaders              "Request Headers">
-<!ENTITY networkPanel.requestCookie               "Sent Cookie">
-<!ENTITY networkPanel.requestBody                 "Request Body">
-<!ENTITY networkPanel.requestFormData             "Sent Form Data">
-
-<!ENTITY networkPanel.responseHeaders             "Response Headers">
-<!ENTITY networkPanel.responseCookie              "Received Cookie">
-<!ENTITY networkPanel.responseBody                "Response Body">
-<!ENTITY networkPanel.responseBodyCached          "Cached Data">
-<!ENTITY networkPanel.responseBodyUnknownType     "Unknown Content Type">
-<!ENTITY networkPanel.responseNoBody              "No Response Body">
-<!ENTITY networkPanel.responseImage               "Received Image">
-<!ENTITY networkPanel.responseImageCached         "Cached Image">
-
 <!-- LOCALIZATION NOTE (saveBodies.label): You can see this string in the Web
    - Console context menu. -->
 <!ENTITY saveBodies.label     "Log Request and Response Bodies">
 <!ENTITY saveBodies.accesskey "L">
 
 <!-- LOCALIZATION NOTE (openURL.label): You can see this string in the Web
    - Console context menu. -->
 <!ENTITY openURL.label     "Open URL in New Tab">
--- a/browser/locales/en-US/chrome/browser/devtools/webconsole.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/webconsole.properties
@@ -35,39 +35,22 @@ update.accesskey=U
 cmd.commandkey=K
 webConsoleCmd.accesskey=W
 
 # LOCALIZATION NOTE (timestampFormat): %1$02S = hours (24-hour clock),
 # %2$02S = minutes, %3$02S = seconds, %4$03S = milliseconds.
 timestampFormat=%02S:%02S:%02S.%03S
 
 helperFuncUnsupportedTypeError=Can't call pprint on this type of object.
-NetworkPanel.label=Inspect Network Request
 
 # LOCALIZATION NOTE (NetworkPanel.deltaDurationMS): this string is used to
 # show the duration between two network events (e.g request and response
 # header or response header and response body). Parameters: %S is the duration.
 NetworkPanel.durationMS=%Sms
 
-# LOCALIZATION NOTE (NetworkPanel.imageSizeDeltaDurationMS): this string is
-# used to show the duration between the response header and the response body
-# event. It also shows the size of the received or cached image. Parameters:
-# %1$S is the width of the inspected image, %2$S is the height of the
-# inspected image, %3$S is the duration between the response header and the
-# response body event. Example: 150x100px, Δ50ms.
-NetworkPanel.imageSizeDeltaDurationMS=%1$Sx%2$Spx, Δ%3$Sms
-
-# LOCALIZATION NOTE (NetworkPanel.responseBodyUnableToDisplay.content): this
-# string is displayed within the response body section of the NetworkPanel if
-# the content type of the network request can't be displayed. E.g. any kind of
-# text is easy to display, but some audio or flash data received from the
-# server can't be displayed. Parameters: %S is the content type that can't be
-# displayed, examples are application/x-shockwave-flash, music/crescendo.
-NetworkPanel.responseBodyUnableToDisplay.content=Unable to display responses of type "%S"
-
 ConsoleAPIDisabled=The Web Console logging API (console.log, console.info, console.warn, console.error) has been disabled by a script on this page.
 
 # LOCALIZATION NOTE (webConsoleWindowTitleAndURL): the Web Console floating
 # panel title. For RTL languages you need to set the LRM in the string to give
 # the URL the correct direction. Parameters: %S is the web page URL.
 webConsoleWindowTitleAndURL=Web Console - %S
 
 # LOCALIZATION NOTE (webConsoleXhrIndicator): the indicator displayed before
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -258,32 +258,33 @@ conversation_has_ended=Your conversation
 restart_call=Rejoin
 
 ## LOCALIZATION NOTE (contact_unavailable_title): The title displayed
 ## when a contact is unavailable. Don't translate the part between {{..}}
 ## because this will be replaced by the contact's name.
 contact_unavailable_title={{contactName}} is unavailable.
 generic_contact_unavailable_title=This person is unavailable.
 
-generic_failure_title=Something went wrong.
+generic_failure_message=We're having technical difficulties…
 generic_failure_with_reason2=You can try again or email a link to be reached at later.
 generic_failure_no_reason2=Would you like to try again?
 
 ## LOCALIZATION NOTE (contact_offline_title): Title which is displayed when the
 ## contact is offline.
 contact_offline_title=This person is not online
 ## LOCALIZATION NOTE (call_timeout_notification_text): Title which is displayed
 ## when the call didn't go through.
 call_timeout_notification_text=Your call did not go through.
 
 ## LOCALIZATION NOTE (retry_call_button, cancel_button, email_link_button):
 ## These buttons are displayed when a call has failed.
 retry_call_button=Retry
 email_link_button=Email Link
 cancel_button=Cancel
+rejoin_button=Rejoin Conversation
 
 cannot_start_call_session_not_ready=Can't start call, session is not ready.
 network_disconnected=The network connection terminated abruptly.
 connection_error_see_console_notification=Call failed; see console for details.
 no_media_failure_message=No camera or microphone found.
 
 ## LOCALIZATION NOTE (legal_text_and_links3): In this item, don't translate the
 ## parts between {{..}} because these will be replaced with links with the labels
@@ -319,17 +320,16 @@ rooms_leave_button_label=Leave
 rooms_list_copy_url_tooltip=Copy Link
 ## LOCALIZATION NOTE (rooms_list_recent_conversations): String is in all caps
 ## for emphasis reasons, it is a heading. Proceed as appropriate for locale.
 rooms_list_recent_conversations=RECENT CONVERSATIONS
 rooms_list_delete_tooltip=Delete conversation
 rooms_list_deleteConfirmation_label=Are you sure?
 rooms_change_failed_label=Conversation cannot be updated
 rooms_new_room_button_label=Start a conversation
-rooms_only_occupant_label=You're the first one here.
 rooms_panel_title=Choose a conversation or start a new one
 rooms_room_full_label=There are already two people in this conversation.
 rooms_room_full_call_to_action_nonFx_label=Download {{brandShortname}} to start your own
 rooms_room_full_call_to_action_label=Learn more about {{clientShortname}} »
 rooms_room_joined_label=Someone has joined the conversation!
 rooms_room_join_label=Join the conversation
 rooms_signout_alert=Open conversations will be closed
 
--- a/browser/modules/SitePermissions.jsm
+++ b/browser/modules/SitePermissions.jsm
@@ -1,18 +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/. */
 
 this.EXPORTED_SYMBOLS = [ "SitePermissions" ];
 
 Components.utils.import("resource://gre/modules/Services.jsm");
 
-const GROUP_PAGE_FUNCTIONALITY = "pagefunctionality";
-const GROUP_SYSTEM_ACCESS = "systemaccess";
 let gStringBundle =
   Services.strings.createBundle("chrome://browser/locale/sitePermissions.properties");
 
 this.SitePermissions = {
 
   UNKNOWN: Services.perms.UNKNOWN_ACTION,
   ALLOW: Services.perms.ALLOW_ACTION,
   BLOCK: Services.perms.DENY_ACTION,
@@ -21,44 +19,19 @@ this.SitePermissions = {
   /* Checks whether a UI for managing permissions should be exposed for a given
    * URI. This excludes file URIs, for instance, as they don't have a host,
    * even though nsIPermissionManager can still handle them.
    */
   isSupportedURI: function (aURI) {
     return aURI.schemeIs("http") || aURI.schemeIs("https");
   },
 
-  /* Returns an array of permission IDs that match a given
-   * group identifier
-   */
-  _listPermissionsByGroup(group) {
-    let array = Object.keys(gPermissionObject).filter(p=> {
-      return gPermissionObject[p].group == group;
-    });
-    array.sort((a, b) => {
-      return this.getPermissionLabel(a).localeCompare(this.getPermissionLabel(b));
-    });
-    return array;
-  },
-
-  /* Returns an array of 'page functionality' permission IDs
-   */
-  listPageFunctionalityPermissions() {
-    return this._listPermissionsByGroup(GROUP_PAGE_FUNCTIONALITY);
-  },
-
-  /* Returns an array of 'system access' permission IDs
-   */
-  listSystemAccessPermissions() {
-    return this._listPermissionsByGroup(GROUP_SYSTEM_ACCESS);
-  },
-
   /* Returns an array of all permission IDs.
    */
-  listPermissions () {
+  listPermissions: function () {
     let array = Object.keys(gPermissionObject);
     array.sort((a, b) => {
       return this.getPermissionLabel(a).localeCompare(this.getPermissionLabel(b));
     });
     return array;
   },
 
   /* Returns an array of permission states to be exposed to the user for a
@@ -158,85 +131,66 @@ let gPermissionObject = {
    *    Allows sub domains to have their own permissions.
    *    Defaults to false.
    *
    *  - getDefault
    *    Called to get the permission's default state.
    *    Defaults to UNKNOWN, indicating that the user will be asked each time
    *    a page asks for that permissions.
    *
-   *  - group
-   *    A string, either 'systemacces' or 'pagefunctionality'.
-   *    Indicates what group this should be listed with in the UI
-   *
    *  - states
    *    Array of permission states to be exposed to the user.
    *    Defaults to ALLOW, BLOCK and the default state (see getDefault).
    */
 
   "image": {
-    group: GROUP_PAGE_FUNCTIONALITY,
     getDefault: function () {
       return Services.prefs.getIntPref("permissions.default.image") == 2 ?
                SitePermissions.BLOCK : SitePermissions.ALLOW;
     }
   },
 
   "cookie": {
-    group: GROUP_PAGE_FUNCTIONALITY,
     states: [ SitePermissions.ALLOW, SitePermissions.SESSION, SitePermissions.BLOCK ],
     getDefault: function () {
       if (Services.prefs.getIntPref("network.cookie.cookieBehavior") == 2)
         return SitePermissions.BLOCK;
 
       if (Services.prefs.getIntPref("network.cookie.lifetimePolicy") == 2)
         return SitePermissions.SESSION;
 
       return SitePermissions.ALLOW;
     }
   },
 
-  "desktop-notification": {
-    group: GROUP_PAGE_FUNCTIONALITY,
-  },
+  "desktop-notification": {},
 
-  "camera": {
-    group: GROUP_SYSTEM_ACCESS,
-  },
-  "microphone": {
-    group: GROUP_SYSTEM_ACCESS,
-  },
+  "camera": {},
+  "microphone": {},
 
   "popup": {
-    group: GROUP_PAGE_FUNCTIONALITY,
     getDefault: function () {
       return Services.prefs.getBoolPref("dom.disable_open_during_load") ?
                SitePermissions.BLOCK : SitePermissions.ALLOW;
     }
   },
 
   "install": {
-    group: GROUP_PAGE_FUNCTIONALITY,
     getDefault: function () {
       return Services.prefs.getBoolPref("xpinstall.whitelist.required") ?
                SitePermissions.BLOCK : SitePermissions.ALLOW;
     }
   },
 
   "geo": {
-    group: GROUP_SYSTEM_ACCESS,
     exactHostMatch: true
   },
 
-  "indexedDB": {
-    group: GROUP_SYSTEM_ACCESS,
-  },
+  "indexedDB": {},
 
   "pointerLock": {
-    group: GROUP_SYSTEM_ACCESS,
     exactHostMatch: true
   },
 
   "push": {
-    group: GROUP_SYSTEM_ACCESS,
     exactHostMatch: true
   }
 };
--- a/browser/modules/test/xpcshell/test_SitePermissions.js
+++ b/browser/modules/test/xpcshell/test_SitePermissions.js
@@ -6,17 +6,9 @@
 Components.utils.import("resource:///modules/SitePermissions.jsm");
 
 add_task(function* testPermissionsListing() {
   Assert.deepEqual(SitePermissions.listPermissions().sort(),
     ["camera","cookie","desktop-notification","geo","image",
      "indexedDB","install","microphone","pointerLock","popup",
      "push"],
     "Correct list of all permissions");
-
-  Assert.deepEqual(SitePermissions.listPageFunctionalityPermissions().sort(),
-    ["cookie","desktop-notification","image","install","popup"],
-    "Correct list of 'page functionality' permissions");
-
-  Assert.deepEqual(SitePermissions.listSystemAccessPermissions().sort(),
-    ["camera","geo","indexedDB","microphone","pointerLock","push"],
-    "Correct list of 'page functionality' permissions");
 });
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -325,17 +325,16 @@ browser.jar:
   skin/classic/browser/devtools/command-eyedropper.png        (../shared/devtools/images/command-eyedropper.png)
   skin/classic/browser/devtools/command-eyedropper@2x.png     (../shared/devtools/images/command-eyedropper@2x.png)
   skin/classic/browser/devtools/command-rulers.png            (../shared/devtools/images/command-rulers.png)
   skin/classic/browser/devtools/command-rulers@2x.png         (../shared/devtools/images/command-rulers@2x.png)
   skin/classic/browser/devtools/alerticon-warning.png (../shared/devtools/images/alerticon-warning.png)
   skin/classic/browser/devtools/alerticon-warning@2x.png      (../shared/devtools/images/alerticon-warning@2x.png)
 * skin/classic/browser/devtools/ruleview.css          (../shared/devtools/ruleview.css)
 * skin/classic/browser/devtools/webconsole.css                  (../shared/devtools/webconsole.css)
-  skin/classic/browser/devtools/webconsole_networkpanel.css     (../shared/devtools/webconsole_networkpanel.css)
   skin/classic/browser/devtools/webconsole.svg                  (../shared/devtools/images/webconsole.svg)
   skin/classic/browser/devtools/commandline.css              (../shared/devtools/commandline.css)
   skin/classic/browser/devtools/markup-view.css       (../shared/devtools/markup-view.css)
   skin/classic/browser/devtools/editor-error.png       (../shared/devtools/images/editor-error.png)
   skin/classic/browser/devtools/editor-breakpoint.png  (../shared/devtools/images/editor-breakpoint.png)
   skin/classic/browser/devtools/editor-debug-location.png (../shared/devtools/images/editor-debug-location.png)
   skin/classic/browser/devtools/editor-debug-location@2x.png (../shared/devtools/images/editor-debug-location@2x.png)
   skin/classic/browser/devtools/breadcrumbs-divider@2x.png      (../shared/devtools/images/breadcrumbs-divider@2x.png)
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -427,17 +427,16 @@ browser.jar:
   skin/classic/browser/devtools/commandline.css             (../shared/devtools/commandline.css)
   skin/classic/browser/devtools/markup-view.css             (../shared/devtools/markup-view.css)
   skin/classic/browser/devtools/editor-error.png             (../shared/devtools/images/editor-error.png)
   skin/classic/browser/devtools/editor-breakpoint.png        (../shared/devtools/images/editor-breakpoint.png)
   skin/classic/browser/devtools/editor-breakpoint@2x.png        (../shared/devtools/images/editor-breakpoint@2x.png)
   skin/classic/browser/devtools/editor-debug-location.png    (../shared/devtools/images/editor-debug-location.png)
   skin/classic/browser/devtools/editor-debug-location@2x.png    (../shared/devtools/images/editor-debug-location@2x.png)
 * skin/classic/browser/devtools/webconsole.css                  (../shared/devtools/webconsole.css)
-  skin/classic/browser/devtools/webconsole_networkpanel.css     (../shared/devtools/webconsole_networkpanel.css)
   skin/classic/browser/devtools/webconsole.svg                  (../shared/devtools/images/webconsole.svg)
   skin/classic/browser/devtools/breadcrumbs-divider@2x.png      (../shared/devtools/images/breadcrumbs-divider@2x.png)
   skin/classic/browser/devtools/breadcrumbs-scrollbutton.png    (../shared/devtools/images/breadcrumbs-scrollbutton.png)
   skin/classic/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png)
   skin/classic/browser/devtools/animationinspector.css          (../shared/devtools/animationinspector.css)
 * skin/classic/browser/devtools/canvasdebugger.css          (../shared/devtools/canvasdebugger.css)
   skin/classic/browser/devtools/debugger.css                (../shared/devtools/debugger.css)
   skin/classic/browser/devtools/eyedropper.css              (../shared/devtools/eyedropper.css)
--- a/browser/themes/shared/controlcenter/panel.inc.css
+++ b/browser/themes/shared/controlcenter/panel.inc.css
@@ -165,21 +165,16 @@
   margin: 0;
 }
 
 .identity-popup-headline {
   margin: 3px 0 4px;
   font-size: 150%;
 }
 
-.identity-popup-subheadline {
-  margin: 0;
-  font-weight: bold;
-}
-
 .identity-popup-warning-gray {
   -moz-padding-start: 24px;
   background: url(chrome://browser/skin/controlcenter/warning-gray.svg) no-repeat 0 50%;
 }
 
 .identity-popup-warning-yellow {
   -moz-padding-start: 24px;
   background: url(chrome://browser/skin/controlcenter/warning-yellow.svg) no-repeat 0 50%;
@@ -288,31 +283,16 @@ description#identity-popup-content-verif
 }
 
 /* PERMISSIONS */
 
 #identity-popup-permissions-content {
   background-image: url(chrome://browser/skin/controlcenter/permissions.svg);
 }
 
-#identity-popup-permissionsView {
-  padding: 0.5em 1em;
-}
-
-#identity-popup-permission-list,
-#identity-popup-permission-subview-list {
+#identity-popup-permission-list {
   margin-top: 5px;
 }
 
-#permission-subview-list-page-functionality,
-#permission-subview-list-system-access {
-  margin: 3px 0;
-  -moz-margin-start: 3px;
-}
-
-#identity-popup-permission-list menulist.identity-popup-permission,
-#identity-popup-permission-subview-list menulist.identity-popup-permission {
-  max-width: 10em;
-}
-
 .identity-popup-permission-label {
   -moz-margin-start: 0;
 }
+
deleted file mode 100644
--- a/browser/themes/shared/devtools/webconsole_networkpanel.css
+++ /dev/null
@@ -1,100 +0,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/. */
-
-body {
-  font-family: sans-serif;
-  font-size: 11px;
-  background: #EEE;
-  color: #000;
-}
-
-#header {
-  padding: 5px;
-  overflow-x:auto;
-  display: block;
-}
-
-h1 {
-  font-size: 13px;
-  line-height: 15px;
-  padding: 3px 10px;
-  vertical-align: bottom;
-  margin: 0px;
-  background: linear-gradient(#BBB, #999);
-  border-radius: 2px;
-  text-shadow: #FFF 0px 1px 0px;
-}
-
-h1 .info {
-  font-size: 11px;
-  line-height: 15px;
-  vertical-align: bottom;
-  float: right;
-  color: #333;
-  padding-right: 3px;
-}
-
-.property-table {
-  padding: 2px 5px;
-  background: linear-gradient(#FFF, #F8F8F8);
-  color: #333;
-  width: 100%;
-  max-height: 330px;
-  overflow: auto;
-  display: block;
-}
-
-.property-name {
-  font-size: 11px;
-  font-weight: bold;
-  padding-right: 4px;
-  color: #000;
-  white-space: nowrap;
-  text-align: end;
-  vertical-align: top;
-  width: 10%;
-}
-
-.property-value {
-  padding-right: 5px;
-  font-size: 11px;
-  word-wrap: break-word;
-  width: 90%;
-}
-
-div.group {
-  margin-top: 10px;
-}
-
-div.group,
-#header {
-  background: #FFF;
-  border-color: #E1E1E1;
-  border-style: solid;
-  border-width: 1px;
-  box-shadow: 0 1px 1.5px rgba(0, 0, 0, 0.2);
-  border-radius: 4px 4px 4px 4px;
-}
-
-img#responseImageNode {
-  box-shadow: rgba(0,0,0,0.2) 0px 3px 3.5px;
-  max-width: 100%;
-}
-
-#responseImageNodeDiv {
-  padding: 5px;
-}
-
-#responseBodyFetchLink, #requestBodyFetchLink {
-  padding: 5px;
-  margin: 0;
-  cursor: pointer;
-  font-weight: bold;
-  font-size: 1.1em;
-  text-decoration: underline;
-}
-
-.longStringEllipsis {
-  margin-left: 0.6em;
-}
--- a/browser/themes/shared/urlbarSearchSuggestionsNotification.inc.css
+++ b/browser/themes/shared/urlbarSearchSuggestionsNotification.inc.css
@@ -21,17 +21,16 @@
 }
 
 #PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > description > label.text-link {
   -moz-margin-start: 0;
 }
 
 #PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > button {
   -moz-appearance: none;
-  -moz-user-focus: ignore;
   min-width: 80px;
   border-radius: 3px;
   padding: 4px 16px;
   margin: 0;
   -moz-margin-start: 10px;
 }
 
 #PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > button[anonid="search-suggestions-notification-disable"] {
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -452,17 +452,16 @@ browser.jar:
         skin/classic/browser/devtools/command-rulers@2x.png         (../shared/devtools/images/command-rulers@2x.png)
         skin/classic/browser/devtools/markup-view.css               (../shared/devtools/markup-view.css)
         skin/classic/browser/devtools/editor-error.png              (../shared/devtools/images/editor-error.png)
         skin/classic/browser/devtools/editor-breakpoint.png         (../shared/devtools/images/editor-breakpoint.png)
         skin/classic/browser/devtools/editor-breakpoint@2x.png         (../shared/devtools/images/editor-breakpoint@2x.png)
         skin/classic/browser/devtools/editor-debug-location.png     (../shared/devtools/images/editor-debug-location.png)
         skin/classic/browser/devtools/editor-debug-location@2x.png     (../shared/devtools/images/editor-debug-location@2x.png)
 *       skin/classic/browser/devtools/webconsole.css                (../shared/devtools/webconsole.css)
-        skin/classic/browser/devtools/webconsole_networkpanel.css   (../shared/devtools/webconsole_networkpanel.css)
         skin/classic/browser/devtools/webconsole.svg                (../shared/devtools/images/webconsole.svg)
         skin/classic/browser/devtools/breadcrumbs-divider@2x.png    (../shared/devtools/images/breadcrumbs-divider@2x.png)
         skin/classic/browser/devtools/breadcrumbs-scrollbutton.png  (../shared/devtools/images/breadcrumbs-scrollbutton.png)
         skin/classic/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png)
         skin/classic/browser/devtools/animationinspector.css        (../shared/devtools/animationinspector.css)
         skin/classic/browser/devtools/eyedropper.css                (../shared/devtools/eyedropper.css)
 *       skin/classic/browser/devtools/canvasdebugger.css            (../shared/devtools/canvasdebugger.css)
         skin/classic/browser/devtools/debugger.css                  (../shared/devtools/debugger.css)
--- a/layout/xul/nsMenuPopupFrame.cpp
+++ b/layout/xul/nsMenuPopupFrame.cpp
@@ -1409,17 +1409,21 @@ nsMenuPopupFrame::SetPopupPosition(nsIFr
                       nsPresContext::CSSPixelsToAppUnits(mScreenRect.y) / factor);
     anchorRect = nsRect(screenPoint, nsSize(0, 0));
 
     // add the margins on the popup
     screenPoint.MoveBy(margin.left + offsetForContextMenu.x,
                        margin.top + offsetForContextMenu.y);
 
     // screen positioned popups can be flipped vertically but never horizontally
+#ifdef XP_MACOSX
+    hFlip = FlipStyle_Outside;
+#else
     vFlip = FlipStyle_Outside;
+#endif // #ifdef XP_MACOSX
   }
 
   // If a panel is being moved or has flip="none", don't constrain or flip it. But always do this for
   // content shells, so that the popup doesn't extend outside the containing frame.
   if (mInContentShell || (mFlip != FlipType_None && (!aIsMove || mPopupType != ePopupTypePanel))) {
     nsRect screenRect = GetConstraintRect(anchorRect, rootScreenRect, popupLevel);
 
     // Ensure that anchorRect is on screen.
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -444,16 +444,18 @@ public class BrowserApp extends GeckoApp
         }
     }
 
     @Override
     public View onCreateView(final String name, final Context context, final AttributeSet attrs) {
         final View view;
         if (BrowserToolbar.class.getName().equals(name)) {
             view = BrowserToolbar.create(context, attrs);
+        } else if (TabsPanel.TabsLayout.class.getName().equals(name)) {
+            view = TabsPanel.createTabsLayout(context, attrs);
         } else {
             view = super.onCreateView(name, context, attrs);
         }
         return view;
     }
 
     @Override
     public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -467,16 +467,17 @@ gbjar.sources += [
     'tabs/TabHistoryController.java',
     'tabs/TabHistoryFragment.java',
     'tabs/TabHistoryItemRow.java',
     'tabs/TabHistoryPage.java',
     'tabs/TabPanelBackButton.java',
     'tabs/TabsGridLayout.java',
     'tabs/TabsLayoutAdapter.java',
     'tabs/TabsLayoutItemView.java',
+    'tabs/TabsListLayout.java',
     'tabs/TabsPanel.java',
     'tabs/TabsPanelThumbnailView.java',
     'Telemetry.java',
     'TelemetryContract.java',
     'TextSelection.java',
     'TextSelectionHandle.java',
     'ThumbnailHelper.java',
     'tiles/Tile.java',
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout-land/tabs_list_item_view.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- 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/. -->
+
+<org.mozilla.gecko.tabs.TabsLayoutItemView xmlns:android="http://schemas.android.com/apk/res/android"
+                                           style="@style/TabsItem"
+                                           android:focusable="true"
+                                           android:id="@+id/info"
+                                           android:layout_width="wrap_content"
+                                           android:layout_height="wrap_content"
+                                           android:paddingTop="@dimen/tab_vertical_padding"
+                                           android:paddingBottom="@dimen/tab_vertical_padding"
+                                           android:paddingLeft="1dip"
+                                           android:paddingRight="1dip"
+                                           android:gravity="center">
+
+    <!-- We set state_private on this View dynamically in TabsListLayout. -->
+    <org.mozilla.gecko.widget.TabThumbnailWrapper
+            android:id="@+id/wrapper"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_margin="@dimen/tab_thumbnail_margin"
+            android:padding="@dimen/tab_thumbnail_padding"
+            android:background="@drawable/tab_thumbnail"
+            android:duplicateParentState="true">
+
+        <org.mozilla.gecko.tabs.TabsPanelThumbnailView android:id="@+id/thumbnail"
+                                                       android:layout_width="@dimen/tab_thumbnail_width"
+                                                       android:layout_height="@dimen/tab_thumbnail_height"
+                                                       android:layout_above="@+id/title_bar"
+                                                       android:layout_alignParentTop="true"/>
+
+        <LinearLayout android:id="@id/title_bar"
+                      android:layout_alignParentBottom="true"
+                      android:layout_width="@dimen/tab_thumbnail_width"
+                      android:layout_height="@dimen/tab_title_height"
+                      android:orientation="horizontal"
+                      android:background="#EFFF"
+                      android:duplicateParentState="true">
+
+            <TextView android:id="@+id/title"
+                      android:layout_width="0dp"
+                      android:layout_height="wrap_content"
+                      android:layout_weight="1.0"
+                      android:padding="4dip"
+                      style="@style/TabLayoutItemTextAppearance"
+                      android:textSize="12sp"
+                      android:textColor="@color/placeholder_active_grey"
+                      android:singleLine="true"
+                      android:duplicateParentState="true"/>
+
+            <ImageButton android:id="@+id/close"
+                         style="@style/TabsItemClose"
+                         android:layout_width="32dip"
+                         android:layout_height="match_parent"
+                         android:background="@drawable/action_bar_button_inverse"
+                         android:scaleType="center"
+                         android:contentDescription="@string/close_tab"
+                         android:src="@drawable/tab_close"/>
+
+        </LinearLayout>
+
+    </org.mozilla.gecko.widget.TabThumbnailWrapper>
+
+</org.mozilla.gecko.tabs.TabsLayoutItemView>
--- a/mobile/android/base/resources/layout/private_tabs_panel.xml
+++ b/mobile/android/base/resources/layout/private_tabs_panel.xml
@@ -9,17 +9,17 @@
     <ImageView android:id="@+id/private_tabs_empty"
                android:layout_height="wrap_content"
                android:layout_width="wrap_content"
                android:src="@drawable/private_masq"
                android:layout_gravity="center"/>
 
     <!-- Note: for an unknown reason, scrolling in the TabsLayout
          does not work unless it is laid out after the empty view. -->
-    <org.mozilla.gecko.tabs.TabsGridLayout
+    <view class="org.mozilla.gecko.tabs.TabsPanel$TabsLayout"
           android:id="@+id/private_tabs_layout"
           style="@style/TabsLayout"
           android:layout_width="match_parent"
           android:layout_height="match_parent"
           android:choiceMode="singleChoice"
           gecko:tabs="tabs_private"/>
 
 </merge>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/tabs_list_item_view.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<org.mozilla.gecko.tabs.TabsLayoutItemView xmlns:android="http://schemas.android.com/apk/res/android"
+                                           style="@style/TabsItem"
+                                           android:focusable="true"
+                                           android:id="@+id/info"
+                                           android:layout_width="match_parent"
+                                           android:layout_height="wrap_content"
+                                           android:paddingLeft="12dip"
+                                           android:paddingTop="6dip"
+                                           android:paddingBottom="6dip"
+                                           android:background="@drawable/tab_row">
+
+    <!-- We set state_private on this View dynamically in TabsListLayout. -->
+    <org.mozilla.gecko.widget.TabThumbnailWrapper
+            android:id="@+id/wrapper"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:padding="4dip"
+            android:background="@drawable/tab_thumbnail"
+            android:duplicateParentState="true">
+
+        <org.mozilla.gecko.tabs.TabsPanelThumbnailView android:id="@+id/thumbnail"
+                                                       android:layout_width="@dimen/tab_thumbnail_width"
+                                                       android:layout_height="@dimen/tab_thumbnail_height"/>
+
+    </org.mozilla.gecko.widget.TabThumbnailWrapper>
+
+    <LinearLayout android:layout_width="0dip"
+                  android:layout_height="match_parent"
+                  android:orientation="vertical"
+                  android:layout_weight="1.0"
+                  android:paddingTop="4dip"
+                  android:paddingLeft="8dip"
+                  android:paddingRight="4dip">
+
+        <TextView android:id="@+id/title"
+                  android:layout_width="match_parent"
+                  android:layout_height="0dip"
+                  android:layout_weight="1.0"
+                  style="@style/TabLayoutItemTextAppearance"
+                  android:textColor="#FFFFFFFF"
+                  android:textSize="14sp"
+                  android:singleLine="false"
+                  android:maxLines="4"
+                  android:duplicateParentState="true"/>
+
+    </LinearLayout>
+
+    <ImageButton android:id="@+id/close"
+                 style="@style/TabsItemClose"
+                 android:layout_width="34dip"
+                 android:layout_height="match_parent"
+                 android:background="@drawable/action_bar_button_inverse"
+                 android:scaleType="center"
+                 android:contentDescription="@string/close_tab"
+                 android:src="@drawable/tab_close"/>
+
+</org.mozilla.gecko.tabs.TabsLayoutItemView>
\ No newline at end of file
--- a/mobile/android/base/resources/layout/tabs_panel_default.xml
+++ b/mobile/android/base/resources/layout/tabs_panel_default.xml
@@ -65,31 +65,31 @@
 
         <View android:layout_width="match_parent"
               android:layout_height="2dp"
               android:layout_alignParentBottom="true"
               android:background="#1A000000"/>
 
     </RelativeLayout>
 
-    <FrameLayout
+    <view class="org.mozilla.gecko.tabs.TabsPanel$TabsLayoutContainer"
           android:id="@+id/tabs_container"
           android:layout_width="match_parent"
           android:layout_height="match_parent">
 
-        <org.mozilla.gecko.tabs.TabsGridLayout
+        <view class="org.mozilla.gecko.tabs.TabsPanel$TabsLayout"
               android:id="@+id/normal_tabs"
               style="@style/TabsLayout"
               android:layout_width="match_parent"
               android:layout_height="match_parent"
               android:choiceMode="singleChoice"
               android:visibility="gone"
               gecko:tabs="tabs_normal"/>
 
         <org.mozilla.gecko.tabs.PrivateTabsPanel
                 android:id="@+id/private_tabs_panel"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
                 android:visibility="gone"/>
 
-    </FrameLayout>
+    </view>
 
 </merge>
--- a/mobile/android/base/resources/values-v11/dimens.xml
+++ b/mobile/android/base/resources/values-v11/dimens.xml
@@ -3,11 +3,11 @@
    - 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/. -->
 
 <resources>
 
     <!-- This is chosen to be close to Android's listPreferredItemHeightSmall.
          TODO: We should inherit these from the system.
          http://androidxref.com/4.2.2_r1/xref/frameworks/base/core/res/res/values/themes.xml#1287 -->
-    <dimen name="menu_item_row_height">44dp</dimen>
+    <dimen name="menu_item_row_height">48dp</dimen>
 
 </resources>
--- a/mobile/android/base/resources/values/dimens.xml
+++ b/mobile/android/base/resources/values/dimens.xml
@@ -132,31 +132,34 @@
     <dimen name="prompt_service_left_right_text_with_icon_padding">10dp</dimen>
     <dimen name="prompt_service_top_bottom_text_with_icon_padding">8dp</dimen>
     <dimen name="tabs_panel_indicator_width">60dp</dimen>
     <dimen name="tabs_panel_button_width">48dp</dimen>
     <dimen name="tabs_strip_height">48dp</dimen>
     <dimen name="tabs_strip_button_width">100dp</dimen>
     <dimen name="tabs_strip_button_padding">18dp</dimen>
     <dimen name="tabs_strip_shadow_size">1dp</dimen>
-    <dimen name="tabs_layout_horizontal_height">156dp</dimen>
     <dimen name="text_selection_handle_width">47dp</dimen>
     <dimen name="text_selection_handle_height">58dp</dimen>
     <dimen name="text_selection_handle_shadow">11dp</dimen>
     <dimen name="validation_message_height">50dp</dimen>
     <dimen name="validation_message_margin_top">6dp</dimen>
 
     <dimen name="tab_thumbnail_width">121dp</dimen>
     <dimen name="tab_thumbnail_height">90dp</dimen>
     <dimen name="tab_panel_column_width">129dp</dimen>
     <dimen name="tab_panel_grid_padding">20dp</dimen>
     <dimen name="tab_panel_grid_vspacing">20dp</dimen>
     <dimen name="tab_panel_grid_padding_top">19dp</dimen>
 
     <dimen name="tab_highlight_stroke_width">4dp</dimen>
+    <dimen name="tab_title_height">22dp</dimen>
+    <dimen name="tab_vertical_padding">6dp</dimen>
+    <dimen name="tab_thumbnail_padding">4dp</dimen>
+    <dimen name="tab_thumbnail_margin">6dp</dimen>
 
     <!-- PageActionButtons dimensions -->
     <dimen name="page_action_button_width">32dp</dimen>
 
     <!-- Banner -->
     <dimen name="home_banner_height">72dp</dimen>
     <dimen name="home_banner_close_width">42dp</dimen>
     <dimen name="home_banner_icon_height">48dip</dimen>
--- a/mobile/android/base/resources/values/layout.xml
+++ b/mobile/android/base/resources/values/layout.xml
@@ -5,9 +5,9 @@
 
 <resources>
     <!-- These items are v11+ resources but are referenced in code shipped with
          API 9 builds. Since v11+ resources don't ship on API 9 builds, in order
          for the resource ID to be found (and thus compilation to succeed), we
          provide dummy values below. -->
     <item type="layout" name="tab_strip">@null</item>
     <item type="layout" name="tabs_panel_back_button">@null</item>
-</resources>
+</resources>
\ No newline at end of file
--- a/mobile/android/base/tabs/PrivateTabsPanel.java
+++ b/mobile/android/base/tabs/PrivateTabsPanel.java
@@ -47,12 +47,17 @@ class PrivateTabsPanel extends FrameLayo
 
     @Override
     public void hide() {
         setVisibility(View.GONE);
         tabsLayout.hide();
     }
 
     @Override
+    public boolean shouldExpand() {
+        return tabsLayout.shouldExpand();
+    }
+
+    @Override
     public void closeAll() {
         tabsLayout.closeAll();
     }
 }
--- a/mobile/android/base/tabs/TabsGridLayout.java
+++ b/mobile/android/base/tabs/TabsGridLayout.java
@@ -180,16 +180,21 @@ class TabsGridLayout extends GridView
     public void hide() {
         lastSelectedTabId = Tabs.getInstance().getSelectedTab().getId();
         setVisibility(View.GONE);
         Tabs.unregisterOnTabsChangedListener(this);
         GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Screenshot:Cancel", ""));
         tabsAdapter.clear();
     }
 
+    @Override
+    public boolean shouldExpand() {
+        return true;
+    }
+
     private void autoHidePanel() {
         tabsPanel.autoHidePanel();
     }
 
     @Override
     public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
         switch (msg) {
             case ADDED:
--- a/mobile/android/base/tabs/TabsLayoutItemView.java
+++ b/mobile/android/base/tabs/TabsLayoutItemView.java
@@ -1,14 +1,15 @@
 /* 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/. */
 
 package org.mozilla.gecko.tabs;
 
+import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.widget.TabThumbnailWrapper;
 
 import org.json.JSONException;
@@ -95,17 +96,19 @@ public class TabsLayoutItemView extends 
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
         mTitle = (TextView) findViewById(R.id.title);
         mThumbnail = (TabsPanelThumbnailView) findViewById(R.id.thumbnail);
         mCloseButton = (ImageView) findViewById(R.id.close);
         mThumbnailWrapper = (TabThumbnailWrapper) findViewById(R.id.wrapper);
 
-        growCloseButtonHitArea();
+        if (HardwareUtils.isTablet() || AppConstants.NIGHTLY_BUILD) {
+            growCloseButtonHitArea();
+        }
     }
 
     private void growCloseButtonHitArea() {
         getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
             @Override
             public boolean onPreDraw() {
                 getViewTreeObserver().removeOnPreDrawListener(this);
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tabs/TabsListLayout.java
@@ -0,0 +1,657 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+/* 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/. */
+package org.mozilla.gecko.tabs;
+
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoEvent;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.animation.PropertyAnimator;
+import org.mozilla.gecko.animation.PropertyAnimator.Property;
+import org.mozilla.gecko.animation.ViewHelper;
+import org.mozilla.gecko.tabs.TabsPanel.TabsLayout;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.widget.TwoWayView;
+import org.mozilla.gecko.widget.themed.ThemedRelativeLayout;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.widget.Button;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class TabsListLayout extends TwoWayView
+                     implements TabsLayout,
+                                Tabs.OnTabsChangedListener {
+
+    private static final String LOGTAG = "Gecko" + TabsListLayout.class.getSimpleName();
+
+    // Time to animate non-flinged tabs of screen, in milliseconds
+    private static final int ANIMATION_DURATION = 250;
+
+    // Time between starting successive tab animations in closeAllTabs.
+    private static final int ANIMATION_CASCADE_DELAY = 75;
+
+    private final boolean isPrivate;
+    private final TabsLayoutAdapter tabsAdapter;
+    private final List<View> pendingClosedTabs;
+    private TabsPanel tabsPanel;
+    private int closeAnimationCount;
+    private int closeAllAnimationCount;
+    private int originalSize;
+
+    public TabsListLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        pendingClosedTabs = new ArrayList<View>();
+
+        setItemsCanFocus(true);
+
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabsLayout);
+        isPrivate = (a.getInt(R.styleable.TabsLayout_tabs, 0x0) == 1);
+        a.recycle();
+
+        tabsAdapter = new TabsListLayoutAdapter(context);
+        setAdapter(tabsAdapter);
+
+        final TabSwipeGestureListener swipeListener = new TabSwipeGestureListener();
+        setOnTouchListener(swipeListener);
+        setOnScrollListener(swipeListener.makeScrollListener());
+        setRecyclerListener(new RecyclerListener() {
+            @Override
+            public void onMovedToScrapHeap(View view) {
+                TabsLayoutItemView item = (TabsLayoutItemView) view;
+                item.setThumbnail(null);
+                item.setCloseVisible(true);
+            }
+        });
+    }
+
+    @Override
+    public void setTabsPanel(TabsPanel panel) {
+        tabsPanel = panel;
+    }
+
+    @Override
+    public void show() {
+        setVisibility(View.VISIBLE);
+        Tabs.getInstance().refreshThumbnails();
+        Tabs.registerOnTabsChangedListener(this);
+        refreshTabsData();
+    }
+
+    @Override
+    public void hide() {
+        setVisibility(View.GONE);
+        Tabs.unregisterOnTabsChangedListener(this);
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Screenshot:Cancel", ""));
+        tabsAdapter.clear();
+    }
+
+    @Override
+    public boolean shouldExpand() {
+        return isVertical();
+    }
+
+    private void autoHidePanel() {
+        tabsPanel.autoHidePanel();
+    }
+
+    @Override
+    public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
+        switch (msg) {
+            case ADDED:
+                // Refresh the list to make sure the new tab is added in the right position.
+                refreshTabsData();
+                break;
+
+            case CLOSED:
+                if (tab.isPrivate() == isPrivate && tabsAdapter.getCount() > 0) {
+                    if (tabsAdapter.removeTab(tab)) {
+                        int selected = tabsAdapter.getPositionForTab(Tabs.getInstance().getSelectedTab());
+                        updateSelectedStyle(selected);
+                    }
+                }
+                break;
+
+            case SELECTED:
+                // Update the selected position, then fall through...
+                updateSelectedPosition();
+            case UNSELECTED:
+                // We just need to update the style for the unselected tab...
+            case THUMBNAIL:
+            case TITLE:
+            case RECORDING_CHANGE:
+            case AUDIO_PLAYING_CHANGE:
+                View view = getChildAt(tabsAdapter.getPositionForTab(tab) - getFirstVisiblePosition());
+                if (view == null) {
+                    return;
+                }
+
+                TabsLayoutItemView item = (TabsLayoutItemView) view;
+                item.assignValues(tab);
+                break;
+        }
+    }
+
+    // Updates the selected position in the list so that it will be scrolled to the right place.
+    private void updateSelectedPosition() {
+        int selected = tabsAdapter.getPositionForTab(Tabs.getInstance().getSelectedTab());
+        updateSelectedStyle(selected);
+        if (selected != -1) {
+            setSelection(selected);
+        }
+    }
+
+    /**
+     * Updates the selected/unselected style for the tabs.
+     *
+     * @param selected position of the selected tab
+     */
+    private void updateSelectedStyle(int selected) {
+        for (int i = 0; i < tabsAdapter.getCount(); i++) {
+            setItemChecked(i, (i == selected));
+        }
+    }
+
+    private void refreshTabsData() {
+        // Store a different copy of the tabs, so that we don't have to worry about
+        // accidentally updating it on the wrong thread.
+        ArrayList<Tab> tabData = new ArrayList<Tab>();
+        Iterable<Tab> allTabs = Tabs.getInstance().getTabsInOrder();
+
+        for (Tab tab : allTabs) {
+            if (tab.isPrivate() == isPrivate) {
+                tabData.add(tab);
+            }
+        }
+
+        tabsAdapter.setTabs(tabData);
+        updateSelectedPosition();
+    }
+
+    public void resetTransforms(View view) {
+        ViewHelper.setAlpha(view, 1);
+
+        if (isVertical()) {
+            ViewHelper.setTranslationX(view, 0);
+        } else {
+            ViewHelper.setTranslationY(view, 0);
+        }
+
+        // We only need to reset the height or width after individual tab close animations.
+        if (originalSize != 0) {
+            if (isVertical()) {
+                ViewHelper.setHeight(view, originalSize);
+            } else {
+                ViewHelper.setWidth(view, originalSize);
+            }
+        }
+    }
+
+    private boolean isVertical() {
+        return (getOrientation().compareTo(TwoWayView.Orientation.VERTICAL) == 0);
+    }
+
+    @Override
+    public void closeAll() {
+        final int childCount = getChildCount();
+
+        // Just close the panel if there are no tabs to close.
+        if (childCount == 0) {
+            autoHidePanel();
+            return;
+        }
+
+        // Disable the view so that gestures won't interfere wth the tab close animation.
+        setEnabled(false);
+
+        // Delay starting each successive animation to create a cascade effect.
+        int cascadeDelay = 0;
+        for (int i = childCount - 1; i >= 0; i--) {
+            final View view = getChildAt(i);
+
+            final PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION);
+            animator.attach(view, Property.ALPHA, 0);
+
+            if (isVertical()) {
+                animator.attach(view, Property.TRANSLATION_X, view.getWidth());
+            } else {
+                animator.attach(view, Property.TRANSLATION_Y, view.getHeight());
+            }
+
+            closeAllAnimationCount++;
+
+            animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
+                @Override
+                public void onPropertyAnimationStart() {
+                }
+
+                @Override
+                public void onPropertyAnimationEnd() {
+                    closeAllAnimationCount--;
+                    if (closeAllAnimationCount > 0) {
+                        return;
+                    }
+
+                    // Hide the panel after the animation is done.
+                    autoHidePanel();
+
+                    // Re-enable the view after the animation is done.
+                    TabsListLayout.this.setEnabled(true);
+
+                    // Then actually close all the tabs.
+                    final Iterable<Tab> tabs = Tabs.getInstance().getTabsInOrder();
+
+                    for (Tab tab : tabs) {
+                        // In the normal panel we want to close all tabs (both private and normal),
+                        // but in the private panel we only want to close private tabs.
+                        if (!isPrivate || tab.isPrivate()) {
+                            Tabs.getInstance().closeTab(tab, false);
+                        }
+                    }
+                }
+            });
+
+            ThreadUtils.getUiHandler().postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    animator.start();
+                }
+            }, cascadeDelay);
+
+            cascadeDelay += ANIMATION_CASCADE_DELAY;
+        }
+    }
+
+    private void animateClose(final View view, int pos) {
+        PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION);
+        animator.attach(view, Property.ALPHA, 0);
+
+        if (isVertical()) {
+            animator.attach(view, Property.TRANSLATION_X, pos);
+        } else {
+            animator.attach(view, Property.TRANSLATION_Y, pos);
+        }
+
+        closeAnimationCount++;
+
+        pendingClosedTabs.add(view);
+
+        animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
+            @Override
+            public void onPropertyAnimationStart() {
+            }
+
+            @Override
+            public void onPropertyAnimationEnd() {
+                closeAnimationCount--;
+                if (closeAnimationCount > 0) {
+                    return;
+                }
+
+                for (View pendingView : pendingClosedTabs) {
+                    animateFinishClose(pendingView);
+                }
+                pendingClosedTabs.clear();
+            }
+        });
+
+        if (tabsAdapter.getCount() == 1) {
+            autoHidePanel();
+        }
+
+        animator.start();
+    }
+
+    private void animateFinishClose(final View view) {
+        final boolean isVertical = isVertical();
+
+        PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION);
+
+        if (isVertical) {
+            animator.attach(view, Property.HEIGHT, 1);
+        } else {
+            animator.attach(view, Property.WIDTH, 1);
+        }
+
+        final int tabId = ((TabsLayoutItemView) view).getTabId();
+
+        // Caching this assumes that all rows are the same height
+        if (originalSize == 0) {
+            originalSize = (isVertical ? view.getHeight() : view.getWidth());
+        }
+
+        animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
+            @Override
+            public void onPropertyAnimationStart() {
+            }
+
+            @Override
+            public void onPropertyAnimationEnd() {
+                Tabs tabs = Tabs.getInstance();
+                Tab tab = tabs.getTab(tabId);
+                tabs.closeTab(tab, true);
+            }
+        });
+
+        animator.start();
+    }
+
+    private void animateCancel(final View view) {
+        PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION);
+        animator.attach(view, Property.ALPHA, 1);
+
+        if (isVertical()) {
+            animator.attach(view, Property.TRANSLATION_X, 0);
+        } else {
+            animator.attach(view, Property.TRANSLATION_Y, 0);
+        }
+
+        animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
+            @Override
+            public void onPropertyAnimationStart() {
+            }
+
+            @Override
+            public void onPropertyAnimationEnd() {
+                TabsLayoutItemView tab = (TabsLayoutItemView) view;
+                tab.setCloseVisible(true);
+            }
+        });
+
+        animator.start();
+    }
+
+    private class TabsListLayoutAdapter extends TabsLayoutAdapter {
+        private final Button.OnClickListener mCloseOnClickListener;
+
+        public TabsListLayoutAdapter(Context context) {
+            super(context, R.layout.tabs_list_item_view);
+            mCloseOnClickListener = new Button.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    // The view here is the close button, which has a reference
+                    // to the parent TabsLayoutItemView in it's tag, hence the getTag() call
+                    TabsLayoutItemView item = (TabsLayoutItemView) v.getTag();
+                    final int pos = (isVertical() ? item.getWidth() : 0 - item.getHeight());
+                    animateClose(item, pos);
+                }
+            };
+        }
+
+        @Override
+        public TabsLayoutItemView newView(int position, ViewGroup parent) {
+            TabsLayoutItemView item = super.newView(position, parent);
+            item.setCloseOnClickListener(mCloseOnClickListener);
+            ((ThemedRelativeLayout) item.findViewById(R.id.wrapper)).setPrivateMode(isPrivate);
+            return item;
+        }
+
+        @Override
+        public void bindView(TabsLayoutItemView view, Tab tab) {
+            super.bindView(view, tab);
+            // If we're recycling this view, there's a chance it was transformed during
+            // the close animation. Remove any of those properties.
+            resetTransforms(view);
+        }
+    }
+
+    private class TabSwipeGestureListener implements View.OnTouchListener {
+        // same value the stock browser uses for after drag animation velocity in pixels/sec
+        // http://androidxref.com/4.0.4/xref/packages/apps/Browser/src/com/android/browser/NavTabScroller.java#61
+        private static final float MIN_VELOCITY = 750;
+
+        private final int swipeThreshold;
+        private final int minFlingVelocity;
+        private final int maxFlingVelocity;
+        private VelocityTracker velocityTracker;
+        private int listWidth = 1;
+        private int listHeight = 1;
+        private View swipeView;
+        private Runnable pendingCheckForTap;
+        private float swipeStartX;
+        private float swipeStartY;
+        private boolean swiping;
+        private boolean enabled;
+
+        public TabSwipeGestureListener() {
+            enabled = true;
+
+            ViewConfiguration vc = ViewConfiguration.get(TabsListLayout.this.getContext());
+
+            swipeThreshold = vc.getScaledTouchSlop();
+            minFlingVelocity = (int) (getContext().getResources().getDisplayMetrics().density * MIN_VELOCITY);
+            maxFlingVelocity = vc.getScaledMaximumFlingVelocity();
+        }
+
+        public void setEnabled(boolean enabled) {
+            this.enabled = enabled;
+        }
+
+        public TwoWayView.OnScrollListener makeScrollListener() {
+            return new TwoWayView.OnScrollListener() {
+
+                @Override
+                public void onScrollStateChanged(TwoWayView twoWayView, int scrollState) {
+                    setEnabled(scrollState != TwoWayView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
+                }
+
+                @Override
+                public void onScroll(TwoWayView twoWayView, int i, int i1, int i2) {
+                }
+            };
+        }
+
+        @Override
+        public boolean onTouch(View view, MotionEvent e) {
+            if (!enabled) {
+                return false;
+            }
+
+            if (listWidth < 2 || listHeight < 2) {
+                listWidth = TabsListLayout.this.getWidth();
+                listHeight = TabsListLayout.this.getHeight();
+            }
+
+            switch (e.getActionMasked()) {
+                case MotionEvent.ACTION_DOWN: {
+                    // Check if we should set pressed state on the
+                    // touched view after a standard delay.
+                    triggerCheckForTap();
+
+                    final float x = e.getRawX();
+                    final float y = e.getRawY();
+
+                    // Find out which view is being touched
+                    swipeView = findViewAt(x, y);
+
+                    if (swipeView != null) {
+                        swipeStartX = e.getRawX();
+                        swipeStartY = e.getRawY();
+                        velocityTracker = VelocityTracker.obtain();
+                        velocityTracker.addMovement(e);
+                    }
+
+                    view.onTouchEvent(e);
+                    return true;
+                }
+                case MotionEvent.ACTION_UP: {
+                    if (swipeView == null) {
+                        break;
+                    }
+
+                    cancelCheckForTap();
+                    swipeView.setPressed(false);
+
+                    if (!swiping) {
+                        TabsLayoutItemView item = (TabsLayoutItemView) swipeView;
+                        Tabs.getInstance().selectTab(item.getTabId());
+                        autoHidePanel();
+                        velocityTracker.recycle();
+                        velocityTracker = null;
+                        break;
+                    }
+
+                    velocityTracker.addMovement(e);
+                    velocityTracker.computeCurrentVelocity(1000, maxFlingVelocity);
+
+                    float velocityX = Math.abs(velocityTracker.getXVelocity());
+                    float velocityY = Math.abs(velocityTracker.getYVelocity());
+                    boolean dismiss = false;
+                    boolean dismissDirection = false;
+                    int dismissTranslation;
+
+                    if (isVertical()) {
+                        float deltaX = ViewHelper.getTranslationX(swipeView);
+
+                        if (Math.abs(deltaX) > listWidth / 2) {
+                            dismiss = true;
+                            dismissDirection = (deltaX > 0);
+                        } else if (minFlingVelocity <= velocityX && velocityX <= maxFlingVelocity
+                                                                 && velocityY < velocityX) {
+
+                            dismiss = swiping && (deltaX * velocityTracker.getXVelocity() > 0);
+                            dismissDirection = (velocityTracker.getXVelocity() > 0);
+                        }
+
+                        dismissTranslation = (dismissDirection ? listWidth : -listWidth);
+                    } else {
+                        float deltaY = ViewHelper.getTranslationY(swipeView);
+
+                        if (Math.abs(deltaY) > listHeight / 2) {
+                            dismiss = true;
+                            dismissDirection = (deltaY > 0);
+                        } else if (minFlingVelocity <= velocityY && velocityY <= maxFlingVelocity
+                                                                 && velocityX < velocityY) {
+
+                            dismiss = swiping && (deltaY * velocityTracker.getYVelocity() > 0);
+                            dismissDirection = (velocityTracker.getYVelocity() > 0);
+                        }
+
+                        dismissTranslation = (dismissDirection ? listHeight : -listHeight);
+                    }
+
+                    if (dismiss) {
+                        animateClose(swipeView, dismissTranslation);
+                    } else {
+                        animateCancel(swipeView);
+                    }
+
+                    velocityTracker.recycle();
+                    velocityTracker = null;
+                    swipeView = null;
+                    swipeStartX = 0;
+                    swipeStartY = 0;
+                    swiping = false;
+                    break;
+                }
+
+                case MotionEvent.ACTION_MOVE: {
+                    if (swipeView == null || velocityTracker == null) {
+                        break;
+                    }
+
+                    velocityTracker.addMovement(e);
+
+                    final boolean isVertical = isVertical();
+                    float deltaX = e.getRawX() - swipeStartX;
+                    float deltaY = e.getRawY() - swipeStartY;
+                    float delta = (isVertical ? deltaX : deltaY);
+                    boolean isScrollingX = Math.abs(deltaX) > swipeThreshold;
+                    boolean isScrollingY = Math.abs(deltaY) > swipeThreshold;
+                    boolean isSwipingToClose = (isVertical ? isScrollingX : isScrollingY);
+
+                    // If we're actually swiping, make sure we don't
+                    // set pressed state on the swiped view.
+                    if (isScrollingX || isScrollingY) {
+                        cancelCheckForTap();
+                    }
+
+                    if (isSwipingToClose) {
+                        swiping = true;
+                        TabsListLayout.this.requestDisallowInterceptTouchEvent(true);
+                        ((TabsLayoutItemView) swipeView).setCloseVisible(false);
+
+                        // Stops listview from highlighting the touched item
+                        // in the list when swiping.
+                        MotionEvent cancelEvent = MotionEvent.obtain(e);
+                        cancelEvent.setAction(MotionEvent.ACTION_CANCEL |
+                                (e.getActionIndex() << MotionEvent.ACTION_POINTER_INDEX_SHIFT));
+
+                        TabsListLayout.this.onTouchEvent(cancelEvent);
+                        cancelEvent.recycle();
+                    }
+                    if (swiping) {
+                        if (isVertical) {
+                            ViewHelper.setTranslationX(swipeView, delta);
+                        } else {
+                            ViewHelper.setTranslationY(swipeView, delta);
+                        }
+
+                        ViewHelper.setAlpha(swipeView, Math.max(0.1f, Math.min(1f,
+                                1f - 2f * Math.abs(delta) / (isVertical ? listWidth : listHeight))));
+
+                        return true;
+                    }
+                    break;
+                }
+            }
+            return false;
+        }
+
+        private View findViewAt(float rawX, float rawY) {
+            Rect rect = new Rect();
+
+            int[] listViewCoords = new int[2];
+            TabsListLayout.this.getLocationOnScreen(listViewCoords);
+
+            int x = (int) rawX - listViewCoords[0];
+            int y = (int) rawY - listViewCoords[1];
+
+            for (int i = 0; i < TabsListLayout.this.getChildCount(); i++) {
+                View child = TabsListLayout.this.getChildAt(i);
+                child.getHitRect(rect);
+
+                if (rect.contains(x, y)) {
+                    return child;
+                }
+            }
+            return null;
+        }
+
+        private void triggerCheckForTap() {
+            if (pendingCheckForTap == null) {
+                pendingCheckForTap = new CheckForTap();
+            }
+            TabsListLayout.this.postDelayed(pendingCheckForTap, ViewConfiguration.getTapTimeout());
+        }
+
+        private void cancelCheckForTap() {
+            if (pendingCheckForTap == null) {
+                return;
+            }
+            TabsListLayout.this.removeCallbacks(pendingCheckForTap);
+        }
+
+        private class CheckForTap implements Runnable {
+            @Override
+            public void run() {
+                if (!swiping && swipeView != null && enabled) {
+                    swipeView.setPressed(true);
+                }
+            }
+        }
+    }
+}
--- a/mobile/android/base/tabs/TabsPanel.java
+++ b/mobile/android/base/tabs/TabsPanel.java
@@ -1,15 +1,16 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
 /* 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/. */
 
 package org.mozilla.gecko.tabs;
 
+import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.GeckoApp;
 import org.mozilla.gecko.GeckoApplication;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.RestrictedProfiles;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.Telemetry;
@@ -21,16 +22,17 @@ import org.mozilla.gecko.lwt.Lightweight
 import org.mozilla.gecko.restrictions.Restriction;
 import org.mozilla.gecko.util.ColorUtils;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.widget.GeckoPopupMenu;
 import org.mozilla.gecko.widget.IconTabWidget;
 
 import android.content.Context;
 import android.content.res.Resources;
+import android.graphics.Rect;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewStub;
 import android.widget.Button;
@@ -50,35 +52,45 @@ public class TabsPanel extends LinearLay
         NORMAL_TABS,
         PRIVATE_TABS,
     }
 
     public interface PanelView {
         void setTabsPanel(TabsPanel panel);
         void show();
         void hide();
+        boolean shouldExpand();
     }
 
     public interface CloseAllPanelView extends PanelView {
         void closeAll();
     }
 
     public interface TabsLayout extends CloseAllPanelView {
         void setEmptyView(View view);
     }
 
     public interface TabsLayoutChangeListener {
         void onTabsLayoutChange(int width, int height);
     }
 
+
+    public static View createTabsLayout(final Context context, final AttributeSet attrs) {
+        if (HardwareUtils.isTablet() || AppConstants.NIGHTLY_BUILD) {
+            return new TabsGridLayout(context, attrs);
+        } else {
+            return new TabsListLayout(context, attrs);
+        }
+    }
+
     private final Context mContext;
     private final GeckoApp mActivity;
     private final LightweightTheme mTheme;
     private RelativeLayout mHeader;
-    private FrameLayout mTabsContainer;
+    private TabsLayoutContainer mTabsContainer;
     private PanelView mPanel;
     private PanelView mPanelNormal;
     private PanelView mPanelPrivate;
     private TabsLayoutChangeListener mLayoutChangeListener;
 
     private IconTabWidget mTabWidget;
     private static View mMenuButton;
     private static ImageButton mAddTab;
@@ -107,17 +119,17 @@ public class TabsPanel extends LinearLay
     }
 
     private void inflateLayout(Context context) {
         LayoutInflater.from(context).inflate(R.layout.tabs_panel_default, this);
     }
 
     private void initialize() {
         mHeader = (RelativeLayout) findViewById(R.id.tabs_panel_header);
-        mTabsContainer = (FrameLayout) findViewById(R.id.tabs_container);
+        mTabsContainer = (TabsLayoutContainer) findViewById(R.id.tabs_container);
 
         mPanelNormal = (PanelView) findViewById(R.id.normal_tabs);
         mPanelNormal.setTabsPanel(this);
 
         mPanelPrivate = (PanelView) findViewById(R.id.private_tabs_panel);
         mPanelPrivate.setTabsPanel(this);
 
         mAddTab = (ImageButton) findViewById(R.id.add_tab);
@@ -229,24 +241,50 @@ public class TabsPanel extends LinearLay
 
         if (itemId == R.id.new_tab || itemId == R.id.new_private_tab) {
             hide();
         }
 
         return mActivity.onOptionsItemSelected(item);
     }
 
-    private static int getTabContainerHeight(View tabsContainer) {
+    private static int getTabContainerHeight(TabsLayoutContainer tabsContainer) {
         Resources resources = tabsContainer.getContext().getResources();
 
         int screenHeight = resources.getDisplayMetrics().heightPixels;
 
         int actionBarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height);
 
-        return screenHeight - actionBarHeight;
+        if (HardwareUtils.isTablet() || AppConstants.NIGHTLY_BUILD) {
+            return screenHeight - actionBarHeight;
+        }
+
+        PanelView panelView = tabsContainer.getCurrentPanelView();
+        if (panelView != null && !panelView.shouldExpand()) {
+
+            // This allows us to accommodate varying height tab previews across different devices.
+            // We should be able to remove once we remove the list view and remove the chrome again
+            return  resources.getDimensionPixelSize(R.dimen.tab_thumbnail_height
+                  + resources.getDimensionPixelSize(R.dimen.tab_title_height)
+                  + 2 * (resources.getDimensionPixelSize(R.dimen.tab_highlight_stroke_width)
+                         + resources.getDimensionPixelSize(R.dimen.tab_vertical_padding)
+                         + resources.getDimensionPixelSize(R.dimen.tab_thumbnail_padding)
+                         + resources.getDimensionPixelSize(R.dimen.tab_thumbnail_margin)));
+        }
+
+        Rect windowRect = new Rect();
+        tabsContainer.getWindowVisibleDisplayFrame(windowRect);
+        int windowHeight = windowRect.bottom - windowRect.top;
+
+        // The web content area should have at least 1.5x the height of the action bar.
+        // The tabs panel shouldn't take less than 50% of the screen height and can take
+        // up to 80% of the window height.
+        return (int) Math.max(screenHeight * 0.5f,
+                Math.min(windowHeight - 2.5f * actionBarHeight, windowHeight * 0.8f) - actionBarHeight);
+
     }
 
     @Override
     public void onAttachedToWindow() {
         super.onAttachedToWindow();
         mTheme.addListener(this);
     }
 
@@ -274,16 +312,45 @@ public class TabsPanel extends LinearLay
     }
 
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
         onLightweightThemeChanged();
     }
 
+    static class TabsLayoutContainer extends FrameLayout {
+
+        public TabsLayoutContainer(Context context, AttributeSet attrs) {
+            super(context, attrs);
+        }
+
+        public PanelView getCurrentPanelView() {
+            final int childCount = getChildCount();
+
+            for (int i = 0; i < childCount; i++) {
+                View child = getChildAt(i);
+                if (!(child instanceof PanelView)) {
+                    continue;
+                }
+
+                if (child.getVisibility() == View.VISIBLE) {
+                    return (PanelView) child;
+                }
+            }
+            return null;
+        }
+
+        @Override
+        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+            int heightSpec = MeasureSpec.makeMeasureSpec(getTabContainerHeight(TabsLayoutContainer.this), MeasureSpec.EXACTLY);
+            super.onMeasure(widthMeasureSpec, heightSpec);
+        }
+    }
+
     // Tabs Panel Toolbar contains the Buttons
     static class TabsPanelToolbar extends LinearLayout
                                   implements LightweightTheme.OnChangeListener {
         private final LightweightTheme mTheme;
 
         public TabsPanelToolbar(Context context, AttributeSet attrs) {
             super(context, attrs);
             mTheme = ((GeckoApplication) context.getApplicationContext()).getLightweightTheme();
@@ -417,16 +484,20 @@ public class TabsPanel extends LinearLay
         mActivity.autoHideTabs();
     }
 
     @Override
     public boolean isShown() {
         return mVisible;
     }
 
+    public Panel getCurrentPanel() {
+        return mCurrentPanel;
+    }
+
     public void setHWLayerEnabled(boolean enabled) {
         if (Versions.preHC) {
             return;
         }
         if (enabled) {
             mHeader.setLayerType(View.LAYER_TYPE_HARDWARE, null);
             mTabsContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
         } else {
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -337,8 +337,10 @@ user_pref("media.webspeech.synth.test", 
 // connections.
 user_pref("browser.urlbar.suggest.searches", false);
 
 // Turn off the location bar search suggestions opt-in.  It interferes with
 // tests that don't expect it to be there.
 user_pref("browser.urlbar.userMadeSearchSuggestionsChoice", true);
 
 user_pref("dom.audiochannel.mutedByDefault", false);
+
+user_pref("view_source.tab", true);
--- a/toolkit/components/viewsource/content/viewSourceUtils.js
+++ b/toolkit/components/viewsource/content/viewSourceUtils.js
@@ -10,16 +10,18 @@
  *
  * This file silently depends on contentAreaUtils.js for
  * getDefaultFileName, getNormalizedLeafName and getDefaultExtension
  */
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ViewSourceBrowser",
   "resource://gre/modules/ViewSourceBrowser.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
+  "resource://gre/modules/Deprecated.jsm");
 
 var gViewSourceUtils = {
 
   mnsIWebBrowserPersist: Components.interfaces.nsIWebBrowserPersist,
   mnsIWebProgress: Components.interfaces.nsIWebProgress,
   mnsIWebPageDescriptor: Components.interfaces.nsIWebPageDescriptor,
 
   /**
@@ -174,48 +176,101 @@ var gViewSourceUtils = {
       const argumentRE = /"([^"]+)"|(\S+)/g;
       while (argumentRE.test(args))
         editorArgs.push(RegExp.$1 || RegExp.$2);
     }
     editorArgs.push(aPath);
     return editorArgs;
   },
 
-  // aCallBack is a function accepting two arguments - result (true=success) and a data object
-  // It defaults to openInInternalViewer if undefined.
-  openInExternalEditor: function(aURL, aPageDescriptor, aDocument, aLineNumber, aCallBack)
-  {
-    var data = {url: aURL, pageDescriptor: aPageDescriptor, doc: aDocument,
-                lineNumber: aLineNumber};
+  /**
+   * Opens an external editor with the view source content.
+   *
+   * @param aArgsOrURL (required)
+   *        This is either an Object containing parameters, or a string
+   *        URL for the page we want to view the source of. In the latter
+   *        case we will be paying attention to the other parameters, as
+   *        we will be supporting the old API for this method.
+   *        If aArgsOrURL is an Object, the other parameters will be ignored.
+   *        aArgsOrURL as an Object can include the following properties:
+   *
+   *        URL (required):
+   *          A string URL for the page we'd like to view the source of.
+   *        browser (optional):
+   *          The browser containing the document that we would like to view the
+   *          source of. This is required if outerWindowID is passed.
+   *        outerWindowID (optional):
+   *          The outerWindowID of the content window containing the document that
+   *          we want to view the source of. Pass this if you want to attempt to
+   *          load the document source out of the network cache.
+   *        lineNumber (optional):
+   *          The line number to focus on once the source is loaded.
+   *
+   * @param aPageDescriptor (deprecated, optional)
+   *        Accepted for compatibility reasons, but is otherwise ignored.
+   * @param aDocument (deprecated, optional)
+   *        The content document we would like to view the source of. This
+   *        function will throw if aDocument is a CPOW.
+   * @param aLineNumber (deprecated, optional)
+   *        The line number to focus on once the source is loaded.
+   * @param aCallBack
+   *        A function accepting two arguments:
+   *          * result (true = success)
+   *          * data object
+   *        The function defaults to opening an internal viewer if external
+   *        viewing fails.
+   */
+  openInExternalEditor: function(aArgsOrURL, aPageDescriptor, aDocument,
+                                 aLineNumber, aCallBack) {
+    let data;
+    if (typeof aArgsOrURL == "string") {
+      Deprecated.warning("The arguments you're passing to " +
+                         "openInExternalEditor are using an out-of-date API.",
+                         "https://developer.mozilla.org/en-US/Add-ons/" +
+                         "Code_snippets/View_Source_for_XUL_Applications");
+      data = {
+        url: aArgsOrURL,
+        pageDescriptor: aPageDescriptor,
+        doc: aDocument,
+        lineNumber: aLineNumber
+      };
+    } else {
+      let { URL, outerWindowID, lineNumber } = aArgsOrURL;
+      data = {
+        url: URL,
+        lineNumber
+      };
+    }
 
     try {
       var editor = this.getExternalViewSourceEditor();
       if (!editor) {
         this.handleCallBack(aCallBack, false, data);
         return;
       }
 
       // make a uri
       var ios = Components.classes["@mozilla.org/network/io-service;1"]
                           .getService(Components.interfaces.nsIIOService);
       var charset = aDocument ? aDocument.characterSet : null;
-      var uri = ios.newURI(aURL, charset, null);
+      var uri = ios.newURI(data.url, charset, null);
       data.uri = uri;
 
       var path;
       var contentType = aDocument ? aDocument.contentType : null;
       if (uri.scheme == "file") {
         // it's a local file; we can open it directly
         path = uri.QueryInterface(Components.interfaces.nsIFileURL).file.path;
 
         var editorArgs = this.buildEditorArgs(path, data.lineNumber);
         editor.runw(false, editorArgs, editorArgs.length);
         this.handleCallBack(aCallBack, true, data);
       } else {
         // set up the progress listener with what we know so far
+        this.viewSourceProgressListener.contentLoaded = false;
         this.viewSourceProgressListener.editor = editor;
         this.viewSourceProgressListener.callBack = aCallBack;
         this.viewSourceProgressListener.data = data;
         if (!aPageDescriptor) {
           // without a page descriptor, loadPage has no chance of working. download the file.
           var file = this.getTemporaryFile(uri, aDocument, contentType);
           this.viewSourceProgressListener.file = file;
 
@@ -358,16 +413,21 @@ var gViewSourceUtils = {
           webNavigation.document.addEventListener("DOMContentLoaded",
                                                   this.onContentLoaded.bind(this));
         }
       }
       return 0;
     },
 
     onContentLoaded: function() {
+      // The progress listener may call this multiple times, so be sure we only
+      // run once.
+      if (this.contentLoaded) {
+        return;
+      }
       try {
         if (!this.file) {
           // it's not saved to file yet, it's in the webshell
 
           // get a temporary filename using the attributes from the data object that
           // openInExternalEditor gave us
           this.file = gViewSourceUtils.getTemporaryFile(this.data.uri, this.data.doc,
                                                         this.data.doc.contentType);
@@ -405,16 +465,17 @@ var gViewSourceUtils = {
             helperService.deleteTemporaryFileOnExit(this.file);
           }
         }
 
         var editorArgs = gViewSourceUtils.buildEditorArgs(this.file.path,
                                                           this.data.lineNumber);
         this.editor.runw(false, editorArgs, editorArgs.length);
 
+        this.contentLoaded = true;
         gViewSourceUtils.handleCallBack(this.callBack, true, this.data);
       } catch (ex) {
         // we failed loading it with the external editor.
         Components.utils.reportError(ex);
         gViewSourceUtils.handleCallBack(this.callBack, false, this.data);
       } finally {
         this.destroy();
       }
--- a/toolkit/content/tests/chrome/test_bug624329.xul
+++ b/toolkit/content/tests/chrome/test_bug624329.xul
@@ -94,22 +94,23 @@ function openContextMenu() {
             clearInterval(interval);
             // Wait further to check that the window does not move again.
             setTimeout(checkPosition, 1000);
         }
     }
 
     function checkPosition() {
         var menubox = menu.boxObject;
-        var winbox = win.document.documentElement.boxObject
+        var winbox = win.document.documentElement.boxObject;
+        var platformIsMac = navigator.userAgent.indexOf("Mac") > -1;
 
         var x = menubox.screenX - winbox.screenX;
         var y = menubox.screenY - winbox.screenY;
 
-        if (navigator.userAgent.indexOf("Mac") > -1)
+        if (platformIsMac)
         {
           // This check is alterered slightly for OSX which adds padding to the top
           // and bottom of its context menus. The menu position calculation must
           // be changed to allow for the pointer to be outside this padding
           // when the menu opens.
           // (Bug 1075089)
           ok(y + 6 >= mouseY,
              "menu top " + (y + 6) + " should be below click point " + mouseY);
@@ -121,18 +122,27 @@ function openContextMenu() {
         }
         
         ok(y <= mouseY + 20,
            "menu top " + y + " should not be too far below click point " + mouseY);
 
         ok(x < mouseX,
            "menu left " + x + " should be left of click point " + mouseX);
         var right = x + menubox.width;
-        ok(right > mouseX,
-           "menu right " + right + " should be right of click point " + mouseX);
+
+        if (platformIsMac) {
+          // Rather than be constrained by the right hand screen edge, OSX menus flip
+          // horizontally and appear to the left of the mouse pointer
+          ok(right < mouseX,
+             "menu right " + right + " should be left of click point " + mouseX);
+        }
+        else {
+          ok(right > mouseX,
+             "menu right " + right + " should be right of click point " + mouseX);
+        }
 
         clearTimeout(timeoutID);
         finish();
     }
 
 }
 
 function finish() {
--- a/toolkit/content/tests/chrome/window_largemenu.xul
+++ b/toolkit/content/tests/chrome/window_largemenu.xul
@@ -14,24 +14,24 @@
 
 <script>
 <![CDATA[
 
 var gOverflowed = false, gUnderflowed = false;
 var gContextMenuTests = false;
 var gScreenY = -1;
 var gTestIndex = 0;
-var gTests = ["open normal", "open flipped position", "open with scrolling",
+var gTests = ["open normal", "open when bottom would overlap", "open with scrolling",
               "open after scrolling", "open small again",
               "menu movement", "panel movement",
               "context menu enough space below",
               "context menu more space above",
               "context menu too big either side",
-              "context menu larger than screen"];
-
+              "context menu larger than screen",
+              "context menu flips horizontally on osx"];
 function getScreenXY(element)
 {
   var screenX, screenY;
   var mouseFn = function(event) {
     screenX = event.screenX - 1;
     screenY = event.screenY - 1;
   }
 
@@ -59,32 +59,34 @@ function runTests()
   nextTest();
 }
 
 function nextTest()
 {
   gOverflowed = false, gUnderflowed = false;
 
   var y = screen.height;
-  if (gTestIndex == 1) // open flipped position test:
+  if (gTestIndex == 1) // open with bottom overlap test:
     y -= 100;
   else
     y /= 2;
 
   var popup = document.getElementById("popup");
   if (gTestIndex == 2) {
     // add some more menuitems so that scrolling will be necessary
-    for (var t = 1; t <= 30; t++) {
+    var moreItemCount = Math.round(screen.height / popup.firstChild.getBoundingClientRect().height);
+    for (var t = 1; t <= moreItemCount; t++) {
       var menu = document.createElement("menuitem");
       menu.setAttribute("label", "More" + t);
       popup.appendChild(menu);
     }
   }
   else if (gTestIndex == 4) {
-    for (var t = 1; t <= 30; t++)
+    // remove the items added in test 2 above
+    while (popup.childNodes.length > 15)
       popup.removeChild(popup.lastChild);
   }
 
   window.requestAnimationFrame(function() {
     setTimeout(
       function() {
         popup.openPopupAtScreen(100, y, false);
       }, 0);
@@ -111,22 +113,32 @@ function popupShown()
     is(Math.round(rect.top) + gScreenY, Math.floor(screen.height / 2),
                               gTests[gTestIndex] + " top");
     ok(Math.round(rect.bottom) + gScreenY < screen.height,
                                 gTests[gTestIndex] + " bottom");
     ok(!gOverflowed && !gUnderflowed, gTests[gTestIndex] + " overflow")
   }
   else if (gTestIndex == 1) {
     // the popup was supposed to open 100 pixels from the bottom, but that
-    // would put it off screen so it should be flipped to have its bottom
-    // edge 100 pixels from the bottom
-    ok(Math.round(rect.top) + gScreenY >= screen.top, gTests[gTestIndex] + " top");
-    is(Math.round(rect.bottom) + gScreenY, screen.height - 100,
-                                gTests[gTestIndex] + " bottom");
-    ok(!gOverflowed && !gUnderflowed, gTests[gTestIndex] + " overflow")
+    // would put it off screen so ...
+    if (platformIsMac()) {
+      // On OSX the popup is constrained so it remains within the
+      // bounds of the screen
+      ok(Math.round(rect.top) + gScreenY >= screen.top, gTests[gTestIndex] + " top");
+      is(Math.round(rect.bottom) + gScreenY, screen.availTop + screen.availHeight, gTests[gTestIndex] + " bottom");
+      ok(!gOverflowed && !gUnderflowed, gTests[gTestIndex] + " overflow");
+    }
+    else {
+      // On other platforms the menu should be flipped to have its bottom
+      // edge 100 pixels from the bottom
+      ok(Math.round(rect.top) + gScreenY >= screen.top, gTests[gTestIndex] + " top");
+      is(Math.round(rect.bottom) + gScreenY, screen.height - 100,
+                                  gTests[gTestIndex] + " bottom");
+      ok(!gOverflowed && !gUnderflowed, gTests[gTestIndex] + " overflow");
+    }
   }
   else if (gTestIndex == 2) {
     // the popup is too large so ensure that it is on screen
     ok(Math.round(rect.top) + gScreenY >= screen.top, gTests[gTestIndex] + " top");
     ok(Math.round(rect.bottom) + gScreenY <= screen.height, gTests[gTestIndex] + " bottom");
     ok(gOverflowed && !gUnderflowed, gTests[gTestIndex] + " overflow")
 
     sbo.scrollTo(0, 40);
@@ -137,17 +149,17 @@ function popupShown()
   }
   else if (gTestIndex == 4) {
     // note that if the height is odd, the y-offset will have been rounded
     // down when we pass the fractional value to openPopupAtScreen above.
     is(Math.round(rect.top) + gScreenY, Math.floor(screen.height / 2),
                               gTests[gTestIndex] + " top");
     ok(Math.round(rect.bottom) + gScreenY < screen.height,
                                 gTests[gTestIndex] + " bottom");
-    ok(!gOverflowed && gUnderflowed, gTests[gTestIndex] + " overflow")
+    ok(!gOverflowed && gUnderflowed, gTests[gTestIndex] + " overflow");
   }
 
   is(sbo.positionY, expectedScrollPos, "menu scroll position");
 
   hidePopup();
 }
 
 function is(l, r, n) { window.opener.wrappedJSObject.SimpleTest.is(l,r,n); }
@@ -213,36 +225,54 @@ function contextMenuPopupShown()
 {
   var popup = document.getElementById("popup");
   var rect = popup.getBoundingClientRect();
   var labelrect = document.getElementById("label").getBoundingClientRect();
   
   // Click to open popup in popupHidden() occurs at (4,4) in label's coordinate space
   var clickX = clickY = 4;
   
-  is(rect.left, labelrect.left + clickX + (platformIsMac() ? 1 : 2), gTests[gTestIndex] + " left");
+  var testPopupAppearedRightOfCursor = true;
   switch (gTests[gTestIndex]) {
     case "context menu enough space below":
       is(rect.top, labelrect.top + clickY + (platformIsMac() ? -6 : 2), gTests[gTestIndex] + " top");
       break;
     case "context menu more space above":
-      is(rect.top, labelrect.top + clickY - rect.height - (platformIsMac() ? 0 : 2), gTests[gTestIndex] + " top");
+      if (platformIsMac()) {
+        let screenY;
+        [, screenY] = getScreenXY(popup);
+        // Macs constrain their popup menus vertically rather than flip them.
+        is(screenY, screen.availTop + screen.availHeight - rect.height, gTests[gTestIndex] + " top");
+      } else {
+        is(rect.top, labelrect.top + clickY - rect.height - 2, gTests[gTestIndex] + " top");
+      }
+
       break;
     case "context menu too big either side":
       [, gScreenY] = getScreenXY(document.documentElement);
       // compare against the available size as well as the total size, as some
       // platforms allow the menu to overlap os chrome and others do not
       var pos = (screen.availTop + screen.availHeight - rect.height) - gScreenY;
       var availPos = (screen.top + screen.height - rect.height) - gScreenY;
       ok(rect.top == pos || rect.top == availPos,
          gTests[gTestIndex] + " top");
       break;
     case "context menu larger than screen":
       ok(rect.top == -(gScreenY - screen.availTop) || rect.top == -(gScreenY - screen.top), gTests[gTestIndex] + " top");
       break;
+    case "context menu flips horizontally on osx":
+      testPopupAppearedRightOfCursor = false;
+      if (platformIsMac()) {
+        is(Math.round(rect.right), labelrect.left + clickX - 1, gTests[gTestIndex] + " right");
+      }
+      break;
+  }
+
+  if (testPopupAppearedRightOfCursor) {
+    is(rect.left, labelrect.left + clickX + (platformIsMac() ? 1 : 2), gTests[gTestIndex] + " left");
   }
 
   hidePopup();
 }
 
 function contextMenuPopupHidden()
 {
   var screenAvailBottom = screen.availTop + screen.availHeight;
@@ -251,16 +281,21 @@ function contextMenuPopupHidden()
     moveWindowTo(window.screenX, screenAvailBottom - 80, nextContextMenuTest, -1);
   }
   else if (gTests[gTestIndex] == "context menu too big either side") {
     moveWindowTo(window.screenX, screenAvailBottom / 2 - 80, nextContextMenuTest, screenAvailBottom / 2 + 120);
   }
   else if (gTests[gTestIndex] == "context menu larger than screen") {
     nextContextMenuTest(screen.availHeight + 80);
   }
+  else if (gTests[gTestIndex] == "context menu flips horizontally on osx") {
+    var popup = document.getElementById("popup");
+    var popupWidth = popup.getBoundingClientRect().width;
+    moveWindowTo(screen.availLeft + screen.availWidth - popupWidth, 100, nextContextMenuTest, -1);
+  }
 }
 
 function nextContextMenuTest(desiredHeight)
 {
   if (desiredHeight >= 0) {
     var popup = document.getElementById("popup");
     var height = popup.getBoundingClientRect().height;
     var itemheight = document.getElementById("firstitem").getBoundingClientRect().height;
@@ -338,17 +373,17 @@ function testPopupMovement()
   is(popup.top, "98", gTests[gTestIndex] + " top is set after moving");
   popup.removeAttribute("left");
   popup.removeAttribute("top");
 
   popup.moveTo(-1, -1);
   [screenX, screenY] = getScreenXY(popup);
   [buttonScreenX, buttonScreenY] = getScreenXY(button);
   is(screenX, buttonScreenX, gTests[gTestIndex] + " original x");
-  is(screenY, buttonScreenY + button.getBoundingClientRect().height, gTests[gTestIndex] + " original y");
+  is(screenY, buttonScreenY + Math.round(button.getBoundingClientRect().height), gTests[gTestIndex] + " original y");
 
   popup.hidePopup();
 }
 
 function platformIsMac()
 {
   return navigator.platform.indexOf("Mac") > -1;
 }
--- a/toolkit/devtools/gcli/commands/calllog.js
+++ b/toolkit/devtools/gcli/commands/calllog.js
@@ -1,16 +1,15 @@
 /* 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/. */
 
 "use strict";
 
 const { Cc, Ci, Cu } = require("chrome");
-const {TargetFactory} = require("devtools/framework/target");
 const l10n = require("gcli/l10n");
 const gcli = require("gcli/index");
 
 loader.lazyRequireGetter(this, "TargetFactory", "devtools/framework/target", true);
 
 loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
 
 loader.lazyGetter(this, "Debugger", () => {
--- a/toolkit/devtools/performance/io.js
+++ b/toolkit/devtools/performance/io.js
@@ -148,17 +148,19 @@ function convertLegacyData (legacyData) 
     profile: profilerData.profile,
     // Fake a configuration object here if there's tick data,
     // so that it can be rendered
     configuration: {
       withTicks: !!ticksData.length,
       withMarkers: false,
       withMemory: false,
       withAllocations: false
-    }
+    },
+    systemHost: {},
+    systemClient: {},
   };
 
   return data;
 }
 
 exports.getUnicodeConverter = getUnicodeConverter;
 exports.saveRecordingToFile = saveRecordingToFile;
 exports.loadRecordingFromFile = loadRecordingFromFile;
--- a/toolkit/devtools/performance/legacy/front.js
+++ b/toolkit/devtools/performance/legacy/front.js
@@ -16,16 +16,20 @@ loader.lazyRequireGetter(this, "Actors",
 loader.lazyRequireGetter(this, "LegacyPerformanceRecording",
   "devtools/toolkit/performance/legacy/recording", true);
 loader.lazyRequireGetter(this, "importRecording",
   "devtools/toolkit/performance/legacy/recording", true);
 loader.lazyRequireGetter(this, "normalizePerformanceFeatures",
   "devtools/toolkit/performance/utils", true);
 loader.lazyRequireGetter(this, "DevToolsUtils",
   "devtools/toolkit/DevToolsUtils");
+loader.lazyRequireGetter(this, "getDeviceFront",
+  "devtools/toolkit/server/actors/device", true);
+loader.lazyRequireGetter(this, "getSystemInfo",
+  "devtools/toolkit/shared/system", true);
 loader.lazyRequireGetter(this, "events",
   "sdk/event/core");
 loader.lazyRequireGetter(this, "EventTarget",
   "sdk/event/target", true);
 loader.lazyRequireGetter(this, "Class",
   "sdk/core/heritage", true);
 
 /**
@@ -328,39 +332,48 @@ const LegacyPerformanceFront = Class({
     // from the LegacyPerformanceFront or via `console-profile-stop` event) and then
     // remove it from the internal store.
     //
     // In the case where a console.profile is generated via the console (so the tools are
     // open), we initialize the Performance tool so it can listen to those events.
     this._recordings.splice(this._recordings.indexOf(model), 1);
 
     let config = model.getConfiguration();
-    let startTime = model.getProfilerStartTime();
+    let startTime = model._getProfilerStartTime();
     let profilerData = yield this._profiler.getProfile({ startTime });
     let timelineEndTime = Date.now();
 
     // Only if there are no more sessions recording do we stop
     // the underlying timeline actors. If we're still recording,
     // juse use Date.now() for the timeline end times, as those
     // are only used in tests.
     if (!this.isRecording()) {
       // This doesn't stop the profiler, just turns off polling for
       // events, and also turns off events on timeline actors.
       yield this._profiler.stop();
       timelineEndTime = yield this._timeline.stop(config);
     }
 
+    let systemDeferred = promise.defer();
+    this._client.listTabs(form => {
+      systemDeferred.resolve(getDeviceFront(this._client, form).getDescription());
+    });
+    let systemHost = yield systemDeferred.promise;
+    let systemClient = yield getSystemInfo();
+
     // Set the results on the LegacyPerformanceRecording itself.
     model._onStopRecording({
       // Data available only at the end of a recording.
       profile: profilerData.profile,
 
       // End times for all the actors.
       profilerEndTime: profilerData.currentTime,
-      timelineEndTime: timelineEndTime
+      timelineEndTime: timelineEndTime,
+      systemHost,
+      systemClient,
     });
 
     events.emit(this, "recording-stopped", model);
     return model;
   }),
 
   /**
    * Creates a recording object when given a nsILocalFile.
--- a/toolkit/devtools/performance/legacy/recording.js
+++ b/toolkit/devtools/performance/legacy/recording.js
@@ -5,16 +5,19 @@
 
 const { Cc, Ci, Cu, Cr } = require("chrome");
 const { Task } = require("resource://gre/modules/Task.jsm");
 
 loader.lazyRequireGetter(this, "PerformanceIO",
   "devtools/toolkit/performance/io");
 loader.lazyRequireGetter(this, "RecordingUtils",
   "devtools/toolkit/performance/utils");
+loader.lazyRequireGetter(this, "PerformanceRecordingCommon",
+  "devtools/toolkit/performance/recording-common", true);
+loader.lazyRequireGetter(this, "merge", "sdk/util/object", true);
 
 /**
  * Model for a wholistic profile, containing the duration, profiling data,
  * frames data, timeline (marker, tick, memory) data, and methods to mark
  * a recording as 'in progress' or 'finished'.
  */
 const LegacyPerformanceRecording = function (options={}) {
   this._label = options.label || "";
@@ -28,59 +31,20 @@ const LegacyPerformanceRecording = funct
     withJITOptimizations: options.withJITOptimizations || false,
     allocationsSampleProbability: options.allocationsSampleProbability || 0,
     allocationsMaxLogLength: options.allocationsMaxLogLength || 0,
     bufferSize: options.bufferSize || 0,
     sampleFrequency: options.sampleFrequency || 1
   };
 };
 
-LegacyPerformanceRecording.prototype = {
-  // Private fields, only needed when a recording is started or stopped.
-  _console: false,
-  _imported: false,
-  _recording: false,
-  _completed: false,
+LegacyPerformanceRecording.prototype = merge({
   _profilerStartTime: 0,
   _timelineStartTime: 0,
   _memoryStartTime: 0,
-  _configuration: {},
-  _startingBufferStatus: null,
-  _bufferPercent: null,
-
-  // Serializable fields, necessary and sufficient for import and export.
-  _label: "",
-  _duration: 0,
-  _markers: null,
-  _frames: null,
-  _memory: null,
-  _ticks: null,
-  _allocations: null,
-  _profile: null,
-
-  /**
-   * Loads a recording from a file.
-   *
-   * @param nsILocalFile file
-   *        The file to import the data form.
-   */
-  importRecording: Task.async(function *(file) {
-    let recordingData = yield PerformanceIO.loadRecordingFromFile(file);
-
-    this._imported = true;
-    this._label = recordingData.label || "";
-    this._duration = recordingData.duration;
-    this._markers = recordingData.markers;
-    this._frames = recordingData.frames;
-    this._memory = recordingData.memory;
-    this._ticks = recordingData.ticks;
-    this._allocations = recordingData.allocations;
-    this._profile = recordingData.profile;
-    this._configuration = recordingData.configuration || {};
-  }),
 
   /**
    * Saves the current recording to a file.
    *
    * @param nsILocalFile file
    *        The file to stream the data into.
    */
   exportRecording: Task.async(function *(file) {
@@ -102,21 +66,21 @@ LegacyPerformanceRecording.prototype = {
     this._profilerStartTime = info.profilerStartTime;
     this._timelineStartTime = info.timelineStartTime;
     this._memoryStartTime = info.memoryStartTime;
     this._startingBufferStatus = {
       position: info.position,
       totalSize: info.totalSize,
       generation: info.generation
     };
-    // initialize the _bufferPercent if the server supports it.
-    this._bufferPercent = info.position !== void 0 ? 0 : null;
 
     this._recording = true;
 
+    this._systemHost = {};
+    this._systemClient = {};
     this._markers = [];
     this._frames = [];
     this._memory = [];
     this._ticks = [];
     this._allocations = { sites: [], timestamps: [], frames: [], sizes: [] };
   },
 
   /**
@@ -129,191 +93,46 @@ LegacyPerformanceRecording.prototype = {
     this._duration = endTime - this._localStartTime;
     this._recording = false;
   },
 
   /**
    * Sets results available from stopping a recording from PerformanceFront.
    * Should only be called by PerformanceFront.
    */
-  _onStopRecording: Task.async(function *({ profilerEndTime, profile }) {
+  _onStopRecording: Task.async(function *({ profilerEndTime, profile, systemClient, systemHost }) {
     // Update the duration with the accurate profilerEndTime, so we don't have
     // samples outside of the approximate duration set in `_onStoppingRecording`.
     this._duration = profilerEndTime - this._profilerStartTime;
     this._profile = profile;
     this._completed = true;
 
     // We filter out all samples that fall out of current profile's range
     // since the profiler is continuously running. Because of this, sample
     // times are not guaranteed to have a zero epoch, so offset the
     // timestamps.
     RecordingUtils.offsetSampleTimes(this._profile, this._profilerStartTime);
 
     // Markers need to be sorted ascending by time, to be properly displayed
     // in a waterfall view.
     this._markers = this._markers.sort((a, b) => (a.start > b.start));
+
+    this._systemHost = systemHost;
+    this._systemClient = systemClient;
   }),
 
   /**
    * Gets the profile's start time.
    * @return number
    */
-  getProfilerStartTime: function () {
+  _getProfilerStartTime: function () {
     return this._profilerStartTime;
   },
 
   /**
-   * Gets the profile's label, from `console.profile(LABEL)`.
-   * @return string
-   */
-  getLabel: function () {
-    return this._label;
-  },
-
-  /**
-   * Gets duration of this recording, in milliseconds.
-   * @return number
-   */
-  getDuration: function () {
-    // Compute an approximate ending time for the current recording if it is
-    // still in progress. This is needed to ensure that the view updates even
-    // when new data is not being generated.
-    if (this._recording) {
-      return Date.now() - this._localStartTime;
-    } else {
-      return this._duration;
-    }
-  },
-
-  /**
-   * Returns configuration object of specifying whether the recording
-   * was started withTicks, withMemory and withAllocations, and other configurations.
-   * @return object
-   */
-  getConfiguration: function () {
-    return this._configuration;
-  },
-
-  /**
-   * Gets the accumulated markers in the current recording.
-   * @return array
-   */
-  getMarkers: function() {
-    return this._markers;
-  },
-
-  /**
-   * Gets the accumulated stack frames in the current recording.
-   * @return array
-   */
-  getFrames: function() {
-    return this._frames;
-  },
-
-  /**
-   * Gets the accumulated memory measurements in this recording.
-   * @return array
-   */
-  getMemory: function() {
-    return this._memory;
-  },
-
-  /**
-   * Gets the accumulated refresh driver ticks in this recording.
-   * @return array
-   */
-  getTicks: function() {
-    return this._ticks;
-  },
-
-  /**
-   * Gets the memory allocations data in this recording.
-   * @return array
-   */
-  getAllocations: function() {
-    return this._allocations;
-  },
-
-  /**
-   * Gets the profiler data in this recording.
-   * @return array
-   */
-  getProfile: function() {
-    return this._profile;
-  },
-
-  /**
-   * Gets all the data in this recording.
-   */
-  getAllData: function() {
-    let label = this.getLabel();
-    let duration = this.getDuration();
-    let markers = this.getMarkers();
-    let frames = this.getFrames();
-    let memory = this.getMemory();
-    let ticks = this.getTicks();
-    let allocations = this.getAllocations();
-    let profile = this.getProfile();
-    let configuration = this.getConfiguration();
-    return { label, duration, markers, frames, memory, ticks, allocations, profile, configuration };
-  },
-
-  /**
-   * Returns a boolean indicating whether or not this recording model
-   * was imported via file.
-   */
-  isImported: function () {
-    return this._imported;
-  },
-
-  /**
-   * Returns a boolean indicating whether or not this recording model
-   * was started via a `console.profile` call.
-   */
-  isConsole: function () {
-    return this._console;
-  },
-
-  /**
-   * Returns a boolean indicating whether or not this recording model
-   * has finished recording.
-   * There is some delay in fetching data between when the recording stops, and
-   * when the recording is considered completed once it has all the profiler and timeline data.
-   */
-  isCompleted: function () {
-    return this._completed || this.isImported();
-  },
-
-  /**
-   * Returns a boolean indicating whether or not this recording model
-   * is recording.
-   * A model may no longer be recording, yet still not have the profiler data. In that
-   * case, use `isCompleted()`.
-   */
-  isRecording: function () {
-    return this._recording;
-  },
-
-  /**
-   * Returns a boolean indicating if this recording is no longer recording, but
-   * not yet completed.
-   */
-  isFinalizing: function () {
-    return !this.isRecording() && !this.isCompleted();
-  },
-
-  /**
-   * Returns the position, generation and totalSize of the profiler
-   * when this recording was started.
-   */
-  getStartingBufferStatus: function () {
-    return this._startingBufferStatus;
-  },
-
-  /**
    * Fired whenever the PerformanceFront emits markers, memory or ticks.
    */
   _addTimelineData: function (eventName, ...data) {
     // If this model isn't currently recording,
     // ignore the timeline data.
     if (!this.isRecording()) {
       return;
     }
@@ -343,11 +162,11 @@ LegacyPerformanceRecording.prototype = {
         let [, timestamps] = data;
         this._ticks = timestamps;
         break;
       }
     }
   },
 
   toString: () => "[object LegacyPerformanceRecording]"
-};
+}, PerformanceRecordingCommon);
 
 exports.LegacyPerformanceRecording = LegacyPerformanceRecording;
--- a/toolkit/devtools/performance/moz.build
+++ b/toolkit/devtools/performance/moz.build
@@ -5,16 +5,17 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
 
 EXTRA_JS_MODULES.devtools.performance += [
   'io.js',
   'process-communication.js',
   'recorder.js',
+  'recording-common.js',
   'utils.js',
 ]
 
 EXTRA_JS_MODULES.devtools.performance.legacy += [
   'legacy/actors.js',
   'legacy/compatibility.js',
   'legacy/front.js',
   'legacy/recording.js',
--- a/toolkit/devtools/performance/recorder.js
+++ b/toolkit/devtools/performance/recorder.js
@@ -20,23 +20,24 @@ loader.lazyRequireGetter(this, "events",
 loader.lazyRequireGetter(this, "Memory",
   "devtools/toolkit/shared/memory", true);
 loader.lazyRequireGetter(this, "Timeline",
   "devtools/toolkit/shared/timeline", true);
 loader.lazyRequireGetter(this, "Profiler",
   "devtools/toolkit/shared/profiler", true);
 loader.lazyRequireGetter(this, "PerformanceRecordingActor",
   "devtools/server/actors/performance-recording", true);
-
 loader.lazyRequireGetter(this, "PerformanceRecordingFront",
   "devtools/server/actors/performance-recording", true);
 loader.lazyRequireGetter(this, "mapRecordingOptions",
   "devtools/toolkit/performance/utils", true);
 loader.lazyRequireGetter(this, "DevToolsUtils",
   "devtools/toolkit/DevToolsUtils");
+loader.lazyRequireGetter(this, "getSystemInfo",
+  "devtools/toolkit/shared/system", true);
 
 const PROFILER_EVENTS = [
   "console-api-profiler",
   "profiler-started",
   "profiler-stopped",
   "profiler-status"
 ];
 
@@ -64,31 +65,36 @@ const PerformanceRecorder = exports.Perf
     this._onTimelineData = this._onTimelineData.bind(this);
     this._onProfilerEvent = this._onProfilerEvent.bind(this);
   },
 
   /**
    * Initializes a connection to the profiler and other miscellaneous actors.
    * If in the process of opening, or already open, nothing happens.
    *
+   * @param {Object} options.systemClient
+   *        Metadata about the client's system to attach to the recording models.
+   *
    * @return object
    *         A promise that is resolved once the connection is established.
    */
-  connect: function () {
+  connect: function (options) {
     if (this._connected) {
       return;
     }
 
     // Sets `this._profiler`, `this._timeline` and `this._memory`.
     // Only initialize the timeline and memory fronts if the respective actors
     // are available. Older Gecko versions don't have existing implementations,
     // in which case all the methods we need can be easily mocked.
     this._connectComponents();
     this._registerListeners();
 
+    this._systemClient = options.systemClient;
+
     this._connected = true;
   },
 
   /**
    * Destroys this connection.
    */
   destroy: function () {
     this._unregisterListeners();
@@ -336,16 +342,19 @@ const PerformanceRecorder = exports.Perf
     // Filter out start times that are not actually used (0 or undefined), and
     // find the earliest time since all sources use same epoch.
     let startTimes = [profilerStartData.currentTime, memoryStartData, timelineStartData].filter(Boolean);
     data.startTime = Math.min(...startTimes);
     data.position = profilerStartData.position;
     data.generation = profilerStartData.generation;
     data.totalSize = profilerStartData.totalSize;
 
+    data.systemClient = this._systemClient;
+    data.systemHost = yield getSystemInfo();
+
     let model = new PerformanceRecordingActor(this.conn, options, data);
     this._recordings.push(model);
 
     events.emit(this, "recording-started", model);
     return model;
   }),
 
   /**
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/performance/recording-common.js
@@ -0,0 +1,97 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * A mixin to be used for PerformanceRecordingActor, PerformanceRecordingFront,
+ * and LegacyPerformanceRecording for helper methods to access data.
+ */
+
+const PerformanceRecordingCommon = exports.PerformanceRecordingCommon = {
+  // Private fields, only needed when a recording is started or stopped.
+  _console: false,
+  _imported: false,
+  _recording: false,
+  _completed: false,
+  _configuration: {},
+  _startingBufferStatus: null,
+  _localStartTime: 0,
+
+  // Serializable fields, necessary and sufficient for import and export.
+  _label: "",
+  _duration: 0,
+  _markers: null,
+  _frames: null,
+  _memory: null,
+  _ticks: null,
+  _allocations: null,
+  _profile: null,
+  _systemHost: null,
+  _systemClient: null,
+
+  /**
+   * Helper methods for returning the status of the recording.
+   * These methods should be consistent on both the front and actor.
+   */
+  isRecording: function () { return this._recording; },
+  isCompleted: function () { return this._completed || this.isImported(); },
+  isFinalizing: function () { return !this.isRecording() && !this.isCompleted(); },
+  isConsole: function () { return this._console; },
+  isImported: function () { return this._imported; },
+
+  /**
+   * Helper methods for returning configuration for the recording.
+   * These methods should be consistent on both the front and actor.
+   */
+  getConfiguration: function () { return this._configuration; },
+  getLabel: function () { return this._label; },
+
+  /**
+   * Gets duration of this recording, in milliseconds.
+   * @return number
+   */
+  getDuration: function () {
+    // Compute an approximate ending time for the current recording if it is
+    // still in progress. This is needed to ensure that the view updates even
+    // when new data is not being generated. If recording is completed, use
+    // the duration from the profiler; if between recording and being finalized,
+    // use the last estimated duration.
+    if (this.isRecording()) {
+      return this._estimatedDuration = Date.now() - this._localStartTime;
+    } else {
+      return this._duration || this._estimatedDuration || 0;
+    }
+  },
+
+  /**
+   * Helper methods for returning recording data.
+   * These methods should be consistent on both the front and actor.
+   */
+  getMarkers: function() { return this._markers; },
+  getFrames: function() { return this._frames; },
+  getMemory: function() { return this._memory; },
+  getTicks: function() { return this._ticks; },
+  getAllocations: function() { return this._allocations; },
+  getProfile: function() { return this._profile; },
+  getHostSystemInfo: function() { return this._systemHost; },
+  getClientSystemInfo: function() { return this._systemClient; },
+  getStartingBufferStatus: function() { return this._startingBufferStatus; },
+
+  getAllData: function () {
+    let label = this.getLabel();
+    let duration = this.getDuration();
+    let markers = this.getMarkers();
+    let frames = this.getFrames();
+    let memory = this.getMemory();
+    let ticks = this.getTicks();
+    let allocations = this.getAllocations();
+    let profile = this.getProfile();
+    let configuration = this.getConfiguration();
+    let systemHost = this.getHostSystemInfo();
+    let systemClient = this.getClientSystemInfo();
+
+    return { label, duration, markers, frames, memory, ticks, allocations, profile, configuration, systemHost, systemClient };
+  },
+};
--- a/toolkit/devtools/server/actors/performance-recording.js
+++ b/toolkit/devtools/server/actors/performance-recording.js
@@ -10,82 +10,18 @@ const { custom, method, RetVal, Arg, Opt
 const { actorBridge } = require("devtools/server/actors/common");
 
 loader.lazyRequireGetter(this, "events", "sdk/event/core");
 loader.lazyRequireGetter(this, "merge", "sdk/util/object", true);
 loader.lazyRequireGetter(this, "PerformanceIO",
   "devtools/toolkit/performance/io");
 loader.lazyRequireGetter(this, "RecordingUtils",
   "devtools/toolkit/performance/utils");
-
-/**
- * A set of functions used by both the front and actor to access
- * internal properties.
- */
-const PerformanceRecordingCommon = {
-  // Private fields, only needed when a recording is started or stopped.
-  _console: false,
-  _imported: false,
-  _recording: false,
-  _completed: false,
-  _configuration: {},
-  _startingBufferStatus: null,
-  _localStartTime: null,
-
-  // Serializable fields, necessary and sufficient for import and export.
-  _label: "",
-  _duration: 0,
-  _markers: null,
-  _frames: null,
-  _memory: null,
-  _ticks: null,
-  _allocations: null,
-  _profile: null,
-
-  /**
-   * Helper methods for returning the status of the recording.
-   * These methods should be consistent on both the front and actor.
-   */
-  isRecording: function () { return this._recording; },
-  isCompleted: function () { return this._completed || this.isImported(); },
-  isFinalizing: function () { return !this.isRecording() && !this.isCompleted(); },
-  isConsole: function () { return this._console; },
-  isImported: function () { return this._imported; },
-
-  /**
-   * Helper methods for returning configuration for the recording.
-   * These methods should be consistent on both the front and actor.
-   */
-  getConfiguration: function () { return this._configuration; },
-  getLabel: function () { return this._label; },
-
-  /**
-   * Helper methods for returning recording data.
-   * These methods should be consistent on both the front and actor.
-   */
-  getMarkers: function() { return this._markers; },
-  getFrames: function() { return this._frames; },
-  getMemory: function() { return this._memory; },
-  getTicks: function() { return this._ticks; },
-  getAllocations: function() { return this._allocations; },
-  getProfile: function() { return this._profile; },
-
-  getAllData: function () {
-    let label = this.getLabel();
-    let duration = this.getDuration();
-    let markers = this.getMarkers();
-    let frames = this.getFrames();
-    let memory = this.getMemory();
-    let ticks = this.getTicks();
-    let allocations = this.getAllocations();
-    let profile = this.getProfile();
-    let configuration = this.getConfiguration();
-    return { label, duration, markers, frames, memory, ticks, allocations, profile, configuration };
-  },
-};
+loader.lazyRequireGetter(this, "PerformanceRecordingCommon",
+  "devtools/toolkit/performance/recording-common", true);
 
 /**
  * This actor wraps the Performance module at toolkit/devtools/shared/performance.js
  * and provides RDP definitions.
  *
  * @see toolkit/devtools/shared/performance.js for documentation.
  */
 let PerformanceRecordingActor = exports.PerformanceRecordingActor = protocol.ActorClass(merge({
@@ -106,19 +42,22 @@ let PerformanceRecordingActor = exports.
       localStartTime: this._localStartTime,
       recording: this._recording,
       completed: this._completed,
       duration: this._duration,
     };
 
     // Only send profiler data once it exists and it has
     // not yet been sent
-    if (this._profile && !this._sentProfilerData) {
-      form.profile = this._profile;
-      this._sentProfilerData = true;
+    if (this._profile && !this._sentFinalizedData) {
+      form.finalizedData = true;
+      form.profile = this.getProfile();
+      form.systemHost = this.getHostSystemInfo();
+      form.systemClient = this.getClientSystemInfo();
+      this._sentFinalizedData = true;
     }
 
     return form;
   },
 
   /**
    * @param {object} conn
    * @param {object} options
@@ -159,16 +98,19 @@ let PerformanceRecordingActor = exports.
       };
 
       this._recording = true;
       this._markers = [];
       this._frames = [];
       this._memory = [];
       this._ticks = [];
       this._allocations = { sites: [], timestamps: [], frames: [], sizes: [] };
+
+      this._systemHost = meta.systemHost || {};
+      this._systemClient = meta.systemClient || {};
     }
   },
 
   destroy: function() {
     protocol.Actor.prototype.destroy.call(this);
   },
 
   /**
@@ -228,18 +170,20 @@ let PerformanceRecordingFront = exports.
     this._console = form.console;
     this._label = form.label;
     this._startTime = form.startTime;
     this._localStartTime = form.localStartTime;
     this._recording = form.recording;
     this._completed = form.completed;
     this._duration = form.duration;
 
-    if (form.profile) {
+    if (form.finalizedData) {
       this._profile = form.profile;
+      this._systemHost = form.systemHost;
+      this._systemClient = form.systemClient;
     }
 
     // Sort again on the client side if we're using realtime markers and the recording
     // just finished. This is because GC/Compositing markers can come into the array out of order with
     // the other markers, leading to strange collapsing in waterfall view.
     if (this._completed && !this._markersSorted) {
       this._markers = this._markers.sort((a, b) => (a.start > b.start));
       this._markersSorted = true;
@@ -266,43 +210,16 @@ let PerformanceRecordingFront = exports.
    *        The file to stream the data into.
    */
   exportRecording: function (file) {
     let recordingData = this.getAllData();
     return PerformanceIO.saveRecordingToFile(recordingData, file);
   },
 
   /**
-   * Returns the position, generation, and totalSize of the profiler
-   * when this recording was started.
-   *
-   * @return {object}
-   */
-  getStartingBufferStatus: function () {
-    return this._form.startingBufferStatus;
-  },
-
-  /**
-   * Gets duration of this recording, in milliseconds.
-   * @return number
-   */
-  getDuration: function () {
-    // Compute an approximate ending time for the current recording if it is
-    // still in progress. This is needed to ensure that the view updates even
-    // when new data is not being generated. If recording is completed, use
-    // the duration from the profiler; if between recording and being finalized,
-    // use the last estimated duration.
-    if (this.isRecording()) {
-      return this._estimatedDuration = Date.now() - this._localStartTime;
-    } else {
-      return this._duration || this._estimatedDuration || 0;
-    }
-  },
-
-  /**
    * Fired whenever the PerformanceFront emits markers, memory or ticks.
    */
   _addTimelineData: function (eventName, data) {
     let config = this.getConfiguration();
 
     switch (eventName) {
       // Accumulate timeline markers into an array. Furthermore, the timestamps
       // do not have a zero epoch, so offset all of them by the start time.
--- a/toolkit/devtools/server/actors/performance.js
+++ b/toolkit/devtools/server/actors/performance.js
@@ -17,16 +17,18 @@ loader.lazyRequireGetter(this, "extend",
 loader.lazyRequireGetter(this, "PerformanceRecorder",
   "devtools/toolkit/performance/recorder", true);
 loader.lazyRequireGetter(this, "PerformanceIO",
   "devtools/toolkit/performance/io");
 loader.lazyRequireGetter(this, "normalizePerformanceFeatures",
   "devtools/toolkit/performance/utils", true);
 loader.lazyRequireGetter(this, "LegacyPerformanceFront",
   "devtools/toolkit/performance/legacy/front", true);
+loader.lazyRequireGetter(this, "getSystemInfo",
+  "devtools/toolkit/shared/system", true);
 
 const PIPE_TO_FRONT_EVENTS = new Set([
   "recording-started", "recording-stopping", "recording-stopped",
   "profiler-status", "timeline-data", "console-profile-start"
 ]);
 
 const RECORDING_STATE_CHANGE_EVENTS = new Set([
   "recording-started", "recording-stopping", "recording-stopped"
@@ -92,20 +94,23 @@ let PerformanceActor = exports.Performan
   },
 
   destroy: function () {
     events.off(this.bridge, "*", this._onRecorderEvent);
     this.bridge.destroy();
     protocol.Actor.prototype.destroy.call(this);
   },
 
-  connect: method(function () {
-    this.bridge.connect();
-    return this.traits;
-  }, { response: RetVal("json") }),
+  connect: method(function (config) {
+    this.bridge.connect({ systemClient: config.systemClient });
+    return { traits: this.traits };
+  }, {
+    request: { options: Arg(0, "nullable:json") },
+    response: RetVal("json")
+  }),
 
   startRecording: method(Task.async(function *(options={}) {
     let normalizedOptions = normalizePerformanceFeatures(options, this.traits.features);
     let recording = yield this.bridge.startRecording(normalizedOptions);
 
     this.manage(recording);
 
     return recording;
@@ -177,19 +182,27 @@ const PerformanceFront = exports.Perform
     this.actorID = form.performanceActor;
     this.manage(this);
   },
 
   destroy: function () {
     protocol.Front.prototype.destroy.call(this);
   },
 
-  connect: custom(function () {
-    return this._connect().then(traits => this._traits = traits);
-  }, {
+  /**
+   * Conenct to the server, and handle once-off tasks like storing traits
+   * or system info.
+   */
+  connect: custom(Task.async(function *() {
+    let systemClient = yield getSystemInfo();
+    let { traits } = yield this._connect({ systemClient });
+    this._traits = traits;
+
+    return this._traits;
+  }), {
     impl: "_connect"
   }),
 
   get traits() {
     if (!this._traits) {
       Cu.reportError("Cannot access traits of PerformanceFront before calling `connect()`.");
     }
     return this._traits;
@@ -234,16 +247,18 @@ const PerformanceFront = exports.Perform
       model._duration = recordingData.duration;
       model._markers = recordingData.markers;
       model._frames = recordingData.frames;
       model._memory = recordingData.memory;
       model._ticks = recordingData.ticks;
       model._allocations = recordingData.allocations;
       model._profile = recordingData.profile;
       model._configuration = recordingData.configuration || {};
+      model._systemHost = recordingData.systemHost;
+      model._systemClient = recordingData.systemClient;
       return model;
     });
   },
 
   /**
    * Store profiler status when the position has been update so we can
    * calculate recording's buffer percentage usage after emitting the event.
    */
--- a/toolkit/devtools/server/actors/webbrowser.js
+++ b/toolkit/devtools/server/actors/webbrowser.js
@@ -1138,16 +1138,22 @@ TabActor.prototype = {
     // (chrome-)webnavigation-create is fired very early during docshell construction.
     // In new root docshells within child processes, involving TabChild,
     // this event is from within this call:
     //   http://hg.mozilla.org/mozilla-central/annotate/74d7fb43bb44/dom/ipc/TabChild.cpp#l912
     // whereas the chromeEventHandler (and most likely other stuff) is set later:
     //   http://hg.mozilla.org/mozilla-central/annotate/74d7fb43bb44/dom/ipc/TabChild.cpp#l944
     // So wait a tick before watching it:
     DevToolsUtils.executeSoon(() => {
+      // Bug 1142752: sometimes, the docshell appears to be immediately destroyed,
+      // bailout early to prevent random exceptions.
+      if (docShell.isBeingDestroyed()) {
+        return;
+      }
+
       // In child processes, we have new root docshells,
       // let's watch them and all their child docshells.
       if (this._isRootDocShell(docShell)) {
         this._progressListener.watch(docShell);
       }
       this._notifyDocShellsUpdate([docShell]);
     });
   },
--- a/toolkit/devtools/server/tests/browser/browser_perf-legacy-front-01.js
+++ b/toolkit/devtools/server/tests/browser/browser_perf-legacy-front-01.js
@@ -48,16 +48,18 @@ add_task(function*() {
   ok(recording.getDuration() >= 0, "duration is a positive number");
   isEmptyArray(recording.getMarkers(), "markers");
   isEmptyArray(recording.getTicks(), "ticks");
   isEmptyArray(recording.getMemory(), "memory");
   isEmptyArray(recording.getAllocations().sites, "allocations.sites");
   isEmptyArray(recording.getAllocations().timestamps, "allocations.timestamps");
   isEmptyArray(recording.getAllocations().frames, "allocations.frames");
   ok(recording.getProfile().threads[0].samples.data.length, "profile data has some samples");
+  checkSystemInfo(recording, "Host");
+  checkSystemInfo(recording, "Client");
 
   yield front.destroy();
   yield closeDebuggerClient(target.client);
   gBrowser.removeCurrentTab();
 });
 
 function isEmptyArray (array, name) {
   ok(Array.isArray(array), `${name} is an array`);
@@ -71,8 +73,15 @@ function getTab (url) {
   content.location = url;
   return loaded.then(() => {
     return new Promise(resolve => {
       let isBlank = url == "about:blank";
       waitForFocus(() => resolve(tab), content, isBlank);
     });
   });
 }
+
+function checkSystemInfo (recording, type) {
+  let data = recording[`get${type}SystemInfo`]();
+  for (let field of ["appid", "apptype", "vendor", "name", "version"]) {
+    ok(data[field], `get${type}SystemInfo() has ${field} property`);
+  }
+}
--- a/toolkit/devtools/server/tests/browser/browser_perf-recording-actor-01.js
+++ b/toolkit/devtools/server/tests/browser/browser_perf-recording-actor-01.js
@@ -44,23 +44,36 @@ add_task(function*() {
     ok(!rec.isCompleted(), "recording is not yet completed on 'recording-stopping'");
     ok(rec.isFinalizing(), "recording is considered finalizing between 'recording-stopping' and 'recording-stopped'");
   }
 
   yield stopped;
   ok(!rec.isRecording(), "on 'recording-stopped', model is still no longer recording");
   ok(rec.isCompleted(), "on 'recording-stopped', model is considered 'complete'");
 
+  checkSystemInfo(rec, "Host");
+  checkSystemInfo(rec, "Client");
+
   // Export and import a rec, and ensure it has the correct state.
   let file = FileUtils.getFile("TmpD", ["tmpprofile.json"]);
   file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
   yield rec.exportRecording(file);
 
   let importedModel = yield front.importRecording(file);
 
   ok(importedModel.isCompleted(), "All imported recordings should be completed");
   ok(!importedModel.isRecording(), "All imported recordings should not be recording");
   ok(importedModel.isImported(), "All imported recordings should be considerd imported");
 
+  checkSystemInfo(importedModel, "Host");
+  checkSystemInfo(importedModel, "Client");
+
   yield front.destroy();
   yield closeDebuggerClient(client);
   gBrowser.removeCurrentTab();
 });
+
+function checkSystemInfo (recording, type) {
+  let data = recording[`get${type}SystemInfo`]();
+  for (let field of ["appid", "apptype", "vendor", "name", "version"]) {
+    ok(data[field], `get${type}SystemInfo() has ${field} property`);
+  }
+}
--- a/toolkit/devtools/shared/system.js
+++ b/toolkit/devtools/shared/system.js
@@ -3,17 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const { Cc, Ci, Cu } = require("chrome");
 const { Task } = require("resource://gre/modules/Task.jsm");
 
 loader.lazyRequireGetter(this, "Services");
 loader.lazyRequireGetter(this, "promise");
-loader.lazyRequireGetter(this, "OS", "resource://gre/modules/commonjs/node/os");
+loader.lazyRequireGetter(this, "OS", "resource://gre/modules/commonjs/node/os.js");
 loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
 loader.lazyRequireGetter(this, "AppConstants",
   "resource://gre/modules/AppConstants.jsm", true);
 loader.lazyGetter(this, "screenManager", () => {
   return Cc["@mozilla.org/gfx/screenmanager;1"].getService(Ci.nsIScreenManager);
 });
 loader.lazyGetter(this, "oscpu", () => {
   return Cc["@mozilla.org/network/protocol;1?name=http"]
@@ -159,28 +159,33 @@ function *getSystemInfo() {
     height,
     brandName,
   };
 
   CACHED_INFO = info;
   return info;
 }
 
-function getProfileLocation() {
-  let profd = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
-  let profservice = Cc["@mozilla.org/toolkit/profile-service;1"].getService(Ci.nsIToolkitProfileService);
-  var profiles = profservice.profiles;
-  while (profiles.hasMoreElements()) {
-    let profile = profiles.getNext().QueryInterface(Ci.nsIToolkitProfile);
-    if (profile.rootDir.path == profd.path) {
-      return profile = profile.name;
+function getProfileLocation () {
+  // In child processes, we cannot access the profile location.
+  try {
+    let profd = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
+    let profservice = Cc["@mozilla.org/toolkit/profile-service;1"].getService(Ci.nsIToolkitProfileService);
+    var profiles = profservice.profiles;
+    while (profiles.hasMoreElements()) {
+      let profile = profiles.getNext().QueryInterface(Ci.nsIToolkitProfile);
+      if (profile.rootDir.path == profd.path) {
+        return profile = profile.name;
+      }
     }
+
+    return profd.leafName;
+  } catch (e) {
+    return "";
   }
-
-  return profd.leafName;
 }
 
 function getAppIniString(section, key) {
   let inifile = Services.dirsvc.get("GreD", Ci.nsIFile);
   inifile.append("application.ini");
 
   if (!inifile.exists()) {
     inifile = Services.dirsvc.get("CurProcD", Ci.nsIFile);